diff --git a/.gitattributes b/.gitattributes
index 0b87a97df9c80f92bf3f332bbb302a404ae1534a..55c422f0f8cee90ff35df00e11c3b3543ed97900 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,3 +1,2 @@
 VERSION merge=ours
 Dangerfile gitlab-language=ruby
-db/schema.rb merge=merge_db_schema
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS
index 6be92710ad9876f1637896d028707664d1fb8373..ddfdf72cf99e47ec41f42518fbe227af0e80665d 100644
--- a/.gitlab/CODEOWNERS
+++ b/.gitlab/CODEOWNERS
@@ -6,8 +6,8 @@
 /doc/ @axil @marcia @eread @mikelewis
 
 # Frontend maintainers should see everything in `app/assets/`
-app/assets/ @ClemMakesApps @fatihacet @filipa @mikegreiling @timzallmann @kushalpandya @pslaughter @wortschi
-*.scss @annabeldunstone @ClemMakesApps @fatihacet @filipa @mikegreiling @timzallmann @kushalpandya @pslaughter @wortschi
+app/assets/ @ClemMakesApps @fatihacet @filipa @mikegreiling @timzallmann @kushalpandya @pslaughter @wortschi @ntepluhina
+*.scss @annabeldunstone @ClemMakesApps @fatihacet @filipa @mikegreiling @timzallmann @kushalpandya @pslaughter @wortschi @ntepluhina
 
 # Database maintainers should review changes in `db/`
 db/ @gitlab-org/maintainers/database
@@ -32,4 +32,5 @@ lib/gitlab/github_import/ @gitlab-org/maintainers/database
 /.gitlab/ci/ @gl-quality/eng-prod
 Dangerfile @gl-quality/eng-prod
 /danger/ @gl-quality/eng-prod
+/lib/gitlab/danger/ @gl-quality/eng-prod
 /scripts/ @gl-quality/eng-prod
diff --git a/.gitlab/ci/docs.gitlab-ci.yml b/.gitlab/ci/docs.gitlab-ci.yml
index 008151f889f7eb3b910dbf8c3ab3f7155f1ffcb7..14eeebb9db9bf865796dca03ee60f24e000da03b 100644
--- a/.gitlab/ci/docs.gitlab-ci.yml
+++ b/.gitlab/ci/docs.gitlab-ci.yml
@@ -67,3 +67,19 @@ docs lint:
     - bundle exec nanoc check internal_links
     # Check the internal anchor links
     - bundle exec nanoc check internal_anchors
+
+graphql-docs-verify:
+  extends:
+    - .only-ee
+    - .default-tags
+    - .default-retry
+    - .default-cache
+    - .default-only
+    - .default-before_script
+    - .only-graphql-changes
+  variables:
+    SETUP_DB: "false"
+  stage: test
+  needs: ["setup-test-env"]
+  script:
+    - bundle exec rake gitlab:graphql:check_docs
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index a3a2ab0691f283699214c2da23584f592c2810aa..2f457bc0ee2cb49920fc37db9f3338ba364bfc02 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -53,7 +53,7 @@
     - gitlab-org
     - docker
 
-gitlab:assets:compile:
+gitlab:assets:compile pull-push-cache:
   extends: .gitlab:assets:compile-metadata
   only:
     refs:
@@ -63,9 +63,6 @@ gitlab:assets:compile:
 
 gitlab:assets:compile pull-cache:
   extends: .gitlab:assets:compile-metadata
-  except:
-    refs:
-      - master
   cache:
     policy: pull
 
@@ -89,14 +86,14 @@ gitlab:assets:compile pull-cache:
     # we override the max_old_space_size to prevent OOM errors
     NODE_OPTIONS: --max_old_space_size=3584
   cache:
-    key: "assets-compile:test:vendor_ruby:.yarn-cache:tmp_cache_assets_sprockets:v6"
+    key: "assets-compile:v7"
   artifacts:
     expire_in: 7d
     paths:
       - node_modules
       - public/assets
 
-compile-assets:
+compile-assets pull-push-cache:
   extends: .compile-assets-metadata
   only:
     refs:
@@ -104,13 +101,25 @@ compile-assets:
   cache:
     policy: pull-push
 
-compile-assets pull-cache:
-  extends: .compile-assets-metadata
-  except:
+compile-assets pull-push-cache foss:
+  extends: [".compile-assets-metadata", ".only-ee-as-if-foss"]
+  only:
     refs:
       - master
+  cache:
+    policy: pull-push
+    key: "assets-compile:v7:foss"
+
+compile-assets pull-cache:
+  extends: .compile-assets-metadata
+  cache:
+    policy: pull
+
+compile-assets pull-cache foss:
+  extends: [".compile-assets-metadata", ".only-ee-as-if-foss"]
   cache:
     policy: pull
+    key: "assets-compile:v7:foss"
 
 .only-code-frontend-job-base:
   extends:
@@ -121,7 +130,9 @@ compile-assets pull-cache:
     - .default-before_script
     - .only-code-changes
     - .use-pg9
-  dependencies: ["compile-assets", "compile-assets pull-cache", "setup-test-env"]
+  stage: test
+  needs: ["setup-test-env", "compile-assets pull-cache"]
+  dependencies: ["setup-test-env", "compile-assets pull-cache"]
 
 .karma-base:
   extends: .only-code-frontend-job-base
@@ -195,6 +206,7 @@ jest-foss:
     - .default-cache
     - .default-only
     - .only-code-changes
+  stage: test
   dependencies: []
   cache:
     key: "$CI_JOB_NAME"
@@ -227,7 +239,9 @@ webpack-dev-server:
     - .default-cache
     - .default-only
     - .only-code-changes
-  dependencies: ["setup-test-env", "compile-assets", "compile-assets pull-cache"]
+  stage: test
+  needs: ["setup-test-env", "compile-assets pull-cache"]
+  dependencies: ["setup-test-env", "compile-assets pull-cache"]
   variables:
     WEBPACK_MEMORY_TEST: "true"
   script:
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index af7c7a0d1522dc9d191fcf684838ed854c49cb07..fc9b00b5d3c78e7d12d9c50ceac1372453e45a70 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -71,6 +71,12 @@
       - "doc/**/*"
       - ".markdownlint.json"
 
+.only-graphql-changes:
+  only:
+    changes:
+      - "{,ee/}app/graphql/**/*"
+      - "{,ee/}lib/gitlab/graphql/**/*"
+
 .only-code-qa-changes:
   only:
     changes:
@@ -153,4 +159,4 @@
 .only-ee-as-if-foss:
   extends: .only-ee
   variables:
-    IS_GITLAB_EE: '0'
+    FOSS_ONLY: '1'
diff --git a/.gitlab/ci/pages.gitlab-ci.yml b/.gitlab/ci/pages.gitlab-ci.yml
index a59b84fe1cff81e7f79b355d610adfa0b08538bf..a30772d5664b4b84ae20abc1e66a9f059bf8e91e 100644
--- a/.gitlab/ci/pages.gitlab-ci.yml
+++ b/.gitlab/ci/pages.gitlab-ci.yml
@@ -11,7 +11,7 @@ pages:
     variables:
       - $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org"
   stage: pages
-  dependencies: ["coverage", "karma", "gitlab:assets:compile"]
+  dependencies: ["coverage", "karma", "gitlab:assets:compile pull-cache"]
   script:
     - mv public/ .public/
     - mkdir public/
diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml
index a73edd3f65fe4253f1f4ad8d80e96be27cab7dcb..1194948a76f67bb6becfc996b196970613494f39 100644
--- a/.gitlab/ci/qa.gitlab-ci.yml
+++ b/.gitlab/ci/qa.gitlab-ci.yml
@@ -71,4 +71,4 @@ schedule:package-and-qa:
     - .package-and-qa-base
     - .only-code-qa-changes
     - .only-canonical-schedules
-  needs: ["build-qa-image", "gitlab:assets:compile"]
+  needs: ["build-qa-image", "gitlab:assets:compile pull-cache"]
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 73b649b4d14f641dbdccb4fb8b647b7f2b6cfdc0..bf478b68765cb001a03c332d2e3664a529dc16ed 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -53,6 +53,8 @@ setup-test-env:
 .rspec-base:
   extends: .only-code-rails-job-base
   stage: test
+  needs: ["setup-test-env", "retrieve-tests-metadata", "compile-assets pull-cache"]
+  dependencies: ["setup-test-env", "retrieve-tests-metadata", "compile-assets pull-cache"]
   script:
     - source scripts/rspec_helpers.sh
     - rspec_paralellized_job "--tag ~quarantine --tag ~geo"
@@ -69,6 +71,11 @@ setup-test-env:
     reports:
       junit: junit_rspec.xml
 
+.rspec-base-foss:
+  extends: [".rspec-base", ".only-ee-as-if-foss"]
+  needs: ["setup-test-env", "retrieve-tests-metadata", "compile-assets pull-cache foss"]
+  dependencies: ["setup-test-env", "retrieve-tests-metadata", "compile-assets pull-cache foss"]
+
 .rspec-base-pg9:
   extends:
     - .rspec-base
@@ -76,9 +83,8 @@ setup-test-env:
 
 .rspec-base-pg9-foss:
   extends:
-    - .rspec-base
+    - .rspec-base-foss
     - .use-pg9
-    - .only-ee-as-if-foss
 
 .rspec-base-pg10:
   extends:
@@ -106,10 +112,9 @@ rspec system pg9:
   extends: .rspec-base-pg9
   parallel: 24
 
-# TODO: This requires FOSS assets
-# rspec system pg9-foss:
-#   extends: .rspec-base-pg9-foss
-#   parallel: 24
+rspec system pg9-foss:
+  extends: .rspec-base-pg9-foss
+  parallel: 24
 
 rspec unit pg10:
   extends: .rspec-base-pg10
@@ -229,7 +234,9 @@ rspec fast_spec_helper:
 
 static-analysis:
   extends: .only-code-qa-rails-job-base
-  dependencies: ["setup-test-env", "compile-assets", "compile-assets pull-cache"]
+  stage: test
+  needs: ["setup-test-env", "compile-assets pull-cache"]
+  dependencies: ["setup-test-env", "compile-assets pull-cache"]
   variables:
     SETUP_DB: "false"
   script:
@@ -252,16 +259,16 @@ downtime_check:
     variables:
       - $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable(-ee)?$/
   stage: test
-  dependencies: ["setup-test-env"]
   needs: ["setup-test-env"]
+  dependencies: ["setup-test-env"]
 
 .db-job-base:
   extends:
     - .only-code-rails-job-base
     - .use-pg9
   stage: test
-  dependencies: ["setup-test-env"]
   needs: ["setup-test-env"]
+  dependencies: ["setup-test-env"]
 
 # DB migration, rollback, and seed jobs
 db:migrate:reset:
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 7c4ba3878f12cc4f4f4b682bb4fb0dc1fa15aa74..fd26711cfcf6416efc32abe9ac9c3009b4f92db9 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -1,7 +1,6 @@
 .except-deploys:
   except:
     refs:
-      - /^[\d-]+-stable(-ee)?$/
       - /^\d+-\d+-auto-deploy-\d+$/
 
 .review-docker:
@@ -81,7 +80,7 @@ schedule:review-build-cng:
   extends:
     - .review-build-cng-base
     - .only-review-schedules
-  needs: ["gitlab:assets:compile"]
+  needs: ["gitlab:assets:compile pull-cache"]
 
 .review-deploy-base:
   extends:
@@ -97,7 +96,10 @@ schedule:review-build-cng:
   variables:
     HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
     DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
-    GITLAB_HELM_CHART_REF: "master"
+    # v2.3.7 + some stability improvements not yet released:
+    # - sidekiq readinessProbe should be `pgrep -f sidekiq`: https://gitlab.com/gitlab-org/charts/gitlab/merge_requests/991
+    # - Allows livenessProbe and readinessProbe to be configured for unicorn: https://gitlab.com/gitlab-org/charts/gitlab/merge_requests/985
+    GITLAB_HELM_CHART_REF: "df7c52dc69df441909880b8f2fd15e938cdb2047"
     GITLAB_EDITION: "ce"
   environment:
     name: review/${CI_COMMIT_REF_NAME}
diff --git a/.gitlab/issue_templates/Problem_Validation.md b/.gitlab/issue_templates/Problem_Validation.md
index 7440b41cf0b95925ee21551758d5616aa0f44b4d..bc1fd3374dfd937340ad31f81c5c70f9c49e5b59 100644
--- a/.gitlab/issue_templates/Problem_Validation.md
+++ b/.gitlab/issue_templates/Problem_Validation.md
@@ -38,4 +38,4 @@
 
 For example, if the solution will take a product manager, designer, and engineer two weeks of effort - you may quantify this as 1.5 (based on 0.5 months x 3 people). -->
 
-/label ~"workflow::problem backlog"
+/label ~"workflow::validation backlog" ~devops:: ~category: ~group::
diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md
index 3e634de4f0c4ae26b477289ecfec19e9f925617f..e06a6fb0cffcd0bebc412cbb53ca6b7db10ad0e0 100644
--- a/.gitlab/issue_templates/Security developer workflow.md	
+++ b/.gitlab/issue_templates/Security developer workflow.md	
@@ -29,7 +29,7 @@ Set the title to: `Description of the original issue`
 
 #### Documentation and final details
 
-- [ ] Check the topic on #security to see when the next release is going to happen and add a link to the [links section](#links)
+- [ ] Check the topic on #releases to see when the next release is going to happen and add a link to the [links section](#links)
 - [ ] Add links to this issue and your MRs in the description of the security release issue
 - [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details)
 - [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml
index 8f8cbc2072a0199e90c997b58d2d2b1b1d4a77c4..211ad359951f57e3fd4ab65d3628cd2581a3ce1e 100644
--- a/.haml-lint_todo.yml
+++ b/.haml-lint_todo.yml
@@ -356,6 +356,7 @@ linters:
       - 'app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml'
       - 'app/views/shared/_commit_message_container.html.haml'
       - 'app/views/shared/_confirm_modal.html.haml'
+      - 'app/views/shared/_confirm_fork_modal.html.haml'
       - 'app/views/shared/_delete_label_modal.html.haml'
       - 'app/views/shared/_group_form.html.haml'
       - 'app/views/shared/_group_tips.html.haml'
diff --git a/.rubocop.yml b/.rubocop.yml
index 835c321c943b8d652e8cc5003ffc7be5ac791e02..049340f90d4149fa1be2c0b71e28a0f8b60abd9c 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -178,6 +178,11 @@ Gitlab/ModuleWithInstanceVariables:
     - spec/support/**/*.rb
     - features/steps/**/*.rb
 
+Gitlab/ConstGetInheritFalse:
+  Enabled: true
+  Exclude:
+    - 'qa/bin/*'
+
 Gitlab/HTTParty:
   Enabled: true
   Exclude:
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 81bb28dacab877cf483f625501dc8a0460a053f2..3ed7af71b4fac7a368e14cb10008864f8367c1e8 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -273,11 +273,6 @@ RSpec/ContextWording:
 RSpec/EmptyLineAfterFinalLet:
   Enabled: false
 
-# Offense count: 232
-# Cop supports --auto-correct.
-RSpec/EmptyLineAfterSubject:
-  Enabled: false
-
 # Offense count: 719
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle.
diff --git a/Dangerfile b/Dangerfile
index 228190cd530a118aac96f3ae93ecafa0434fdbcb..b65a9074078a022bc08c4e9dd893d15fb2809978 100644
--- a/Dangerfile
+++ b/Dangerfile
@@ -1,6 +1,7 @@
 # frozen_string_literal: true
 
 require_relative 'lib/gitlab_danger'
+require_relative 'lib/gitlab/danger/request_helper'
 
 danger.import_plugin('danger/plugins/helper.rb')
 danger.import_plugin('danger/plugins/roulette.rb')
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 65ee0959841f14d2e0a0fc971df20d8e761d6e8d..832e9afb6c139c0dc63a83499963772e78c67e27 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.67.0
+1.70.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 4149c39eec6fafa82d256ecc53771684108f2242..2bd6f7e39277d958d71e245c10304ca8c111c683 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-10.1.0
+10.2.0
diff --git a/Gemfile b/Gemfile
index 920f778c053f1ad4129766047c6bc460ecdfafa5..eaea54cf8da08b8de6dab1af690717915018dc33 100644
--- a/Gemfile
+++ b/Gemfile
@@ -448,9 +448,9 @@ end
 # Gitaly GRPC protocol definitions
 gem 'gitaly', '~> 1.65.0'
 
-gem 'grpc', '~> 1.19.0'
+gem 'grpc', '~> 1.24.0'
 
-gem 'google-protobuf', '~> 3.7.1'
+gem 'google-protobuf', '~> 3.8.0'
 
 gem 'toml-rb', '~> 1.0.0', require: false
 
diff --git a/Gemfile.lock b/Gemfile.lock
index e879fdc65fc8c7039fa1fde439c7450344bd48c1..902a8281101ebfbefb7bca77d0f8a42c526f65ab 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -95,7 +95,7 @@ GEM
     babosa (1.0.2)
     base32 (0.3.2)
     batch-loader (1.4.0)
-    bcrypt (3.1.13)
+    bcrypt (3.1.12)
     bcrypt_pbkdf (1.0.0)
     benchmark-ips (2.3.0)
     benchmark-memory (0.1.2)
@@ -400,7 +400,7 @@ GEM
       mime-types (~> 3.0)
       representable (~> 3.0)
       retriable (>= 2.0, < 4.0)
-    google-protobuf (3.7.1)
+    google-protobuf (3.8.0)
     googleapis-common-protos-types (1.0.4)
       google-protobuf (~> 3.0)
     googleauth (0.6.6)
@@ -440,9 +440,9 @@ GEM
       graphql (~> 1.6)
       html-pipeline (~> 2.8)
       sass (~> 3.4)
-    grpc (1.19.0)
-      google-protobuf (~> 3.1)
-      googleapis-common-protos-types (~> 1.0.0)
+    grpc (1.24.0)
+      google-protobuf (~> 3.8)
+      googleapis-common-protos-types (~> 1.0)
     gssapi (1.2.0)
       ffi (>= 1.0.1)
     haml (5.0.4)
@@ -1181,7 +1181,7 @@ DEPENDENCIES
   gitlab_omniauth-ldap (~> 2.1.1)
   gon (~> 6.2)
   google-api-client (~> 0.23)
-  google-protobuf (~> 3.7.1)
+  google-protobuf (~> 3.8.0)
   gpgme (~> 2.0.18)
   grape (~> 1.1.0)
   grape-entity (~> 0.7.1)
@@ -1190,7 +1190,7 @@ DEPENDENCIES
   graphiql-rails (~> 1.4.10)
   graphql (~> 1.9.11)
   graphql-docs (~> 1.6.0)
-  grpc (~> 1.19.0)
+  grpc (~> 1.24.0)
   gssapi
   haml_lint (~> 0.31.0)
   hamlit (~> 2.8.8)
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index d57be10f47278f88f41687b09bc40a39839741a4..908dc730aa41dd824ff1cdc28bbfca49b19f5830 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -36,6 +36,7 @@ const Api = {
   branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
   createBranchPath: '/api/:version/projects/:id/repository/branches',
   releasesPath: '/api/:version/projects/:id/releases',
+  releasePath: '/api/:version/projects/:id/releases/:tag_name',
   mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines',
   adminStatisticsPath: 'api/:version/application/statistics',
 
@@ -391,6 +392,22 @@ const Api = {
     return axios.get(url);
   },
 
+  release(projectPath, tagName) {
+    const url = Api.buildUrl(this.releasePath)
+      .replace(':id', encodeURIComponent(projectPath))
+      .replace(':tag_name', encodeURIComponent(tagName));
+
+    return axios.get(url);
+  },
+
+  updateRelease(projectPath, tagName, release) {
+    const url = Api.buildUrl(this.releasePath)
+      .replace(':id', encodeURIComponent(projectPath))
+      .replace(':tag_name', encodeURIComponent(tagName));
+
+    return axios.put(url, release);
+  },
+
   adminStatistics() {
     const url = Api.buildUrl(this.adminStatisticsPath);
     return axios.get(url);
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue
index b865c9deb72003a29c87aa1360327d8d4ad4152a..22ee368b8e06b89f44a0f3b91db53bf6db4e3d51 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue
@@ -12,11 +12,18 @@ export default {
       type: String,
       required: true,
     },
+    kubernetesIntegrationHelpPath: {
+      type: String,
+      required: true,
+    },
   },
 };
 </script>
 <template>
-  <eks-cluster-configuration-form
-    :gitlab-managed-cluster-help-path="gitlabManagedClusterHelpPath"
-  />
+  <div class="js-create-eks-cluster">
+    <eks-cluster-configuration-form
+      :gitlab-managed-cluster-help-path="gitlabManagedClusterHelpPath"
+      :kubernetes-integration-help-path="kubernetesIntegrationHelpPath"
+    />
+  </div>
 </template>
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
index d451516dd35e2512c7b977689d6b8fbc27572494..1188cf088505206a7b24d629f101ba22c2ab1a40 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
@@ -35,6 +35,10 @@ export default {
       type: String,
       required: true,
     },
+    kubernetesIntegrationHelpPath: {
+      type: String,
+      required: true,
+    },
   },
   computed: {
     ...mapState([
@@ -94,6 +98,20 @@ export default {
     securityGroupDropdownDisabled() {
       return !this.selectedVpc;
     },
+    kubernetesIntegrationHelpText() {
+      const escapedUrl = _.escape(this.kubernetesIntegrationHelpPath);
+
+      return sprintf(
+        s__(
+          'ClusterIntegration|Read our %{link_start}help page%{link_end} on Kubernetes cluster integration.',
+        ),
+        {
+          link_start: `<a href="${escapedUrl}" target="_blank" rel="noopener noreferrer">`,
+          link_end: '</a>',
+        },
+        false,
+      );
+    },
     roleDropdownHelpText() {
       return sprintf(
         s__(
@@ -212,6 +230,10 @@ export default {
 </script>
 <template>
   <form name="eks-cluster-configuration-form">
+    <h2>
+      {{ s__('ClusterIntegration|Enter the details for your Amazon EKS Kubernetes cluster') }}
+    </h2>
+    <p v-html="kubernetesIntegrationHelpText"></p>
     <div class="form-group">
       <label class="label-bold" for="eks-cluster-name">{{
         s__('ClusterIntegration|Kubernetes cluster name')
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/index.js b/app/assets/javascripts/create_cluster/eks_cluster/index.js
index 77454a2bc00dcbd17491b31e9d7ddce6cf6d9c9e..1f595e9b2dfd8c8d5863ee8d7a438d682ff9adfa 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/index.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/index.js
@@ -5,25 +5,22 @@ import createStore from './store';
 
 Vue.use(Vuex);
 
-export default () =>
-  new Vue({
-    el: '.js-create-eks-cluster-form-container',
+export default el => {
+  const { gitlabManagedClusterHelpPath, kubernetesIntegrationHelpPath } = el.dataset;
+
+  return new Vue({
+    el,
     store: createStore(),
     components: {
       CreateEksCluster,
     },
-    data() {
-      const { gitlabManagedClusterHelpPath } = document.querySelector(this.$options.el).dataset;
-
-      return {
-        gitlabManagedClusterHelpPath,
-      };
-    },
     render(createElement) {
       return createElement('create-eks-cluster', {
         props: {
-          gitlabManagedClusterHelpPath: this.gitlabManagedClusterHelpPath,
+          gitlabManagedClusterHelpPath,
+          kubernetesIntegrationHelpPath,
         },
       });
     },
   });
+};
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
index 3528f0a933585e5426996b51316027fc7be45b43..cd298e2c6928e5e1bfa64282af9e957f8cc9aeb5 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -4,6 +4,8 @@ import { GlEmptyState, GlButton, GlLink, GlLoadingIcon, GlTable } from '@gitlab/
 import Icon from '~/vue_shared/components/icon.vue';
 import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
 import { __ } from '~/locale';
+import TrackEventDirective from '~/vue_shared/directives/track_event';
+import { trackViewInSentryOptions, trackClickErrorLinkToSentryOptions } from '../utils';
 
 export default {
   fields: [
@@ -21,6 +23,9 @@ export default {
     Icon,
     TimeAgo,
   },
+  directives: {
+    TrackEvent: TrackEventDirective,
+  },
   props: {
     indexPath: {
       type: String,
@@ -53,6 +58,8 @@ export default {
   },
   methods: {
     ...mapActions(['startPolling', 'restartPolling']),
+    trackViewInSentryOptions,
+    trackClickErrorLinkToSentryOptions,
   },
 };
 </script>
@@ -65,7 +72,13 @@ export default {
       </div>
       <div v-else>
         <div class="d-flex justify-content-end">
-          <gl-button class="my-3 ml-auto" variant="primary" :href="externalUrl" target="_blank">
+          <gl-button
+            v-track-event="trackViewInSentryOptions(externalUrl)"
+            class="my-3 ml-auto"
+            variant="primary"
+            :href="externalUrl"
+            target="_blank"
+          >
             {{ __('View in Sentry') }}
             <icon name="external-link" class="flex-shrink-0" />
           </gl-button>
@@ -80,7 +93,12 @@ export default {
           </template>
           <template slot="error" slot-scope="errors">
             <div class="d-flex flex-column">
-              <gl-link :href="errors.item.externalUrl" class="d-flex text-dark" target="_blank">
+              <gl-link
+                v-track-event="trackClickErrorLinkToSentryOptions(errors.item.externalUrl)"
+                :href="errors.item.externalUrl"
+                class="d-flex text-dark"
+                target="_blank"
+              >
                 <strong class="text-truncate">{{ errors.item.title.trim() }}</strong>
                 <icon name="external-link" class="ml-1 flex-shrink-0" />
               </gl-link>
diff --git a/app/assets/javascripts/error_tracking/utils.js b/app/assets/javascripts/error_tracking/utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..b832b1371b1fa8d48afa255bd30e35fed82e542e
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/utils.js
@@ -0,0 +1,23 @@
+/* eslint-disable @gitlab/i18n/no-non-i18n-strings */
+
+/**
+ * Tracks snowplow event when user clicks View in Sentry btn
+ * @param {String}  externalUrl that will be send as a property for the event
+ */
+export const trackViewInSentryOptions = url => ({
+  category: 'Error Tracking',
+  action: 'click_view_in_sentry',
+  label: 'External Url',
+  property: url,
+});
+
+/**
+ * Tracks snowplow event when User clicks on error link to Sentry
+ * @param {String}  externalUrl that will be send as a property for the event
+ */
+export const trackClickErrorLinkToSentryOptions = url => ({
+  category: 'Error Tracking',
+  action: 'click_error_link_to_sentry',
+  label: 'Error Link',
+  property: url,
+});
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index 830385941d8b9a9862760bff0288df8ed586d5f2..ede74d18ed4c9bbbcb8cde4e58ac1c462be4f82a 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -104,11 +104,11 @@ export default {
       />
       <div
         :class="{ 'd-sm-flex': !group.isChildrenLoading }"
-        class="avatar-container rect-avatar s32 d-none flex-grow-0 flex-shrink-0 "
+        class="avatar-container rect-avatar s40 d-none flex-grow-0 flex-shrink-0 "
       >
         <a :href="group.relativePath" class="no-expand">
-          <img v-if="hasAvatar" :src="group.avatarUrl" class="avatar s32" />
-          <identicon v-else :entity-id="group.id" :entity-name="group.name" size-class="s32" />
+          <img v-if="hasAvatar" :src="group.avatarUrl" class="avatar s40" />
+          <identicon v-else :entity-id="group.id" :entity-name="group.name" size-class="s40" />
         </a>
       </div>
       <div class="group-text-container d-flex flex-fill align-items-center">
diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue
index 9ad9d4455b5d6f49f30af3dc69e9c19fab16cef6..52ca61c06b09d1561165a7c3b06c9498d247e79a 100644
--- a/app/assets/javascripts/ide/components/jobs/stage.vue
+++ b/app/assets/javascripts/ide/components/jobs/stage.vue
@@ -58,6 +58,7 @@ export default {
 <template>
   <div class="ide-stage card prepend-top-default">
     <div
+      ref="cardHeader"
       :class="{
         'border-bottom-0': stage.isCollapsed,
       }"
@@ -79,7 +80,7 @@ export default {
       </div>
       <icon :name="collapseIcon" class="ide-stage-collapse-icon" />
     </div>
-    <div v-show="!stage.isCollapsed" class="card-body">
+    <div v-show="!stage.isCollapsed" ref="jobList" class="card-body">
       <gl-loading-icon v-if="showLoadingIcon" />
       <template v-else>
         <item v-for="job in stage.jobs" :key="job.id" :job="job" @clickViewLog="clickViewLog" />
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 2587b57a8170d8f435811ced5d2f340518a59d37..e84e2782e46fc81375a48683af8d7cf4cb248e9b 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -73,16 +73,15 @@ export default {
       const entry = data.entries[key];
       const foundEntry = state.entries[key];
 
+      // NOTE: We can't clone `entry` in any of the below assignments because
+      // we need `state.entries` and the `entry.tree` to reference the same object.
       if (!foundEntry) {
         Object.assign(state.entries, {
           [key]: entry,
         });
       } else if (foundEntry.deleted) {
         Object.assign(state.entries, {
-          [key]: {
-            ...entry,
-            replaces: true,
-          },
+          [key]: Object.assign(entry, { replaces: true }),
         });
       } else {
         const tree = entry.tree.filter(
diff --git a/app/assets/javascripts/issuable_sidebar/components/sidebar_app.vue b/app/assets/javascripts/issuable_sidebar/components/sidebar_app.vue
new file mode 100644
index 0000000000000000000000000000000000000000..06c50f62aabc6550bf5b3c18840732b5ff96835b
--- /dev/null
+++ b/app/assets/javascripts/issuable_sidebar/components/sidebar_app.vue
@@ -0,0 +1,23 @@
+<script>
+export default {
+  props: {
+    signedIn: {
+      type: Boolean,
+      required: true,
+    },
+    sidebarStatusClass: {
+      type: String,
+      required: false,
+      default: '',
+    },
+  },
+};
+</script>
+
+<template>
+  <aside
+    :class="sidebarStatusClass"
+    class="right-sidebar js-right-sidebar js-issuable-sidebar"
+    aria-live="polite"
+  ></aside>
+</template>
diff --git a/app/assets/javascripts/issuable_sidebar/sidebar_bundle.js b/app/assets/javascripts/issuable_sidebar/sidebar_bundle.js
new file mode 100644
index 0000000000000000000000000000000000000000..c8acafa8cd89fd9f7b5cf701412c95eaa57ce966
--- /dev/null
+++ b/app/assets/javascripts/issuable_sidebar/sidebar_bundle.js
@@ -0,0 +1,27 @@
+import Vue from 'vue';
+
+import SidebarApp from './components/sidebar_app.vue';
+
+export default () => {
+  const el = document.getElementById('js-vue-issuable-sidebar');
+
+  if (!el) {
+    return false;
+  }
+
+  const { sidebarStatusClass } = el.dataset;
+  // An empty string is present when user is signed in.
+  const signedIn = el.dataset.signedIn === '';
+
+  return new Vue({
+    el,
+    components: { SidebarApp },
+    render: createElement =>
+      createElement('sidebar-app', {
+        props: {
+          signedIn,
+          sidebarStatusClass,
+        },
+      }),
+  });
+};
diff --git a/app/assets/javascripts/jobs/components/environments_block.vue b/app/assets/javascripts/jobs/components/environments_block.vue
index 8cda7dac51f3c875ddc17e5952201391889ceab4..163849d3c404b4127a0c8f13a3dedc26bd3cf92b 100644
--- a/app/assets/javascripts/jobs/components/environments_block.vue
+++ b/app/assets/javascripts/jobs/components/environments_block.vue
@@ -19,69 +19,18 @@ export default {
   },
   computed: {
     environment() {
-      let environmentText;
       switch (this.deploymentStatus.status) {
         case 'last':
-          environmentText = sprintf(
-            __('This job is the most recent deployment to %{link}.'),
-            { link: this.environmentLink },
-            false,
-          );
-          break;
+          return this.lastEnvironmentMessage();
         case 'out_of_date':
-          if (this.hasLastDeployment) {
-            environmentText = sprintf(
-              __(
-                'This job is an out-of-date deployment to %{environmentLink}. View the most recent deployment %{deploymentLink}.',
-              ),
-              {
-                environmentLink: this.environmentLink,
-                deploymentLink: this.deploymentLink(`#${this.lastDeployment.iid}`),
-              },
-              false,
-            );
-          } else {
-            environmentText = sprintf(
-              __('This job is an out-of-date deployment to %{environmentLink}.'),
-              { environmentLink: this.environmentLink },
-              false,
-            );
-          }
-
-          break;
+          return this.outOfDateEnvironmentMessage();
         case 'failed':
-          environmentText = sprintf(
-            __('The deployment of this job to %{environmentLink} did not succeed.'),
-            { environmentLink: this.environmentLink },
-            false,
-          );
-          break;
+          return this.failedEnvironmentMessage();
         case 'creating':
-          if (this.hasLastDeployment) {
-            environmentText = sprintf(
-              __(
-                'This job is creating a deployment to %{environmentLink} and will overwrite the %{deploymentLink}.',
-              ),
-              {
-                environmentLink: this.environmentLink,
-                deploymentLink: this.deploymentLink(__('latest deployment')),
-              },
-              false,
-            );
-          } else {
-            environmentText = sprintf(
-              __('This job is creating a deployment to %{environmentLink}.'),
-              { environmentLink: this.environmentLink },
-              false,
-            );
-          }
-          break;
+          return this.creatingEnvironmentMessage();
         default:
-          break;
+          return '';
       }
-      return environmentText && this.hasCluster
-        ? `${environmentText} ${this.clusterText}`
-        : environmentText;
     },
     environmentLink() {
       if (this.hasEnvironment) {
@@ -137,11 +86,6 @@ export default {
         false,
       );
     },
-    clusterText() {
-      return this.hasCluster
-        ? sprintf(__('Cluster %{cluster} was used.'), { cluster: this.clusterNameOrLink }, false)
-        : '';
-    },
   },
   methods: {
     deploymentLink(name) {
@@ -155,6 +99,91 @@ export default {
         false,
       );
     },
+    failedEnvironmentMessage() {
+      const { environmentLink } = this;
+
+      return sprintf(
+        __('The deployment of this job to %{environmentLink} did not succeed.'),
+        { environmentLink },
+        false,
+      );
+    },
+    lastEnvironmentMessage() {
+      const { environmentLink, clusterNameOrLink, hasCluster } = this;
+
+      const message = hasCluster
+        ? __('This job is deployed to %{environmentLink} using cluster %{clusterNameOrLink}.')
+        : __('This job is deployed to %{environmentLink}.');
+
+      return sprintf(message, { environmentLink, clusterNameOrLink }, false);
+    },
+    outOfDateEnvironmentMessage() {
+      const { hasLastDeployment, hasCluster, environmentLink, clusterNameOrLink } = this;
+
+      if (hasLastDeployment) {
+        const message = hasCluster
+          ? __(
+              'This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink}. View the %{deploymentLink}.',
+            )
+          : __(
+              'This job is an out-of-date deployment to %{environmentLink}. View the %{deploymentLink}.',
+            );
+
+        return sprintf(
+          message,
+          {
+            environmentLink,
+            clusterNameOrLink,
+            deploymentLink: this.deploymentLink(__('most recent deployment')),
+          },
+          false,
+        );
+      }
+
+      const message = hasCluster
+        ? __(
+            'This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink}.',
+          )
+        : __('This job is an out-of-date deployment to %{environmentLink}.');
+
+      return sprintf(
+        message,
+        {
+          environmentLink,
+          clusterNameOrLink,
+        },
+        false,
+      );
+    },
+    creatingEnvironmentMessage() {
+      const { hasLastDeployment, hasCluster, environmentLink, clusterNameOrLink } = this;
+
+      if (hasLastDeployment) {
+        const message = hasCluster
+          ? __(
+              'This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink}. This will overwrite the %{deploymentLink}.',
+            )
+          : __(
+              'This job is creating a deployment to %{environmentLink}. This will overwrite the %{deploymentLink}.',
+            );
+
+        return sprintf(
+          message,
+          {
+            environmentLink,
+            clusterNameOrLink,
+            deploymentLink: this.deploymentLink(__('latest deployment')),
+          },
+          false,
+        );
+      }
+
+      return sprintf(
+        __('This job is creating a deployment to %{environmentLink}.'),
+        { environmentLink },
+        false,
+      );
+    },
   },
 };
 </script>
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index b4b124d5db115e942e3fc028001c6f12d06f0a13..859f839741f062d681e8d9215d826c4292e33579 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -130,6 +130,10 @@ export default {
 
       return title;
     },
+
+    shouldRenderHeaderCallout() {
+      return this.shouldRenderCalloutMessage && !this.hasUnmetPrerequisitesFailure;
+    },
   },
   watch: {
     // Once the job log is loaded,
@@ -239,10 +243,9 @@ export default {
             />
           </div>
 
-          <callout
-            v-if="shouldRenderCalloutMessage && !hasUnmetPrerequisitesFailure"
-            :message="job.callout_message"
-          />
+          <callout v-if="shouldRenderHeaderCallout">
+            <div v-html="job.callout_message"></div>
+          </callout>
         </header>
         <!-- EO Header Section -->
 
diff --git a/app/assets/javascripts/jobs/components/log/duration_badge.vue b/app/assets/javascripts/jobs/components/log/duration_badge.vue
index 31a101d2c95074a6d92a8d0d13f298f6cf78b379..8e5dcdcc9021e60d9cc2a938f25c5de5b9cd7311 100644
--- a/app/assets/javascripts/jobs/components/log/duration_badge.vue
+++ b/app/assets/javascripts/jobs/components/log/duration_badge.vue
@@ -9,7 +9,7 @@ export default {
 };
 </script>
 <template>
-  <div class="log-duration-badge rounded align-self-start px-2 ml-2 flex-shrink-0">
+  <div class="log-duration-badge rounded align-self-start px-2 ml-2 flex-shrink-0 ws-normal">
     {{ duration }}
   </div>
 </template>
diff --git a/app/assets/javascripts/jobs/components/log/line.vue b/app/assets/javascripts/jobs/components/log/line.vue
index 9fae541125e7b99301dafc107475e2af818db496..33ee84bd4ee5c6281873749fb5ec484c3a607672 100644
--- a/app/assets/javascripts/jobs/components/log/line.vue
+++ b/app/assets/javascripts/jobs/components/log/line.vue
@@ -21,8 +21,12 @@ export default {
 <template>
   <div class="js-line log-line">
     <line-number :line-number="line.lineNumber" :path="path" />
-    <span v-for="(content, i) in line.content" :key="i" :class="content.style">{{
-      content.text
-    }}</span>
+    <span
+      v-for="(content, i) in line.content"
+      :key="i"
+      :class="content.style"
+      class="ws-pre-wrap"
+      >{{ content.text }}</span
+    >
   </div>
 </template>
diff --git a/app/assets/javascripts/jobs/components/log/line_header.vue b/app/assets/javascripts/jobs/components/log/line_header.vue
index 92cf3b3cf5fc26c524993a543d6e9a31798cfc40..85ccd5996b59cdc4219306a4ac23f475578dd461 100644
--- a/app/assets/javascripts/jobs/components/log/line_header.vue
+++ b/app/assets/javascripts/jobs/components/log/line_header.vue
@@ -43,15 +43,19 @@ export default {
 
 <template>
   <div
-    class="log-line collapsible-line d-flex justify-content-between"
+    class="log-line collapsible-line d-flex justify-content-between ws-normal"
     role="button"
     @click="handleOnClick"
   >
     <icon :name="iconName" class="arrow position-absolute" />
     <line-number :line-number="line.lineNumber" :path="path" />
-    <span v-for="(content, i) in line.content" :key="i" class="line-text" :class="content.style">{{
-      content.text
-    }}</span>
+    <span
+      v-for="(content, i) in line.content"
+      :key="i"
+      class="line-text w-100 ws-pre-wrap"
+      :class="content.style"
+      >{{ content.text }}</span
+    >
     <duration-badge v-if="duration" :duration="duration" />
   </div>
 </template>
diff --git a/app/assets/javascripts/jobs/components/log/line_number.vue b/app/assets/javascripts/jobs/components/log/line_number.vue
index 08c4a7ed3304214323c309e10ad5478e1f999f33..ae96c32874b3c532153e20df69172a1f071cd386 100644
--- a/app/assets/javascripts/jobs/components/log/line_number.vue
+++ b/app/assets/javascripts/jobs/components/log/line_number.vue
@@ -48,7 +48,7 @@ export default {
 <template>
   <gl-link
     :id="lineNumberId"
-    class="d-inline-block text-right line-number"
+    class="d-inline-block text-right line-number flex-shrink-0"
     :href="buildLineNumber"
     >{{ parsedLineNumber }}</gl-link
   >
diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js
index 412ae146ca02313c93c329f5bc5f3d78e39cce36..77c68cac4a673a9f2d48f34f28c23910af5a6811 100644
--- a/app/assets/javascripts/jobs/store/mutations.js
+++ b/app/assets/javascripts/jobs/store/mutations.js
@@ -19,14 +19,14 @@ export default {
     state.isSidebarOpen = true;
   },
 
-  [types.RECEIVE_TRACE_SUCCESS](state, log) {
+  [types.RECEIVE_TRACE_SUCCESS](state, log = {}) {
     if (log.state) {
       state.traceState = log.state;
     }
 
     if (log.append) {
       if (isNewJobLogActive()) {
-        state.trace = updateIncrementalTrace(log.lines, state.trace);
+        state.trace = log.lines ? updateIncrementalTrace(log.lines, state.trace) : state.trace;
       } else {
         state.trace += log.html;
       }
@@ -35,9 +35,9 @@ export default {
       // When the job still does not have a trace
       // the trace response will not have a defined
       // html or size. We keep the old value otherwise these
-      // will be set to `undefined`
+      // will be set to `null`
       if (isNewJobLogActive()) {
-        state.trace = logLinesParser(log.lines) || state.trace;
+        state.trace = log.lines ? logLinesParser(log.lines) : state.trace;
       } else {
         state.trace = log.html || state.trace;
       }
diff --git a/app/assets/javascripts/jobs/store/utils.js b/app/assets/javascripts/jobs/store/utils.js
index 12069e0c123380cff2e481fec806b533b03ceb5f..58e49f54d96ed7178fe00788ea18a46eeb0719a2 100644
--- a/app/assets/javascripts/jobs/store/utils.js
+++ b/app/assets/javascripts/jobs/store/utils.js
@@ -147,13 +147,15 @@ export const findOffsetAndRemove = (newLog = [], oldParsed = []) => {
 
   const firstNew = newLog[0];
 
-  if (last.offset === firstNew.offset || (last.line && last.line.offset === firstNew.offset)) {
-    cloneOldLog.splice(lastIndex);
-  } else if (last.lines && last.lines.length) {
-    const lastNestedIndex = last.lines.length - 1;
-    const lastNested = last.lines[lastNestedIndex];
-    if (lastNested.offset === firstNew.offset) {
-      last.lines.splice(lastNestedIndex);
+  if (last && firstNew) {
+    if (last.offset === firstNew.offset || (last.line && last.line.offset === firstNew.offset)) {
+      cloneOldLog.splice(lastIndex);
+    } else if (last.lines && last.lines.length) {
+      const lastNestedIndex = last.lines.length - 1;
+      const lastNested = last.lines[lastNestedIndex];
+      if (lastNested.offset === firstNew.offset) {
+        last.lines.splice(lastNestedIndex);
+      }
     }
   }
 
@@ -170,7 +172,7 @@ export const findOffsetAndRemove = (newLog = [], oldParsed = []) => {
  * @param array oldLog
  * @param array newLog
  */
-export const updateIncrementalTrace = (newLog, oldParsed = []) => {
+export const updateIncrementalTrace = (newLog = [], oldParsed = []) => {
   const parsedLog = findOffsetAndRemove(newLog, oldParsed);
 
   return logLinesParser(newLog, parsedLog);
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 22b062563b544d1519e3995d86dc4b6df89ee3d5..72de3b5d726238653e4c20c7f4df8504631cde58 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -32,6 +32,7 @@ export default class LabelsSelect {
         $selectbox,
         $sidebarCollapsedValue,
         $value,
+        $dropdownMenu,
         abilityName,
         defaultLabel,
         issueUpdateURL,
@@ -67,6 +68,7 @@ export default class LabelsSelect {
       $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
       $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
       $value = $block.find('.value');
+      $dropdownMenu = $dropdown.parent().find('.dropdown-menu');
       $loading = $block.find('.block-loading').fadeOut();
       fieldName = $dropdown.data('fieldName');
       initialSelected = $selectbox
@@ -454,9 +456,21 @@ export default class LabelsSelect {
             }
 
             $loading.fadeIn();
+            const oldLabels = boardsStore.detail.issue.labels;
 
             boardsStore.detail.issue
               .update($dropdown.attr('data-issue-update'))
+              .then(() => {
+                if (isScopedLabel(label)) {
+                  const prevIds = oldLabels.map(label => label.id);
+                  const newIds = boardsStore.detail.issue.labels.map(label => label.id);
+                  const differentIds = _.difference(prevIds, newIds);
+                  $dropdown.data('marked', newIds);
+                  $dropdownMenu
+                    .find(differentIds.map(id => `[data-label-id="${id}"]`).join(','))
+                    .removeClass('is-active');
+                }
+              })
               .then(fadeOutLoader)
               .catch(fadeOutLoader);
           } else if (handleClick) {
diff --git a/app/assets/javascripts/lib/utils/set.js b/app/assets/javascripts/lib/utils/set.js
new file mode 100644
index 0000000000000000000000000000000000000000..3845d648b61f45f72c49d72880fad343b9111fbd
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/set.js
@@ -0,0 +1,9 @@
+/**
+ * Checks if the first argument is a subset of the second argument.
+ * @param {Set} subset The set to be considered as the subset.
+ * @param {Set} superset The set to be considered as the superset.
+ * @returns {boolean}
+ */
+// eslint-disable-next-line import/prefer-default-export
+export const isSubset = (subset, superset) =>
+  Array.from(subset).every(value => superset.has(value));
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index 76fb35aaa7821a932de66533818b4da6edf00f3e..78fe575717ac45e113434f6f135803cf02fc1813 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -63,6 +63,11 @@ export default {
       required: false,
       default: s__('Metrics|Max'),
     },
+    groupId: {
+      type: String,
+      required: false,
+      default: '',
+    },
   },
   data() {
     return {
@@ -290,6 +295,7 @@ export default {
       :is="glChartComponent"
       ref="chart"
       v-bind="$attrs"
+      :group-id="groupId"
       :data="chartData"
       :option="chartOptions"
       :format-tooltip-text="formatTooltipText"
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 9ecb9324f8ce5e4364286cbdccbe25fe5f2ca1e2..b4ea415bb5174333b53f67203a663fd35508f8bc 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -12,16 +12,19 @@ import {
   GlTooltipDirective,
 } from '@gitlab/ui';
 import { __, s__ } from '~/locale';
+import createFlash from '~/flash';
 import Icon from '~/vue_shared/components/icon.vue';
-import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
+import { getParameterValues, mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
 import invalidUrl from '~/lib/utils/invalid_url';
 import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
+import DateTimePicker from './date_time_picker/date_time_picker.vue';
 import MonitorTimeSeriesChart from './charts/time_series.vue';
 import MonitorSingleStatChart from './charts/single_stat.vue';
 import GraphGroup from './graph_group.vue';
 import EmptyState from './empty_state.vue';
-import { sidebarAnimationDuration, timeWindows } from '../constants';
-import { getTimeDiff, getTimeWindow } from '../utils';
+import { sidebarAnimationDuration } from '../constants';
+import TrackEventDirective from '~/vue_shared/directives/track_event';
+import { getTimeDiff, isValidDate, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
 
 let sidebarMutationObserver;
 
@@ -39,10 +42,12 @@ export default {
     GlDropdownItem,
     GlFormGroup,
     GlModal,
+    DateTimePicker,
   },
   directives: {
     GlModal: GlModalDirective,
     GlTooltip: GlTooltipDirective,
+    TrackEvent: TrackEventDirective,
   },
   props: {
     externalDashboardUrl: {
@@ -163,10 +168,8 @@ export default {
     return {
       state: 'gettingStarted',
       elWidth: 0,
-      selectedTimeWindow: '',
-      selectedTimeWindowKey: '',
       formIsValid: null,
-      timeWindows: {},
+      selectedTimeWindow: {},
       isRearrangingPanels: false,
     };
   },
@@ -229,11 +232,13 @@ export default {
         end,
       };
 
-      this.timeWindows = timeWindows;
-      this.selectedTimeWindowKey = getTimeWindow(range);
-      this.selectedTimeWindow = this.timeWindows[this.selectedTimeWindowKey];
+      this.selectedTimeWindow = range;
 
-      this.fetchData(range);
+      if (!isValidDate(start) || !isValidDate(end)) {
+        this.showInvalidDateError();
+      } else {
+        this.fetchData(range);
+      }
 
       sidebarMutationObserver = new MutationObserver(this.onSidebarMutation);
       sidebarMutationObserver.observe(document.querySelector('.layout-page'), {
@@ -290,6 +295,9 @@ export default {
       // See https://gitlab.com/gitlab-org/gitlab/issues/27835
       metrics.splice(graphIndex, 1);
     },
+    showInvalidDateError() {
+      createFlash(s__('Metrics|Link contains an invalid time window.'));
+    },
     generateLink(group, title, yLabel) {
       const dashboard = this.currentDashboard || this.firstDashboard.path;
       const params = _.pick({ dashboard, group, title, y_label: yLabel }, value => value != null);
@@ -312,16 +320,14 @@ export default {
     submitCustomMetricsForm() {
       this.$refs.customMetricsForm.submit();
     },
-    activeTimeWindow(key) {
-      return this.timeWindows[key] === this.selectedTimeWindow;
-    },
-    setTimeWindowParameter(key) {
-      const { start, end } = getTimeDiff(key);
-      return `?start=${encodeURIComponent(start)}&end=${encodeURIComponent(end)}`;
-    },
     groupHasData(group) {
       return this.chartsWithData(group.metrics).length > 0;
     },
+    onDateTimePickerApply(timeWindowUrlParams) {
+      return redirectTo(mergeUrlParams(timeWindowUrlParams, window.location.href));
+    },
+    downloadCSVOptions,
+    generateLinkToChartOptions,
   },
   addMetric: {
     title: s__('Metrics|Add metric'),
@@ -332,14 +338,14 @@ export default {
 
 <template>
   <div class="prometheus-graphs">
-    <div class="gl-p-3 pb-0 border-bottom bg-gray-light">
+    <div class="prometheus-graphs-header gl-p-3 pb-0 border-bottom bg-gray-light">
       <div class="row">
         <template v-if="environmentsEndpoint">
           <gl-form-group
             :label="__('Dashboard')"
             label-size="sm"
             label-for="monitor-dashboards-dropdown"
-            class="col-sm-12 col-md-4 col-lg-2"
+            class="col-sm-12 col-md-6 col-lg-2"
           >
             <gl-dropdown
               id="monitor-dashboards-dropdown"
@@ -362,7 +368,7 @@ export default {
             :label="s__('Metrics|Environment')"
             label-size="sm"
             label-for="monitor-environments-dropdown"
-            class="col-sm-6 col-md-4 col-lg-2"
+            class="col-sm-6 col-md-6 col-lg-2"
           >
             <gl-dropdown
               id="monitor-environments-dropdown"
@@ -387,36 +393,24 @@ export default {
             :label="s__('Metrics|Show last')"
             label-size="sm"
             label-for="monitor-time-window-dropdown"
-            class="col-sm-6 col-md-4 col-lg-2"
+            class="col-sm-6 col-md-6 col-lg-4"
           >
-            <gl-dropdown
-              id="monitor-time-window-dropdown"
-              class="mb-0 d-flex js-time-window-dropdown"
-              toggle-class="dropdown-menu-toggle"
-              :text="selectedTimeWindow"
-            >
-              <gl-dropdown-item
-                v-for="(value, key) in timeWindows"
-                :key="key"
-                :active="activeTimeWindow(key)"
-                :href="setTimeWindowParameter(key)"
-                active-class="active"
-                >{{ value }}</gl-dropdown-item
-              >
-            </gl-dropdown>
+            <date-time-picker
+              :selected-time-window="selectedTimeWindow"
+              @onApply="onDateTimePickerApply"
+            />
           </gl-form-group>
         </template>
 
         <gl-form-group
           v-if="addingMetricsAvailable || showRearrangePanelsBtn || externalDashboardUrl.length"
           label-for="prometheus-graphs-dropdown-buttons"
-          class="dropdown-buttons col-lg d-lg-flex align-items-end"
+          class="dropdown-buttons col-md d-md-flex col-lg d-lg-flex align-items-end"
         >
           <div id="prometheus-graphs-dropdown-buttons">
             <gl-button
               v-if="showRearrangePanelsBtn"
               :pressed="isRearrangingPanels"
-              new-style
               variant="default"
               class="mr-2 mt-1 js-rearrange-button"
               @click="toggleRearrangingPanels"
@@ -426,7 +420,6 @@ export default {
             <gl-button
               v-if="addingMetricsAvailable"
               v-gl-modal="$options.addMetric.modalId"
-              new-style
               variant="outline-success"
               class="mr-2 mt-1 js-add-metric-button"
             >
@@ -554,10 +547,19 @@ export default {
                 <template slot="button-content">
                   <icon name="ellipsis_v" class="text-secondary" />
                 </template>
-                <gl-dropdown-item :href="downloadCsv(graphData)" download="chart_metrics.csv">
+                <gl-dropdown-item
+                  v-track-event="downloadCSVOptions(graphData.title)"
+                  :href="downloadCsv(graphData)"
+                  download="chart_metrics.csv"
+                >
                   {{ __('Download CSV') }}
                 </gl-dropdown-item>
                 <gl-dropdown-item
+                  v-track-event="
+                    generateLinkToChartOptions(
+                      generateLink(groupData.group, graphData.title, graphData.y_label),
+                    )
+                  "
                   class="js-chart-link"
                   :data-clipboard-text="
                     generateLink(groupData.group, graphData.title, graphData.y_label)
diff --git a/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker.vue b/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker.vue
new file mode 100644
index 0000000000000000000000000000000000000000..4616a767295aaffb6bc48c80a320ad98579d96ca
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker.vue
@@ -0,0 +1,151 @@
+<script>
+import { GlButton, GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+import DateTimePickerInput from './date_time_picker_input.vue';
+import {
+  getTimeDiff,
+  getTimeWindow,
+  stringToISODate,
+  ISODateToString,
+  truncateZerosInDateTime,
+  isDateTimePickerInputValid,
+} from '~/monitoring/utils';
+import { timeWindows } from '~/monitoring/constants';
+
+export default {
+  components: {
+    Icon,
+    DateTimePickerInput,
+    GlFormGroup,
+    GlButton,
+    GlDropdown,
+    GlDropdownItem,
+  },
+  props: {
+    timeWindows: {
+      type: Object,
+      required: false,
+      default: () => timeWindows,
+    },
+    selectedTimeWindow: {
+      type: Object,
+      required: false,
+      default: () => {},
+    },
+  },
+  data() {
+    return {
+      selectedTimeWindowText: '',
+      customTime: {
+        from: null,
+        to: null,
+      },
+    };
+  },
+  computed: {
+    applyEnabled() {
+      return Boolean(this.inputState.from && this.inputState.to);
+    },
+    inputState() {
+      const { from, to } = this.customTime;
+      return {
+        from: from && isDateTimePickerInputValid(from),
+        to: to && isDateTimePickerInputValid(to),
+      };
+    },
+  },
+  mounted() {
+    const range = getTimeWindow(this.selectedTimeWindow);
+    if (range) {
+      this.selectedTimeWindowText = this.timeWindows[range];
+    } else {
+      this.customTime = {
+        from: truncateZerosInDateTime(ISODateToString(this.selectedTimeWindow.start)),
+        to: truncateZerosInDateTime(ISODateToString(this.selectedTimeWindow.end)),
+      };
+      this.selectedTimeWindowText = sprintf(s__('%{from} to %{to}'), this.customTime);
+    }
+  },
+  methods: {
+    activeTimeWindow(key) {
+      return this.timeWindows[key] === this.selectedTimeWindowText;
+    },
+    setCustomTimeWindowParameter() {
+      this.$emit('onApply', {
+        start: stringToISODate(this.customTime.from),
+        end: stringToISODate(this.customTime.to),
+      });
+    },
+    setTimeWindowParameter(key) {
+      const { start, end } = getTimeDiff(key);
+      this.$emit('onApply', {
+        start,
+        end,
+      });
+    },
+    closeDropdown() {
+      this.$refs.dropdown.hide();
+    },
+  },
+};
+</script>
+<template>
+  <gl-dropdown
+    ref="dropdown"
+    :text="selectedTimeWindowText"
+    menu-class="time-window-dropdown-menu"
+    class="js-time-window-dropdown"
+  >
+    <div class="d-flex justify-content-between time-window-dropdown-menu-container">
+      <gl-form-group
+        :label="__('Custom range')"
+        label-for="custom-from-time"
+        class="custom-time-range-form-group col-md-7 p-0 m-0"
+      >
+        <date-time-picker-input
+          id="custom-time-from"
+          v-model="customTime.from"
+          :label="__('From')"
+          :state="inputState.from"
+        />
+        <date-time-picker-input
+          id="custom-time-to"
+          v-model="customTime.to"
+          :label="__('To')"
+          :state="inputState.to"
+        />
+        <gl-form-group>
+          <gl-button @click="closeDropdown">{{ __('Cancel') }}</gl-button>
+          <gl-button
+            variant="success"
+            :disabled="!applyEnabled"
+            @click="setCustomTimeWindowParameter"
+            >{{ __('Apply') }}</gl-button
+          >
+        </gl-form-group>
+      </gl-form-group>
+      <gl-form-group
+        :label="__('Quick range')"
+        label-for="group-id-dropdown"
+        label-align="center"
+        class="col-md-4 p-0 m-0"
+      >
+        <gl-dropdown-item
+          v-for="(value, key) in timeWindows"
+          :key="key"
+          :active="activeTimeWindow(key)"
+          active-class="active"
+          @click="setTimeWindowParameter(key)"
+        >
+          <icon
+            name="mobile-issue-close"
+            class="align-bottom"
+            :class="{ invisible: !activeTimeWindow(key) }"
+          />
+          {{ value }}
+        </gl-dropdown-item>
+      </gl-form-group>
+    </div>
+  </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker_input.vue b/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker_input.vue
new file mode 100644
index 0000000000000000000000000000000000000000..0388a6190d915064172093d6154cc4fec3c0cd6a
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker_input.vue
@@ -0,0 +1,77 @@
+<script>
+import _ from 'underscore';
+import { s__, sprintf } from '~/locale';
+import { GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { dateFormats } from '~/monitoring/constants';
+
+const inputGroupText = {
+  invalidFeedback: sprintf(s__('Format: %{dateFormat}'), {
+    dateFormat: dateFormats.dateTimePicker.format,
+  }),
+  placeholder: dateFormats.dateTimePicker.format,
+};
+
+export default {
+  components: {
+    GlFormGroup,
+    GlFormInput,
+  },
+  props: {
+    state: {
+      default: null,
+      required: true,
+      validator: prop => typeof prop === 'boolean' || prop === null,
+    },
+    value: {
+      default: null,
+      required: false,
+      validator: prop => typeof prop === 'string' || prop === null,
+    },
+    label: {
+      type: String,
+      default: '',
+      required: true,
+    },
+    id: {
+      type: String,
+      required: false,
+      default: () => _.uniqueId('dateTimePicker_'),
+    },
+  },
+  data() {
+    return {
+      inputGroupText,
+    };
+  },
+  computed: {
+    invalidFeedback() {
+      return this.state ? '' : this.inputGroupText.invalidFeedback;
+    },
+    inputState() {
+      // When the state is valid we want to show no
+      // green outline. Hence passing null and not true.
+      if (this.state === true) {
+        return null;
+      }
+      return this.state;
+    },
+  },
+  methods: {
+    onInputBlur(e) {
+      this.$emit('input', e.target.value.trim() || null);
+    },
+  },
+};
+</script>
+
+<template>
+  <gl-form-group :label="label" label-size="sm" :label-for="id" :invalid-feedback="invalidFeedback">
+    <gl-form-input
+      :id="id"
+      :value="value"
+      :state="inputState"
+      :placeholder="inputGroupText.placeholder"
+      @blur="onInputBlur"
+    />
+  </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/embed.vue b/app/assets/javascripts/monitoring/components/embed.vue
index da1e88071ab7ee9db75fedd4a6ee28a498aacff4..7857aaa6ecc02a7f31cc0657f4c640542e40f40d 100644
--- a/app/assets/javascripts/monitoring/components/embed.vue
+++ b/app/assets/javascripts/monitoring/components/embed.vue
@@ -98,7 +98,7 @@ export default {
         class="w-100"
         :graph-data="graphData"
         :container-width="elWidth"
-        group-id="monitor-area-chart"
+        :group-id="dashboardUrl"
         :project-path="null"
         :show-border="true"
         :single-embed="isSingleChart"
diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue
index af0d8335c43ee0b517d2af0ae648b1c0a8527f9b..1a14d06f4c87b5092294db897fd093a1932020f2 100644
--- a/app/assets/javascripts/monitoring/components/panel_type.vue
+++ b/app/assets/javascripts/monitoring/components/panel_type.vue
@@ -13,6 +13,8 @@ import Icon from '~/vue_shared/components/icon.vue';
 import MonitorTimeSeriesChart from './charts/time_series.vue';
 import MonitorSingleStatChart from './charts/single_stat.vue';
 import MonitorEmptyChart from './charts/empty_chart.vue';
+import TrackEventDirective from '~/vue_shared/directives/track_event';
+import { downloadCSVOptions, generateLinkToChartOptions } from '../utils';
 
 export default {
   components: {
@@ -27,6 +29,7 @@ export default {
   directives: {
     GlModal: GlModalDirective,
     GlTooltip: GlTooltipDirective,
+    TrackEvent: TrackEventDirective,
   },
   props: {
     clipboardText: {
@@ -84,6 +87,8 @@ export default {
     showToast() {
       this.$toast.show(__('Link copied'));
     },
+    downloadCSVOptions,
+    generateLinkToChartOptions,
   },
 };
 </script>
@@ -121,13 +126,18 @@ export default {
         <template slot="button-content">
           <icon name="ellipsis_v" class="text-secondary" />
         </template>
-        <gl-dropdown-item :href="downloadCsv" download="chart_metrics.csv">
+        <gl-dropdown-item
+          v-track-event="downloadCSVOptions(graphData.title)"
+          :href="downloadCsv"
+          download="chart_metrics.csv"
+        >
           {{ __('Download CSV') }}
         </gl-dropdown-item>
         <gl-dropdown-item
+          v-track-event="generateLinkToChartOptions(clipboardText)"
           class="js-chart-link"
           :data-clipboard-text="clipboardText"
-          @click="showToast"
+          @click="showToast(clipboardText)"
         >
           {{ __('Generate link to chart') }}
         </gl-dropdown-item>
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index 13aba3d9f4472b2e44c14d7b15ed86501da55476..2836fe4fc26ffd10f71881d8a317747552b5ad77 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -3,6 +3,11 @@ import { __ } from '~/locale';
 export const sidebarAnimationDuration = 300; // milliseconds.
 
 export const chartHeight = 300;
+/**
+ * Valid strings for this regex are
+ * 2019-10-01 and 2019-10-01 01:02:03
+ */
+export const dateTimePickerRegex = /^(\d{4})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])(?: (0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]))?$/;
 
 export const graphTypes = {
   deploymentData: 'scatter',
@@ -28,6 +33,11 @@ export const timeWindows = {
 export const dateFormats = {
   timeOfDay: 'h:MM TT',
   default: 'dd mmm yyyy, h:MMTT',
+  dateTimePicker: {
+    format: 'yyyy-mm-dd hh:mm:ss',
+    ISODate: "yyyy-mm-dd'T'HH:MM:ss'Z'",
+    stringDate: 'yyyy-mm-dd HH:MM:ss',
+  },
 };
 
 export const secondsIn = {
diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js
index a134b4e3c334f14d89da3ae71e9447299e8bb44f..00f188c1d5a6f261c14f10ea532afbddd0e1c3c7 100644
--- a/app/assets/javascripts/monitoring/utils.js
+++ b/app/assets/javascripts/monitoring/utils.js
@@ -1,4 +1,5 @@
-import { secondsIn, timeWindowsKeyNames } from './constants';
+import dateformat from 'dateformat';
+import { secondsIn, dateTimePickerRegex, dateFormats } from './constants';
 
 const secondsToMilliseconds = seconds => seconds * 1000;
 
@@ -19,7 +20,49 @@ export const getTimeWindow = ({ start, end }) =>
       return timeRange;
     }
     return acc;
-  }, timeWindowsKeyNames.eightHours);
+  }, null);
+
+export const isDateTimePickerInputValid = val => dateTimePickerRegex.test(val);
+
+export const truncateZerosInDateTime = datetime => datetime.replace(' 00:00:00', '');
+
+/**
+ * The URL params start and end need to be validated
+ * before passing them down to other components.
+ *
+ * @param {string} dateString
+ */
+export const isValidDate = dateString => {
+  try {
+    // dateformat throws error that can be caught.
+    // This is better than using `new Date()`
+    if (dateString && dateString.trim()) {
+      dateformat(dateString, 'isoDateTime');
+      return true;
+    }
+    return false;
+  } catch (e) {
+    return false;
+  }
+};
+
+/**
+ * Convert the input in Time picker component to ISO date.
+ *
+ * @param {string} val
+ * @returns {string}
+ */
+export const stringToISODate = val =>
+  dateformat(new Date(val.replace(/-/g, '/')), dateFormats.dateTimePicker.ISODate, true);
+
+/**
+ * Convert the ISO date received from the URL to string
+ * for the Time picker component.
+ *
+ * @param {Date} date
+ * @returns {string}
+ */
+export const ISODateToString = date => dateformat(date, dateFormats.dateTimePicker.stringDate);
 
 /**
  * This method is used to validate if the graph data format for a chart component
@@ -45,4 +88,47 @@ export const graphDataValidatorForValues = (isValues, graphData) => {
   );
 };
 
+/* eslint-disable @gitlab/i18n/no-non-i18n-strings */
+/**
+ * Checks that element that triggered event is located on cluster health check dashboard
+ * @param {HTMLElement}  element to check against
+ * @returns {boolean}
+ */
+const isClusterHealthBoard = () => (document.body.dataset.page || '').includes(':clusters:show');
+
+/**
+ * Tracks snowplow event when user generates link to metric chart
+ * @param {String}  chart link that will be sent as a property for the event
+ * @return {Object} config object for event tracking
+ */
+export const generateLinkToChartOptions = chartLink => {
+  const isCLusterHealthBoard = isClusterHealthBoard();
+
+  const category = isCLusterHealthBoard
+    ? 'Cluster Monitoring'
+    : 'Incident Management::Embedded metrics';
+  const action = isCLusterHealthBoard
+    ? 'generate_link_to_cluster_metric_chart'
+    : 'generate_link_to_metrics_chart';
+
+  return { category, action, label: 'Chart link', property: chartLink };
+};
+
+/**
+ * Tracks snowplow event when user downloads CSV of cluster metric
+ * @param {String}  chart title that will be sent as a property for the event
+ */
+export const downloadCSVOptions = title => {
+  const isCLusterHealthBoard = isClusterHealthBoard();
+
+  const category = isCLusterHealthBoard
+    ? 'Cluster Monitoring'
+    : 'Incident Management::Embedded metrics';
+  const action = isCLusterHealthBoard
+    ? 'download_csv_of_cluster_metric_chart'
+    : 'download_csv_of_metrics_dashboard_chart';
+
+  return { category, action, label: 'Chart title', property: title };
+};
+
 export default {};
diff --git a/app/assets/javascripts/pages/groups/registry/repositories/index.js b/app/assets/javascripts/pages/groups/registry/repositories/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..b663defad0e7db9119cb21e51b16103b79091cf2
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/registry/repositories/index.js
@@ -0,0 +1,3 @@
+import initRegistryImages from '~/registry';
+
+document.addEventListener('DOMContentLoaded', initRegistryImages);
diff --git a/app/assets/javascripts/pages/projects/clusters/new/index.js b/app/assets/javascripts/pages/projects/clusters/new/index.js
index 55aa29c97970ee7e797fa40769900691f311dd3d..14d5ab215550bc18d0f018700ce95ef8ccdc3c9a 100644
--- a/app/assets/javascripts/pages/projects/clusters/new/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/new/index.js
@@ -1,7 +1,13 @@
 document.addEventListener('DOMContentLoaded', () => {
   if (gon.features.createEksClusters) {
     import(/* webpackChunkName: 'eks_cluster' */ '~/create_cluster/eks_cluster')
-      .then(({ default: initCreateEKSCluster }) => initCreateEKSCluster())
+      .then(({ default: initCreateEKSCluster }) => {
+        const el = document.querySelector('.js-create-eks-cluster-form-container');
+
+        if (el) {
+          initCreateEKSCluster(el);
+        }
+      })
       .catch(() => {});
   }
 });
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
index 0447d1f79fbb0a7783b2210159d23e3ef307d9b3..28a136a5fa58aba8973a4656be2c58e94bd7dea2 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -5,6 +5,7 @@ import ZenMode from '~/zen_mode';
 import '~/notes/index';
 import initIssueableApp from '~/issue_show';
 import initRelatedMergeRequestsApp from '~/related_merge_requests';
+import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle';
 
 export default function() {
   initIssueableApp();
@@ -12,5 +13,9 @@ export default function() {
   new Issue(); // eslint-disable-line no-new
   new ShortcutsIssuable(); // eslint-disable-line no-new
   new ZenMode(); // eslint-disable-line no-new
-  initIssuableSidebar();
+  if (gon.features && gon.features.vueIssuableSidebar) {
+    initVueIssuableSidebarApp();
+  } else {
+    initIssuableSidebar();
+  }
 }
diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js
index 7968dfd7a123985aac7bef21e4ee6a20216368a4..ce74a6de11f6b4a802ecc7919e918cb663d90456 100644
--- a/app/assets/javascripts/pages/projects/issues/show/index.js
+++ b/app/assets/javascripts/pages/projects/issues/show/index.js
@@ -3,5 +3,7 @@ import initShow from '../show';
 
 document.addEventListener('DOMContentLoaded', () => {
   initShow();
-  initSidebarBundle();
+  if (gon.features && !gon.features.vueIssuableSidebar) {
+    initSidebarBundle();
+  }
 });
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index 7bfb83a2204eefbca1ba19af8b098fd29a1a536d..fa1de1f13cb630f9a3e3b0986a294212951395a7 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -4,11 +4,16 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
 import { handleLocationHash } from '~/lib/utils/common_utils';
 import howToMerge from '~/how_to_merge';
 import initPipelines from '~/commit/pipelines/pipelines_bundle';
+import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle';
 import initWidget from '../../../vue_merge_request_widget';
 
 export default function() {
   new ZenMode(); // eslint-disable-line no-new
-  initIssuableSidebar();
+  if (gon.features && gon.features.vueIssuableSidebar) {
+    initVueIssuableSidebarApp();
+  } else {
+    initIssuableSidebar();
+  }
   initPipelines();
   new ShortcutsIssuable(true); // eslint-disable-line no-new
   handleLocationHash();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
index f61f4db78d58365d2b4eca13ce1bd688faea0e67..ddc648702f16fc1dba18424363ddc3ca8d06e805 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
@@ -4,6 +4,8 @@ import initShow from '../init_merge_request_show';
 
 document.addEventListener('DOMContentLoaded', () => {
   initShow();
-  initSidebarBundle();
+  if (gon.features && !gon.features.vueIssuableSidebar) {
+    initSidebarBundle();
+  }
   initMrNotes();
 });
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index b99408e3609b4c81872af908e5141e8ce9cc634d..435e87058039adc15f9179a23728ce7b8180c5bc 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -1,9 +1,9 @@
-/* eslint-disable func-names, no-var, no-return-assign, vars-on-top */
+/* eslint-disable func-names, no-var, no-return-assign */
 
 import $ from 'jquery';
 import Cookies from 'js-cookie';
 import { __ } from '~/locale';
-import { visitUrl, mergeUrlParams } from '~/lib/utils/url_utility';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
 import { serializeForm } from '~/lib/utils/forms';
 import axios from '~/lib/utils/axios_utils';
 import flash from '~/flash';
@@ -105,6 +105,10 @@ export default class Project {
       var selected = $dropdown.data('selected');
       var fieldName = $dropdown.data('fieldName');
       var shouldVisit = Boolean($dropdown.data('visit'));
+      var $form = $dropdown.closest('form');
+      var action = $form.attr('action');
+      var linkTarget = mergeUrlParams(serializeForm($form[0]), action);
+
       return $dropdown.glDropdown({
         data(term, callback) {
           axios
@@ -126,21 +130,18 @@ export default class Project {
         renderRow(ref) {
           var li = refListItem.cloneNode(false);
 
-          if (ref.header != null) {
-            li.className = 'dropdown-header';
-            li.textContent = ref.header;
-          } else {
-            var link = refLink.cloneNode(false);
-
-            if (ref === selected) {
-              link.className = 'is-active';
-            }
-
-            link.textContent = ref;
-            link.dataset.ref = ref;
+          var link = refLink.cloneNode(false);
 
-            li.appendChild(link);
+          if (ref === selected) {
+            link.className = 'is-active';
           }
+          link.textContent = ref;
+          link.dataset.ref = ref;
+          if (ref.length > 0 && shouldVisit) {
+            link.href = mergeUrlParams({ [fieldName]: ref }, linkTarget);
+          }
+
+          li.appendChild(link);
 
           return li;
         },
@@ -152,15 +153,11 @@ export default class Project {
         },
         clicked(options) {
           const { e } = options;
-          e.preventDefault();
-          if ($(`input[name="${fieldName}"]`).length) {
-            var $form = $dropdown.closest('form');
-            var action = $form.attr('action');
-
-            if (shouldVisit) {
-              visitUrl(mergeUrlParams(serializeForm($form[0]), action));
-            }
+          if (!shouldVisit) {
+            e.preventDefault();
           }
+          /* The actual process is removed since `link.href` in `RenderRow` contains the full target.
+           * It makes the visitable link can be visited when opening on a new tab of browser */
         },
       });
     });
diff --git a/app/assets/javascripts/pages/projects/releases/edit/index.js b/app/assets/javascripts/pages/projects/releases/edit/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..98ec196fc373ec0d0532830571ce01c35b489fc0
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/releases/edit/index.js
@@ -0,0 +1,7 @@
+import ZenMode from '~/zen_mode';
+import initEditRelease from '~/releases/detail';
+
+document.addEventListener('DOMContentLoaded', () => {
+  new ZenMode(); // eslint-disable-line no-new
+  initEditRelease();
+});
diff --git a/app/assets/javascripts/pages/registrations/welcome/index.js b/app/assets/javascripts/pages/registrations/welcome/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..2d555fa7977bfc234dfe36cca711326738296a72
--- /dev/null
+++ b/app/assets/javascripts/pages/registrations/welcome/index.js
@@ -0,0 +1,7 @@
+import LengthValidator from '~/pages/sessions/new/length_validator';
+import NoEmojiValidator from '~/emoji/no_emoji_validator';
+
+document.addEventListener('DOMContentLoaded', () => {
+  new LengthValidator(); // eslint-disable-line no-new
+  new NoEmojiValidator(); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue
index 7ae06af02cfc4a0500874f2fcf84a94bfc2aca36..11b2c3b7016f352b7ac1cf4a4c4b85e07f466a08 100644
--- a/app/assets/javascripts/registry/components/app.vue
+++ b/app/assets/javascripts/registry/components/app.vue
@@ -2,28 +2,34 @@
 import { mapGetters, mapActions } from 'vuex';
 import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
 import store from '../stores';
-import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
 import CollapsibleContainer from './collapsible_container.vue';
+import ProjectEmptyState from './project_empty_state.vue';
+import GroupEmptyState from './group_empty_state.vue';
 import { s__, sprintf } from '../../locale';
 
 export default {
   name: 'RegistryListApp',
   components: {
-    clipboardButton,
     CollapsibleContainer,
     GlEmptyState,
     GlLoadingIcon,
+    ProjectEmptyState,
+    GroupEmptyState,
   },
   props: {
-    endpoint: {
-      type: String,
-      required: true,
-    },
     characterError: {
       type: Boolean,
       required: false,
       default: false,
     },
+    containersErrorImage: {
+      type: String,
+      required: true,
+    },
+    endpoint: {
+      type: String,
+      required: true,
+    },
     helpPagePath: {
       type: String,
       required: true,
@@ -32,14 +38,30 @@ export default {
       type: String,
       required: true,
     },
-    containersErrorImage: {
+    personalAccessTokensHelpLink: {
       type: String,
-      required: true,
+      required: false,
+      default: null,
+    },
+    registryHostUrlWithPort: {
+      type: String,
+      required: false,
+      default: null,
     },
     repositoryUrl: {
       type: String,
       required: true,
     },
+    isGroupPage: {
+      type: Boolean,
+      default: false,
+      required: false,
+    },
+    twoFactorAuthHelpLink: {
+      type: String,
+      required: false,
+      default: null,
+    },
   },
   store,
   computed: {
@@ -79,17 +101,10 @@ export default {
         false,
       );
     },
-    dockerBuildCommand() {
-      // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
-      return `docker build -t ${this.repositoryUrl} .`;
-    },
-    dockerPushCommand() {
-      // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
-      return `docker push ${this.repositoryUrl}`;
-    },
   },
   created() {
     this.setMainEndpoint(this.endpoint);
+    this.setIsDeleteDisabled(this.isGroupPage);
   },
   mounted() {
     if (!this.characterError) {
@@ -97,7 +112,7 @@ export default {
     }
   },
   methods: {
-    ...mapActions(['setMainEndpoint', 'fetchRepos']),
+    ...mapActions(['setMainEndpoint', 'fetchRepos', 'setIsDeleteDisabled']),
   },
 };
 </script>
@@ -120,46 +135,19 @@ export default {
       <p v-html="introText"></p>
       <collapsible-container v-for="item in repos" :key="item.id" :repo="item" />
     </div>
-
-    <gl-empty-state
-      v-else
-      :title="s__('ContainerRegistry|There are no container images stored for this project')"
-      :svg-path="noContainersImage"
-      class="container-message"
-    >
-      <template #description>
-        <p class="js-no-container-images-text" v-html="noContainerImagesText"></p>
-        <h5>{{ s__('ContainerRegistry|Quick Start') }}</h5>
-        <p>
-          {{
-            s__(
-              'ContainerRegistry|You can add an image to this registry with the following commands:',
-            )
-          }}
-        </p>
-
-        <div class="input-group append-bottom-10">
-          <input :value="dockerBuildCommand" type="text" class="form-control monospace" readonly />
-          <span class="input-group-append">
-            <clipboard-button
-              :text="dockerBuildCommand"
-              :title="s__('ContainerRegistry|Copy build command')"
-              class="input-group-text"
-            />
-          </span>
-        </div>
-
-        <div class="input-group">
-          <input :value="dockerPushCommand" type="text" class="form-control monospace" readonly />
-          <span class="input-group-append">
-            <clipboard-button
-              :text="dockerPushCommand"
-              :title="s__('ContainerRegistry|Copy push command')"
-              class="input-group-text"
-            />
-          </span>
-        </div>
-      </template>
-    </gl-empty-state>
+    <project-empty-state
+      v-else-if="!isGroupPage"
+      :no-containers-image="noContainersImage"
+      :help-page-path="helpPagePath"
+      :repository-url="repositoryUrl"
+      :two-factor-auth-help-link="twoFactorAuthHelpLink"
+      :personal-access-tokens-help-link="personalAccessTokensHelpLink"
+      :registry-host-url-with-port="registryHostUrlWithPort"
+    />
+    <group-empty-state
+      v-else-if="isGroupPage"
+      :no-containers-image="noContainersImage"
+      :help-page-path="helpPagePath"
+    />
   </div>
 </template>
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
index 3e31d24088e2ac29a72cae219ad007d86662799b..95f8270b5d0d2689bfc29218b1f1544f0c7d7b9c 100644
--- a/app/assets/javascripts/registry/components/collapsible_container.vue
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -1,6 +1,13 @@
 <script>
-import { mapActions } from 'vuex';
-import { GlLoadingIcon, GlButton, GlTooltipDirective, GlModal, GlModalDirective } from '@gitlab/ui';
+import { mapActions, mapGetters } from 'vuex';
+import {
+  GlLoadingIcon,
+  GlButton,
+  GlTooltipDirective,
+  GlModal,
+  GlModalDirective,
+  GlEmptyState,
+} from '@gitlab/ui';
 import createFlash from '../../flash';
 import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
 import Icon from '../../vue_shared/components/icon.vue';
@@ -17,6 +24,7 @@ export default {
     GlButton,
     Icon,
     GlModal,
+    GlEmptyState,
   },
   directives: {
     GlTooltip: GlTooltipDirective,
@@ -35,9 +43,13 @@ export default {
     };
   },
   computed: {
+    ...mapGetters(['isDeleteDisabled']),
     iconName() {
       return this.isOpen ? 'angle-up' : 'angle-right';
     },
+    canDeleteRepo() {
+      return this.repo.canDelete && !this.isDeleteDisabled;
+    },
   },
   methods: {
     ...mapActions(['fetchRepos', 'fetchList', 'deleteItem']),
@@ -80,7 +92,7 @@ export default {
 
       <div class="controls d-none d-sm-block float-right">
         <gl-button
-          v-if="repo.canDelete"
+          v-if="canDeleteRepo"
           v-gl-tooltip
           v-gl-modal="modalId"
           :title="s__('ContainerRegistry|Remove repository')"
@@ -98,11 +110,19 @@ export default {
     <gl-loading-icon v-if="repo.isLoading" size="md" class="append-bottom-20" />
 
     <div v-else-if="!repo.isLoading && isOpen" class="container-image-tags">
-      <table-registry v-if="repo.list.length" :repo="repo" />
-
-      <div v-else class="nothing-here-block">
-        {{ s__('ContainerRegistry|No tags in Container Registry for this container image.') }}
-      </div>
+      <table-registry v-if="repo.list.length" :repo="repo" :can-delete-repo="canDeleteRepo" />
+      <gl-empty-state
+        v-else
+        :title="s__('ContainerRegistry|This image has no active tags')"
+        :description="
+          s__(
+            `ContainerRegistry|The last tag related to this image was recently removed.
+            This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process.
+            If you have any questions, contact your administrator.`,
+          )
+        "
+        class="mx-auto my-0"
+      />
     </div>
     <gl-modal :modal-id="modalId" ok-variant="danger" @ok="handleDeleteRepository">
       <template v-slot:modal-title>{{ s__('ContainerRegistry|Remove repository') }}</template>
diff --git a/app/assets/javascripts/registry/components/group_empty_state.vue b/app/assets/javascripts/registry/components/group_empty_state.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7885fd2146d250b42de6145b9858005c78c08f07
--- /dev/null
+++ b/app/assets/javascripts/registry/components/group_empty_state.vue
@@ -0,0 +1,46 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+
+export default {
+  name: 'GroupEmptyState',
+  components: {
+    GlEmptyState,
+  },
+  props: {
+    noContainersImage: {
+      type: String,
+      required: true,
+    },
+    helpPagePath: {
+      type: String,
+      required: true,
+    },
+  },
+  computed: {
+    noContainerImagesText() {
+      return sprintf(
+        s__(
+          `ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. Push at least one Docker image in one of this group's projects in order to show up here. %{docLinkStart}More Information%{docLinkEnd}`,
+        ),
+        {
+          docLinkStart: `<a href="${this.helpPagePath}" target="_blank">`,
+          docLinkEnd: '</a>',
+        },
+        false,
+      );
+    },
+  },
+};
+</script>
+<template>
+  <gl-empty-state
+    :title="s__('ContainerRegistry|There are no container images available in this group')"
+    :svg-path="noContainersImage"
+    class="container-message"
+  >
+    <template #description>
+      <p class="js-no-container-images-text" v-html="noContainerImagesText"></p>
+    </template>
+  </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/registry/components/project_empty_state.vue b/app/assets/javascripts/registry/components/project_empty_state.vue
new file mode 100644
index 0000000000000000000000000000000000000000..80ef31004c8a8eac207290ec48c7933ca000ffe7
--- /dev/null
+++ b/app/assets/javascripts/registry/components/project_empty_state.vue
@@ -0,0 +1,133 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { s__, sprintf } from '~/locale';
+
+export default {
+  name: 'ProjectEmptyState',
+  components: {
+    ClipboardButton,
+    GlEmptyState,
+  },
+  props: {
+    noContainersImage: {
+      type: String,
+      required: true,
+    },
+    repositoryUrl: {
+      type: String,
+      required: true,
+    },
+    helpPagePath: {
+      type: String,
+      required: true,
+    },
+    twoFactorAuthHelpLink: {
+      type: String,
+      required: true,
+    },
+    personalAccessTokensHelpLink: {
+      type: String,
+      required: true,
+    },
+    registryHostUrlWithPort: {
+      type: String,
+      required: true,
+    },
+  },
+  computed: {
+    dockerBuildCommand() {
+      // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
+      return `docker build -t ${this.repositoryUrl} .`;
+    },
+    dockerPushCommand() {
+      // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
+      return `docker push ${this.repositoryUrl}`;
+    },
+    dockerLoginCommand() {
+      // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
+      return `docker login ${this.registryHostUrlWithPort}`;
+    },
+    noContainerImagesText() {
+      return sprintf(
+        s__(`ContainerRegistry|With the Container Registry, every project can have its own space to
+            store its Docker images. %{docLinkStart}More Information%{docLinkEnd}`),
+        {
+          docLinkStart: `<a href="${this.helpPagePath}" target="_blank">`,
+          docLinkEnd: '</a>',
+        },
+        false,
+      );
+    },
+    notLoggedInToRegistryText() {
+      return sprintf(
+        s__(`ContainerRegistry|If you are not already logged in, you need to authenticate to
+             the Container Registry by using your GitLab username and password. If you have
+             %{twofaDocLinkStart}Two-Factor Authentication%{twofaDocLinkEnd} enabled, use a
+             %{personalAccessTokensDocLinkStart}Personal Access Token%{personalAccessTokensDocLinkEnd}
+            instead of a password.`),
+        {
+          twofaDocLinkStart: `<a href="${this.twoFactorAuthHelpLink}" target="_blank">`,
+          twofaDocLinkEnd: '</a>',
+          personalAccessTokensDocLinkStart: `<a href="${this.personalAccessTokensHelpLink}" target="_blank">`,
+          personalAccessTokensDocLinkEnd: '</a>',
+        },
+        false,
+      );
+    },
+  },
+};
+</script>
+<template>
+  <gl-empty-state
+    :title="s__('ContainerRegistry|There are no container images stored for this project')"
+    :svg-path="noContainersImage"
+    class="container-message"
+  >
+    <template #description>
+      <p class="js-no-container-images-text" v-html="noContainerImagesText"></p>
+      <h5>{{ s__('ContainerRegistry|Quick Start') }}</h5>
+      <p class="js-not-logged-in-to-registry-text" v-html="notLoggedInToRegistryText"></p>
+      <div class="input-group append-bottom-10">
+        <input :value="dockerLoginCommand" type="text" class="form-control monospace" readonly />
+        <span class="input-group-append">
+          <clipboard-button
+            :text="dockerLoginCommand"
+            :title="s__('ContainerRegistry|Copy login command')"
+            class="input-group-text"
+          />
+        </span>
+      </div>
+      <p></p>
+      <p>
+        {{
+          s__(
+            'ContainerRegistry|You can add an image to this registry with the following commands:',
+          )
+        }}
+      </p>
+
+      <div class="input-group append-bottom-10">
+        <input :value="dockerBuildCommand" type="text" class="form-control monospace" readonly />
+        <span class="input-group-append">
+          <clipboard-button
+            :text="dockerBuildCommand"
+            :title="s__('ContainerRegistry|Copy build command')"
+            class="input-group-text"
+          />
+        </span>
+      </div>
+
+      <div class="input-group">
+        <input :value="dockerPushCommand" type="text" class="form-control monospace" readonly />
+        <span class="input-group-append">
+          <clipboard-button
+            :text="dockerPushCommand"
+            :title="s__('ContainerRegistry|Copy push command')"
+            class="input-group-text"
+          />
+        </span>
+      </div>
+    </template>
+  </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index 00acc0eb04aac5e49953605f95880fd1cc50ccb5..8470fbc2b596ac9ae38505065bc3eb7b0d887ba0 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -1,5 +1,5 @@
 <script>
-import { mapActions } from 'vuex';
+import { mapActions, mapGetters } from 'vuex';
 import {
   GlButton,
   GlFormCheckbox,
@@ -35,9 +35,15 @@ export default {
       type: Object,
       required: true,
     },
+    canDeleteRepo: {
+      type: Boolean,
+      default: false,
+      required: false,
+    },
   },
   data() {
     return {
+      selectedItems: [],
       itemsToBeDeleted: [],
       modalId: `confirm-image-deletion-modal-${this.repo.id}`,
       selectAllChecked: false,
@@ -45,6 +51,7 @@ export default {
     };
   },
   computed: {
+    ...mapGetters(['isDeleteDisabled']),
     bulkDeletePath() {
       return this.repo.tagsPath ? this.repo.tagsPath.replace('?format=json', '/bulk_destroy') : '';
     },
@@ -90,6 +97,7 @@ export default {
     },
     deleteSingleItem(index) {
       this.setModalDescription(index);
+      this.itemsToBeDeleted = [index];
 
       this.$refs.deleteModal.$refs.modal.$once('ok', () => {
         this.removeModalEvents();
@@ -97,9 +105,10 @@ export default {
       });
     },
     deleteMultipleItems() {
-      if (this.itemsToBeDeleted.length === 1) {
+      this.itemsToBeDeleted = [...this.selectedItems];
+      if (this.selectedItems.length === 1) {
         this.setModalDescription(this.itemsToBeDeleted[0]);
-      } else if (this.itemsToBeDeleted.length > 1) {
+      } else if (this.selectedItems.length > 1) {
         this.setModalDescription();
       }
 
@@ -109,6 +118,7 @@ export default {
       });
     },
     handleSingleDelete(itemToDelete) {
+      this.itemsToBeDeleted = [];
       this.deleteItem(itemToDelete)
         .then(() => this.fetchList({ repo: this.repo }))
         .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
@@ -116,6 +126,7 @@ export default {
     handleMultipleDelete() {
       const { itemsToBeDeleted } = this;
       this.itemsToBeDeleted = [];
+      this.selectedItems = [];
 
       if (this.bulkDeletePath) {
         this.multiDeleteItems({
@@ -144,27 +155,30 @@ export default {
       }
     },
     selectAll() {
-      this.itemsToBeDeleted = this.repo.list.map((x, index) => index);
+      this.selectedItems = this.repo.list.map((x, index) => index);
       this.selectAllChecked = true;
     },
     deselectAll() {
-      this.itemsToBeDeleted = [];
+      this.selectedItems = [];
       this.selectAllChecked = false;
     },
-    updateItemsToBeDeleted(index) {
-      const delIndex = this.itemsToBeDeleted.findIndex(x => x === index);
+    updateselectedItems(index) {
+      const delIndex = this.selectedItems.findIndex(x => x === index);
 
       if (delIndex > -1) {
-        this.itemsToBeDeleted.splice(delIndex, 1);
+        this.selectedItems.splice(delIndex, 1);
         this.selectAllChecked = false;
       } else {
-        this.itemsToBeDeleted.push(index);
+        this.selectedItems.push(index);
 
-        if (this.itemsToBeDeleted.length === this.repo.list.length) {
+        if (this.selectedItems.length === this.repo.list.length) {
           this.selectAllChecked = true;
         }
       }
     },
+    canDeleteRow(item) {
+      return item && item.canDelete && !this.isDeleteDisabled;
+    },
   },
 };
 </script>
@@ -175,7 +189,7 @@ export default {
         <tr>
           <th>
             <gl-form-checkbox
-              v-if="repo.canDelete"
+              v-if="canDeleteRepo"
               class="js-select-all-checkbox"
               :checked="selectAllChecked"
               @change="onSelectAllChange"
@@ -187,10 +201,10 @@ export default {
           <th>{{ s__('ContainerRegistry|Last Updated') }}</th>
           <th>
             <gl-button
-              v-if="repo.canDelete"
+              v-if="canDeleteRepo"
               v-gl-tooltip
               v-gl-modal="modalId"
-              :disabled="!itemsToBeDeleted || itemsToBeDeleted.length === 0"
+              :disabled="!selectedItems || selectedItems.length === 0"
               class="js-delete-registry float-right"
               data-track-event="click_button"
               data-track-label="bulk_registry_tag_delete"
@@ -208,10 +222,10 @@ export default {
         <tr v-for="(item, index) in repo.list" :key="item.tag" class="registry-image-row">
           <td class="check">
             <gl-form-checkbox
-              v-if="item.canDelete"
+              v-if="canDeleteRow(item)"
               class="js-select-checkbox"
-              :checked="itemsToBeDeleted && itemsToBeDeleted.includes(index)"
-              @change="updateItemsToBeDeleted(index)"
+              :checked="selectedItems && selectedItems.includes(index)"
+              @change="updateselectedItems(index)"
             />
           </td>
           <td class="monospace">
@@ -244,7 +258,7 @@ export default {
 
           <td class="content action-buttons">
             <gl-button
-              v-if="item.canDelete"
+              v-if="canDeleteRow(item)"
               v-gl-modal="modalId"
               :title="s__('ContainerRegistry|Remove tag')"
               :aria-label="s__('ContainerRegistry|Remove tag')"
diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js
index d8daec29fda195a430999bf0ad0eec871aec128b..18fd360f5867310f97ae4fced2eef6c44f1337b9 100644
--- a/app/assets/javascripts/registry/index.js
+++ b/app/assets/javascripts/registry/index.js
@@ -13,23 +13,24 @@ export default () =>
     data() {
       const { dataset } = document.querySelector(this.$options.el);
       return {
-        endpoint: dataset.endpoint,
-        characterError: Boolean(dataset.characterError),
-        helpPagePath: dataset.helpPagePath,
-        noContainersImage: dataset.noContainersImage,
-        containersErrorImage: dataset.containersErrorImage,
-        repositoryUrl: dataset.repositoryUrl,
+        registryData: {
+          endpoint: dataset.endpoint,
+          characterError: Boolean(dataset.characterError),
+          helpPagePath: dataset.helpPagePath,
+          noContainersImage: dataset.noContainersImage,
+          containersErrorImage: dataset.containersErrorImage,
+          repositoryUrl: dataset.repositoryUrl,
+          isGroupPage: dataset.isGroupPage,
+          personalAccessTokensHelpLink: dataset.personalAccessTokensHelpLink,
+          registryHostUrlWithPort: dataset.registryHostUrlWithPort,
+          twoFactorAuthHelpLink: dataset.twoFactorAuthHelpLink,
+        },
       };
     },
     render(createElement) {
       return createElement('registry-app', {
         props: {
-          endpoint: this.endpoint,
-          characterError: this.characterError,
-          helpPagePath: this.helpPagePath,
-          noContainersImage: this.noContainersImage,
-          containersErrorImage: this.containersErrorImage,
-          repositoryUrl: this.repositoryUrl,
+          ...this.registryData,
         },
       });
     },
diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js
index a2e0130e79efa99caf22c033b6415737528ca105..2121f518a7a130448ace0257badddd7aa6abd5c1 100644
--- a/app/assets/javascripts/registry/stores/actions.js
+++ b/app/assets/javascripts/registry/stores/actions.js
@@ -20,7 +20,6 @@ export const fetchRepos = ({ commit, state }) => {
 
 export const fetchList = ({ commit }, { repo, page }) => {
   commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
-
   return axios
     .get(repo.tagsPath, { params: { page } })
     .then(response => {
@@ -40,6 +39,7 @@ export const multiDeleteItems = (_, { path, items }) =>
   axios.delete(path, { params: { ids: items } });
 
 export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
+export const setIsDeleteDisabled = ({ commit }, data) => commit(types.SET_IS_DELETE_DISABLED, data);
 export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
 
 // prevent babel-plugin-rewire from generating an invalid default during karma tests
diff --git a/app/assets/javascripts/registry/stores/getters.js b/app/assets/javascripts/registry/stores/getters.js
index f49235125786f4bb8a7b0ec05bd0a048e022717b..ac90bde1b2a27dc5124b54212218a0e10548ccb5 100644
--- a/app/assets/javascripts/registry/stores/getters.js
+++ b/app/assets/javascripts/registry/stores/getters.js
@@ -1,5 +1,6 @@
 export const isLoading = state => state.isLoading;
 export const repos = state => state.repos;
+export const isDeleteDisabled = state => state.isDeleteDisabled;
 
 // prevent babel-plugin-rewire from generating an invalid default during karma tests
 export default () => {};
diff --git a/app/assets/javascripts/registry/stores/mutation_types.js b/app/assets/javascripts/registry/stores/mutation_types.js
index 2c69bf11807366455f77082e0d805141284d3435..6740bfede1a2edae0399a4449e8e368541abaf99 100644
--- a/app/assets/javascripts/registry/stores/mutation_types.js
+++ b/app/assets/javascripts/registry/stores/mutation_types.js
@@ -1,4 +1,5 @@
 export const SET_MAIN_ENDPOINT = 'SET_MAIN_ENDPOINT';
+export const SET_IS_DELETE_DISABLED = 'SET_IS_DELETE_DISABLED';
 
 export const SET_REPOS_LIST = 'SET_REPOS_LIST';
 export const TOGGLE_MAIN_LOADING = 'TOGGLE_MAIN_LOADING';
diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js
index 8ace6657ad1a325e55b136b23e9101ae0764620e..ea5925247d17ba7fefed3731c5568759e600afde 100644
--- a/app/assets/javascripts/registry/stores/mutations.js
+++ b/app/assets/javascripts/registry/stores/mutations.js
@@ -6,6 +6,10 @@ export default {
     Object.assign(state, { endpoint });
   },
 
+  [types.SET_IS_DELETE_DISABLED](state, isDeleteDisabled) {
+    Object.assign(state, { isDeleteDisabled });
+  },
+
   [types.SET_REPOS_LIST](state, list) {
     Object.assign(state, {
       repos: list.map(el => ({
@@ -17,6 +21,7 @@ export default {
         location: el.location,
         name: el.path,
         tagsPath: el.tags_path,
+        projectId: el.project_id,
       })),
     });
   },
diff --git a/app/assets/javascripts/registry/stores/state.js b/app/assets/javascripts/registry/stores/state.js
index feeac10cbe1b432f0682f0ef4c3df932bde0bbb7..724c64b49946dce58a4af3cfbc3c2c5fcb032c80 100644
--- a/app/assets/javascripts/registry/stores/state.js
+++ b/app/assets/javascripts/registry/stores/state.js
@@ -1,6 +1,7 @@
 export default () => ({
   isLoading: false,
   endpoint: '', // initial endpoint to fetch the repos list
+  isDeleteDisabled: false, // controls the delete buttons in the registry
   /**
    * Each object in `repos` has the following strucure:
    * {
diff --git a/app/assets/javascripts/releases/detail/components/app.vue b/app/assets/javascripts/releases/detail/components/app.vue
new file mode 100644
index 0000000000000000000000000000000000000000..54a441de8866a34b2de1f8b23012157da1556b9e
--- /dev/null
+++ b/app/assets/javascripts/releases/detail/components/app.vue
@@ -0,0 +1,156 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import { GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
+
+export default {
+  name: 'ReleaseDetailApp',
+  components: {
+    GlFormInput,
+    GlFormGroup,
+    GlButton,
+    MarkdownField,
+  },
+  directives: {
+    autofocusonshow,
+  },
+  computed: {
+    ...mapState([
+      'isFetchingRelease',
+      'fetchError',
+      'markdownDocsPath',
+      'markdownPreviewPath',
+      'releasesPagePath',
+    ]),
+    showForm() {
+      return !this.isFetchingRelease && !this.fetchError;
+    },
+    subtitleText() {
+      return sprintf(
+        __(
+          'Releases are based on Git tags. We recommend naming tags that fit within semantic versioning, for example %{codeStart}v1.0%{codeEnd}, %{codeStart}v2.0-pre%{codeEnd}.',
+        ),
+        {
+          codeStart: '<code>',
+          codeEnd: '</code>',
+        },
+        false,
+      );
+    },
+    tagName() {
+      return this.$store.state.release.tagName;
+    },
+    releaseTitle: {
+      get() {
+        return this.$store.state.release.name;
+      },
+      set(title) {
+        this.updateReleaseTitle(title);
+      },
+    },
+    releaseNotes: {
+      get() {
+        return this.$store.state.release.description;
+      },
+      set(notes) {
+        this.updateReleaseNotes(notes);
+      },
+    },
+  },
+  created() {
+    this.fetchRelease();
+  },
+  methods: {
+    ...mapActions([
+      'fetchRelease',
+      'updateRelease',
+      'updateReleaseTitle',
+      'updateReleaseNotes',
+      'navigateToReleasesPage',
+    ]),
+  },
+};
+</script>
+<template>
+  <div class="d-flex flex-column">
+    <p class="pt-3 js-subtitle-text" v-html="subtitleText"></p>
+    <form v-if="showForm" @submit.prevent="updateRelease()">
+      <div class="row">
+        <gl-form-group class="col-md-6 col-lg-5 col-xl-4">
+          <label for="git-ref">{{ __('Tag name') }}</label>
+          <gl-form-input
+            id="git-ref"
+            v-model="tagName"
+            type="text"
+            class="form-control"
+            aria-describedby="tag-name-help"
+            disabled
+          />
+          <div id="tag-name-help" class="form-text text-muted">
+            {{ __('Choose an existing tag, or create a new one') }}
+          </div>
+        </gl-form-group>
+      </div>
+      <gl-form-group>
+        <label for="release-title">{{ __('Release title') }}</label>
+        <gl-form-input
+          id="release-title"
+          ref="releaseTitleInput"
+          v-model="releaseTitle"
+          v-autofocusonshow
+          autofocus
+          type="text"
+          class="form-control"
+        />
+      </gl-form-group>
+      <gl-form-group>
+        <label for="release-notes">{{ __('Release notes') }}</label>
+        <div class="bordered-box pr-3 pl-3">
+          <markdown-field
+            :can-attach-file="true"
+            :markdown-preview-path="markdownPreviewPath"
+            :markdown-docs-path="markdownDocsPath"
+            :add-spacing-classes="false"
+            class="prepend-top-10 append-bottom-10"
+          >
+            <textarea
+              id="release-notes"
+              slot="textarea"
+              v-model="releaseNotes"
+              class="note-textarea js-gfm-input js-autosize markdown-area"
+              dir="auto"
+              data-supports-quick-actions="false"
+              :aria-label="__('Release notes')"
+              :placeholder="__('Write your release notes or drag your files here…')"
+              @keydown.meta.enter="updateRelease()"
+              @keydown.ctrl.enter="updateRelease()"
+            >
+            </textarea>
+          </markdown-field>
+        </div>
+      </gl-form-group>
+
+      <div class="d-flex pt-3">
+        <gl-button
+          class="mr-auto js-submit-button"
+          variant="success"
+          type="submit"
+          :aria-label="__('Save changes')"
+        >
+          {{ __('Save changes') }}
+        </gl-button>
+        <gl-button
+          class="js-cancel-button"
+          variant="default"
+          type="button"
+          :aria-label="__('Cancel')"
+          @click="navigateToReleasesPage()"
+        >
+          {{ __('Cancel') }}
+        </gl-button>
+      </div>
+    </form>
+  </div>
+</template>
diff --git a/app/assets/javascripts/releases/detail/index.js b/app/assets/javascripts/releases/detail/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..3da971e6d9033d9e6ccdbef7ae4a231ef5165330
--- /dev/null
+++ b/app/assets/javascripts/releases/detail/index.js
@@ -0,0 +1,19 @@
+import Vue from 'vue';
+import ReleaseDetailApp from './components/app.vue';
+import createStore from './store';
+
+export default () => {
+  const el = document.getElementById('js-edit-release-page');
+
+  const store = createStore(el.dataset);
+  store.dispatch('setInitialState', el.dataset);
+
+  return new Vue({
+    el,
+    store,
+    components: { ReleaseDetailApp },
+    render(createElement) {
+      return createElement('release-detail-app');
+    },
+  });
+};
diff --git a/app/assets/javascripts/releases/detail/store/actions.js b/app/assets/javascripts/releases/detail/store/actions.js
new file mode 100644
index 0000000000000000000000000000000000000000..c9749582f5c3eedae471d684699e057e09395329
--- /dev/null
+++ b/app/assets/javascripts/releases/detail/store/actions.js
@@ -0,0 +1,62 @@
+import * as types from './mutation_types';
+import api from '~/api';
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+export const setInitialState = ({ commit }, initialState) =>
+  commit(types.SET_INITIAL_STATE, initialState);
+
+export const requestRelease = ({ commit }) => commit(types.REQUEST_RELEASE);
+export const receiveReleaseSuccess = ({ commit }, data) =>
+  commit(types.RECEIVE_RELEASE_SUCCESS, data);
+export const receiveReleaseError = ({ commit }, error) => {
+  commit(types.RECEIVE_RELEASE_ERROR, error);
+  createFlash(s__('Release|Something went wrong while getting the release details'));
+};
+
+export const fetchRelease = ({ dispatch, state }) => {
+  dispatch('requestRelease');
+
+  return api
+    .release(state.projectId, state.tagName)
+    .then(({ data: release }) => {
+      const camelCasedRelease = convertObjectPropsToCamelCase(release, { deep: true });
+      dispatch('receiveReleaseSuccess', camelCasedRelease);
+    })
+    .catch(error => {
+      dispatch('receiveReleaseError', error);
+    });
+};
+
+export const updateReleaseTitle = ({ commit }, title) => commit(types.UPDATE_RELEASE_TITLE, title);
+export const updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes);
+
+export const requestUpdateRelease = ({ commit }) => commit(types.REQUEST_UPDATE_RELEASE);
+export const receiveUpdateReleaseSuccess = ({ commit, dispatch }) => {
+  commit(types.RECEIVE_UPDATE_RELEASE_SUCCESS);
+  dispatch('navigateToReleasesPage');
+};
+export const receiveUpdateReleaseError = ({ commit }, error) => {
+  commit(types.RECEIVE_UPDATE_RELEASE_ERROR, error);
+  createFlash(s__('Release|Something went wrong while saving the release details'));
+};
+
+export const updateRelease = ({ dispatch, state }) => {
+  dispatch('requestUpdateRelease');
+
+  return api
+    .updateRelease(state.projectId, state.tagName, {
+      name: state.release.name,
+      description: state.release.description,
+    })
+    .then(() => dispatch('receiveUpdateReleaseSuccess'))
+    .catch(error => {
+      dispatch('receiveUpdateReleaseError', error);
+    });
+};
+
+export const navigateToReleasesPage = ({ state }) => {
+  redirectTo(state.releasesPagePath);
+};
diff --git a/app/assets/javascripts/releases/detail/store/index.js b/app/assets/javascripts/releases/detail/store/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..e8623a4935662fd5576b968b60437ba16c1afd51
--- /dev/null
+++ b/app/assets/javascripts/releases/detail/store/index.js
@@ -0,0 +1,14 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import mutations from './mutations';
+import state from './state';
+
+Vue.use(Vuex);
+
+export default () =>
+  new Vuex.Store({
+    actions,
+    mutations,
+    state,
+  });
diff --git a/app/assets/javascripts/releases/detail/store/mutation_types.js b/app/assets/javascripts/releases/detail/store/mutation_types.js
new file mode 100644
index 0000000000000000000000000000000000000000..75e1d78a6455c4ead1db069f5ee2d763053f6ab0
--- /dev/null
+++ b/app/assets/javascripts/releases/detail/store/mutation_types.js
@@ -0,0 +1,12 @@
+export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
+
+export const REQUEST_RELEASE = 'REQUEST_RELEASE';
+export const RECEIVE_RELEASE_SUCCESS = 'RECEIVE_RELEASE_SUCCESS';
+export const RECEIVE_RELEASE_ERROR = 'RECEIVE_RELEASE_ERROR';
+
+export const UPDATE_RELEASE_TITLE = 'UPDATE_RELEASE_TITLE';
+export const UPDATE_RELEASE_NOTES = 'UPDATE_RELEASE_NOTES';
+
+export const REQUEST_UPDATE_RELEASE = 'REQUEST_UPDATE_RELEASE';
+export const RECEIVE_UPDATE_RELEASE_SUCCESS = 'RECEIVE_UPDATE_RELEASE_SUCCESS';
+export const RECEIVE_UPDATE_RELEASE_ERROR = 'RECEIVE_UPDATE_RELEASE_ERROR';
diff --git a/app/assets/javascripts/releases/detail/store/mutations.js b/app/assets/javascripts/releases/detail/store/mutations.js
new file mode 100644
index 0000000000000000000000000000000000000000..d739978d755da7184c86d5963b9765d6c042585b
--- /dev/null
+++ b/app/assets/javascripts/releases/detail/store/mutations.js
@@ -0,0 +1,42 @@
+import * as types from './mutation_types';
+
+export default {
+  [types.SET_INITIAL_STATE](state, initialState) {
+    Object.keys(state).forEach(key => {
+      state[key] = initialState[key];
+    });
+  },
+
+  [types.REQUEST_RELEASE](state) {
+    state.isFetchingRelease = true;
+  },
+  [types.RECEIVE_RELEASE_SUCCESS](state, data) {
+    state.fetchError = undefined;
+    state.isFetchingRelease = false;
+    state.release = data;
+  },
+  [types.RECEIVE_RELEASE_ERROR](state, error) {
+    state.fetchError = error;
+    state.isFetchingRelease = false;
+    state.release = undefined;
+  },
+
+  [types.UPDATE_RELEASE_TITLE](state, title) {
+    state.release.name = title;
+  },
+  [types.UPDATE_RELEASE_NOTES](state, notes) {
+    state.release.description = notes;
+  },
+
+  [types.REQUEST_UPDATE_RELEASE](state) {
+    state.isUpdatingRelease = true;
+  },
+  [types.RECEIVE_UPDATE_RELEASE_SUCCESS](state) {
+    state.updateError = undefined;
+    state.isUpdatingRelease = false;
+  },
+  [types.RECEIVE_UPDATE_RELEASE_ERROR](state, error) {
+    state.updateError = error;
+    state.isUpdatingRelease = false;
+  },
+};
diff --git a/app/assets/javascripts/releases/detail/store/state.js b/app/assets/javascripts/releases/detail/store/state.js
new file mode 100644
index 0000000000000000000000000000000000000000..ff98e2bed78af1cbed9d854973a8f8dcd586aad8
--- /dev/null
+++ b/app/assets/javascripts/releases/detail/store/state.js
@@ -0,0 +1,15 @@
+export default () => ({
+  projectId: null,
+  tagName: null,
+  releasesPagePath: null,
+  markdownDocsPath: null,
+  markdownPreviewPath: null,
+
+  release: null,
+
+  isFetchingRelease: false,
+  fetchError: null,
+
+  isUpdatingRelease: false,
+  updateError: null,
+});
diff --git a/app/assets/javascripts/releases/list/components/release_block.vue b/app/assets/javascripts/releases/list/components/release_block.vue
index c9c5a6db303d1d7fcce81f20d46d6edcfced80e3..8d4b32e9dc0f72de390ea6deb6747e91522fac63 100644
--- a/app/assets/javascripts/releases/list/components/release_block.vue
+++ b/app/assets/javascripts/releases/list/components/release_block.vue
@@ -1,7 +1,7 @@
 <script>
 /* eslint-disable @gitlab/vue-i18n/no-bare-strings */
 import _ from 'underscore';
-import { GlTooltipDirective, GlLink, GlBadge } from '@gitlab/ui';
+import { GlTooltipDirective, GlLink, GlBadge, GlButton } from '@gitlab/ui';
 import Icon from '~/vue_shared/components/icon.vue';
 import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
 import timeagoMixin from '~/vue_shared/mixins/timeago';
@@ -9,19 +9,21 @@ import { __, n__, sprintf } from '~/locale';
 import { slugify } from '~/lib/utils/text_utility';
 import { getLocationHash } from '~/lib/utils/url_utility';
 import { scrollToElement } from '~/lib/utils/common_utils';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
 
 export default {
   name: 'ReleaseBlock',
   components: {
     GlLink,
     GlBadge,
+    GlButton,
     Icon,
     UserAvatarLink,
   },
   directives: {
     GlTooltip: GlTooltipDirective,
   },
-  mixins: [timeagoMixin],
+  mixins: [timeagoMixin, glFeatureFlagsMixin()],
   props: {
     release: {
       type: Object,
@@ -72,6 +74,11 @@ export default {
     labelText() {
       return n__('Milestone', 'Milestones', this.release.milestones.length);
     },
+    shouldShowEditButton() {
+      return Boolean(
+        this.glFeatures.releaseEditPage && this.release._links && this.release._links.edit,
+      );
+    },
   },
   mounted() {
     const hash = getLocationHash();
@@ -89,12 +96,23 @@ export default {
 <template>
   <div :id="id" :class="{ 'bg-line-target-blue': isHighlighted }" class="card release-block">
     <div class="card-body">
-      <h2 class="card-title mt-0">
-        {{ release.name }}
-        <gl-badge v-if="release.upcoming_release" variant="warning" class="align-middle">{{
-          __('Upcoming Release')
-        }}</gl-badge>
-      </h2>
+      <div class="d-flex align-items-start">
+        <h2 class="card-title mt-0 mr-auto">
+          {{ release.name }}
+          <gl-badge v-if="release.upcoming_release" variant="warning" class="align-middle">{{
+            __('Upcoming Release')
+          }}</gl-badge>
+        </h2>
+        <gl-link
+          v-if="shouldShowEditButton"
+          v-gl-tooltip
+          class="btn btn-default js-edit-button ml-2"
+          :title="__('Edit this release')"
+          :href="release._links.edit"
+        >
+          <icon name="pencil" />
+        </gl-link>
+      </div>
 
       <div class="card-subtitle d-flex flex-wrap text-secondary">
         <div class="append-right-8">
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index f6722ff7bcafe160d967ac513e41fab9862b4cfd..e08a67ec60455d7caa3ccf7d55983ad178d3671d 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -95,10 +95,10 @@ export class SearchAutocomplete {
       this.createAutocomplete();
     }
 
-    this.searchInput.addClass('disabled');
     this.saveTextLength();
     this.bindEvents();
     this.dropdownToggle.dropdown();
+    this.searchInput.addClass('js-autocomplete-disabled');
   }
 
   // Finds an element inside wrapper element
@@ -338,7 +338,7 @@ export class SearchAutocomplete {
     if (!this.dropdown.hasClass('show')) {
       this.loadingSuggestions = false;
       this.dropdownToggle.dropdown('toggle');
-      return this.searchInput.removeClass('disabled');
+      return this.searchInput.removeClass('js-autocomplete-disabled');
     }
   }
 
@@ -432,8 +432,8 @@ export class SearchAutocomplete {
   }
 
   disableAutocomplete() {
-    if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('show')) {
-      this.searchInput.addClass('disabled');
+    if (!this.searchInput.hasClass('js-autocomplete-disabled') && this.dropdown.hasClass('show')) {
+      this.searchInput.addClass('js-autocomplete-disabled');
       this.dropdown.removeClass('show').trigger('hidden.bs.dropdown');
       this.restoreMenu();
     }
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
index ae1d9368008af1c58f677ceb8e6ba2fd74721435..36f291e995cc5456c6d0198fb9696e3d1cb4f8d9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
@@ -39,9 +39,6 @@ export default {
     ariaLabel() {
       return this.isCollapsed ? __('Expand') : __('Collapse');
     },
-    isButtonDisabled() {
-      return this.isLoading || this.hasError;
-    },
   },
   methods: {
     toggleCollapsed() {
@@ -53,25 +50,35 @@ export default {
 <template>
   <div>
     <div class="mr-widget-extension d-flex align-items-center pl-3">
-      <gl-button
-        class="btn-blank btn s32 square append-right-default"
-        :aria-label="ariaLabel"
-        :disabled="isButtonDisabled"
-        @click="toggleCollapsed"
-      >
-        <gl-loading-icon v-if="isLoading" />
-        <icon v-else :name="arrowIconName" class="js-icon" />
-      </gl-button>
-      <gl-button
-        variant="link"
-        class="js-title"
-        :disabled="isButtonDisabled"
-        :class="{ 'border-0': isButtonDisabled }"
-        @click="toggleCollapsed"
-      >
-        <template v-if="isCollapsed">{{ title }}</template>
-        <template v-else>{{ __('Collapse') }}</template>
-      </gl-button>
+      <div v-if="hasError" class="ci-widget media">
+        <div class="media-body">
+          <span class="gl-font-size-small mr-widget-margin-left gl-line-height-24 js-error-state">{{
+            title
+          }}</span>
+        </div>
+      </div>
+
+      <template v-else>
+        <gl-button
+          class="btn-blank btn s32 square append-right-default"
+          :aria-label="ariaLabel"
+          :disabled="isLoading"
+          @click="toggleCollapsed"
+        >
+          <gl-loading-icon v-if="isLoading" />
+          <icon v-else :name="arrowIconName" class="js-icon" />
+        </gl-button>
+        <gl-button
+          variant="link"
+          class="js-title"
+          :disabled="isLoading"
+          :class="{ 'border-0': isLoading }"
+          @click="toggleCollapsed"
+        >
+          <template v-if="isCollapsed">{{ title }}</template>
+          <template v-else>{{ __('Collapse') }}</template>
+        </gl-button>
+      </template>
     </div>
 
     <div v-if="!isCollapsed" class="border-top js-slot-container">
diff --git a/app/assets/javascripts/vue_shared/directives/track_event.js b/app/assets/javascripts/vue_shared/directives/track_event.js
new file mode 100644
index 0000000000000000000000000000000000000000..d1c05c5c26701f7e21d314b572e8e474a32c335d
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/directives/track_event.js
@@ -0,0 +1,20 @@
+import Tracking from '~/tracking';
+
+export default {
+  bind(el, binding) {
+    el.dataset.trackingOptions = JSON.stringify(binding.value || {});
+
+    el.addEventListener('click', () => {
+      const { category, action, label, property, value } = JSON.parse(el.dataset.trackingOptions);
+      if (!category || !action) {
+        return;
+      }
+      Tracking.event(category, action, { label, property, value });
+    });
+  },
+  update(el, binding) {
+    if (binding.value !== binding.oldValue) {
+      el.dataset.trackingOptions = JSON.stringify(binding.value || {});
+    }
+  },
+};
diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss
index cbd390e714513d4a072a7f1cbc03f2c735eca80f..7dd7ab339dd21c22de52573a567fbff678e0d8af 100644
--- a/app/assets/stylesheets/framework/blank.scss
+++ b/app/assets/stylesheets/framework/blank.scss
@@ -14,13 +14,12 @@
 .blank-state-row {
   display: flex;
   flex-wrap: wrap;
-  justify-content: space-around;
-  height: 100%;
+  justify-content: space-between;
 }
 
 .blank-state-welcome {
   text-align: center;
-  padding: 20px 0 40px;
+  padding: $gl-padding 0 ($gl-padding * 2);
 
   .blank-state-welcome-title {
     font-size: 24px;
@@ -32,23 +31,9 @@
 }
 
 .blank-state-link {
-  display: block;
   color: $gl-text-color;
-  flex: 0 0 100%;
   margin-bottom: 15px;
 
-  @include media-breakpoint-up(sm) {
-    flex: 0 0 49%;
-
-    &:nth-child(odd) {
-      margin-right: 5px;
-    }
-
-    &:nth-child(even) {
-      margin-left: 5px;
-    }
-  }
-
   &:hover {
     background-color: $gray-light;
     text-decoration: none;
@@ -63,15 +48,25 @@
 }
 
 .blank-state {
-  padding: 20px;
+  display: flex;
+  align-items: center;
+  padding: 20px 50px;
   border: 1px solid $border-color;
   border-radius: $border-radius-default;
+  min-height: 240px;
+  margin-bottom: $gl-padding;
+  width: calc(50% - #{$gl-padding-8});
+
+  @include media-breakpoint-down(sm) {
+    width: 100%;
+    flex-direction: column;
+    justify-content: center;
+    padding: 50px 20px;
+
+    .column-small & {
+      width: 100%;
+    }
 
-  @include media-breakpoint-up(sm) {
-    display: flex;
-    height: 100%;
-    align-items: center;
-    padding: 50px 30px;
   }
 }
 
@@ -90,7 +85,7 @@
   }
 
   .blank-state-body {
-    @include media-breakpoint-down(xs) {
+    @include media-breakpoint-down(sm) {
       text-align: center;
       margin-top: 20px;
     }
@@ -121,9 +116,3 @@
     }
   }
 }
-
-@include media-breakpoint-down(xs) {
-  .blank-state-icon svg {
-    width: 315px;
-  }
-}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 16cb63fc0dffbcf95412ad967928603db381aa97..4b89a2f2b0485d48994a311bf9913f1e94acbae4 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -440,6 +440,7 @@ img.emoji {
 .flex-no-shrink { flex-shrink: 0; }
 .ws-initial { white-space: initial; }
 .ws-normal { white-space: normal; }
+.ws-pre-wrap { white-space: pre-wrap; }
 .overflow-auto { overflow: auto; }
 
 .d-flex-center {
@@ -559,3 +560,6 @@ img.emoji {
     }
   }
 }
+
+.gl-font-size-small { font-size: $gl-font-size-small; }
+.gl-line-height-24 { line-height: $gl-line-height-24; }
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index b793a12317e9cb17d7514c1ebffea0fdee9d42d8..487fbf0fcff84af5e7c9daf4fa52b9e8d2c97e23 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -108,12 +108,14 @@
     background: $white-light;
 
     &.image_file,
+    &.audio,
     &.video {
       background: $gray-darker;
       text-align: center;
       padding: 30px;
 
       img,
+      audio,
       video {
         max-width: 80%;
       }
diff --git a/app/assets/stylesheets/framework/job_log.scss b/app/assets/stylesheets/framework/job_log.scss
index f26c475c3c158782419bbae5f8e2c77278d1c408..4a57a458c50b3247b7bf6912024ebe960d13b619 100644
--- a/app/assets/stylesheets/framework/job_log.scss
+++ b/app/assets/stylesheets/framework/job_log.scss
@@ -9,7 +9,6 @@
   border-radius: $border-radius-small;
   min-height: 42px;
   background-color: $builds-trace-bg;
-  white-space: pre-wrap;
 }
 
 .log-line {
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index c73db2668ec483387097d35e3f7396e864d805e7..ecd32dcd0ce08ae45c93404e1833aab8b976b2fa 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -123,7 +123,7 @@ ul.content-list {
       font-weight: $gl-font-weight-bold;
     }
 
-    a:not(.default-link-color) {
+    a {
       color: $gl-text-color;
     }
 
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 9028bfa8ec9d9d1b930f41e9e98c54cf1ee20cb8..3876d1c10d43fbaf354f3cae1e5bf664c1e24b52 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -69,10 +69,6 @@
 
   details {
     margin-bottom: $gl-padding;
-
-    summary {
-      margin-bottom: $gl-padding;
-    }
   }
 
   // Single code lines should wrap
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index f352ee3353509cbe5c1e37d3c415cc42ec769fd8..dfc39d8e03b4c6a25f7ce4e67b0b49929891c653 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -833,6 +833,7 @@ Merge Requests
 */
 $mr-tabs-height: 48px;
 $mr-version-controls-height: 56px;
+$mr-widget-margin-left: 40px;
 
 /*
 Compare Branches
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index e6feded1d4f6cc4779fbb27dd786273c17af03be..971f3b2c308d485f3a21ac9f33e79930893a1b11 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -19,6 +19,8 @@
   border-top: 1px solid $border-color;
 }
 
+.mr-widget-margin-left { margin-left: $mr-widget-margin-left; }
+
 .media-section {
   @include media-breakpoint-down(md) {
     align-items: flex-start;
diff --git a/app/assets/stylesheets/pages/prometheus.scss b/app/assets/stylesheets/pages/prometheus.scss
index ceafff94719115d1b2fda12b6f46e32dff74355f..154e505f7a4cb583e8bf050719430a45761e2f97 100644
--- a/app/assets/stylesheets/pages/prometheus.scss
+++ b/app/assets/stylesheets/pages/prometheus.scss
@@ -46,6 +46,20 @@
   }
 }
 
+.prometheus-graphs-header {
+  .time-window-dropdown-menu {
+    padding: $gl-padding $gl-padding 0 $gl-padding-12;
+  }
+
+  .time-window-dropdown-menu-container {
+    width: 360px;
+  }
+
+  .custom-time-range-form-group > label {
+    padding-bottom: $gl-padding;
+  }
+}
+
 .prometheus-panel {
   margin-top: 20px;
 }
diff --git a/app/assets/stylesheets/pages/tags.scss b/app/assets/stylesheets/pages/tags.scss
new file mode 100644
index 0000000000000000000000000000000000000000..a6d30522ff78b3701257977df7540a81511b2176
--- /dev/null
+++ b/app/assets/stylesheets/pages/tags.scss
@@ -0,0 +1,3 @@
+.tag-release-link {
+  color: $blue-600 !important;
+}
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index c36bbaab23b5a5e1606b3daf99af54dbf66b2196..f24ce9b5d03d5f9261672acdd99c96afc6908a02 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -2,6 +2,7 @@
 
 class Admin::DashboardController < Admin::ApplicationController
   include CountHelper
+  helper_method :show_license_breakdown?
 
   COUNTED_ITEMS = [Project, User, Group].freeze
 
@@ -13,6 +14,10 @@ def index
     @groups = Group.order_id_desc.with_route.limit(10)
   end
   # rubocop: enable CodeReuse/ActiveRecord
+
+  def show_license_breakdown?
+    false
+  end
 end
 
 Admin::DashboardController.prepend_if_ee('EE::Admin::DashboardController')
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index f5939b61948fffb8ef09543b6bbe0d84d3514a80..1443a71f6b1611fffa2c8a0328f5b2da5c2590cc 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -29,6 +29,7 @@ class ApplicationController < ActionController::Base
   before_action :active_user_check, unless: :devise_controller?
   before_action :set_usage_stats_consent_flag
   before_action :check_impersonation_availability
+  before_action :require_role
 
   around_action :set_locale
   around_action :set_session_storage
@@ -547,6 +548,16 @@ def allow_gitaly_ref_name_caching
   def current_user_mode
     @current_user_mode ||= Gitlab::Auth::CurrentUserMode.new(current_user)
   end
+
+  # A user requires a role when they are part of the experimental signup flow (executed by the Growth team). Users
+  # are redirected to the welcome page when their role is required and the experiment is enabled for the current user.
+  def require_role
+    return unless current_user && current_user.role_required? && experiment_enabled?(:signup_flow)
+
+    store_location_for :user, request.fullpath
+
+    redirect_to users_sign_up_welcome_path
+  end
 end
 
 ApplicationController.prepend_if_ee('EE::ApplicationController')
diff --git a/app/controllers/boards/application_controller.rb b/app/controllers/boards/application_controller.rb
index eab908ba5ed5a6a328c8685dd383734ebcf1cd86..15ef6698472effef893861c90ef035cbdebc1449 100644
--- a/app/controllers/boards/application_controller.rb
+++ b/app/controllers/boards/application_controller.rb
@@ -13,7 +13,7 @@ def board
     end
 
     def board_parent
-      @board_parent ||= board.parent
+      @board_parent ||= board.resource_parent
     end
 
     def record_not_found(exception)
diff --git a/app/controllers/boards/lists_controller.rb b/app/controllers/boards/lists_controller.rb
index 90e04414d8d4828b987601258146861154aebb63..880f75007088913c97640ccd65845e5c41bee826 100644
--- a/app/controllers/boards/lists_controller.rb
+++ b/app/controllers/boards/lists_controller.rb
@@ -9,7 +9,7 @@ class ListsController < Boards::ApplicationController
     skip_before_action :authenticate_user!, only: [:index]
 
     def index
-      lists = Boards::Lists::ListService.new(board.parent, current_user).execute(board)
+      lists = Boards::Lists::ListService.new(board.resource_parent, current_user).execute(board)
 
       List.preload_preferences_for_user(lists, current_user)
 
@@ -17,7 +17,7 @@ def index
     end
 
     def create
-      list = Boards::Lists::CreateService.new(board.parent, current_user, create_list_params).execute(board)
+      list = Boards::Lists::CreateService.new(board.resource_parent, current_user, create_list_params).execute(board)
 
       if list.valid?
         render json: serialize_as_json(list)
diff --git a/app/controllers/concerns/invisible_captcha.rb b/app/controllers/concerns/invisible_captcha.rb
index 45c0a5c58ef1a1c8485262202e6fda371896fdd0..d56f1d7fa5fd0dad6f8f190e0d0b8a92beb40e30 100644
--- a/app/controllers/concerns/invisible_captcha.rb
+++ b/app/controllers/concerns/invisible_captcha.rb
@@ -8,7 +8,7 @@ module InvisibleCaptcha
   end
 
   def on_honeypot_spam_callback
-    return unless Feature.enabled?(:invisible_captcha)
+    return unless Feature.enabled?(:invisible_captcha) || experiment_enabled?(:signup_flow)
 
     invisible_captcha_honeypot_counter.increment
     log_request('Invisible_Captcha_Honeypot_Request')
@@ -17,7 +17,7 @@ def on_honeypot_spam_callback
   end
 
   def on_timestamp_spam_callback
-    return unless Feature.enabled?(:invisible_captcha)
+    return unless Feature.enabled?(:invisible_captcha) || experiment_enabled?(:signup_flow)
 
     invisible_captcha_timestamp_counter.increment
     log_request('Invisible_Captcha_Timestamp_Request')
diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb
index 1ead631663ef5eb0bacc13c0933c1969ae066b3f..672d31ec779c1ae54e84c32db0250c52f51989c4 100644
--- a/app/controllers/concerns/milestone_actions.rb
+++ b/app/controllers/concerns/milestone_actions.rb
@@ -35,7 +35,7 @@ def labels
 
         render json: tabs_json("shared/milestones/_labels_tab", {
           labels: milestone_labels.map do |label|
-            label.present(issuable_subject: @milestone.parent)
+            label.present(issuable_subject: @milestone.resource_parent)
           end
         })
       end
diff --git a/app/controllers/concerns/redirects_for_missing_path_on_tree.rb b/app/controllers/concerns/redirects_for_missing_path_on_tree.rb
new file mode 100644
index 0000000000000000000000000000000000000000..085afbf3975cf74d07f3f543967345544637d707
--- /dev/null
+++ b/app/controllers/concerns/redirects_for_missing_path_on_tree.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module RedirectsForMissingPathOnTree
+  def redirect_to_tree_root_for_missing_path(project, ref, path)
+    redirect_to project_tree_path(project, ref), notice: missing_path_on_ref(path, ref)
+  end
+
+  private
+
+  def missing_path_on_ref(path, ref)
+    _('"%{path}" did not exist on "%{ref}"') % { path: truncate_path(path), ref: ref }
+  end
+
+  def truncate_path(path)
+    path.reverse.truncate(60, separator: "/").reverse
+  end
+end
diff --git a/app/controllers/explore/snippets_controller.rb b/app/controllers/explore/snippets_controller.rb
index d4c6aae2ca8239b760ba92cbe8c2161f23270beb..61068df77d18c62433954c2024ad86a631022e61 100644
--- a/app/controllers/explore/snippets_controller.rb
+++ b/app/controllers/explore/snippets_controller.rb
@@ -5,7 +5,7 @@ class Explore::SnippetsController < Explore::ApplicationController
   include Gitlab::NoteableMetadata
 
   def index
-    @snippets = SnippetsFinder.new(current_user)
+    @snippets = SnippetsFinder.new(current_user, explore: true)
       .execute
       .page(params[:page])
       .inc_author
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 1eacae0645732a70af3a6c6852c22a18c3cc0dfc..1e9d51cf970294113f05d73d2b2594dd0d3dd6d5 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -44,7 +44,7 @@ def update
     # all projects milestones states at once.
     milestones, update_params = get_milestones_for_update
     milestones.each do |milestone|
-      Milestones::UpdateService.new(milestone.parent, current_user, update_params).execute(milestone)
+      Milestones::UpdateService.new(milestone.resource_parent, current_user, update_params).execute(milestone)
     end
 
     redirect_to milestone_path
diff --git a/app/controllers/groups/registry/repositories_controller.rb b/app/controllers/groups/registry/repositories_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e09a9e6eb21f120526688ddba776439047065356
--- /dev/null
+++ b/app/controllers/groups/registry/repositories_controller.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+module Groups
+  module Registry
+    class RepositoriesController < Groups::ApplicationController
+      before_action :verify_container_registry_enabled!
+      before_action :authorize_read_container_image!
+      before_action :feature_flag_group_container_registry_browser!
+
+      def index
+        track_event(:list_repositories)
+
+        respond_to do |format|
+          format.html
+          format.json do
+            @images = group.container_repositories.with_api_entity_associations
+
+            render json: ContainerRepositoriesSerializer
+              .new(current_user: current_user)
+              .represent(@images)
+          end
+        end
+      end
+
+      private
+
+      def feature_flag_group_container_registry_browser!
+        render_404 unless Feature.enabled?(:group_container_registry_browser, group)
+      end
+
+      def verify_container_registry_enabled!
+        render_404 unless Gitlab.config.registry.enabled
+      end
+
+      def authorize_read_container_image!
+        return render_404 unless can?(current_user, :read_container_image, group)
+      end
+    end
+  end
+end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 3204e1e388b6426c5f2fa97dd8119fc764993107..35e364abba35b0b7042629ae309e45074a942605 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -104,7 +104,6 @@ def update
       redirect_to edit_group_path(@group, anchor: params[:update_section]), notice: "Group '#{@group.name}' was successfully updated."
     else
       @group.path = @group.path_before_last_save || @group.path_was
-
       render action: "edit"
     end
   end
@@ -124,7 +123,7 @@ def transfer
       flash[:notice] = "Group '#{@group.name}' was successfully transferred."
       redirect_to group_path(@group)
     else
-      flash[:alert] = service.error
+      flash[:alert] = service.error.html_safe
       redirect_to edit_group_path(@group)
     end
   end
diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb
index d88ec06a18bd5ef3bacc0c84268f74fab01d7d90..efd5f0fc6079fa5cf750deeecc377fb116eec2a7 100644
--- a/app/controllers/health_controller.rb
+++ b/app/controllers/health_controller.rb
@@ -4,18 +4,31 @@ class HealthController < ActionController::Base
   protect_from_forgery with: :exception, prepend: true
   include RequiresWhitelistedMonitoringClient
 
+  CHECKS = [
+    Gitlab::HealthChecks::DbCheck,
+    Gitlab::HealthChecks::Redis::RedisCheck,
+    Gitlab::HealthChecks::Redis::CacheCheck,
+    Gitlab::HealthChecks::Redis::QueuesCheck,
+    Gitlab::HealthChecks::Redis::SharedStateCheck,
+    Gitlab::HealthChecks::GitalyCheck
+  ].freeze
+
   def readiness
-    render_probe(::Gitlab::HealthChecks::Probes::Readiness)
+    # readiness check is a collection with all above application-level checks
+    render_checks(*CHECKS)
   end
 
   def liveness
-    render_probe(::Gitlab::HealthChecks::Probes::Liveness)
+    # liveness check is a collection without additional checks
+    render_checks
   end
 
   private
 
-  def render_probe(probe_class)
-    result = probe_class.new.execute
+  def render_checks(*checks)
+    result = Gitlab::HealthChecks::Probes::Collection
+      .new(*checks)
+      .execute
 
     # disable static error pages at the gitlab-workhorse level, we want to see this error response even in production
     headers["X-GitLab-Custom-Error"] = 1 unless result.success?
diff --git a/app/controllers/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb
index 43c4f4d220ee755074af4db733e426bf37418235..c97fec0a6ee36523af344cb90f7dc0362246432a 100644
--- a/app/controllers/notification_settings_controller.rb
+++ b/app/controllers/notification_settings_controller.rb
@@ -50,8 +50,6 @@ def render_response(response_template = "shared/notifications/_button", btn_clas
   end
 
   def notification_setting_params_for(source)
-    allowed_fields = NotificationSetting.email_events(source).dup
-    allowed_fields << :level
-    params.require(:notification_setting).permit(allowed_fields)
+    params.require(:notification_setting).permit(NotificationSetting.allowed_fields(source))
   end
 end
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index ab4ca56bb494639edff0c1ad62b9c59fe6125a41..12dc2d1af1ceb7f84364e88a9246e4caf879e197 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -5,6 +5,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
   include Gitlab::Allowable
   include PageLayoutHelper
   include OauthApplications
+  include Gitlab::Experimentation::ControllerConcern
 
   before_action :verify_user_oauth_applications_enabled, except: :index
   before_action :authenticate_user!
diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb
index 705389749d8f3d055833c1d6f8bf66b673b9a61f..e65726dffbf206b9ddc1b7ec636028173a569f3f 100644
--- a/app/controllers/oauth/authorizations_controller.rb
+++ b/app/controllers/oauth/authorizations_controller.rb
@@ -1,6 +1,7 @@
 # frozen_string_literal: true
 
 class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
+  include Gitlab::Experimentation::ControllerConcern
   layout 'profile'
 
   # Overridden from Doorkeeper::AuthorizationsController to
diff --git a/app/controllers/profiles/groups_controller.rb b/app/controllers/profiles/groups_controller.rb
index c755bcb718a15f0571c68f546c41b35dd3ead1eb..04b5ee270dc7cd4bf6954e0ab33d423cc34d2483 100644
--- a/app/controllers/profiles/groups_controller.rb
+++ b/app/controllers/profiles/groups_controller.rb
@@ -5,7 +5,7 @@ class Profiles::GroupsController < Profiles::ApplicationController
 
   def update
     group = find_routable!(Group, params[:id])
-    notification_setting = current_user.notification_settings.find_by(source: group) # rubocop: disable CodeReuse/ActiveRecord
+    notification_setting = current_user.notification_settings_for(group)
 
     if notification_setting.update(update_params)
       flash[:notice] = "Notification settings for #{group.name} saved"
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index 617e5bb7cb3f9956350a1765f54f88e1f5eabdf8..5f44e55f3efcdb1d4064478b1c3ed9811fd6094d 100644
--- a/app/controllers/profiles/notifications_controller.rb
+++ b/app/controllers/profiles/notifications_controller.rb
@@ -3,9 +3,14 @@
 class Profiles::NotificationsController < Profiles::ApplicationController
   # rubocop: disable CodeReuse/ActiveRecord
   def show
-    @user                        = current_user
-    @group_notifications         = current_user.notification_settings.for_groups.order(:id)
-    @project_notifications       = current_user.notification_settings.for_projects.order(:id)
+    @user = current_user
+    @group_notifications = current_user.notification_settings.for_groups.order(:id)
+    @group_notifications += GroupsFinder.new(
+      current_user,
+      all_available: false,
+      exclude_group_ids: @group_notifications.select(:source_id)
+    ).execute.map { |group| current_user.notification_settings_for(group, inherit: true) }
+    @project_notifications = current_user.notification_settings.for_projects.order(:id)
     @global_notification_setting = current_user.global_notification_setting
   end
   # rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 958a24b6c0e0a7d90d3771540bc9fca14315dcaf..2b7571e42b74879ae1b443a5213dbf3e6fff1be1 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -100,6 +100,7 @@ def user_params
       :avatar,
       :bio,
       :email,
+      :role,
       :hide_no_password,
       :hide_no_ssh_key,
       :hide_project_limit,
diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb
index 9076bdb9f0440ec9b154d59c73551bcd21050b82..92655d593dde7e29ec28ba055f632ecef077cf58 100644
--- a/app/controllers/projects/blame_controller.rb
+++ b/app/controllers/projects/blame_controller.rb
@@ -3,6 +3,7 @@
 # Controller for viewing a file's blame
 class Projects::BlameController < Projects::ApplicationController
   include ExtractsPath
+  include RedirectsForMissingPathOnTree
 
   before_action :require_non_empty_project
   before_action :assign_ref_vars
@@ -11,7 +12,9 @@ class Projects::BlameController < Projects::ApplicationController
   def show
     @blob = @repository.blob_at(@commit.id, @path)
 
-    return render_404 unless @blob
+    unless @blob
+      return redirect_to_tree_root_for_missing_path(@project, @ref, @path)
+    end
 
     environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
     @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 7c3d43fb49a8c6ec8356426204b3f755a47ed2c9..205ec288ce9c2e1277d8c7659c9e5f2112e30243 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -7,6 +7,7 @@ class Projects::BlobController < Projects::ApplicationController
   include RendersBlob
   include NotesHelper
   include ActionView::Helpers::SanitizeHelper
+  include RedirectsForMissingPathOnTree
   prepend_before_action :authenticate_user!, only: [:edit]
 
   around_action :allow_gitaly_ref_name_caching, only: [:show]
@@ -119,7 +120,7 @@ def blob
         end
       end
 
-      return render_404
+      return redirect_to_tree_root_for_missing_path(@project, @ref, @path)
     end
   end
 
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index 514b03e23b5cef32c256054d8546d3829dbcb063..f13fb4d0b3dc2e972ba1009167717e615bd2e9c4 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -73,6 +73,10 @@ def deploy_key
     @deploy_key ||= DeployKey.find(params[:id])
   end
 
+  def deploy_keys_project
+    @deploy_keys_project ||= deploy_key.deploy_keys_project_for(@project)
+  end
+
   def create_params
     create_params = params.require(:deploy_key)
                           .permit(:key, :title, deploy_keys_projects_attributes: [:can_push])
@@ -81,10 +85,16 @@ def create_params
   end
 
   def update_params
-    params.require(:deploy_key).permit(:title, deploy_keys_projects_attributes: [:id, :can_push])
+    permitted_params = [deploy_keys_projects_attributes: [:id, :can_push]]
+    permitted_params << :title if can?(current_user, :update_deploy_key, deploy_key)
+
+    params.require(:deploy_key).permit(*permitted_params)
   end
 
   def authorize_update_deploy_key!
-    access_denied! unless can?(current_user, :update_deploy_key, deploy_key)
+    if !can?(current_user, :update_deploy_key, deploy_key) &&
+        !can?(current_user, :update_deploy_keys_project, deploy_keys_project)
+      access_denied!
+    end
   end
 end
diff --git a/app/controllers/projects/deployments_controller.rb b/app/controllers/projects/deployments_controller.rb
index 32111b07a0b833fbd3519c666fc4989ab523641e..766e2f86ea217270aa1a9b98b9707c0ff4925209 100644
--- a/app/controllers/projects/deployments_controller.rb
+++ b/app/controllers/projects/deployments_controller.rb
@@ -47,11 +47,9 @@ def deployment_metrics
     @deployment_metrics ||= DeploymentMetrics.new(deployment.project, deployment)
   end
 
-  # rubocop: disable CodeReuse/ActiveRecord
   def deployment
-    @deployment ||= environment.deployments.find_by(iid: params[:id])
+    @deployment ||= environment.deployments.find_successful_deployment!(params[:id])
   end
-  # rubocop: enable CodeReuse/ActiveRecord
 
   def environment
     @environment ||= project.environments.find(params[:environment_id])
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 7a192a9ec2d54fcc2a1a49582fef37352ef704c0..96cb400950ba87f9016d76dba37f32a8c6b01de5 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -42,6 +42,10 @@ def set_issuables_index_only_actions
   before_action :authorize_import_issues!, only: [:import_csv]
   before_action :authorize_download_code!, only: [:related_branches]
 
+  before_action do
+    push_frontend_feature_flag(:vue_issuable_sidebar, project.group)
+  end
+
   respond_to :html
 
   alias_method :designs, :show
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 0fdd4d4f33d8231a11ced706100341db5f82c106..1d914ab60119274526f4d8b45067f22219224d51 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -11,8 +11,8 @@ class Projects::JobsController < Projects::ApplicationController
   before_action :authorize_erase_build!, only: [:erase]
   before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_websocket_authorize]
   before_action :verify_api_request!, only: :terminal_websocket_authorize
-  before_action only: [:trace] do
-    push_frontend_feature_flag(:job_log_json)
+  before_action only: [:show] do
+    push_frontend_feature_flag(:job_log_json, project)
   end
 
   layout 'project'
@@ -67,38 +67,27 @@ def show
   # rubocop: enable CodeReuse/ActiveRecord
 
   def trace
-    if Feature.enabled?(:job_log_json, @project)
-      json_trace
-    else
-      html_trace
-    end
-  end
-
-  def html_trace
     build.trace.read do |stream|
       respond_to do |format|
         format.json do
-          result = {
-            id: @build.id, status: @build.status, complete: @build.complete?
-          }
-
-          if stream.valid?
-            stream.limit
-            state = params[:state].presence
-            trace = stream.html_with_state(state)
-            result.merge!(trace.to_h)
-          end
-
-          render json: result
+          # TODO: when the feature flag is removed we should not pass
+          # content_format to serialize method.
+          content_format = Feature.enabled?(:job_log_json, @project) ? :json : :html
+
+          build_trace = Ci::BuildTrace.new(
+            build: @build,
+            stream: stream,
+            state: params[:state],
+            content_format: content_format)
+
+          render json: BuildTraceSerializer
+            .new(project: @project, current_user: @current_user)
+            .represent(build_trace)
         end
       end
     end
   end
 
-  def json_trace
-    # will be implemented with https://gitlab.com/gitlab-org/gitlab-foss/issues/66454
-  end
-
   def retry
     return respond_422 unless @build.retryable?
 
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 1913d7cd580f2709fc564908b9c14cb3edb65d54..4a37dfe5c19ffc3d46bb71a053635266ca8b60a4 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -51,9 +51,7 @@ def preloadable_mr_relations
   def render_diffs
     @environment = @merge_request.environments_for(current_user).last
 
-    note_positions = renderable_notes.map(&:position).compact
-    @diffs.unfold_diff_files(note_positions)
-
+    @diffs.unfold_diff_files(note_positions.unfoldable)
     @diffs.write_cache
 
     request = {
@@ -140,6 +138,10 @@ def define_diff_comment_vars
     @notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes), @merge_request)
   end
 
+  def note_positions
+    @note_positions ||= Gitlab::Diff::PositionCollection.new(renderable_notes.map(&:position))
+  end
+
   def renderable_notes
     define_diff_comment_vars unless @notes
 
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index bd22226da5c8a646533e1d07bbeec4111d8b89f9..e6032323c13b2c998062d195238741594c19cf02 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -13,7 +13,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
   skip_before_action :merge_request, only: [:index, :bulk_update]
   before_action :whitelist_query_limiting, only: [:assign_related_issues, :update]
   before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
-  before_action :authorize_test_reports!, only: [:test_reports]
+  before_action :authorize_read_actual_head_pipeline!, only: [:test_reports, :exposed_artifacts]
   before_action :set_issuables_index, only: [:index]
   before_action :authenticate_user!, only: [:assign_related_issues]
   before_action :check_user_can_push_to_source_branch!, only: [:rebase]
@@ -21,6 +21,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
     push_frontend_feature_flag(:diffs_batch_load, @project)
   end
 
+  before_action do
+    push_frontend_feature_flag(:vue_issuable_sidebar, @project.group)
+  end
+
   around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]
 
   def index
@@ -111,6 +115,14 @@ def test_reports
     reports_response(@merge_request.compare_test_reports)
   end
 
+  def exposed_artifacts
+    if @merge_request.has_exposed_artifacts?
+      reports_response(@merge_request.find_exposed_artifacts)
+    else
+      head :no_content
+    end
+  end
+
   def edit
     define_edit_vars
   end
@@ -353,8 +365,7 @@ def reports_response(report_comparison)
     end
   end
 
-  def authorize_test_reports!
-    # MergeRequest#actual_head_pipeline is the pipeline accessed in MergeRequest#compare_reports.
+  def authorize_read_actual_head_pipeline!
     return render_404 unless can?(current_user, :read_build, merge_request.actual_head_pipeline)
   end
 end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 1bacdc0b1b2ee4cfe9aab9ad8919825461b53ef9..106ef1b72c1fe88277b566e5620a399a26870a43 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -184,7 +184,7 @@ def render_show
   end
 
   def show_represent_params
-    { grouped: true }
+    { grouped: true, expanded: params[:expanded].to_a.map(&:to_i) }
   end
 
   def create_params
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
index 4c39ee4045f349148b4b00266db0a8a68c8877a7..717df9f09e0396d67319c77e97c464e3013ffe12 100644
--- a/app/controllers/projects/releases_controller.rb
+++ b/app/controllers/projects/releases_controller.rb
@@ -4,6 +4,9 @@ class Projects::ReleasesController < Projects::ApplicationController
   # Authorize
   before_action :require_non_empty_project
   before_action :authorize_read_release!
+  before_action do
+    push_frontend_feature_flag(:release_edit_page, project)
+  end
 
   def index
   end
diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb
index 6110a7759ad5a475a8e6f0ce6310d3b3488f35d4..5bf3618b3891cec01754e5be3dfa38e2842d0949 100644
--- a/app/controllers/projects/settings/operations_controller.rb
+++ b/app/controllers/projects/settings/operations_controller.rb
@@ -13,9 +13,14 @@ def show
       def update
         result = ::Projects::Operations::UpdateService.new(project, current_user, update_params).execute
 
+        track_events(result)
         render_update_response(result)
       end
 
+      # overridden in EE
+      def track_events(result)
+      end
+
       private
 
       # overridden in EE
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index 7509cc29a76e8a41dce685f44736c17b2b844daa..eec89afe354a95540f1148311f3a7a63bd08adc5 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -5,6 +5,7 @@ class Projects::TreeController < Projects::ApplicationController
   include ExtractsPath
   include CreatesCommit
   include ActionView::Helpers::SanitizeHelper
+  include RedirectsForMissingPathOnTree
 
   around_action :allow_gitaly_ref_name_caching, only: [:show]
 
@@ -19,12 +20,9 @@ def show
 
     if tree.entries.empty?
       if @repository.blob_at(@commit.id, @path)
-        return redirect_to(
-          project_blob_path(@project,
-                                      File.join(@ref, @path))
-        )
+        return redirect_to project_blob_path(@project, File.join(@ref, @path))
       elsif @path.present?
-        return render_404
+        return redirect_to_tree_root_for_missing_path(@project, @ref, @path)
       end
     end
 
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 3a2975e92d6bd001ef10f01d24407d0d665e4830..4a746fc915d934e20faf8cadddce6a47e258df8e 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -8,13 +8,14 @@ class RegistrationsController < Devise::RegistrationsController
 
   layout :choose_layout
 
+  skip_before_action :require_role, only: [:welcome, :update_role]
   prepend_before_action :check_captcha, only: :create
   before_action :whitelist_query_limiting, only: [:destroy]
   before_action :ensure_terms_accepted,
     if: -> { action_name == 'create' && Gitlab::CurrentSettings.current_application_settings.enforce_terms? }
 
   def new
-    if helpers.use_experimental_separate_sign_up_flow?
+    if experiment_enabled?(:signup_flow)
       @resource = build_resource
     else
       redirect_to new_user_session_path(anchor: 'register-pane')
@@ -26,8 +27,13 @@ def create
 
     super do |new_user|
       persist_accepted_terms_if_required(new_user)
+      set_role_required(new_user)
       yield new_user if block_given?
     end
+
+    # Do not show the signed_up notice message when the signup_flow experiment is enabled.
+    # Instead, show it after succesfully updating the role.
+    flash[:notice] = nil if experiment_enabled?(:signup_flow)
   rescue Gitlab::Access::AccessDeniedError
     redirect_to(new_user_session_path)
   end
@@ -42,6 +48,26 @@ def destroy
     end
   end
 
+  def welcome
+    return redirect_to new_user_registration_path unless current_user
+    return redirect_to stored_location_or_dashboard_or_almost_there_path(current_user) if current_user.role.present?
+
+    current_user.name = nil
+    render layout: 'devise_experimental_separate_sign_up_flow'
+  end
+
+  def update_role
+    user_params = params.require(:user).permit(:name, :role)
+    result = ::Users::UpdateService.new(current_user, user_params.merge(user: current_user)).execute
+
+    if result[:status] == :success
+      set_flash_message! :notice, :signed_up
+      redirect_to stored_location_or_dashboard_or_almost_there_path(current_user)
+    else
+      redirect_to users_sign_up_welcome_path, alert: result[:message]
+    end
+  end
+
   protected
 
   def persist_accepted_terms_if_required(new_user)
@@ -54,6 +80,10 @@ def persist_accepted_terms_if_required(new_user)
     end
   end
 
+  def set_role_required(new_user)
+    new_user.set_role_required! if new_user.persisted? && experiment_enabled?(:signup_flow)
+  end
+
   def destroy_confirmation_valid?
     if current_user.confirm_deletion_with_password?
       current_user.valid_password?(params[:password])
@@ -76,7 +106,10 @@ def build_resource(hash = nil)
 
   def after_sign_up_path_for(user)
     Gitlab::AppLogger.info(user_created_message(confirmed: user.confirmed?))
-    confirmed_or_unconfirmed_access_allowed(user) ? stored_location_or_dashboard(user) : users_almost_there_path
+
+    return users_sign_up_welcome_path if experiment_enabled?(:signup_flow)
+
+    stored_location_or_dashboard_or_almost_there_path(user)
   end
 
   def after_inactive_sign_up_path_for(resource)
@@ -103,6 +136,7 @@ def check_captcha
     ensure_correct_params!
 
     return unless Feature.enabled?(:registrations_recaptcha, default_enabled: true) # reCAPTCHA on the UI will still display however
+    return if experiment_enabled?(:signup_flow) # when the experimental signup flow is enabled for the current user, disable the reCAPTCHA check
     return unless show_recaptcha_sign_up?
     return unless Gitlab::Recaptcha.load_configurations!
 
@@ -114,7 +148,13 @@ def check_captcha
   end
 
   def sign_up_params
-    params.require(:user).permit(:username, :email, :email_confirmation, :name, :password)
+    clean_params = params.require(:user).permit(:username, :email, :email_confirmation, :name, :password)
+
+    if experiment_enabled?(:signup_flow)
+      clean_params[:name] = clean_params[:username]
+    end
+
+    clean_params
   end
 
   def resource_name
@@ -144,17 +184,21 @@ def terms_accepted?
   end
 
   def confirmed_or_unconfirmed_access_allowed(user)
-    user.confirmed? || Feature.enabled?(:soft_email_confirmation)
+    user.confirmed? || Feature.enabled?(:soft_email_confirmation) || experiment_enabled?(:signup_flow)
   end
 
   def stored_location_or_dashboard(user)
     stored_location_for(user) || dashboard_projects_path
   end
 
+  def stored_location_or_dashboard_or_almost_there_path(user)
+    confirmed_or_unconfirmed_access_allowed(user) ? stored_location_or_dashboard(user) : users_almost_there_path
+  end
+
   # Part of an experiment to build a new sign up flow. Will be resolved
   # with https://gitlab.com/gitlab-org/growth/engineering/issues/64
   def choose_layout
-    if helpers.use_experimental_separate_sign_up_flow?
+    if experiment_enabled?(:signup_flow)
       'devise_experimental_separate_sign_up_flow'
     else
       'devise'
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 2364777cdc5d5fe8fbc49d96cabd7965a61ebc4d..477093ddadfb96493e2e3adb06b4c9e67394e1c7 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -161,7 +161,7 @@ def count_by_state
     labels_count = label_names.any? ? label_names.count : 1
     labels_count = 1 if use_cte_for_search?
 
-    finder.execute.reorder(nil).group(:state).count.each do |key, value|
+    finder.execute.reorder(nil).group(:state_id).count.each do |key, value|
       counts[count_key(key)] += value / labels_count
     end
 
@@ -385,7 +385,8 @@ def attempt_project_search_optimizations?
   end
 
   def count_key(value)
-    Array(value).last.to_sym
+    value = Array(value).last
+    klass.available_states.key(value)
   end
 
   # Negates all params found in `negatable_params`
@@ -444,7 +445,6 @@ def by_closed_at(items)
     items
   end
 
-  # rubocop: disable CodeReuse/ActiveRecord
   def by_state(items)
     case params[:state].to_s
     when 'closed'
@@ -454,12 +454,11 @@ def by_state(items)
     when 'opened'
       items.opened
     when 'locked'
-      items.where(state: 'locked')
+      items.with_state(:locked)
     else
       items
     end
   end
-  # rubocop: enable CodeReuse/ActiveRecord
 
   def by_group(items)
     # Selection by group is already covered by `by_project` and `projects`
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index 6e2bb1ded43adfe8d139c524e081dcf628d5200c..bd6b6190fb538d3eba229e27834983e434cce056 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -41,13 +41,14 @@
 class SnippetsFinder < UnionFinder
   include FinderMethods
 
-  attr_accessor :current_user, :project, :author, :scope
+  attr_accessor :current_user, :project, :author, :scope, :explore
 
   def initialize(current_user = nil, params = {})
     @current_user = current_user
     @project = params[:project]
     @author = params[:author]
     @scope = params[:scope].to_s
+    @explore = params[:explore]
 
     if project && author
       raise(
@@ -66,13 +67,23 @@ def execute
   private
 
   def init_collection
-    if project
+    if explore
+      snippets_for_explore
+    elsif project
       snippets_for_a_single_project
     else
       snippets_for_multiple_projects
     end
   end
 
+  # Produces a query that retrieves snippets for the Explore page
+  #
+  # We only show personal snippets here because this page is meant for
+  # discovery, and project snippets are of limited interest here.
+  def snippets_for_explore
+    Snippet.public_to_user(current_user).only_personal_snippets
+  end
+
   # Produces a query that retrieves snippets from multiple projects.
   #
   # The resulting query will, depending on the user's permissions, include the
@@ -86,7 +97,7 @@ def init_collection
   # Each collection is constructed in isolation, allowing for greater control
   # over the resulting SQL query.
   def snippets_for_multiple_projects
-    queries = [global_snippets]
+    queries = [personal_snippets]
 
     if Ability.allowed?(current_user, :read_cross_project)
       queries << snippets_of_visible_projects
@@ -100,8 +111,8 @@ def snippets_for_a_single_project
     Snippet.for_project_with_user(project, current_user)
   end
 
-  def global_snippets
-    snippets_for_author_or_visible_to_user.only_global_snippets
+  def personal_snippets
+    snippets_for_author_or_visible_to_user.only_personal_snippets
   end
 
   # Returns the snippets that the current user (logged in or not) can view.
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index 2932e558a3798926a3488dcf5ee36186b2e8b0fc..2b46e51290f31d8d8a2ec37f335fcceb600b35a6 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -33,6 +33,8 @@ def initialize(current_user, params = {})
   end
 
   def execute
+    return Todo.none if current_user.nil?
+
     items = current_user.todos
     items = by_action_id(items)
     items = by_action(items)
@@ -180,11 +182,9 @@ def by_project(items)
   end
 
   def by_group(items)
-    if group?
-      items.for_group_and_descendants(group)
-    else
-      items
-    end
+    return items unless group?
+
+    items.for_group_ids_and_descendants(params[:group_id])
   end
 
   def by_state(items)
diff --git a/app/graphql/mutations/concerns/mutations/resolves_group.rb b/app/graphql/mutations/concerns/mutations/resolves_group.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4306ce512f1f4d8f7fa2952635d72644c3929b13
--- /dev/null
+++ b/app/graphql/mutations/concerns/mutations/resolves_group.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Mutations
+  module ResolvesGroup
+    extend ActiveSupport::Concern
+
+    def resolve_group(full_path:)
+      resolver.resolve(full_path: full_path)
+    end
+
+    def resolver
+      Resolvers::GroupResolver.new(object: nil, context: context)
+    end
+  end
+end
diff --git a/app/graphql/resolvers/todo_resolver.rb b/app/graphql/resolvers/todo_resolver.rb
new file mode 100644
index 0000000000000000000000000000000000000000..38a4539f34ab0e566c5ad9528aeddc823da3c2aa
--- /dev/null
+++ b/app/graphql/resolvers/todo_resolver.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+module Resolvers
+  class TodoResolver < BaseResolver
+    type Types::TodoType, null: true
+
+    alias_method :user, :object
+
+    argument :action, [Types::TodoActionEnum],
+             required: false,
+             description: 'The action to be filtered'
+
+    argument :author_id, [GraphQL::ID_TYPE],
+             required: false,
+             description: 'The ID of an author'
+
+    argument :project_id, [GraphQL::ID_TYPE],
+             required: false,
+             description: 'The ID of a project'
+
+    argument :group_id, [GraphQL::ID_TYPE],
+             required: false,
+             description: 'The ID of a group'
+
+    argument :state, [Types::TodoStateEnum],
+             required: false,
+             description: 'The state of the todo'
+
+    argument :type, [Types::TodoTargetEnum],
+             required: false,
+             description: 'The type of the todo'
+
+    def resolve(**args)
+      return Todo.none if user != context[:current_user]
+
+      TodosFinder.new(user, todo_finder_params(args)).execute
+    end
+
+    private
+
+    # TODO: Support multiple queries for e.g. state and type on TodosFinder:
+    #
+    # https://gitlab.com/gitlab-org/gitlab/merge_requests/18487
+    # https://gitlab.com/gitlab-org/gitlab/merge_requests/18518
+    #
+    # As soon as these MR's are merged, we can refactor this to query by
+    # multiple contents.
+    #
+    def todo_finder_params(args)
+      {
+        state: first_state(args),
+        type: first_type(args),
+        group_id: first_group_id(args),
+        author_id: first_author_id(args),
+        action_id: first_action(args),
+        project_id: first_project(args)
+      }
+    end
+
+    def first_project(args)
+      first_query_field(args, :project_id)
+    end
+
+    def first_action(args)
+      first_query_field(args, :action)
+    end
+
+    def first_author_id(args)
+      first_query_field(args, :author_id)
+    end
+
+    def first_group_id(args)
+      first_query_field(args, :group_id)
+    end
+
+    def first_state(args)
+      first_query_field(args, :state)
+    end
+
+    def first_type(args)
+      first_query_field(args, :type)
+    end
+
+    def first_query_field(query, field)
+      return unless query.key?(field)
+
+      query[field].first if query[field].respond_to?(:first)
+    end
+  end
+end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 1baaa33c819668af4f5529c5ad202480f6d172ea..71a65dc67132663b3a963ee9e61fe6ffb001cf69 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -55,12 +55,27 @@ class MergeRequestType < BaseObject
     field :web_url, GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
     field :upvotes, GraphQL::INT_TYPE, null: false # rubocop:disable Graphql/Descriptions
     field :downvotes, GraphQL::INT_TYPE, null: false # rubocop:disable Graphql/Descriptions
-    field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false # rubocop:disable Graphql/Descriptions
 
     field :head_pipeline, Types::Ci::PipelineType, null: true, method: :actual_head_pipeline # rubocop:disable Graphql/Descriptions
     field :pipelines, Types::Ci::PipelineType.connection_type, # rubocop:disable Graphql/Descriptions
           resolver: Resolvers::MergeRequestPipelinesResolver
 
+    field :milestone, Types::MilestoneType, description: 'The milestone this merge request is linked to',
+          null: true,
+          resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Milestone, obj.milestone_id).find }
+    field :assignees, Types::UserType.connection_type, null: true, complexity: 5, description: 'The list of assignees for the merge request'
+    field :participants, Types::UserType.connection_type, null: true, complexity: 5, description: 'The list of participants on the merge request'
+    field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false, complexity: 5,
+          description: 'Boolean flag for whether the currently logged in user is subscribed to this MR'
+    field :labels, Types::LabelType.connection_type, null: true, complexity: 5, description: 'The list of labels on the merge request'
+    field :discussion_locked, GraphQL::BOOLEAN_TYPE, description: 'Boolean flag determining if comments on the merge request are locked to members only',
+          null: false,
+          resolve: -> (obj, _args, _ctx) { !!obj.discussion_locked }
+    field :time_estimate, GraphQL::INT_TYPE, null: false, description: 'The time estimate for the merge request'
+    field :total_time_spent, GraphQL::INT_TYPE, null: false, description: 'Total time reported as spent on the merge request'
+    field :reference, GraphQL::STRING_TYPE, null: false, method: :to_reference, description: 'Internal merge request reference. Returned in shortened format by default' do
+      argument :full, GraphQL::BOOLEAN_TYPE, required: false, default_value: false, description: 'Boolean option specifying whether the reference should be returned in full'
+    end
     field :task_completion_status, Types::TaskCompletionStatus, null: false # rubocop:disable Graphql/Descriptions
   end
 end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index bbf94fb92df43f7d5900f20879a5b5f39a338a9c..996bf225976192ed464b88f789a601c9bc786dd5 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -14,6 +14,11 @@ class QueryType < ::Types::BaseObject
           resolver: Resolvers::GroupResolver,
           description: "Find a group"
 
+    field :current_user, Types::UserType,
+          null: true,
+          resolve: -> (_obj, _args, context) { context[:current_user] },
+          description: "Get information about current user"
+
     field :namespace, Types::NamespaceType,
           null: true,
           resolver: Resolvers::NamespaceResolver,
diff --git a/app/graphql/types/todo_action_enum.rb b/app/graphql/types/todo_action_enum.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0e53883847449f3c7558949b17e1e335f4eb08c4
--- /dev/null
+++ b/app/graphql/types/todo_action_enum.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Types
+  class TodoActionEnum < BaseEnum
+    value 'assigned', value: 1
+    value 'mentioned', value: 2
+    value 'build_failed', value: 3
+    value 'marked', value: 4
+    value 'approval_required', value: 5
+    value 'unmergeable', value: 6
+    value 'directly_addressed', value: 7
+  end
+end
diff --git a/app/graphql/types/todo_state_enum.rb b/app/graphql/types/todo_state_enum.rb
new file mode 100644
index 0000000000000000000000000000000000000000..29a28b5208d75ed64f692933e21cda39f6ac3658
--- /dev/null
+++ b/app/graphql/types/todo_state_enum.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Types
+  class TodoStateEnum < BaseEnum
+    value 'pending'
+    value 'done'
+  end
+end
diff --git a/app/graphql/types/todo_target_enum.rb b/app/graphql/types/todo_target_enum.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9a7391dcd9970bae1491826fa9a9f61838843727
--- /dev/null
+++ b/app/graphql/types/todo_target_enum.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Types
+  class TodoTargetEnum < BaseEnum
+    value 'Issue'
+    value 'MergeRequest'
+    value 'Epic'
+  end
+end
diff --git a/app/graphql/types/todo_type.rb b/app/graphql/types/todo_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d36daaf7dec1266fd232af789eefc3b055ff0def
--- /dev/null
+++ b/app/graphql/types/todo_type.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Types
+  class TodoType < BaseObject
+    graphql_name 'Todo'
+    description 'Representing a todo entry'
+
+    present_using TodoPresenter
+
+    authorize :read_todo
+
+    field :id, GraphQL::ID_TYPE,
+          description: 'Id of the todo',
+          null: false
+
+    field :project, Types::ProjectType,
+          description: 'The project this todo is associated with',
+          null: true,
+          authorize: :read_project,
+          resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, todo.project_id).find }
+
+    field :group, Types::GroupType,
+          description: 'Group this todo is associated with',
+          null: true,
+          authorize: :read_group,
+          resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, todo.group_id).find }
+
+    field :author, Types::UserType,
+          description: 'The owner of this todo',
+          null: false,
+          resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, todo.author_id).find }
+
+    field :action, Types::TodoActionEnum,
+          description: 'Action of the todo',
+          null: false
+
+    field :target_type, Types::TodoTargetEnum,
+          description: 'Target type of the todo',
+          null: false
+
+    field :body, GraphQL::STRING_TYPE,
+          description: 'Body of the todo',
+          null: false
+
+    field :state, Types::TodoStateEnum,
+          description: 'State of the todo',
+          null: false
+
+    field :created_at, Types::TimeType,
+          description: 'Timestamp this todo was created',
+          null: false
+  end
+end
diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb
index 9f7d2a171d651064a775ffdea728381e98f9451f..1ba37927b409273b264726ff40d0c661caa28e4d 100644
--- a/app/graphql/types/user_type.rb
+++ b/app/graphql/types/user_type.rb
@@ -12,5 +12,8 @@ class UserType < BaseObject
     field :username, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
     field :avatar_url, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
     field :web_url, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
+    field :todos, Types::TodoType.connection_type, null: false,
+          resolver: Resolvers::TodoResolver,
+          description: 'Todos of this user'
   end
 end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 5c2420e80f2b861ed9105bbff69822171b35a935..ecaeb7060c87273948012c5d8b9797086154dbc5 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -108,6 +108,11 @@ def extra_config
     Gitlab.config.extra
   end
 
+  # shortcut for gitlab registry config
+  def registry_config
+    Gitlab.config.registry
+  end
+
   # Render a `time` element with Javascript-based relative date and tooltip
   #
   # time       - Time object
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 42fe42398f10468b4c10ad88dd3a78a752c51f91..df17b82412fb4972a343b2fd4f284f3ff939638a 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -289,7 +289,10 @@ def visible_attributes
       :snowplow_collector_hostname,
       :snowplow_cookie_domain,
       :snowplow_enabled,
-      :snowplow_site_id
+      :snowplow_site_id,
+      :push_event_hooks_limit,
+      :push_event_activities_limit,
+      :custom_http_clone_url_root
     ]
   end
 
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 6d1ec16b0c28532ef655e15e2f3c87f55e0bc461..5c24b0e1704e31089c0891ce6b7e155eef701225 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -32,6 +32,14 @@ def ide_edit_path(project = @project, ref = @ref, path = @path, options = {})
     File.join(segments)
   end
 
+  def ide_fork_and_edit_path(project = @project, ref = @ref, path = @path, options = {})
+    if current_user
+      project_forks_path(project,
+                        namespace_key: current_user&.namespace&.id,
+                        continue: edit_blob_fork_params(ide_edit_path(project, ref, path)))
+    end
+  end
+
   def encode_ide_path(path)
     url_encode(path).gsub('%2F', '/')
   end
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
index a5fe6bb8f0799237edf6f18a6c55d3732b6d1c1c..2def34881843ea4670cae454ab145f028ccd38fb 100644
--- a/app/helpers/builds_helper.rb
+++ b/app/helpers/builds_helper.rb
@@ -4,12 +4,12 @@ module BuildsHelper
   def build_summary(build, skip: false)
     if build.has_trace?
       if skip
-        link_to _("View job trace"), pipeline_job_url(build.pipeline, build)
+        link_to _("View job log"), pipeline_job_url(build.pipeline, build)
       else
         build.trace.html(last_lines: 10).html_safe
       end
     else
-      _("No job trace")
+      _("No job log")
     end
   end
 
diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb
index 2b7320817ed20f2cd26c13574693d9d4d0be12d9..52f189b122fb087050fa831554ef647dcc1120cc 100644
--- a/app/helpers/environment_helper.rb
+++ b/app/helpers/environment_helper.rb
@@ -18,12 +18,16 @@ def environment_link_for_build(project, build)
     end
   end
 
+  def deployment_path(deployment)
+    [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable]
+  end
+
   def deployment_link(deployment, text: nil)
     return unless deployment
 
     link_label = text ? text : "##{deployment.iid}"
 
-    link_to link_label, [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable]
+    link_to link_label, deployment_path(deployment)
   end
 
   def last_deployment_link_for_environment_build(project, build)
@@ -32,4 +36,31 @@ def last_deployment_link_for_environment_build(project, build)
 
     deployment_link(environment.last_deployment)
   end
+
+  def render_deployment_status(deployment)
+    status = deployment.status
+
+    status_text =
+      case status
+      when 'created'
+        s_('Deployment|created')
+      when 'running'
+        s_('Deployment|running')
+      when 'success'
+        s_('Deployment|success')
+      when 'failed'
+        s_('Deployment|failed')
+      when 'canceled'
+        s_('Deployment|canceled')
+      end
+
+    klass = "ci-status ci-#{status.dasherize}"
+    text = "#{ci_icon_for_status(status)} #{status_text}".html_safe
+
+    if deployment.deployable
+      link_to(text, deployment_path(deployment), class: klass)
+    else
+      content_tag(:span, text, class: klass)
+    end
+  end
 end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index f05218efe0c36dbaf7267ea55e25e0f9588a6941..4f31cc67cccaa8ab151633bcc4fe142dc37ad2a8 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -76,10 +76,10 @@ def preview_markdown_path(parent, *args)
   end
 
   def edit_milestone_path(entity, *args)
-    if entity.parent.is_a?(Group)
-      edit_group_milestone_path(entity.parent, entity, *args)
+    if entity.resource_parent.is_a?(Group)
+      edit_group_milestone_path(entity.resource_parent, entity, *args)
     else
-      edit_project_milestone_path(entity.parent, entity, *args)
+      edit_project_milestone_path(entity.resource_parent, entity, *args)
     end
   end
 
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 9cba87ac4444438e89928a5058b84468f441b8ca..6ddcbf610903ac977ad2e0ca98577ed5bc55d334 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -15,6 +15,18 @@ def group_nav_link_paths
     %w[groups#projects groups#edit badges#index ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index]
   end
 
+  def group_packages_nav_link_paths
+    %w[
+      groups/container_registries#index
+    ]
+  end
+
+  def group_container_registry_nav?
+    Gitlab.config.registry.enabled &&
+      can?(current_user, :read_container_image, @group) &&
+      Feature.enabled?(:group_container_registry_browser, @group)
+  end
+
   def group_sidebar_links
     @group_sidebar_links ||= get_group_sidebar_links
   end
diff --git a/app/helpers/releases_helper.rb b/app/helpers/releases_helper.rb
index 3186bbd932280dd0f843aa446a14e9f002908c53..68a19152d8f8d7e65f9f7fb4720fdfe62a7fc3cf 100644
--- a/app/helpers/releases_helper.rb
+++ b/app/helpers/releases_helper.rb
@@ -19,4 +19,14 @@ def data_for_releases_page
       documentation_path: help_page
     }
   end
+
+  def data_for_edit_release_page
+    {
+      project_id: @project.id,
+      tag_name: @release.tag,
+      markdown_preview_path: preview_markdown_path(@project),
+      markdown_docs_path: help_page_path('user/markdown'),
+      releases_page_path: project_releases_path(@project, anchor: @release.tag)
+    }
+  end
 end
diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb
index 2a5a3b9eac60aff8ce11d7e25feb3da30a2c14d1..af98a611b8b23dc8e292501b6aa646e26f1d2a64 100644
--- a/app/helpers/sessions_helper.rb
+++ b/app/helpers/sessions_helper.rb
@@ -4,8 +4,4 @@ module SessionsHelper
   def unconfirmed_email?
     flash[:alert] == t(:unconfirmed, scope: [:devise, :failure])
   end
-
-  def use_experimental_separate_sign_up_flow?
-    ::Gitlab.dev_env_or_com? && Feature.enabled?(:experimental_separate_sign_up_flow)
-  end
 end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index a919c068c426b275be3609009e8617718e9eda7d..dce0842060dd970d7a52d8f79ab4827e5a56daa9 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -45,8 +45,8 @@ def todo_target_title(todo)
   end
 
   def todo_parent_path(todo)
-    if todo.parent.is_a?(Group)
-      link_to todo.parent.name, group_path(todo.parent)
+    if todo.resource_parent.is_a?(Group)
+      link_to todo.resource_parent.name, group_path(todo.resource_parent)
     else
       link_to_project(todo.project)
     end
@@ -64,7 +64,7 @@ def todo_target_path(todo)
     if todo.for_commit?
       project_commit_path(todo.project, todo.target, path_options)
     else
-      path = [todo.parent, todo.target]
+      path = [todo.resource_parent, todo.target]
 
       path.unshift(:pipelines) if todo.build_failed?
 
diff --git a/app/mailers/emails/releases.rb b/app/mailers/emails/releases.rb
new file mode 100644
index 0000000000000000000000000000000000000000..137858d31e85a8330a4ad048d23b586a44ddabd5
--- /dev/null
+++ b/app/mailers/emails/releases.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Emails
+  module Releases
+    def new_release_email(user_id, release, reason = nil)
+      @release = release
+      @project = @release.project
+      @target_url = namespace_project_releases_url(
+        namespace_id: @project.namespace,
+        project_id: @project
+      )
+
+      user = User.find(user_id)
+
+      mail(
+        to: user.notification_email_for(@project.group),
+        subject: subject(release_email_subject)
+      )
+    end
+
+    private
+
+    def release_email_subject
+      release_info = [@release.name, @release.tag].select(&:presence).join(' - ')
+      "New release: #{release_info}"
+    end
+  end
+end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index d0b43b4397f700a392c0845e340ef58b3723ff01..c7cfefeec9b07be1c60d975faf4e9ed4319106ef 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -16,6 +16,7 @@ class Notify < BaseMailer
   include Emails::Members
   include Emails::AutoDevops
   include Emails::RemoteMirrors
+  include Emails::Releases
 
   helper MilestonesHelper
   helper MergeRequestsHelper
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 02f214341fb7db182c29a722b2878f649a957f1e..a07933d4975cbed96ba0271a3a0c268963844f84 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -214,6 +214,12 @@ class ApplicationSetting < ApplicationRecord
             length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') },
             allow_nil: false
 
+  validates :push_event_hooks_limit,
+            numericality: { greater_than_or_equal_to: 0 }
+
+  validates :push_event_activities_limit,
+            numericality: { greater_than_or_equal_to: 0 }
+
   SUPPORTED_KEY_TYPES.each do |type|
     validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
   end
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index e2579316fdda21d7f0ffe7db98ecb12387e50631..0c0ffb67c9ab43228641ca4a470244762acb6e6e 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -82,6 +82,8 @@ def defaults
         polling_interval_multiplier: 1,
         project_export_enabled: true,
         protected_ci_variables: false,
+        push_event_hooks_limit: 3,
+        push_event_activities_limit: 3,
         raw_blob_request_limit: 300,
         recaptcha_enabled: false,
         login_recaptcha_protection_enabled: false,
@@ -126,7 +128,8 @@ def defaults
         snowplow_collector_hostname: nil,
         snowplow_cookie_domain: nil,
         snowplow_enabled: false,
-        snowplow_site_id: nil
+        snowplow_site_id: nil,
+        custom_http_clone_url_root: nil
       }
     end
 
diff --git a/app/models/aws/role.rb b/app/models/aws/role.rb
new file mode 100644
index 0000000000000000000000000000000000000000..836107435ad8bd50d4364217460ae897a147f2a7
--- /dev/null
+++ b/app/models/aws/role.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Aws
+  class Role < ApplicationRecord
+    self.table_name = 'aws_roles'
+
+    belongs_to :user, inverse_of: :aws_role
+
+    validates :role_external_id, uniqueness: true, length: { in: 1..64 }
+    validates :role_arn,
+      length: 1..2048,
+      format: {
+        with: Gitlab::Regex.aws_arn_regex,
+        message: Gitlab::Regex.aws_arn_regex_message
+      }
+  end
+end
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 1495aed65989d0585e1b73cd9b994c0dbc389aa6..cc089715b06c3b00db06749eb9e3cf7113dd39ac 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -32,6 +32,7 @@ class Blob < SimpleDelegator
     BlobViewer::Balsamiq,
 
     BlobViewer::Video,
+    BlobViewer::Audio,
 
     BlobViewer::PDF,
 
diff --git a/app/models/blob_viewer/audio.rb b/app/models/blob_viewer/audio.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cc7fe3b0d903ba7ef09dbfe3136d0046e5c74cf3
--- /dev/null
+++ b/app/models/blob_viewer/audio.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module BlobViewer
+  class Audio < Base
+    include Rich
+    include ClientSide
+
+    self.partial_name = 'audio'
+    self.extensions = UploaderHelper::SAFE_AUDIO_EXT
+    self.binary = true
+  end
+end
diff --git a/app/models/blob_viewer/video.rb b/app/models/blob_viewer/video.rb
index d35b8e7342e542210d6f3757d14fe2f99bae2125..3ec4e90b24e27c7af672a27777fe2f073631398b 100644
--- a/app/models/blob_viewer/video.rb
+++ b/app/models/blob_viewer/video.rb
@@ -8,7 +8,5 @@ class Video < Base
     self.partial_name = 'video'
     self.extensions = UploaderHelper::SAFE_VIDEO_EXT
     self.binary = true
-    self.switcher_icon = 'film'
-    self.switcher_title = 'video'
   end
 end
diff --git a/app/models/board.rb b/app/models/board.rb
index 31011dc4742801123512a2f41d61a5de0a80ff87..f3f938224a4462678dfb2c03d8889f55336482d3 100644
--- a/app/models/board.rb
+++ b/app/models/board.rb
@@ -16,10 +16,9 @@ def project_needed?
     !group
   end
 
-  def parent
-    @parent ||= group || project
+  def resource_parent
+    @resource_parent ||= group || project
   end
-  alias_method :resource_parent, :parent
 
   def group_board?
     group_id.present?
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 3dacd6a6224b67248e60c29767a000b75b1162c3..4089fcf70971bb1f14eae7482b6aa8c323373c30 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -42,6 +42,7 @@ class Build < CommitStatus
 
     has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent
     has_many :job_variables, class_name: 'Ci::JobVariable', foreign_key: :job_id
+    has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_job_id
 
     Ci::JobArtifact.file_types.each do |key, value|
       has_one :"job_artifacts_#{key}", -> { where(file_type: value) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
@@ -117,6 +118,11 @@ def persisted_environment
 
     scope :eager_load_job_artifacts, -> { includes(:job_artifacts) }
 
+    scope :with_exposed_artifacts, -> do
+      joins(:metadata).merge(Ci::BuildMetadata.with_exposed_artifacts)
+        .includes(:metadata, :job_artifacts_metadata)
+    end
+
     scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
     scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) }
     scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
@@ -594,6 +600,14 @@ def erase_old_trace!
       update_column(:trace, nil)
     end
 
+    def artifacts_expose_as
+      options.dig(:artifacts, :expose_as)
+    end
+
+    def artifacts_paths
+      options.dig(:artifacts, :paths)
+    end
+
     def needs_touch?
       Time.now - updated_at > 15.minutes.to_i
     end
@@ -753,6 +767,10 @@ def valid_dependency?
       true
     end
 
+    def invalid_dependencies
+      dependencies.reject(&:valid_dependency?)
+    end
+
     def runner_required_feature_names
       strong_memoize(:runner_required_feature_names) do
         RUNNER_FEATURES.select do |feature, method|
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index 3097e40dd3b7df4761dcc57651e9c0ab7471c42d..0df5ebfe843be0a13cb2577f605c3cd136d1bd1f 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -27,6 +27,7 @@ class BuildMetadata < ApplicationRecord
 
     scope :scoped_build, -> { where('ci_builds_metadata.build_id = ci_builds.id') }
     scope :with_interruptible, -> { where(interruptible: true) }
+    scope :with_exposed_artifacts, -> { where(has_exposed_artifacts: true) }
 
     enum timeout_source: {
         unknown_timeout_source: 1,
diff --git a/app/models/ci/build_trace.rb b/app/models/ci/build_trace.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b9db1559836e29136602a7ebf5fc1b410636fe70
--- /dev/null
+++ b/app/models/ci/build_trace.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Ci
+  class BuildTrace
+    CONVERTERS = {
+      html: Gitlab::Ci::Ansi2html,
+      json: Gitlab::Ci::Ansi2json
+    }.freeze
+
+    attr_reader :trace, :build
+
+    delegate :state, :append, :truncated, :offset, :size, :total, to: :trace, allow_nil: true
+    delegate :id, :status, :complete?, to: :build, prefix: true
+
+    def initialize(build:, stream:, state:, content_format:)
+      @build = build
+      @content_format = content_format
+
+      if stream.valid?
+        stream.limit
+        @trace = CONVERTERS.fetch(content_format).convert(stream.stream, state)
+      end
+    end
+
+    def json?
+      @content_format == :json
+    end
+
+    def html?
+      @content_format == :html
+    end
+
+    def json_lines
+      @trace&.lines if json?
+    end
+
+    def html_lines
+      @trace&.html if html?
+    end
+  end
+end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index b34fd3f1ec96c5626697fe997bffef51d397cd72..913253e4e9225f647ea437f4108b7164b6a52cce 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -52,9 +52,15 @@ class Pipeline < ApplicationRecord
 
     has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id'
     has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
+    has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_pipeline_id
 
+    has_one :source_pipeline, class_name: 'Ci::Sources::Pipeline', inverse_of: :pipeline
     has_one :chat_data, class_name: 'Ci::PipelineChatData'
 
+    has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline
+    has_one :triggered_by_pipeline, through: :source_pipeline, source: :source_pipeline
+    has_one :source_job, through: :source_pipeline, source: :source_job
+
     accepts_nested_attributes_for :variables, reject_if: :persisted?
 
     delegate :id, to: :project, prefix: true
@@ -211,6 +217,8 @@ class Pipeline < ApplicationRecord
     scope :for_sha, -> (sha) { where(sha: sha) }
     scope :for_source_sha, -> (source_sha) { where(source_sha: source_sha) }
     scope :for_sha_or_source_sha, -> (sha) { for_sha(sha).or(for_source_sha(sha)) }
+    scope :for_ref, -> (ref) { where(ref: ref) }
+    scope :for_id, -> (id) { where(id: id) }
     scope :created_after, -> (time) { where('ci_pipelines.created_at > ?', time) }
 
     scope :triggered_by_merge_request, -> (merge_request) do
@@ -775,6 +783,10 @@ def test_reports
       end
     end
 
+    def has_exposed_artifacts?
+      complete? && builds.latest.with_exposed_artifacts.exists?
+    end
+
     def branch_updated?
       strong_memoize(:branch_updated) do
         push_details.branch_updated?
diff --git a/app/models/ci/pipeline_enums.rb b/app/models/ci/pipeline_enums.rb
index cb92aef4bdac36c527f2f8ad668eafa55d81a52a..859abc4a0d558b58d6ad5890f1d4b1e122962100 100644
--- a/app/models/ci/pipeline_enums.rb
+++ b/app/models/ci/pipeline_enums.rb
@@ -22,6 +22,7 @@ def self.sources
         schedule: 4,
         api: 5,
         external: 6,
+        pipeline: 7,
         chat: 8,
         merge_request_event: 10,
         external_pull_request_event: 11
diff --git a/ee/app/models/ci/sources/pipeline.rb b/app/models/ci/sources/pipeline.rb
similarity index 90%
rename from ee/app/models/ci/sources/pipeline.rb
rename to app/models/ci/sources/pipeline.rb
index 94e49e686915d5f438945b99e575cbcbbeb99af8..feaec27281c9ac65485cb768856b55c6cdee1a25 100644
--- a/ee/app/models/ci/sources/pipeline.rb
+++ b/app/models/ci/sources/pipeline.rb
@@ -10,7 +10,6 @@ class Pipeline < ApplicationRecord
 
       belongs_to :source_project, class_name: "Project", foreign_key: :source_project_id
       belongs_to :source_job, class_name: "CommitStatus", foreign_key: :source_job_id
-      belongs_to :source_bridge, class_name: "Ci::Bridge", foreign_key: :source_job_id
       belongs_to :source_pipeline, class_name: "Ci::Pipeline", foreign_key: :source_pipeline_id
 
       validates :project, presence: true
@@ -22,3 +21,5 @@ class Pipeline < ApplicationRecord
     end
   end
 end
+
+::Ci::Sources::Pipeline.prepend_if_ee('::EE::Ci::Sources::Pipeline')
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index c49d5063f8bf6652aaeaafd005296a173e8b35d0..d6f5d7c3f93b3217f23d990c5d86dd42026da959 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -35,6 +35,7 @@ class Cluster < ApplicationRecord
 
     # we force autosave to happen when we save `Cluster` model
     has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
+    has_one :provider_aws, class_name: 'Clusters::Providers::Aws', autosave: true
 
     has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', inverse_of: :cluster, autosave: true
 
@@ -96,14 +97,20 @@ def self.has_one_cluster_application(name) # rubocop:disable Naming/PredicateNam
 
     enum provider_type: {
       user: 0,
-      gcp: 1
+      gcp: 1,
+      aws: 2
     }
 
     scope :enabled, -> { where(enabled: true) }
     scope :disabled, -> { where(enabled: false) }
-    scope :user_provided, -> { where(provider_type: ::Clusters::Cluster.provider_types[:user]) }
-    scope :gcp_provided, -> { where(provider_type: ::Clusters::Cluster.provider_types[:gcp]) }
+
+    scope :user_provided, -> { where(provider_type: :user) }
+    scope :gcp_provided, -> { where(provider_type: :gcp) }
+    scope :aws_provided, -> { where(provider_type: :aws) }
+
     scope :gcp_installed, -> { gcp_provided.joins(:provider_gcp).merge(Clusters::Providers::Gcp.with_status(:created)) }
+    scope :aws_installed, -> { aws_provided.joins(:provider_aws).merge(Clusters::Providers::Aws.with_status(:created)) }
+
     scope :managed, -> { where(managed: true) }
 
     scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
@@ -140,7 +147,11 @@ def applications
     end
 
     def provider
-      return provider_gcp if gcp?
+      if gcp?
+        provider_gcp
+      elsif aws?
+        provider_aws
+      end
     end
 
     def platform
diff --git a/app/models/clusters/concerns/application_version.rb b/app/models/clusters/concerns/application_version.rb
index db94e8e08c93251aec72e597cff67732709e1bfe..6c0b014662cd0d90152c5fd283d4596e443b58ee 100644
--- a/app/models/clusters/concerns/application_version.rb
+++ b/app/models/clusters/concerns/application_version.rb
@@ -8,13 +8,13 @@ module ApplicationVersion
       included do
         state_machine :status do
           before_transition any => [:installed, :updated] do |application|
-            application.version = application.class.const_get(:VERSION)
+            application.version = application.class.const_get(:VERSION, false)
           end
         end
       end
 
       def update_available?
-        version != self.class.const_get(:VERSION)
+        version != self.class.const_get(:VERSION, false)
       end
     end
   end
diff --git a/app/models/clusters/concerns/provider_status.rb b/app/models/clusters/concerns/provider_status.rb
index 4d1974777eae5b456430ecb65018aaa3026a12ab..2da1ee7aabbef53183dc2f4db724223b5b985fd5 100644
--- a/app/models/clusters/concerns/provider_status.rb
+++ b/app/models/clusters/concerns/provider_status.rb
@@ -42,6 +42,10 @@ module ProviderStatus
         def on_creation?
           scheduled? || creating?
         end
+
+        def assign_operation_id(_)
+          # Implemented by individual providers if operation ID is supported.
+        end
       end
     end
   end
diff --git a/app/models/clusters/providers/aws.rb b/app/models/clusters/providers/aws.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ae4156896bc5adf7d09b783c074af43463ec4f95
--- /dev/null
+++ b/app/models/clusters/providers/aws.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Clusters
+  module Providers
+    class Aws < ApplicationRecord
+      include Clusters::Concerns::ProviderStatus
+
+      self.table_name = 'cluster_providers_aws'
+
+      belongs_to :cluster, inverse_of: :provider_aws, class_name: 'Clusters::Cluster'
+      belongs_to :created_by_user, class_name: 'User'
+
+      default_value_for :region, 'us-east-1'
+      default_value_for :num_nodes, 3
+      default_value_for :instance_type, 'm5.large'
+
+      attr_encrypted :secret_access_key,
+        mode: :per_attribute_iv,
+        key: Settings.attr_encrypted_db_key_base_truncated,
+        algorithm: 'aes-256-gcm'
+
+      validates :role_arn,
+        length: 1..2048,
+        format: {
+          with: Gitlab::Regex.aws_arn_regex,
+          message: Gitlab::Regex.aws_arn_regex_message
+        }
+
+      validates :num_nodes,
+        numericality: {
+          only_integer: true,
+          greater_than: 0
+        }
+
+      validates :key_name, :region, :instance_type, :security_group_id, length: { in: 1..255 }
+      validates :subnet_ids, presence: true
+
+      def nullify_credentials
+        assign_attributes(
+          access_key_id: nil,
+          secret_access_key: nil,
+          session_token: nil
+        )
+      end
+    end
+  end
+end
diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb
index 7a7e485a95a42a0f305b6734247461cc88dac2a3..64df265dc25303ea8bd2df2b3155571353c41f84 100644
--- a/app/models/concerns/atomic_internal_id.rb
+++ b/app/models/concerns/atomic_internal_id.rb
@@ -57,8 +57,7 @@ def has_internal_id(column, scope:, init:, ensure_if: nil, presence: true) # rub
       end
 
       define_method("track_#{scope}_#{column}!") do
-        iid_always_track = Feature.enabled?(:iid_always_track, default_enabled: true)
-        return unless @internal_id_needs_tracking || iid_always_track
+        return unless @internal_id_needs_tracking
 
         scope_value = internal_id_read_scope(scope)
         return unless scope_value
diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb
index 91dda8030316abc6f1bc5ca2d4914c731ee1c485..49d6f3d399c46b43d04b7812d71009c215a433a8 100644
--- a/app/models/concerns/ci/contextable.rb
+++ b/app/models/concerns/ci/contextable.rb
@@ -78,6 +78,7 @@ def predefined_variables # rubocop:disable Metrics/AbcSize
         variables.append(key: "CI_JOB_MANUAL", value: 'true') if action?
         variables.append(key: "CI_NODE_INDEX", value: self.options[:instance].to_s) if self.options&.include?(:instance)
         variables.append(key: "CI_NODE_TOTAL", value: (self.options&.dig(:parallel) || 1).to_s)
+        variables.append(key: "CI_DEFAULT_BRANCH", value: project.default_branch)
         variables.concat(legacy_variables)
       end
     end
diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb
index a0ca8a34c6d255e2dd0d3a8c10191cc3e95a4298..17d431bacf2808399010b2c90ad8da2df6f26e4f 100644
--- a/app/models/concerns/ci/metadatable.rb
+++ b/app/models/concerns/ci/metadatable.rb
@@ -16,6 +16,7 @@ module Metadatable
 
       delegate :timeout, to: :metadata, prefix: true, allow_nil: true
       delegate :interruptible, to: :metadata, prefix: false, allow_nil: true
+      delegate :has_exposed_artifacts?, to: :metadata, prefix: false, allow_nil: true
       before_create :ensure_metadata
     end
 
@@ -45,6 +46,9 @@ def yaml_variables
 
     def options=(value)
       write_metadata_attribute(:options, :config_options, value)
+
+      # Store presence of exposed artifacts in build metadata to make it easier to query
+      ensure_metadata.has_exposed_artifacts = value&.dig(:artifacts, :expose_as).present?
     end
 
     def yaml_variables=(value)
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 74cbdb29abdddc700b1af232589c59ec9f13d7c4..852576dbbc2ff0fe0db40ebb96d969f33958c523 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -25,11 +25,19 @@ module Issuable
   include UpdatedAtFilterable
   include IssuableStates
   include ClosedAtFilterable
+  include VersionedDescription
 
   TITLE_LENGTH_MAX = 255
   TITLE_HTML_LENGTH_MAX = 800
-  DESCRIPTION_LENGTH_MAX = 16000
-  DESCRIPTION_HTML_LENGTH_MAX = 48000
+  DESCRIPTION_LENGTH_MAX = 1.megabyte
+  DESCRIPTION_HTML_LENGTH_MAX = 5.megabytes
+
+  STATE_ID_MAP = {
+    opened: 1,
+    closed: 2,
+    merged: 3,
+    locked: 4
+  }.with_indifferent_access.freeze
 
   # This object is used to gather issuable meta data for displaying
   # upvotes, downvotes, notes and closing merge requests count for issues and merge requests
@@ -172,13 +180,17 @@ def search(query)
       fuzzy_search(query, [:title])
     end
 
-    # Available state values persisted in state_id column using state machine
+    def available_states
+      @available_states ||= STATE_ID_MAP.slice(*available_state_names)
+    end
+
+    # Available state names used to persist state_id column using state machine
     #
     # Override this on subclasses if different states are needed
     #
-    # Check MergeRequest.available_states for example
-    def available_states
-      @available_states ||= { opened: 1, closed: 2 }.with_indifferent_access
+    # Check MergeRequest.available_states_names for example
+    def available_state_names
+      [:opened, :closed]
     end
 
     # Searches for records with a matching title or description.
@@ -297,6 +309,14 @@ def parent_class
     end
   end
 
+  def state
+    self.class.available_states.key(state_id)
+  end
+
+  def state=(value)
+    self.state_id = self.class.available_states[value]
+  end
+
   def resource_parent
     project
   end
diff --git a/app/models/concerns/issuable_states.rb b/app/models/concerns/issuable_states.rb
index 33bc41d7f443eb6f4d2af742bd20ea6f18fdbe74..f0b9f0d1f3a8961848cd6c90869fe19fe1da82d1 100644
--- a/app/models/concerns/issuable_states.rb
+++ b/app/models/concerns/issuable_states.rb
@@ -4,22 +4,20 @@ module IssuableStates
   extend ActiveSupport::Concern
 
   # The state:string column is being migrated to state_id:integer column
-  # This is a temporary hook to populate state_id column with new values
-  # and should be removed after the state column is removed.
-  # Check https://gitlab.com/gitlab-org/gitlab-foss/issues/51789 for more information
+  # This is a temporary hook to keep state column in sync until it is removed.
+  # Check https: https://gitlab.com/gitlab-org/gitlab/issues/33814 for more information
+  # The state column can be safely removed after 2019-10-27
   included do
-    before_save :set_state_id
+    before_save :sync_issuable_deprecated_state
   end
 
-  def set_state_id
-    return if state.nil? || state.empty?
+  def sync_issuable_deprecated_state
+    return if self.is_a?(Epic)
+    return unless respond_to?(:state)
+    return if state_id.nil?
 
-    # Needed to prevent breaking some migration specs that
-    # rollback database to a point where state_id does not exist.
-    # We can use this guard clause for now since this file will
-    # be removed in the next release.
-    return unless self.has_attribute?(:state_id)
+    deprecated_state = self.class.available_states.key(state_id)
 
-    self.state_id = self.class.available_states[state]
+    self.write_attribute(:state, deprecated_state)
   end
 end
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 3deb86da6cf141590c7e3e583e9aee6208db502e..42b370990ac1bfdc6e8bf366274fc47a5f4bb63e 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -6,7 +6,9 @@ def total_issues_count(user)
   end
 
   def closed_issues_count(user)
-    count_issues_by_state(user)['closed'].to_i
+    closed_state_id = Issue.available_states[:closed]
+
+    count_issues_by_state(user)[closed_state_id].to_i
   end
 
   def complete?(user)
@@ -117,7 +119,7 @@ def human_total_issue_time_estimate
 
   def count_issues_by_state(user)
     memoize_per_user(user, :count_issues_by_state) do
-      issues_visible_to_user(user).reorder(nil).group(:state).count
+      issues_visible_to_user(user).reorder(nil).group(:state_id).count
     end
   end
 
diff --git a/app/models/concerns/prometheus_adapter.rb b/app/models/concerns/prometheus_adapter.rb
index aab0589f7ca1afcbcc7bfd5de4b7482976bf5337..9df77b565da9ccad1b025caf3f10b5f81b175d76 100644
--- a/app/models/concerns/prometheus_adapter.rb
+++ b/app/models/concerns/prometheus_adapter.rb
@@ -44,7 +44,7 @@ def calculate_reactive_cache(query_class_name, *args)
     end
 
     def query_klass_for(query_name)
-      Gitlab::Prometheus::Queries.const_get("#{query_name.to_s.classify}Query")
+      Gitlab::Prometheus::Queries.const_get("#{query_name.to_s.classify}Query", false)
     end
 
     def build_query_args(*args)
diff --git a/app/models/concerns/stepable.rb b/app/models/concerns/stepable.rb
index d00a049a0043fe2ff110f30151296e7edd4ab42a..dea241c5dbe9e25f40cd898af332f8cc8ec2336c 100644
--- a/app/models/concerns/stepable.rb
+++ b/app/models/concerns/stepable.rb
@@ -11,15 +11,15 @@ def execute_steps
     initial_result = {}
 
     steps.inject(initial_result) do |previous_result, callback|
-      result = method(callback).call
+      result = method(callback).call(previous_result)
 
-      if result[:status] == :error
-        result[:failed_step] = callback
+      if result[:status] != :success
+        result[:last_step] = callback
 
         break result
       end
 
-      previous_result.merge(result)
+      result
     end
   end
 
diff --git a/app/models/concerns/versioned_description.rb b/app/models/concerns/versioned_description.rb
new file mode 100644
index 0000000000000000000000000000000000000000..63a24aadc8ad064ef309ab7ad12b7b4ec83514b9
--- /dev/null
+++ b/app/models/concerns/versioned_description.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module VersionedDescription
+  extend ActiveSupport::Concern
+
+  included do
+    attr_accessor :saved_description_version
+
+    has_many :description_versions
+
+    after_update :save_description_version
+  end
+
+  private
+
+  def save_description_version
+    self.saved_description_version = nil
+
+    return unless Feature.enabled?(:save_description_versions, issuing_parent)
+    return unless saved_change_to_description?
+
+    unless description_versions.exists?
+      description_versions.create!(
+        description: description_before_last_save,
+        created_at: created_at
+      )
+    end
+
+    self.saved_description_version = description_versions.create!(description: description)
+  end
+end
diff --git a/app/models/concerns/worker_attributes.rb b/app/models/concerns/worker_attributes.rb
new file mode 100644
index 0000000000000000000000000000000000000000..af40e9e3b19fed9425badcf3bdce4c2b03e9d0ab
--- /dev/null
+++ b/app/models/concerns/worker_attributes.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module WorkerAttributes
+  extend ActiveSupport::Concern
+
+  class_methods do
+    def feature_category(value)
+      raise "Invalid category. Use `feature_category_not_owned!` to mark a worker as not owned" if value == :not_owned
+
+      worker_attributes[:feature_category] = value
+    end
+
+    # Special case: mark this work as not associated with a feature category
+    # this should be used for cross-cutting concerns, such as mailer workers.
+    def feature_category_not_owned!
+      worker_attributes[:feature_category] = :not_owned
+    end
+
+    def get_feature_category
+      get_worker_attribute(:feature_category)
+    end
+
+    def feature_category_not_owned?
+      get_worker_attribute(:feature_category) == :not_owned
+    end
+
+    protected
+
+    # Returns a worker attribute declared on this class or its parent class.
+    # This approach allows declared attributes to be inherited by
+    # child classes.
+    def get_worker_attribute(name)
+      worker_attributes[name] || superclass_worker_attributes(name)
+    end
+
+    private
+
+    def worker_attributes
+      @attributes ||= {}
+    end
+
+    def superclass_worker_attributes(name)
+      return unless superclass.include? WorkerAttributes
+
+      superclass.get_worker_attribute(name)
+    end
+  end
+end
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 583e23d127471e8ae287c84dbf49a5e74210c160..27bb76835c73599ff21dd42e89dccd5d4088c903 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -11,6 +11,7 @@ class ContainerRepository < ApplicationRecord
   delegate :client, to: :registry
 
   scope :ordered, -> { order(:name) }
+  scope :with_api_entity_associations, -> { preload(:project) }
 
   # rubocop: disable CodeReuse/ServiceClass
   def registry
@@ -67,11 +68,9 @@ def root_repository?
   def delete_tags!
     return unless has_tags?
 
-    digests = tags.map { |tag| tag.digest }.to_set
+    digests = tags.map { |tag| tag.digest }.compact.to_set
 
-    digests.all? do |digest|
-      delete_tag_by_digest(digest)
-    end
+    digests.map(&method(:delete_tag_by_digest)).all?
   end
 
   def delete_tag_by_digest(digest)
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 30694313f7aca77fbf5e533e8925654b030dd7d4..7ccd5e98360d17f1fe8a2712a32226131c821502 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -9,7 +9,7 @@ class Deployment < ApplicationRecord
   belongs_to :environment, required: true
   belongs_to :cluster, class_name: 'Clusters::Cluster', optional: true
   belongs_to :user
-  belongs_to :deployable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
+  belongs_to :deployable, polymorphic: true, optional: true # rubocop:disable Cop/PolymorphicAssociations
 
   has_internal_id :iid, scope: :project, init: ->(s) do
     Deployment.where(project: s.project).maximum(:iid) if s&.project
@@ -22,6 +22,8 @@ class Deployment < ApplicationRecord
 
   scope :for_environment, -> (environment) { where(environment_id: environment) }
 
+  scope :visible, -> { where(status: %i[running success failed canceled]) }
+
   state_machine :status, initial: :created do
     event :run do
       transition created: :running
@@ -73,6 +75,10 @@ def self.last_for_environment(environment)
     find(ids)
   end
 
+  def self.find_successful_deployment!(iid)
+    success.find_by!(iid: iid)
+  end
+
   def commit
     project.commit(sha)
   end
diff --git a/app/models/description_version.rb b/app/models/description_version.rb
new file mode 100644
index 0000000000000000000000000000000000000000..abab7f9421208a16e725d323639301d2a46f7e3a
--- /dev/null
+++ b/app/models/description_version.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class DescriptionVersion < ApplicationRecord
+  belongs_to :issue
+  belongs_to :merge_request
+
+  validate :exactly_one_issuable
+
+  def self.issuable_attrs
+    %i(issue merge_request).freeze
+  end
+
+  private
+
+  def exactly_one_issuable
+    issuable_count = self.class.issuable_attrs.count { |attr| self["#{attr}_id"] }
+
+    errors.add(:base, "Exactly one of #{self.class.issuable_attrs.join(', ')} is required") if issuable_count != 1
+  end
+end
+
+DescriptionVersion.prepend_if_ee('EE::DescriptionVersion')
diff --git a/app/models/environment.rb b/app/models/environment.rb
index fe438b142b29b0e384ac8002482446e2df2de2a9..af0c219d9a01e22bc8c4b453baf1b34a63c2e0a4 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -6,7 +6,8 @@ class Environment < ApplicationRecord
 
   belongs_to :project, required: true
 
-  has_many :deployments, -> { success }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+  has_many :deployments, -> { visible }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+  has_many :successful_deployments, -> { success }, class_name: 'Deployment'
 
   has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment'
 
@@ -81,6 +82,10 @@ def self.pluck_names
     pluck(:name)
   end
 
+  def self.find_or_create_by_name(name)
+    find_or_create_by(name: name)
+  end
+
   def predefined_variables
     Gitlab::Ci::Variables::Collection.new
       .append(key: 'CI_ENVIRONMENT_NAME', value: name)
diff --git a/app/models/evidence.rb b/app/models/evidence.rb
new file mode 100644
index 0000000000000000000000000000000000000000..69a00f1cb3f636515cd8b5831c40e9edcbad1f9d
--- /dev/null
+++ b/app/models/evidence.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class Evidence < ApplicationRecord
+  include ShaAttribute
+
+  belongs_to :release
+
+  before_validation :generate_summary_and_sha
+
+  default_scope { order(created_at: :asc) }
+
+  sha_attribute :summary_sha
+
+  def milestones
+    @milestones ||= release.milestones.includes(:issues)
+  end
+
+  private
+
+  def generate_summary_and_sha
+    summary = Evidences::EvidenceSerializer.new.represent(self) # rubocop: disable CodeReuse/Serializer
+    return unless summary
+
+    self.summary = summary
+    self.summary_sha = Gitlab::CryptoHelper.sha256(summary)
+  end
+end
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index 1d553fc8312b34608b61f3b39e97ca974f553a70..7d766e1f25c81745e53ed9da453a76179d926afe 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -11,7 +11,7 @@ class GlobalMilestone
 
   delegate :title, :state, :due_date, :start_date, :participants, :project,
            :group, :expires_at, :closed?, :iid, :group_milestone?, :safe_title,
-           :milestoneish_id, :parent, to: :milestone
+           :milestoneish_id, :resource_parent, to: :milestone
 
   def to_hash
     {
diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb
index 46cac1d41bb6bac4a4016efc38d005babf34bf4b..0c36e51120f6d4fe93ca673ebb6ff303f5330ca3 100644
--- a/app/models/gpg_signature.rb
+++ b/app/models/gpg_signature.rb
@@ -23,6 +23,8 @@ class GpgSignature < ApplicationRecord
   validates :project_id, presence: true
   validates :gpg_key_primary_keyid, presence: true
 
+  scope :by_commit_sha, ->(shas) { where(commit_sha: shas) }
+
   def self.with_key_and_subkeys(gpg_key)
     subkey_ids = gpg_key.subkeys.pluck(:id)
 
diff --git a/app/models/group.rb b/app/models/group.rb
index 0501fe944409d35f841cb031c29620407c252a5a..042201ffa1451b32e7619cafb2b3a189b7646867 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -259,6 +259,10 @@ def has_maintainer?(user)
     members_with_parents.maintainers.exists?(user_id: user)
   end
 
+  def has_container_repositories?
+    container_repositories.exists?
+  end
+
   # @deprecated
   alias_method :has_master?, :has_maintainer?
 
@@ -469,6 +473,12 @@ def visibility_level_allowed_by_sub_groups
 
     errors.add(:visibility_level, "#{visibility} is not allowed since there are sub-groups with higher visibility.")
   end
+
+  def self.groups_including_descendants_by(group_ids)
+    Gitlab::ObjectHierarchy
+      .new(Group.where(id: group_ids))
+      .base_and_descendants
+  end
 end
 
 Group.prepend_if_ee('EE::Group')
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 16fc7fdbd484271ea3f46efd50106526415e56b3..e51b1c41059d2334d460338e98be8e4d5803b270 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -13,7 +13,7 @@ class WebHook < ApplicationRecord
                  algorithm: 'aes-256-gcm',
                  key:       Settings.attr_encrypted_db_key_base_32
 
-  has_many :web_hook_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+  has_many :web_hook_logs
 
   validates :url, presence: true
   validates :url, public_url: true, unless: ->(hook) { hook.is_a?(SystemHook) }
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
index 946a3aafe18d9a3af6981c6d3d9908249b3435fe..8d3eeaf2461646a0379ac473c5c2eafb41a15a33 100644
--- a/app/models/internal_id.rb
+++ b/app/models/internal_id.rb
@@ -54,7 +54,7 @@ def update_and_save(&block)
     last_value
   end
 
-  # Temporary instrumentation to track for-update locks
+  # Instrumentation to track for-update locks
   def update_and_save_counter
     strong_memoize(:update_and_save_counter) do
       Gitlab::Metrics.counter(:gitlab_internal_id_for_update_lock, 'Number of ROW SHARE (FOR UPDATE) locks on individual records from internal_ids')
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 86672cbf29dfb1947309fb1b30a8a84943599144..497e67cebab49fdd669f640a8996652ab02d804f 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -72,7 +72,7 @@ class Issue < ApplicationRecord
   attr_spammable :title, spam_title: true
   attr_spammable :description, spam_description: true
 
-  state_machine :state, initial: :opened do
+  state_machine :state_id, initial: :opened do
     event :close do
       transition [:opened] => :closed
     end
@@ -81,8 +81,8 @@ class Issue < ApplicationRecord
       transition closed: :opened
     end
 
-    state :opened
-    state :closed
+    state :opened, value: Issue.available_states[:opened]
+    state :closed, value: Issue.available_states[:closed]
 
     before_transition any => :closed do |issue|
       issue.closed_at = issue.system_note_timestamp
@@ -94,6 +94,13 @@ class Issue < ApplicationRecord
     end
   end
 
+  # Alias to state machine .with_state_id method
+  # This needs to be defined after the state machine block to avoid errors
+  class << self
+    alias_method :with_state, :with_state_id
+    alias_method :with_states, :with_state_ids
+  end
+
   def self.relative_positioning_query_base(issue)
     in_projects(issue.parent_ids)
   end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 50efe5d6aa0fd8b9db719ca4a30433e9ecfcd935..6ef84c5f59b892ca6ace9fcbcc59f78e62c93b0a 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -85,7 +85,13 @@ def merge_request_diff
   # when creating new merge request
   attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
 
-  state_machine :state, initial: :opened do
+  # Keep states definition to be evaluated before the state_machine block to avoid spec failures.
+  # If this gets evaluated after, the `merged` and `locked` states which are overrided can be nil.
+  def self.available_state_names
+    super + [:merged, :locked]
+  end
+
+  state_machine :state_id, initial: :opened do
     event :close do
       transition [:opened] => :closed
     end
@@ -116,10 +122,17 @@ def merge_request_diff
       end
     end
 
-    state :opened
-    state :closed
-    state :merged
-    state :locked
+    state :opened, value: MergeRequest.available_states[:opened]
+    state :closed, value: MergeRequest.available_states[:closed]
+    state :merged, value: MergeRequest.available_states[:merged]
+    state :locked, value: MergeRequest.available_states[:locked]
+  end
+
+  # Alias to state machine .with_state_id method
+  # This needs to be defined after the state machine block to avoid errors
+  class << self
+    alias_method :with_state, :with_state_id
+    alias_method :with_states, :with_state_ids
   end
 
   state_machine :merge_status, initial: :unchecked do
@@ -211,10 +224,6 @@ def self.reference_prefix
     '!'
   end
 
-  def self.available_states
-    @available_states ||= super.merge(merged: 3, locked: 4)
-  end
-
   # Returns the top 100 target branches
   #
   # The returned value is a Array containing branch names
@@ -1246,6 +1255,27 @@ def compare_test_reports
     compare_reports(Ci::CompareTestReportsService)
   end
 
+  def has_exposed_artifacts?
+    return false unless Feature.enabled?(:ci_expose_arbitrary_artifacts_in_mr, default_enabled: true)
+
+    actual_head_pipeline&.has_exposed_artifacts?
+  end
+
+  # TODO: this method and compare_test_reports use the same
+  # result type, which is handled by the controller's #reports_response.
+  # we should minimize mistakes by isolating the common parts.
+  # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
+  def find_exposed_artifacts
+    unless has_exposed_artifacts?
+      return { status: :error, status_reason: 'This merge request does not have exposed artifacts' }
+    end
+
+    compare_reports(Ci::GenerateExposedArtifactsReportService)
+  end
+
+  # TODO: consider renaming this as with exposed artifacts we generate reports,
+  # not always compare
+  # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
   def compare_reports(service_class, current_user = nil)
     with_reactive_cache(service_class.name, current_user&.id) do |data|
       unless service_class.new(project, current_user)
@@ -1260,6 +1290,8 @@ def compare_reports(service_class, current_user = nil)
   def calculate_reactive_cache(identifier, current_user_id = nil, *args)
     service_class = identifier.constantize
 
+    # TODO: the type check should change to something that includes exposed artifacts service
+    # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
     raise NameError, service_class unless service_class < Ci::CompareReportsBaseService
 
     current_user = User.find_by(id: current_user_id)
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index ca50820a8794365cbd8895031237a99ed3c606eb..735ad046f2287615ccd72b0bb17a933ad55bd809 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -83,7 +83,7 @@ class MergeRequestDiff < ApplicationRecord
 
     metrics_join = mr_diffs.join(mr_metrics).on(metrics_join_condition)
 
-    condition = MergeRequest.arel_table[:state].eq(:merged)
+    condition = MergeRequest.arel_table[:state_id].eq(MergeRequest.available_states[:merged])
       .and(MergeRequest::Metrics.arel_table[:merged_at].lteq(before))
       .and(MergeRequest::Metrics.arel_table[:merged_at].not_eq(nil))
 
@@ -91,7 +91,7 @@ class MergeRequestDiff < ApplicationRecord
   end
 
   scope :old_closed_diffs, -> (before) do
-    condition = MergeRequest.arel_table[:state].eq(:closed)
+    condition = MergeRequest.arel_table[:state_id].eq(MergeRequest.available_states[:closed])
       .and(MergeRequest::Metrics.arel_table[:latest_closed_at].lteq(before))
 
     joins(merge_request: :metrics).where(condition)
@@ -136,6 +136,7 @@ def self.ids_for_external_storage_migration(limit:)
   # All diff information is collected from repository after object is created.
   # It allows you to override variables like head_commit_sha before getting diff.
   after_create :save_git_content, unless: :importing?
+  after_create_commit :set_as_latest_diff
 
   after_save :update_external_diff_store, if: -> { !importing? && saved_change_to_external_diff? }
 
@@ -150,10 +151,6 @@ def viewable?
   # Collect information about commits and diff from repository
   # and save it to the database as serialized data
   def save_git_content
-    MergeRequest
-      .where('id = ? AND COALESCE(latest_merge_request_diff_id, 0) < ?', self.merge_request_id, self.id)
-      .update_all(latest_merge_request_diff_id: self.id)
-
     ensure_commit_shas
     save_commits
     save_diffs
@@ -168,6 +165,12 @@ def save_git_content
     keep_around_commits
   end
 
+  def set_as_latest_diff
+    MergeRequest
+      .where('id = ? AND COALESCE(latest_merge_request_diff_id, 0) < ?', self.merge_request_id, self.id)
+      .update_all(latest_merge_request_diff_id: self.id)
+  end
+
   def ensure_commit_shas
     self.start_commit_sha ||= merge_request.target_branch_sha
     self.head_commit_sha  ||= merge_request.source_branch_sha
@@ -502,11 +505,6 @@ def outdated_by_closure?
     merge_request.closed? && merge_request.metrics.latest_closed_at < EXTERNAL_DIFF_CUTOFF.ago
   end
 
-  # We can't rely on `merge_request.latest_merge_request_diff_id` because that
-  # may have been changed in `save_git_content` without being reflected in
-  # the association's instance. This query is always subject to races, but
-  # the worst case is that we *don't* make a diff external when we could. The
-  # background worker will make it external at a later date.
   def old_version?
     latest_id = MergeRequest
       .where(id: merge_request_id)
@@ -514,7 +512,7 @@ def old_version?
       .pluck(:latest_merge_request_diff_id)
       .first
 
-    self.id != latest_id
+    latest_id && self.id < latest_id
   end
 
   def load_diffs(options)
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 916c11a8d033673919b09f8d7de65ddf866db9eb..2fa0cfc9b93765b974398d83fc304087e6685102 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -257,10 +257,9 @@ def safe_title
     title.to_slug.normalize.to_s
   end
 
-  def parent
+  def resource_parent
     group || project
   end
-  alias_method :resource_parent, :parent
 
   def group_milestone?
     group_id.present?
diff --git a/app/models/note.rb b/app/models/note.rb
index 34736482387bb27419fcef9a55cbe1fe2c164f9a..43f349c6fa2bc08b1753d2a4a5546d7a1009f1b8 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -24,7 +24,7 @@ module SpecialRole
 
     class << self
       def values
-        constants.map {|const| self.const_get(const)}
+        constants.map {|const| self.const_get(const, false)}
       end
 
       def value?(val)
@@ -145,6 +145,9 @@ def value?(val)
   end
   scope :with_metadata, -> { includes(:system_note_metadata) }
 
+  scope :for_note_or_capitalized_note, ->(text) { where(note: [text, text.capitalize]) }
+  scope :like_note_or_capitalized_note, ->(text) { where('(note LIKE ? OR note LIKE ?)', text, text.capitalize) }
+
   after_initialize :ensure_discussion_id
   before_validation :nullify_blank_type, :nullify_blank_line_code
   before_validation :set_discussion_id, on: :create
@@ -480,10 +483,9 @@ def retrieve_upload(_identifier, paths)
     Upload.find_by(model: self, path: paths)
   end
 
-  def parent
+  def resource_parent
     project
   end
-  alias_method :resource_parent, :parent
 
   private
 
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 981590b688f1e602e7a54bb57bf7bb4614280e7b..2b3443f24d76a7a14af5a90dbb5fd9fbf4e4ab5c 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -25,6 +25,7 @@ class NotificationSetting < ApplicationRecord
   end
 
   EMAIL_EVENTS = [
+    :new_release,
     :new_note,
     :new_issue,
     :reopen_issue,
@@ -46,6 +47,10 @@ def self.email_events(source = nil)
     EMAIL_EVENTS
   end
 
+  def self.allowed_fields(source = nil)
+    NotificationSetting.email_events(source).dup + %i(level notification_email)
+  end
+
   def email_events
     self.class.email_events(source)
   end
diff --git a/app/models/project.rb b/app/models/project.rb
index d7e3dc676cab87b601e1576ba659feda1cddda11..3525f37f8d5dff8ca714b69b9a2e36a0c19440a2 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -281,7 +281,7 @@ class Project < ApplicationRecord
   has_many :variables, class_name: 'Ci::Variable'
   has_many :triggers, class_name: 'Ci::Trigger'
   has_many :environments
-  has_many :deployments, -> { success }
+  has_many :deployments
   has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
   has_many :project_deploy_tokens
   has_many :deploy_tokens, through: :project_deploy_tokens
@@ -297,6 +297,9 @@ class Project < ApplicationRecord
 
   has_many :external_pull_requests, inverse_of: :project
 
+  has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_project_id
+  has_many :source_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :project_id
+
   has_one :pages_metadatum, class_name: 'ProjectPagesMetadatum', inverse_of: :project
 
   accepts_nested_attributes_for :variables, allow_destroy: true
@@ -1033,8 +1036,8 @@ def to_human_reference(from = nil)
     end
   end
 
-  def web_url
-    Gitlab::Routing.url_helpers.project_url(self)
+  def web_url(only_path: nil)
+    Gitlab::Routing.url_helpers.project_url(self, only_path: only_path)
   end
 
   def readme_url
@@ -1313,7 +1316,18 @@ def ssh_url_to_repo
   end
 
   def http_url_to_repo
-    "#{web_url}.git"
+    custom_root = Gitlab::CurrentSettings.custom_http_clone_url_root
+
+    project_url = if custom_root.present?
+                    Gitlab::Utils.append_path(
+                      custom_root,
+                      web_url(only_path: true)
+                    )
+                  else
+                    web_url
+                  end
+
+    "#{project_url}.git"
   end
 
   # Is overridden in EE
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 1d0b37abf72034df1b8cfb2c4eeaf3a9d3b7f362..019bd54f48c647bcfe4739fe643b81349b2e1cdd 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -161,7 +161,7 @@ def create_issue_message(data)
     obj_attr = data[:object_attributes]
     obj_attr = HashWithIndifferentAccess.new(obj_attr)
     title = render_line(obj_attr[:title])
-    state = obj_attr[:state]
+    state = Issue.available_states.key(obj_attr[:state_id])
     issue_iid = obj_attr[:iid]
     issue_url = obj_attr[:url]
     description = obj_attr[:description]
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 2bc6567ca15c553d2ffc3f5c6f2d7647ceee95f4..ba61810e26f22f5affecf1f111b0c07a457980c4 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -122,9 +122,13 @@ def new_issue_url
   end
 
   alias_method :original_url, :url
-
   def url
-    original_url&.chomp('/')
+    original_url&.delete_suffix('/')
+  end
+
+  alias_method :original_api_url, :api_url
+  def api_url
+    original_api_url&.delete_suffix('/')
   end
 
   def execute(push)
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 218be97421863db3fa2f02af04d0e90b50d1d9a0..bb222ac7629776496b7dc558f83d06b8854fa567 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -54,7 +54,7 @@ def ssh_url_to_repo
   end
 
   def http_url_to_repo
-    "#{Gitlab.config.gitlab.url}/#{full_path}.git"
+    @project.http_url_to_repo.sub(%r{git\z}, 'wiki.git')
   end
 
   def wiki_base_path
diff --git a/app/models/push_event.rb b/app/models/push_event.rb
index 4698df3973047601aa4cd7cd1776a92317fef160..5cab686f20b28bb971e42e7e9eb1e5c5ba33ab71 100644
--- a/app/models/push_event.rb
+++ b/app/models/push_event.rb
@@ -26,6 +26,8 @@ class PushEvent < Event
   delegate :commit_count, to: :push_event_payload
   alias_method :commits_count, :commit_count
 
+  delegate :ref_count, to: :push_event_payload
+
   # Returns events of pushes that either pushed to an existing ref or created a
   # new one.
   def self.created_or_pushed
@@ -52,7 +54,7 @@ def self.without_existing_merge_requests
       .select(1)
       .where('merge_requests.source_project_id = events.project_id')
       .where('merge_requests.source_branch = push_event_payloads.ref')
-      .where(state: :opened)
+      .with_state(:opened)
 
     # For reasons unknown the use of #eager_load will result in the
     # "push_event_payload" association not being set. Because of this we're
diff --git a/app/models/release.rb b/app/models/release.rb
index 8759e38060c2881c5b1920ce3ffc86c061c2a4ba..5a7bfe2d49576e7967077267fa627625e1092863 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -14,6 +14,7 @@ class Release < ApplicationRecord
 
   has_many :milestone_releases
   has_many :milestones, through: :milestone_releases
+  has_one :evidence
 
   default_value_for :released_at, allows_nil: false do
     Time.zone.now
@@ -25,9 +26,13 @@ class Release < ApplicationRecord
   validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
 
   scope :sorted, -> { order(released_at: :desc) }
+  scope :with_project_and_namespace, -> { includes(project: :namespace) }
 
   delegate :repository, to: :project
 
+  after_commit :create_evidence!, on: :create
+  after_commit :notify_new_release, on: :create
+
   def commit
     strong_memoize(:commit) do
       repository.commit(actual_sha)
@@ -66,6 +71,14 @@ def actual_tag
       repository.find_tag(tag)
     end
   end
+
+  def create_evidence!
+    CreateEvidenceWorker.perform_async(self.id)
+  end
+
+  def notify_new_release
+    NewReleaseWorker.perform_async(id)
+  end
 end
 
 Release.prepend_if_ee('EE::Release')
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 4e693a94c6b9d1c79eb456e73c01cda2a3c118b5..4010a3e216722006b0832a08f960c0797c103b2a 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -33,8 +33,6 @@ def content_html_invalidated?
     default_content_html_invalidator || file_name_changed?
   end
 
-  default_value_for(:visibility_level) { Gitlab::CurrentSettings.default_snippet_visibility }
-
   belongs_to :author, class_name: 'User'
   belongs_to :project
 
@@ -73,7 +71,7 @@ def self.with_optional_visibility(value = nil)
     end
   end
 
-  def self.only_global_snippets
+  def self.only_personal_snippets
     where(project_id: nil)
   end
 
@@ -139,6 +137,24 @@ def self.link_reference_pattern
     @link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/)
   end
 
+  def initialize(attributes = {})
+    # We can't use default_value_for because the database has a default
+    # value of 0 for visibility_level. If someone attempts to create a
+    # private snippet, default_value_for will assume that the
+    # visibility_level hasn't changed and will use the application
+    # setting default, which could be internal or public.
+    #
+    # To fix the problem, we assign the actual snippet default if no
+    # explicit visibility has been initialized.
+    attributes ||= {}
+
+    unless visibility_attribute_present?(attributes)
+      attributes[:visibility_level] = Gitlab::CurrentSettings.default_snippet_visibility
+    end
+
+    super
+  end
+
   def to_reference(from = nil, full: false)
     reference = "#{self.class.reference_prefix}#{id}"
 
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 8ec90ca25d348b88c37fe71ab95fcc7dda51e48c..11cbeb60bba6758a2a79a74e22032175d992fe01 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -23,6 +23,7 @@ class SystemNoteMetadata < ApplicationRecord
   validates :action, inclusion: { in: :icon_types }, allow_nil: true
 
   belongs_to :note
+  belongs_to :description_version
 
   def icon_types
     ICON_TYPES
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 6b71845856a5d7547a3333f5ccbbea19dd027c39..1927b54510ecbde729f3e2709f976c26bf005e27 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -75,13 +75,13 @@ class Todo < ApplicationRecord
   after_save :keep_around_commit, if: :commit_id
 
   class << self
-    # Returns all todos for the given group and its descendants.
+    # Returns all todos for the given group ids and their descendants.
     #
-    # group - A `Group` to retrieve todos for.
+    # group_ids - Group Ids to retrieve todos for.
     #
     # Returns an `ActiveRecord::Relation`.
-    def for_group_and_descendants(group)
-      groups = group.self_and_descendants
+    def for_group_ids_and_descendants(group_ids)
+      groups = Group.groups_including_descendants_by(group_ids)
 
       from_union([
         for_project(Project.for_group(groups)),
@@ -144,10 +144,9 @@ def order_by_labels_priority
     end
   end
 
-  def parent
+  def resource_parent
     project
   end
-  alias_method :resource_parent, :parent
 
   def unmergeable?
     action == UNMERGEABLE
diff --git a/app/models/upload.rb b/app/models/upload.rb
index df8f9c56fa83ecd4e8b5057d6c0072d08187016f..8c409641452cdc0a38f2cd80fcfe492a7e9ec82e 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -138,7 +138,7 @@ def relative_path?
   end
 
   def uploader_class
-    Object.const_get(uploader)
+    Object.const_get(uploader, false)
   end
 
   def identifier
diff --git a/app/models/user.rb b/app/models/user.rb
index c4075f06dff60417c0fa204d3a0f5ab64e4fa47e..321a4080484508f183ec4c880e0af80722914cef 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -99,6 +99,7 @@ def update_tracked_fields!(request)
   has_many :u2f_registrations, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
   has_many :chat_names, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
   has_one :user_synced_attributes_metadata, autosave: true
+  has_one :aws_role, class_name: 'Aws::Role'
 
   # Groups
   has_many :members
@@ -230,6 +231,10 @@ def update_tracked_fields!(request)
   # Note: When adding an option, it MUST go on the end of the array.
   enum project_view: [:readme, :activity, :files]
 
+  # User's role
+  # Note: When adding an option, it MUST go on the end of the array.
+  enum role: [:software_developer, :development_team_lead, :devops_engineer, :systems_administrator, :security_analyst, :data_analyst, :product_manager, :product_designer, :other], _suffix: true
+
   delegate :path, to: :namespace, allow_nil: true, prefix: true
   delegate :notes_filter_for, to: :user_preference
   delegate :set_notes_filter, to: :user_preference
@@ -1316,14 +1321,27 @@ def notification_email_for(notification_group)
     notification_group&.notification_email_for(self) || notification_email
   end
 
-  def notification_settings_for(source)
+  def notification_settings_for(source, inherit: false)
     if notification_settings.loaded?
       notification_settings.find do |notification|
         notification.source_type == source.class.base_class.name &&
           notification.source_id == source.id
       end
     else
-      notification_settings.find_or_initialize_by(source: source)
+      notification_settings.find_or_initialize_by(source: source) do |ns|
+        next unless source.is_a?(Group) && inherit
+
+        # If we're here it means we're trying to create a NotificationSetting for a group that doesn't have one.
+        # Find the closest parent with a notification_setting that's not Global level, or that has an email set.
+        ancestor_ns = source
+                        .notification_settings(hierarchy_order: :asc)
+                        .where(user: self)
+                        .find_by('level != ? OR notification_email IS NOT NULL', NotificationSetting.levels[:global])
+        # Use it to seed the settings
+        ns.assign_attributes(ancestor_ns&.slice(*NotificationSetting.allowed_fields))
+        ns.source = source
+        ns.user = self
+      end
     end
   end
 
@@ -1557,6 +1575,20 @@ def last_active_at
     [last_activity, last_sign_in].compact.max
   end
 
+  # Below is used for the signup_flow experiment. Should be removed
+  # when experiment finishes.
+  # See https://gitlab.com/gitlab-org/growth/engineering/issues/64
+  REQUIRES_ROLE_VALUE = 99
+
+  def role_required?
+    role_before_type_cast == REQUIRES_ROLE_VALUE
+  end
+
+  def set_role_required!
+    update_column(:role, REQUIRES_ROLE_VALUE)
+  end
+  # End of signup_flow experiment methods
+
   # @deprecated
   alias_method :owned_or_masters_groups, :owned_or_maintainers_groups
 
diff --git a/app/policies/board_policy.rb b/app/policies/board_policy.rb
index b8435dad3f11123056d904ffa605863d915d3258..e2b16249c853a927664c9f9bdbae220fe7d879c8 100644
--- a/app/policies/board_policy.rb
+++ b/app/policies/board_policy.rb
@@ -3,7 +3,7 @@
 class BoardPolicy < BasePolicy
   include FindGroupProjects
 
-  delegate { @subject.parent }
+  delegate { @subject.resource_parent }
 
   condition(:is_group_board) { @subject.group_board? }
   condition(:is_project_board) { @subject.project_board? }
@@ -19,7 +19,7 @@ class BoardPolicy < BasePolicy
   condition(:reporter_of_group_projects) do
     next unless @user
 
-    group_projects_for(user: @user, group: @subject.parent)
+    group_projects_for(user: @user, group: @subject.resource_parent)
       .visible_to_user_and_access_level(@user, ::Gitlab::Access::REPORTER)
       .exists?
   end
diff --git a/app/policies/deploy_keys_project_policy.rb b/app/policies/deploy_keys_project_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..368377048a4c3da332347572ed03a8bbda3906a4
--- /dev/null
+++ b/app/policies/deploy_keys_project_policy.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class DeployKeysProjectPolicy < BasePolicy
+  delegate { @subject.project }
+
+  with_options scope: :subject, score: 0
+  condition(:public_deploy_key) { @subject.deploy_key.public? }
+
+  rule { public_deploy_key & can?(:admin_project) }.enable :update_deploy_keys_project
+end
diff --git a/app/policies/deployment_policy.rb b/app/policies/deployment_policy.rb
index d4f2f3c52b13d7d4b1e6e34a7b7a02ecfa5f33f7..1a92b735e36bcc760eafea5b4d3463546e16b0fe 100644
--- a/app/policies/deployment_policy.rb
+++ b/app/policies/deployment_policy.rb
@@ -7,8 +7,20 @@ class DeploymentPolicy < BasePolicy
     can?(:update_build, @subject.deployable)
   end
 
-  rule { ~can_retry_deployable }.policy do
+  condition(:has_deployable) do
+    @subject.deployable.present?
+  end
+
+  condition(:can_update_deployment) do
+    can?(:update_deployment, @subject.environment)
+  end
+
+  rule { has_deployable & ~can_retry_deployable }.policy do
     prevent :create_deployment
     prevent :update_deployment
   end
+
+  rule { ~can_update_deployment }.policy do
+    prevent :update_deployment
+  end
 end
diff --git a/app/policies/milestone_policy.rb b/app/policies/milestone_policy.rb
index 2d56eea6a78d985a23d3ae04ab142c7b46a07a4f..9cea8ddd7b32f74313c430dd2e8bd606469fae0d 100644
--- a/app/policies/milestone_policy.rb
+++ b/app/policies/milestone_policy.rb
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
 class MilestonePolicy < BasePolicy
-  delegate { @subject.parent }
+  delegate { @subject.resource_parent }
 end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index a3540f310771eba0f46ead19b9bbba0b29a248b2..ea2be37d7e644f12c5ce336ad5e1263c760e6ae2 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -262,6 +262,7 @@ class ProjectPolicy < BasePolicy
     enable :destroy_container_image
     enable :create_environment
     enable :create_deployment
+    enable :update_deployment
     enable :create_release
     enable :update_release
   end
diff --git a/app/policies/todo_policy.rb b/app/policies/todo_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f8644217f043445f74c24c9e9793c713d0b1889b
--- /dev/null
+++ b/app/policies/todo_policy.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class TodoPolicy < BasePolicy
+  desc 'User can only read own todos'
+  condition(:own_todo) do
+    @user && @subject.user_id == @user.id
+  end
+
+  rule { own_todo }.enable :read_todo
+end
diff --git a/app/presenters/projects/settings/deploy_keys_presenter.rb b/app/presenters/projects/settings/deploy_keys_presenter.rb
index 6f8c4e1f9029507f65312b6c01da8fde7d7f512b..9bb7fe13593b718c16229d7d4d6e640e3c710f3b 100644
--- a/app/presenters/projects/settings/deploy_keys_presenter.rb
+++ b/app/presenters/projects/settings/deploy_keys_presenter.rb
@@ -40,7 +40,7 @@ def available_public_keys
 
       def as_json
         serializer = DeployKeySerializer.new # rubocop: disable CodeReuse/Serializer
-        opts = { user: current_user }
+        opts = { user: current_user, project: project }
 
         {
           enabled_keys: serializer.represent(enabled_keys.with_projects, opts),
diff --git a/app/presenters/todo_presenter.rb b/app/presenters/todo_presenter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b57fc712c5a12832854872deb8e8d5db38595c7f
--- /dev/null
+++ b/app/presenters/todo_presenter.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class TodoPresenter < Gitlab::View::Presenter::Delegated
+  include GlobalID::Identification
+
+  presents :todo
+end
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 0c7541572677735d1b1be595081bce0ad54293be..480a8cab6fffddb9dcdc2fb449a7f99c43ed2518 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -121,4 +121,28 @@ def can_create_build_terminal?
   def can_admin_build?
     can?(request.current_user, :admin_build, project)
   end
+
+  def callout_message
+    return super unless build.failure_reason.to_sym == :missing_dependency_failure
+
+    docs_url = "https://docs.gitlab.com/ce/ci/yaml/README.html#dependencies"
+
+    [
+      failure_message.html_safe,
+      help_message(docs_url).html_safe
+    ].join("<br />")
+  end
+
+  def invalid_dependencies
+    build.invalid_dependencies.map(&:name).join(', ')
+  end
+
+  def failure_message
+    _("This job depends on other jobs with expired/erased artifacts: %{invalid_dependencies}") %
+      { invalid_dependencies: invalid_dependencies }
+  end
+
+  def help_message(docs_url)
+    _("Please refer to <a href=\"%{docs_url}\">%{docs_url}</a>") % { docs_url: docs_url }
+  end
 end
diff --git a/app/serializers/build_trace_entity.rb b/app/serializers/build_trace_entity.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b5bac8a5d641d80b429ae41e4af47613f1a22563
--- /dev/null
+++ b/app/serializers/build_trace_entity.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class BuildTraceEntity < Grape::Entity
+  expose :build_id, as: :id
+  expose :build_status, as: :status
+  expose :build_complete?, as: :complete
+
+  expose :state
+  expose :append
+  expose :truncated
+  expose :offset
+  expose :size
+  expose :total
+
+  expose :json_lines, as: :lines, if: ->(*) { object.json? }
+  expose :html_lines, as: :html, if: ->(*) { object.html? }
+end
diff --git a/app/serializers/build_trace_serializer.rb b/app/serializers/build_trace_serializer.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c95158f10a47207b497e66ea6c67e6c92cb6c2b2
--- /dev/null
+++ b/app/serializers/build_trace_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class BuildTraceSerializer < BaseSerializer
+  entity BuildTraceEntity
+end
diff --git a/app/serializers/container_repository_entity.rb b/app/serializers/container_repository_entity.rb
index cc746698a05d2835baaecc07d003dc3eab60a9bc..db9cf1c78359c4ae4f1fe4e5bc1dd14bdf602e38 100644
--- a/app/serializers/container_repository_entity.rb
+++ b/app/serializers/container_repository_entity.rb
@@ -18,7 +18,7 @@ class ContainerRepositoryEntity < Grape::Entity
   alias_method :repository, :object
 
   def project
-    request.project
+    request.respond_to?(:project) ? request.project : object.project
   end
 
   def can_destroy?
diff --git a/app/serializers/deploy_key_entity.rb b/app/serializers/deploy_key_entity.rb
index e47d64547801be4603802da2b0da8814548c9657..9a558d12bec417a0e491abb3c17bc404edd9179d 100644
--- a/app/serializers/deploy_key_entity.rb
+++ b/app/serializers/deploy_key_entity.rb
@@ -20,6 +20,7 @@ class DeployKeyEntity < Grape::Entity
   private
 
   def can_edit
-    Ability.allowed?(options[:user], :update_deploy_key, object)
+    Ability.allowed?(options[:user], :update_deploy_key, object) ||
+      Ability.allowed?(options[:user], :update_deploy_keys_project, object.deploy_keys_project_for(options[:project]))
   end
 end
diff --git a/app/serializers/evidences/author_entity.rb b/app/serializers/evidences/author_entity.rb
deleted file mode 100644
index 9023c64dad205cb58cf7fcfd1d6a5c7079ffb76a..0000000000000000000000000000000000000000
--- a/app/serializers/evidences/author_entity.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-module Evidences
-  class AuthorEntity < Grape::Entity
-    expose :id
-    expose :name
-    expose :email
-  end
-end
diff --git a/app/serializers/evidences/evidence_entity.rb b/app/serializers/evidences/evidence_entity.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9689ae10895c25d49e057ad04751191c9404b60f
--- /dev/null
+++ b/app/serializers/evidences/evidence_entity.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Evidences
+  class EvidenceEntity < Grape::Entity
+    expose :release, using: Evidences::ReleaseEntity
+  end
+end
diff --git a/app/serializers/evidences/evidence_serializer.rb b/app/serializers/evidences/evidence_serializer.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d03032bc65c9d78dbd48bb25790a4d0f15414a7f
--- /dev/null
+++ b/app/serializers/evidences/evidence_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Evidences
+  class EvidenceSerializer < BaseSerializer
+    entity EvidenceEntity
+  end
+end
diff --git a/app/serializers/evidences/issue_entity.rb b/app/serializers/evidences/issue_entity.rb
index 883256bf38a6ea3838b03579a22a313cba067d9e..2f1f5dc3d18aac30967e5ae33bf3096b15e3d6a9 100644
--- a/app/serializers/evidences/issue_entity.rb
+++ b/app/serializers/evidences/issue_entity.rb
@@ -5,7 +5,6 @@ class IssueEntity < Grape::Entity
     expose :id
     expose :title
     expose :description
-    expose :author, using: AuthorEntity
     expose :state
     expose :iid
     expose :confidential
diff --git a/app/serializers/evidences/milestone_entity.rb b/app/serializers/evidences/milestone_entity.rb
index 8118cab44038e2c6ff93c9766f41654db131556b..eeb3d58d4c7895bc058c8a26c668a683dedeb195 100644
--- a/app/serializers/evidences/milestone_entity.rb
+++ b/app/serializers/evidences/milestone_entity.rb
@@ -9,6 +9,6 @@ class MilestoneEntity < Grape::Entity
     expose :iid
     expose :created_at
     expose :due_date
-    expose :issues, using: IssueEntity
+    expose :issues, using: Evidences::IssueEntity
   end
 end
diff --git a/app/serializers/evidences/release_entity.rb b/app/serializers/evidences/release_entity.rb
index 8916ce67b4c9a336b8b63226d3eb27a0dd10da00..59e379a3c080c3d96c5d81c890f5f9415cea5196 100644
--- a/app/serializers/evidences/release_entity.rb
+++ b/app/serializers/evidences/release_entity.rb
@@ -7,7 +7,7 @@ class ReleaseEntity < Grape::Entity
     expose :name
     expose :description
     expose :created_at
-    expose :project, using: ProjectEntity
-    expose :milestones, using: MilestoneEntity
+    expose :project, using: Evidences::ProjectEntity
+    expose :milestones, using: Evidences::MilestoneEntity
   end
 end
diff --git a/app/serializers/merge_request_poll_widget_entity.rb b/app/serializers/merge_request_poll_widget_entity.rb
index 854349e85075fcb7c620ed7e1a8d8d54ae4206f1..2a61187a85630473c5d410792a5a471033f733e0 100644
--- a/app/serializers/merge_request_poll_widget_entity.rb
+++ b/app/serializers/merge_request_poll_widget_entity.rb
@@ -65,6 +65,12 @@ class MergeRequestPollWidgetEntity < IssuableEntity
     end
   end
 
+  expose :exposed_artifacts_path do |merge_request|
+    if merge_request.has_exposed_artifacts?
+      exposed_artifacts_project_merge_request_path(merge_request.project, merge_request, format: :json)
+    end
+  end
+
   expose :create_issue_to_resolve_discussions_path do |merge_request|
     presenter(merge_request).create_issue_to_resolve_discussions_path
   end
diff --git a/app/serializers/pipeline_details_entity.rb b/app/serializers/pipeline_details_entity.rb
index 808e87c3fcf06bb2d74e1e71802569d8e2ed11d8..71589ac8315c9500dc9bb6b07719718278cab6d3 100644
--- a/app/serializers/pipeline_details_entity.rb
+++ b/app/serializers/pipeline_details_entity.rb
@@ -10,6 +10,7 @@ class PipelineDetailsEntity < PipelineEntity
     expose :manual_actions, using: BuildActionEntity
     expose :scheduled_actions, using: BuildActionEntity
   end
-end
 
-PipelineDetailsEntity.prepend_if_ee('EE::PipelineDetailsEntity')
+  expose :triggered_by_pipeline, as: :triggered_by, with: TriggeredPipelineEntity
+  expose :triggered_pipelines, as: :triggered, using: TriggeredPipelineEntity
+end
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
index eaaeaf040a2459c656404fdb8e96ddd4c9284599..fc3160e3c69791ad1dab76101c0d71dcd3955619 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -54,9 +54,9 @@ def preloaded_relations
         artifacts: {
           project: [:route, { namespace: :route }]
         }
-      }
+      },
+      { triggered_by_pipeline: [:project, :user] },
+      { triggered_pipelines: [:project, :user] }
     ]
   end
 end
-
-PipelineSerializer.prepend_if_ee('EE::PipelineSerializer')
diff --git a/app/serializers/projects/serverless/service_entity.rb b/app/serializers/projects/serverless/service_entity.rb
index 40ac52d96af1253422ee754eac5bd83323edc8a4..a1e0bf02d118f9d9dff079882896b12e035ad984 100644
--- a/app/serializers/projects/serverless/service_entity.rb
+++ b/app/serializers/projects/serverless/service_entity.rb
@@ -44,7 +44,7 @@ class ServiceEntity < Grape::Entity
       end
 
       expose :url do |service|
-        service.dig('status', 'url')
+        service.dig('status', 'url') || "http://#{service.dig('status', 'domain')}"
       end
 
       expose :description do |service|
diff --git a/ee/app/serializers/triggered_pipeline_entity.rb b/app/serializers/triggered_pipeline_entity.rb
similarity index 100%
rename from ee/app/serializers/triggered_pipeline_entity.rb
rename to app/serializers/triggered_pipeline_entity.rb
diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb
index 6400b182715c48d77d4ee85a625a4fa62b338a9c..df9217bea326a9c3119a9fe508d4fdbc64cb3e70 100644
--- a/app/services/application_settings/update_service.rb
+++ b/app/services/application_settings/update_service.rb
@@ -20,7 +20,11 @@ def execute
       add_to_outbound_local_requests_whitelist(@params.delete(:add_to_outbound_local_requests_whitelist))
 
       if params.key?(:performance_bar_allowed_group_path)
-        params[:performance_bar_allowed_group_id] = performance_bar_allowed_group_id
+        group_id = process_performance_bar_allowed_group_id
+
+        return false if application_setting.errors.any?
+
+        params[:performance_bar_allowed_group_id] = group_id
       end
 
       if usage_stats_updated? && !params.delete(:skip_usage_stats_user)
@@ -65,12 +69,27 @@ def update_terms(terms)
       @application_setting.reset_memoized_terms
     end
 
-    def performance_bar_allowed_group_id
-      performance_bar_enabled = !params.key?(:performance_bar_enabled) || params.delete(:performance_bar_enabled)
+    def process_performance_bar_allowed_group_id
       group_full_path = params.delete(:performance_bar_allowed_group_path)
-      return unless Gitlab::Utils.to_boolean(performance_bar_enabled)
+      enable_param_on = Gitlab::Utils.to_boolean(params.delete(:performance_bar_enabled))
+      performance_bar_enabled = enable_param_on.nil? || enable_param_on # Default to true
+
+      return if group_full_path.blank?
+      return if enable_param_on == false # Explicitly disabling
+
+      unless performance_bar_enabled
+        application_setting.errors.add(:performance_bar_allowed_group_id, 'not allowed when performance bar is disabled')
+        return
+      end
+
+      group = Group.find_by_full_path(group_full_path.chomp('/'))
+
+      unless group
+        application_setting.errors.add(:performance_bar_allowed_group_id, 'not found')
+        return
+      end
 
-      Group.find_by_full_path(group_full_path)&.id if group_full_path.present?
+      group.id
     end
 
     def bypass_external_auth?
diff --git a/app/services/bulk_push_event_payload_service.rb b/app/services/bulk_push_event_payload_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..54157bc23f91c442b25df86161aa121f27e88f5b
--- /dev/null
+++ b/app/services/bulk_push_event_payload_service.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class BulkPushEventPayloadService
+  def initialize(event, push_data)
+    @event = event
+    @push_data = push_data
+  end
+
+  def execute
+    @event.build_push_event_payload(
+      action: @push_data[:action],
+      commit_count: 0,
+      ref_count: @push_data[:ref_count],
+      ref_type: @push_data[:ref_type]
+    )
+
+    @event.push_event_payload.tap(&:save!)
+  end
+end
diff --git a/app/services/ci/compare_reports_base_service.rb b/app/services/ci/compare_reports_base_service.rb
index 5b76e1824e4734a76e1f337ea833f072d5f52b4f..83ba70e84372c237c65f5a2d71c284a046377fbe 100644
--- a/app/services/ci/compare_reports_base_service.rb
+++ b/app/services/ci/compare_reports_base_service.rb
@@ -1,6 +1,11 @@
 # frozen_string_literal: true
 
 module Ci
+  # TODO: when using this class with exposed artifacts we see that there are
+  # 2 responsibilities:
+  # 1. reactive caching interface (same in all cases)
+  # 2. data generator (report comparison in most of the case but not always)
+  # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
   class CompareReportsBaseService < ::BaseService
     def execute(base_pipeline, head_pipeline)
       comparer = comparer_class.new(get_report(base_pipeline), get_report(head_pipeline))
diff --git a/app/services/ci/find_exposed_artifacts_service.rb b/app/services/ci/find_exposed_artifacts_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5c75af294bf79ac6a10b082c6cc0842282c199e7
--- /dev/null
+++ b/app/services/ci/find_exposed_artifacts_service.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module Ci
+  # This class loops through all builds with exposed artifacts and returns
+  # basic information about exposed artifacts for given jobs for the frontend
+  # to display them as custom links in the merge request.
+  #
+  # This service must be used with care.
+  # Looking for exposed artifacts is very slow and should be done asynchronously.
+  class FindExposedArtifactsService < ::BaseService
+    include Gitlab::Routing
+
+    MAX_EXPOSED_ARTIFACTS = 10
+
+    def for_pipeline(pipeline, limit: MAX_EXPOSED_ARTIFACTS)
+      results = []
+
+      pipeline.builds.latest.with_exposed_artifacts.find_each do |job|
+        if job_exposed_artifacts = for_job(job)
+          results << job_exposed_artifacts
+        end
+
+        break if results.size >= limit
+      end
+
+      results
+    end
+
+    def for_job(job)
+      return unless job.has_exposed_artifacts?
+
+      metadata_entries = first_2_metadata_entries_for_artifacts_paths(job)
+      return if metadata_entries.empty?
+
+      {
+        text: job.artifacts_expose_as,
+        url: path_for_entries(metadata_entries, job),
+        job_path: project_job_path(project, job),
+        job_name: job.name
+      }
+    end
+
+    private
+
+    # we don't need to fetch all artifacts entries for a job because
+    # it could contain many. We only need to know whether it has 1 or more
+    # artifacts, so fetching the first 2 would be sufficient.
+    def first_2_metadata_entries_for_artifacts_paths(job)
+      job.artifacts_paths
+        .lazy
+        .map { |path| job.artifacts_metadata_entry(path, recursive: true) }
+        .select { |entry| entry.exists? }
+        .first(2)
+    end
+
+    def path_for_entries(entries, job)
+      return if entries.empty?
+
+      if single_artifact?(entries)
+        file_project_job_artifacts_path(project, job, entries.first.path)
+      else
+        browse_project_job_artifacts_path(project, job)
+      end
+    end
+
+    def single_artifact?(entries)
+      entries.size == 1 && entries.first.file?
+    end
+  end
+end
diff --git a/app/services/ci/generate_exposed_artifacts_report_service.rb b/app/services/ci/generate_exposed_artifacts_report_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b9bf580bcbc4e9a78d930466357c27964c05e670
--- /dev/null
+++ b/app/services/ci/generate_exposed_artifacts_report_service.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Ci
+  # TODO: a couple of points with this approach:
+  # + reuses existing architecture and reactive caching
+  # - it's not a report comparison and some comparing features must be turned off.
+  # see CompareReportsBaseService for more notes.
+  # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
+  class GenerateExposedArtifactsReportService < CompareReportsBaseService
+    def execute(base_pipeline, head_pipeline)
+      data = FindExposedArtifactsService.new(project, current_user).for_pipeline(head_pipeline)
+      {
+        status: :parsed,
+        key: key(base_pipeline, head_pipeline),
+        data: data
+      }
+    rescue => e
+      Gitlab::Sentry.track_acceptable_exception(e, extra: { project_id: project.id })
+      {
+        status: :error,
+        key: key(base_pipeline, head_pipeline),
+        status_reason: _('An error occurred while fetching exposed artifacts.')
+      }
+    end
+
+    def latest?(base_pipeline, head_pipeline, data)
+      data&.fetch(:key, nil) == key(base_pipeline, head_pipeline)
+    end
+  end
+end
diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb
index 0e99f1424920f98ae8a64f76b35acf01d302360c..37b9b4c362c094b569431bc9eaf88018746d9465 100644
--- a/app/services/ci/pipeline_trigger_service.rb
+++ b/app/services/ci/pipeline_trigger_service.rb
@@ -38,11 +38,34 @@ def trigger_from_token
     end
 
     def create_pipeline_from_job(job)
-      # overridden in EE
+      # this check is to not leak the presence of the project if user cannot read it
+      return unless can?(job.user, :read_project, project)
+
+      return error("400 Job has to be running", 400) unless job.running?
+
+      pipeline = Ci::CreatePipelineService.new(project, job.user, ref: params[:ref])
+        .execute(:pipeline, ignore_skip_ci: true) do |pipeline|
+          source = job.sourced_pipelines.build(
+            source_pipeline: job.pipeline,
+            source_project: job.project,
+            pipeline: pipeline,
+            project: project)
+
+          pipeline.source_pipeline = source
+          pipeline.variables.build(variables)
+        end
+
+      if pipeline.persisted?
+        success(pipeline: pipeline)
+      else
+        error(pipeline.errors.messages, 400)
+      end
     end
 
     def job_from_token
-      # overridden in EE
+      strong_memoize(:job) do
+        Ci::Build.find_by_token(params[:token].to_s)
+      end
     end
 
     def variables
@@ -52,5 +75,3 @@ def variables
     end
   end
 end
-
-Ci::PipelineTriggerService.prepend_if_ee('EE::Ci::PipelineTriggerService')
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
index 110e589e30d22a2d5d74bcb7dd495b8c5a89e512..d58cb0f9e2bab30dde66d0a523f7f6a7df1f8462 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -14,7 +14,7 @@ def execute(branch_name, ref, create_master_if_empty: true)
     if new_branch
       success(new_branch)
     else
-      error('Invalid reference name')
+      error("Invalid reference name: #{branch_name}")
     end
   rescue Gitlab::Git::PreReceiveError => ex
     error(ex.message)
diff --git a/app/services/deployments/after_create_service.rb b/app/services/deployments/after_create_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2572802e6a15c0f015284a89e907a48c3fdf4bf2
--- /dev/null
+++ b/app/services/deployments/after_create_service.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Deployments
+  class AfterCreateService
+    attr_reader :deployment
+    attr_reader :deployable
+
+    delegate :environment, to: :deployment
+    delegate :variables, to: :deployable
+    delegate :options, to: :deployable, allow_nil: true
+
+    def initialize(deployment)
+      @deployment = deployment
+      @deployable = deployment.deployable
+    end
+
+    def execute
+      deployment.create_ref
+      deployment.invalidate_cache
+
+      update_environment(deployment)
+
+      deployment
+    end
+
+    def update_environment(deployment)
+      ActiveRecord::Base.transaction do
+        if (url = expanded_environment_url)
+          environment.external_url = url
+        end
+
+        environment.fire_state_event(action)
+
+        if environment.save && !environment.stopped?
+          deployment.update_merge_request_metrics!
+        end
+      end
+    end
+
+    private
+
+    def environment_options
+      options&.dig(:environment) || {}
+    end
+
+    def expanded_environment_url
+      ExpandVariables.expand(environment_url, -> { variables }) if environment_url
+    end
+
+    def environment_url
+      environment_options[:url]
+    end
+
+    def action
+      environment_options[:action] || 'start'
+    end
+  end
+end
+
+Deployments::AfterCreateService.prepend_if_ee('EE::Deployments::AfterCreateService')
diff --git a/app/services/deployments/create_service.rb b/app/services/deployments/create_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..89e3f7c8b830ad300b90c4e32d10682fce98ca5e
--- /dev/null
+++ b/app/services/deployments/create_service.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Deployments
+  class CreateService
+    attr_reader :environment, :current_user, :params
+
+    def initialize(environment, current_user, params)
+      @environment = environment
+      @current_user = current_user
+      @params = params
+    end
+
+    def execute
+      create_deployment.tap do |deployment|
+        AfterCreateService.new(deployment).execute if deployment.persisted?
+      end
+    end
+
+    def create_deployment
+      environment.deployments.create(deployment_attributes)
+    end
+
+    def deployment_attributes
+      # We use explicit parameters here so we never by accident allow parameters
+      # to be set that one should not be able to set (e.g. the row ID).
+      {
+        cluster_id: environment.deployment_platform&.cluster_id,
+        project_id: environment.project_id,
+        environment_id: environment.id,
+        ref: params[:ref],
+        tag: params[:tag],
+        sha: params[:sha],
+        user: current_user,
+        on_stop: params[:on_stop],
+        status: params[:status]
+      }
+    end
+  end
+end
diff --git a/app/services/deployments/update_service.rb b/app/services/deployments/update_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7c8215d28f2a8a3fb793677a3faab6651d554029
--- /dev/null
+++ b/app/services/deployments/update_service.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Deployments
+  class UpdateService
+    attr_reader :deployment, :params
+
+    def initialize(deployment, params)
+      @deployment = deployment
+      @params = params
+    end
+
+    def execute
+      deployment.update(status: params[:status])
+    end
+  end
+end
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index 395c5fe09ac785abf669cf2db3884eba8edd60e4..f7282c22a5203aefcd23b9d62a8c7d59225e60c2 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -73,15 +73,27 @@ def create_project(project, current_user)
   end
 
   def push(project, current_user, push_data)
+    create_push_event(PushEventPayloadService, project, current_user, push_data)
+  end
+
+  def bulk_push(project, current_user, push_data)
+    create_push_event(BulkPushEventPayloadService, project, current_user, push_data)
+  end
+
+  private
+
+  def create_record_event(record, current_user, status)
+    create_event(record.resource_parent, current_user, status, target_id: record.id, target_type: record.class.name)
+  end
+
+  def create_push_event(service_class, project, current_user, push_data)
     # We're using an explicit transaction here so that any errors that may occur
     # when creating push payload data will result in the event creation being
     # rolled back as well.
     event = Event.transaction do
       new_event = create_event(project, current_user, Event::PUSHED)
 
-      PushEventPayloadService
-        .new(new_event, push_data)
-        .execute
+      service_class.new(new_event, push_data).execute
 
       new_event
     end
@@ -92,12 +104,6 @@ def push(project, current_user, push_data)
     Users::ActivityService.new(current_user, 'push').execute
   end
 
-  private
-
-  def create_record_event(record, current_user, status)
-    create_event(record.resource_parent, current_user, status, target_id: record.id, target_type: record.class.name)
-  end
-
   def create_event(resource_parent, current_user, status, attributes = {})
     attributes.reverse_merge!(
       action: status,
diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb
index 97047d96de11cf8a963f7bc8c519ff3afad23085..0801fd4d03f9621b1f1fb42e71aeb6f9da4d023e 100644
--- a/app/services/git/base_hooks_service.rb
+++ b/app/services/git/base_hooks_service.rb
@@ -48,6 +48,8 @@ def invalidated_file_types
     # Push events in the activity feed only show information for the
     # last commit.
     def create_events
+      return unless params.fetch(:create_push_event, true)
+
       EventCreateService.new.push(project, current_user, event_push_data)
     end
 
@@ -62,6 +64,8 @@ def create_pipelines
     end
 
     def execute_project_hooks
+      return unless params.fetch(:execute_project_hooks, true)
+
       # Creating push_data invokes one CommitDelta RPC per commit. Only
       # build this data if we actually need it.
       project.execute_hooks(push_data, hook_name) if project.has_active_hooks?(hook_name)
diff --git a/app/services/git/process_ref_changes_service.rb b/app/services/git/process_ref_changes_service.rb
index 33925147750b4035e7c51d78def9205cd11cb285..3052bed51bc4408ca2957faac06e8791f04d0506 100644
--- a/app/services/git/process_ref_changes_service.rb
+++ b/app/services/git/process_ref_changes_service.rb
@@ -16,8 +16,8 @@ def execute
     def process_changes_by_action(ref_type, changes)
       changes_by_action = group_changes_by_action(changes)
 
-      changes_by_action.each do |_, changes|
-        process_changes(ref_type, changes) if changes.any?
+      changes_by_action.each do |action, changes|
+        process_changes(ref_type, action, changes, execute_project_hooks: execute_project_hooks?(changes)) if changes.any?
       end
     end
 
@@ -34,18 +34,36 @@ def change_action(change)
       :pushed
     end
 
-    def process_changes(ref_type, changes)
+    def execute_project_hooks?(changes)
+      (changes.size <= Gitlab::CurrentSettings.push_event_hooks_limit) || Feature.enabled?(:git_push_execute_all_project_hooks, project)
+    end
+
+    def process_changes(ref_type, action, changes, execute_project_hooks:)
       push_service_class = push_service_class_for(ref_type)
 
+      create_bulk_push_event = changes.size > Gitlab::CurrentSettings.push_event_activities_limit
+
       changes.each do |change|
         push_service_class.new(
           project,
           current_user,
           change: change,
           push_options: params[:push_options],
-          create_pipelines: change[:index] < PIPELINE_PROCESS_LIMIT || Feature.enabled?(:git_push_create_all_pipelines, project)
+          create_pipelines: change[:index] < PIPELINE_PROCESS_LIMIT || Feature.enabled?(:git_push_create_all_pipelines, project),
+          execute_project_hooks: execute_project_hooks,
+          create_push_event: !create_bulk_push_event
         ).execute
       end
+
+      create_bulk_push_event(ref_type, action, changes) if create_bulk_push_event
+    end
+
+    def create_bulk_push_event(ref_type, action, changes)
+      EventCreateService.new.bulk_push(
+        project,
+        current_user,
+        Gitlab::DataBuilder::Push.build_bulk(action: action, ref_type: ref_type, changes: changes)
+      )
     end
 
     def push_service_class_for(ref_type)
diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb
index fe7e07ef9f0856646531d6a234ef535bde8c80b2..6902b7bd5296d0ff85b48492dce40c2d999cab41 100644
--- a/app/services/groups/transfer_service.rb
+++ b/app/services/groups/transfer_service.rb
@@ -7,7 +7,8 @@ class TransferService < Groups::BaseService
       namespace_with_same_path: s_('TransferGroup|The parent group already has a subgroup with the same path.'),
       group_is_already_root: s_('TransferGroup|Group is already a root group.'),
       same_parent_as_current: s_('TransferGroup|Group is already associated to the parent group.'),
-      invalid_policies: s_("TransferGroup|You don't have enough permissions.")
+      invalid_policies: s_("TransferGroup|You don't have enough permissions."),
+      group_contains_images: s_('TransferGroup|Cannot update the path because there are projects under this group that contain Docker images in their Container Registry. Please remove the images from your projects first and try again.')
     }.freeze
 
     TransferError = Class.new(StandardError)
@@ -46,6 +47,7 @@ def ensure_allowed_transfer
       raise_transfer_error(:same_parent_as_current) if same_parent?
       raise_transfer_error(:invalid_policies) unless valid_policies?
       raise_transfer_error(:namespace_with_same_path) if namespace_with_same_path?
+      raise_transfer_error(:group_contains_images) if group_projects_contain_registry_images?
     end
 
     def group_is_already_root?
@@ -72,6 +74,10 @@ def namespace_with_same_path?
     end
     # rubocop: enable CodeReuse/ActiveRecord
 
+    def group_projects_contain_registry_images?
+      @group.has_container_repositories?
+    end
+
     def update_group_attributes
       if @new_parent_group && @new_parent_group.visibility_level < @group.visibility_level
         update_children_and_projects_visibility
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index 534de601e202a4a4c6752377a961fc06396c300f..be7502a193e2ef7a6cc4f50336c9b07a8b050933 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -8,6 +8,11 @@ def execute
       reject_parent_id!
       remove_unallowed_params
 
+      if renaming_group_with_container_registry_images?
+        group.errors.add(:base, container_images_error)
+        return false
+      end
+
       return false unless valid_visibility_level_change?(group, params[:visibility_level])
 
       return false unless valid_share_with_group_lock_change?
@@ -35,6 +40,17 @@ def before_assignment_hook(group, params)
       # overridden in EE
     end
 
+    def renaming_group_with_container_registry_images?
+      new_path = params[:path]
+
+      new_path && new_path != group.path &&
+        group.has_container_repositories?
+    end
+
+    def container_images_error
+      s_("GroupSettings|Cannot update the path because there are projects under this group that contain Docker images in their Container Registry. Please remove the images from your projects first and try again.")
+    end
+
     def after_update
       if group.previous_changes.include?(:visibility_level) && group.private?
         # don't enqueue immediately to prevent todos removal in case of a mistake
diff --git a/app/services/issuable/clone/content_rewriter.rb b/app/services/issuable/clone/content_rewriter.rb
index f75b51c4be3e17b69d61648fc5a5df43efe24d29..67d2f9fd3feb1690f09ddaedddcabb02df09ec2f 100644
--- a/app/services/issuable/clone/content_rewriter.rb
+++ b/app/services/issuable/clone/content_rewriter.rb
@@ -39,6 +39,10 @@ def rewrite_notes
 
           if note.system_note_metadata
             new_params[:system_note_metadata] = note.system_note_metadata.dup
+
+            # TODO: Implement copying of description versions when an issue is moved
+            # https://gitlab.com/gitlab-org/gitlab/issues/32300
+            new_params[:system_note_metadata].description_version = nil
           end
 
           new_note.update(new_params)
diff --git a/app/services/issues/zoom_link_service.rb b/app/services/issues/zoom_link_service.rb
index de4ab33bdacdbe5a28d2549695a0cd20b7c60dcd..655c63243f80f6deaae0d058c2ea5071803c2d46 100644
--- a/app/services/issues/zoom_link_service.rb
+++ b/app/services/issues/zoom_link_service.rb
@@ -43,9 +43,19 @@ def parse_link(link)
 
     def fetch_added_meeting
       ZoomMeeting.canonical_meeting(@issue)
+    def issue_description
+      issue.description || ''
     end
 
-    def create_zoom_meeting(link)
+    def track_meeting_added_event
+      ::Gitlab::Tracking.event('IncidentManagement::ZoomIntegration', 'add_zoom_meeting', label: 'Issue ID', value: issue.id)
+    end
+
+    def track_meeting_removed_event
+      ::Gitlab::Tracking.event('IncidentManagement::ZoomIntegration', 'remove_zoom_meeting', label: 'Issue ID', value: issue.id)
+    end
+
+    def add_zoom_meeting(link)
       ZoomMeeting.create(
         issue: @issue,
         project: @issue.project,
diff --git a/app/services/metrics/dashboard/grafana_metric_embed_service.rb b/app/services/metrics/dashboard/grafana_metric_embed_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f302a786ba8fa402879fc30bb00ffb7c371fbcf5
--- /dev/null
+++ b/app/services/metrics/dashboard/grafana_metric_embed_service.rb
@@ -0,0 +1,157 @@
+# frozen_string_literal: true
+
+# Responsible for returning a gitlab-compatible dashboard
+# containing info based on a grafana dashboard and datasource.
+#
+# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards.
+module Metrics
+  module Dashboard
+    class GrafanaMetricEmbedService < ::Metrics::Dashboard::BaseService
+      include ReactiveCaching
+
+      SEQUENCE = [
+        ::Gitlab::Metrics::Dashboard::Stages::GrafanaFormatter
+      ].freeze
+
+      self.reactive_cache_key = ->(service) { service.cache_key }
+      self.reactive_cache_lease_timeout = 30.seconds
+      self.reactive_cache_refresh_interval = 30.minutes
+      self.reactive_cache_lifetime = 30.minutes
+      self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
+
+      class << self
+        # Determines whether the provided params are sufficient
+        # to uniquely identify a grafana dashboard.
+        def valid_params?(params)
+          [
+            params[:embedded],
+            params[:grafana_url]
+          ].all?
+        end
+
+        def from_cache(project_id, user_id, grafana_url)
+          project = Project.find(project_id)
+          user = User.find(user_id)
+
+          new(project, user, grafana_url: grafana_url)
+        end
+      end
+
+      def get_dashboard
+        with_reactive_cache(*cache_key) { |result| result }
+      end
+
+      # Inherits the primary logic from the parent class and
+      # maintains the service's API while including ReactiveCache
+      def calculate_reactive_cache(*)
+        ::Metrics::Dashboard::BaseService
+          .instance_method(:get_dashboard)
+          .bind(self)
+          .call() # rubocop:disable Style/MethodCallWithoutArgsParentheses
+      end
+
+      def cache_key(*args)
+        [project.id, current_user.id, grafana_url]
+      end
+
+      # Required for ReactiveCaching; Usage overridden by
+      # self.reactive_cache_worker_finder
+      def id
+        nil
+      end
+
+      private
+
+      def get_raw_dashboard
+        raise MissingIntegrationError unless client
+
+        grafana_dashboard = fetch_dashboard
+        datasource = fetch_datasource(grafana_dashboard)
+
+        params.merge!(grafana_dashboard: grafana_dashboard, datasource: datasource)
+
+        {}
+      end
+
+      def fetch_dashboard
+        uid = GrafanaUidParser.new(grafana_url, project).parse
+        raise DashboardProcessingError.new('Dashboard uid not found') unless uid
+
+        response = client.get_dashboard(uid: uid)
+
+        parse_json(response.body)
+      end
+
+      def fetch_datasource(dashboard)
+        name = DatasourceNameParser.new(grafana_url, dashboard).parse
+        raise DashboardProcessingError.new('Datasource name not found') unless name
+
+        response = client.get_datasource(name: name)
+
+        parse_json(response.body)
+      end
+
+      def grafana_url
+        params[:grafana_url]
+      end
+
+      def client
+        project.grafana_integration&.client
+      end
+
+      def allowed?
+        Ability.allowed?(current_user, :read_project, project)
+      end
+
+      def sequence
+        SEQUENCE
+      end
+
+      def parse_json(json)
+        JSON.parse(json, symbolize_names: true)
+      rescue JSON::ParserError
+        raise DashboardProcessingError.new('Grafana response contains invalid json')
+      end
+    end
+
+    # Identifies the uid of the dashboard based on url format
+    class GrafanaUidParser
+      def initialize(grafana_url, project)
+        @grafana_url, @project = grafana_url, project
+      end
+
+      def parse
+        @grafana_url.match(uid_regex) { |m| m.named_captures['uid'] }
+      end
+
+      private
+
+      # URLs are expected to look like https://domain.com/d/:uid/other/stuff
+      def uid_regex
+        base_url = @project.grafana_integration.grafana_url.chomp('/')
+
+        %r{(#{Regexp.escape(base_url)}\/d\/(?<uid>\w+)\/)}x
+      end
+    end
+
+    # Identifies the name of the datasource for a dashboard
+    # based on the panelId query parameter found in the url
+    class DatasourceNameParser
+      def initialize(grafana_url, grafana_dashboard)
+        @grafana_url, @grafana_dashboard = grafana_url, grafana_dashboard
+      end
+
+      def parse
+        @grafana_dashboard[:dashboard][:panels]
+          .find { |panel| panel[:id].to_s == query_params[:panelId] }
+          .try(:[], :datasource)
+      end
+
+      private
+
+      def query_params
+        Gitlab::Metrics::Dashboard::Url.parse_query(@grafana_url)
+      end
+    end
+  end
+end
diff --git a/app/services/note_summary.rb b/app/services/note_summary.rb
index 60a6856883392951babdb48dabecbe9f3e946e99..6fe14939aaa73787cf88920a2e439c7e74c42c5c 100644
--- a/app/services/note_summary.rb
+++ b/app/services/note_summary.rb
@@ -10,6 +10,10 @@ def initialize(noteable, project, author, body, action: nil, commit_count: nil)
               project: project, author: author, note: body }
     @metadata = { action: action, commit_count: commit_count }.compact
 
+    if action == 'description' && noteable.saved_description_version
+      @metadata[:description_version] = noteable.saved_description_version
+    end
+
     set_commit_params if note[:noteable].is_a?(Commit)
   end
 
diff --git a/app/services/notes/quick_actions_service.rb b/app/services/notes/quick_actions_service.rb
index 076df10bf6fbd3287a7dba1f2586f5c8c38cdd7f..7e6568b5b259323cfa27ebd47e7f99cd07950f2d 100644
--- a/app/services/notes/quick_actions_service.rb
+++ b/app/services/notes/quick_actions_service.rb
@@ -50,7 +50,7 @@ def apply_updates(update_params, note)
       return if update_params.empty?
       return unless supported?(note)
 
-      self.class.noteable_update_service(note).new(note.parent, current_user, update_params).execute(note.noteable)
+      self.class.noteable_update_service(note).new(note.resource_parent, current_user, update_params).execute(note.noteable)
     end
   end
 end
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index fca64270cae58d06df55154ee62382b904364cc1..9afbb678f5d8fee6da1115f082945779ddb2c42f 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -28,6 +28,10 @@ def self.build_project_maintainers_recipients(*args)
     Builder::ProjectMaintainers.new(*args).notification_recipients
   end
 
+  def self.build_new_release_recipients(*args)
+    Builder::NewRelease.new(*args).notification_recipients
+  end
+
   module Builder
     class Base
       def initialize(*)
@@ -359,6 +363,26 @@ def acting_user
       end
     end
 
+    class NewRelease < Base
+      attr_reader :target
+
+      def initialize(target)
+        @target = target
+      end
+
+      def build!
+        add_recipients(target.project.authorized_users, :custom, nil)
+      end
+
+      def custom_action
+        :new_release
+      end
+
+      def acting_user
+        target.author
+      end
+    end
+
     class MergeRequestUnmergeable < Base
       attr_reader :target
       def initialize(merge_request)
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index ed357aa039207d1af2a5490b03b1d9850eebc71a..b56b2cf14e361a204660c822716fea115fe4e083 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -289,6 +289,15 @@ def send_new_note_notifications(note)
     end
   end
 
+  # Notify users when a new release is created
+  def send_new_release_notifications(release)
+    recipients = NotificationRecipientService.build_new_release_recipients(release)
+
+    recipients.each do |recipient|
+      mailer.new_release_email(recipient.user.id, release, recipient.reason).deliver_later
+    end
+  end
+
   # Members
   def new_access_request(member)
     return true unless member.notifiable?(:subscription)
diff --git a/app/services/projects/container_repository/delete_tags_service.rb b/app/services/projects/container_repository/delete_tags_service.rb
index 22656b29c582d01b214493d11ab57cb7f0ee4840..5129e2269a8c11fe2944b01d83b260260e34eeec 100644
--- a/app/services/projects/container_repository/delete_tags_service.rb
+++ b/app/services/projects/container_repository/delete_tags_service.rb
@@ -48,10 +48,10 @@ def smart_delete(container_repository, tag_names)
         # rubocop: disable CodeReuse/ActiveRecord
         Gitlab::Sentry.track_exception(ArgumentError.new('multiple tag digests')) if tag_digests.many?
 
-        # deletes the dummy image
-        # all created tag digests are the same since they all have the same dummy image.
+        # Deletes the dummy image
+        # All created tag digests are the same since they all have the same dummy image.
         # a single delete is sufficient to remove all tags with it
-        if container_repository.client.delete_repository_tag(container_repository.path, tag_digests.first)
+        if container_repository.delete_tag_by_digest(tag_digests.first)
           success(deleted: tag_names)
         else
           error('could not delete tags')
diff --git a/app/services/search/snippet_service.rb b/app/services/search/snippet_service.rb
index 3c8847d3c185c4e5fc9c98828933e001b1dbabaf..e686d3bf7c28677004df9c7fe46fb8536da712bd 100644
--- a/app/services/search/snippet_service.rb
+++ b/app/services/search/snippet_service.rb
@@ -1,13 +1,7 @@
 # frozen_string_literal: true
 
 module Search
-  class SnippetService
-    attr_accessor :current_user, :params
-
-    def initialize(user, params)
-      @current_user, @params = user, params.dup
-    end
-
+  class SnippetService < Search::GlobalService
     def execute
       Gitlab::SnippetSearchResults.new(current_user, params[:search])
     end
diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb
index 4377f437798b9961f909a603acde656682bb86b7..6fffd2ed4bf95918cf5180f2b15f5ff7128c0f87 100644
--- a/app/services/system_notes/issuables_service.rb
+++ b/app/services/system_notes/issuables_service.rb
@@ -288,18 +288,16 @@ def cross_reference_note_content(gfm_reference)
       "#{self.class.cross_reference_note_prefix}#{gfm_reference}"
     end
 
-    # rubocop: disable CodeReuse/ActiveRecord
     def notes_for_mentioner(mentioner, noteable, notes)
       if mentioner.is_a?(Commit)
         text = "#{self.class.cross_reference_note_prefix}%#{mentioner.to_reference(nil)}"
-        notes.where('(note LIKE ? OR note LIKE ?)', text, text.capitalize)
+        notes.like_note_or_capitalized_note(text)
       else
         gfm_reference = mentioner.gfm_reference(noteable.project || noteable.group)
         text = cross_reference_note_content(gfm_reference)
-        notes.where(note: [text, text.capitalize])
+        notes.for_note_or_capitalized_note(text)
       end
     end
-    # rubocop: enable CodeReuse/ActiveRecord
 
     def self.cross_reference_note_prefix
       'mentioned in '
diff --git a/app/services/update_deployment_service.rb b/app/services/update_deployment_service.rb
deleted file mode 100644
index 730210c611a3e34e4d9797667a463ef34ef66020..0000000000000000000000000000000000000000
--- a/app/services/update_deployment_service.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# frozen_string_literal: true
-
-class UpdateDeploymentService
-  attr_reader :deployment
-  attr_reader :deployable
-
-  delegate :environment, to: :deployment
-  delegate :variables, to: :deployable
-
-  def initialize(deployment)
-    @deployment = deployment
-    @deployable = deployment.deployable
-  end
-
-  def execute
-    deployment.create_ref
-    deployment.invalidate_cache
-
-    ActiveRecord::Base.transaction do
-      environment.external_url = expanded_environment_url if
-        expanded_environment_url
-
-      environment.fire_state_event(action)
-
-      break unless environment.save
-      break if environment.stopped?
-
-      deployment.tap(&:update_merge_request_metrics!)
-    end
-
-    deployment
-  end
-
-  private
-
-  def environment_options
-    @environment_options ||= deployable.options&.dig(:environment) || {}
-  end
-
-  def expanded_environment_url
-    return @expanded_environment_url if defined?(@expanded_environment_url)
-    return unless environment_url
-
-    @expanded_environment_url =
-      ExpandVariables.expand(environment_url, -> { variables })
-  end
-
-  def environment_url
-    environment_options[:url]
-  end
-
-  def action
-    environment_options[:action] || 'start'
-  end
-end
-
-UpdateDeploymentService.prepend_if_ee('EE::UpdateDeploymentService')
diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb
index f99ad98715601e09947c8ae7a2a4b7c7dc621df9..36bde629f9cb3a70217388392ce0457ea5a8e3f1 100644
--- a/app/uploaders/object_storage.rb
+++ b/app/uploaders/object_storage.rb
@@ -180,10 +180,11 @@ def serialization_column(model_class, mount_point)
       end
 
       def workhorse_authorize(has_length:, maximum_size: nil)
-        {
-          RemoteObject: workhorse_remote_upload_options(has_length: has_length, maximum_size: maximum_size),
-          TempPath: workhorse_local_upload_path
-        }.compact
+        if self.object_store_enabled? && self.direct_upload_enabled?
+          { RemoteObject: workhorse_remote_upload_options(has_length: has_length, maximum_size: maximum_size) }
+        else
+          { TempPath: workhorse_local_upload_path }
+        end
       end
 
       def workhorse_local_upload_path
diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml
index b52171afc69e9ee5eb1ee26969cfd4ad414d30fd..6b02521a0f0fbb31767c90564539aac7d0ad6d84 100644
--- a/app/views/admin/application_settings/_performance.html.haml
+++ b/app/views/admin/application_settings/_performance.html.haml
@@ -20,5 +20,15 @@
       = f.number_field :raw_blob_request_limit, class: 'form-control'
       .form-text.text-muted
         = _('Highest number of requests per minute for each raw path, default to 300. To disable throttling set to 0.')
+    .form-group
+      = f.label :push_event_hooks_limit, class: 'label-bold'
+      = f.number_field :push_event_hooks_limit, class: 'form-control'
+      .form-text.text-muted
+        = _("Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value.")
+    .form-group
+      = f.label :push_event_activities_limit, class: 'label-bold'
+      = f.number_field :push_event_activities_limit, class: 'form-control'
+      .form-text.text-muted
+        = _('Number of changes (branches or tags) in a single push to determine whether individual push events or bulk push event will be created. Bulk push event will be created if it surpasses that value.')
 
   = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml
index e57ef1ea18f1c6ed332be9aa3fbe05f7aa9939c3..be5f1f4f9a828acc9971ca5afdbd1bb0b06456d1 100644
--- a/app/views/admin/application_settings/_visibility_and_access.html.haml
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -53,6 +53,11 @@
       = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
       %span.form-text.text-muted#clone-protocol-help
         = _('Allow only the selected protocols to be used for Git access.')
+    .form-group
+      = f.label :custom_http_clone_url_root, _('Custom Git clone URL for HTTP(S)'), class: 'label-bold'
+      = f.text_field :custom_http_clone_url_root, class: 'form-control', placeholder: 'https://git.example.com', :'aria-describedby' => 'custom_http_clone_url_root_help_block'
+      %span.form-text.text-muted#custom_http_clone_url_root_help_block
+        = _('Replaces the clone URL root.')
 
     - ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
       - field_name = :"#{type}_key_restriction"
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 2b913808c2f81e1c98cb25c792b0be03b6c68c0c..41147950c40028cecd890500b1ccf213c4583bf8 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -1,6 +1,7 @@
 - breadcrumb_title "Dashboard"
 
-= render_if_exists 'admin/licenses/breakdown', license: @license
+- if show_license_breakdown?
+  = render_if_exists 'admin/licenses/breakdown', license: @license
 
 .admin-dashboard.prepend-top-default
   .row
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 545e53e6b09d3662fe0b57b4098070988689806e..2bf2b5fce8da36b973a6181d4bcafea400d206c5 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -45,12 +45,11 @@
     = form_tag admin_runners_path, id: 'runners-search', method: :get, class: 'filter-form js-filter-form' do
       .filtered-search-wrapper.d-flex
         .filtered-search-box
-          = dropdown_tag(custom_icon('icon_history'),
+          = dropdown_tag(_('Recent searches'),
             options: { wrapper_class: 'filtered-search-history-dropdown-wrapper',
             toggle_class: 'filtered-search-history-dropdown-toggle-button',
             dropdown_class: 'filtered-search-history-dropdown',
-            content_class: 'filtered-search-history-dropdown-content',
-            title: _('Recent searches') }) do
+            content_class: 'filtered-search-history-dropdown-content' }) do
             .js-filtered-search-history-dropdown{ data: { full_path: admin_runners_path } }
           .filtered-search-box-input-container.droplab-dropdown
             .scroll-container
diff --git a/app/views/admin/sessions/_new_base.html.haml b/app/views/admin/sessions/_new_base.html.haml
index 55aea0296e7ed2984753f4169d7dd53c0ff7ec33..3d77a439d615970124823404e16ad0b03b77d2e0 100644
--- a/app/views/admin/sessions/_new_base.html.haml
+++ b/app/views/admin/sessions/_new_base.html.haml
@@ -4,4 +4,4 @@
     = password_field_tag :password, nil, class: 'form-control', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' }
 
   .submit-container.move-submit-down
-    = submit_tag _('Enter admin mode'), class: 'btn btn-success', data: { qa_selector: 'sign_in_button' }
+    = submit_tag _('Enter Admin Mode'), class: 'btn btn-success', data: { qa_selector: 'sign_in_button' }
diff --git a/app/views/admin/sessions/_tabs_normal.html.haml b/app/views/admin/sessions/_tabs_normal.html.haml
index f5dedb5ad760ea64b9ba996dcff31eb8366669b4..20830051d316ed35215340d00f24ebf5c160d24c 100644
--- a/app/views/admin/sessions/_tabs_normal.html.haml
+++ b/app/views/admin/sessions/_tabs_normal.html.haml
@@ -1,3 +1,3 @@
 %ul.nav-links.new-session-tabs.nav-tabs.nav{ role: 'tablist' }
   %li.nav-item{ role: 'presentation' }
-    %a.nav-link.active{ href: '#login-pane', data: { toggle: 'tab', qa_selector: 'sign_in_tab' }, role: 'tab' }= _('Enter admin mode')
+    %a.nav-link.active{ href: '#login-pane', data: { toggle: 'tab', qa_selector: 'sign_in_tab' }, role: 'tab' }= _('Enter Admin Mode')
diff --git a/app/views/admin/sessions/new.html.haml b/app/views/admin/sessions/new.html.haml
index ee06b4a17415cf06f90d5a81191fbc18498ba312..73028e78ea5763e3c622341fd47c142b03d5631a 100644
--- a/app/views/admin/sessions/new.html.haml
+++ b/app/views/admin/sessions/new.html.haml
@@ -1,5 +1,5 @@
 - @hide_breadcrumbs = true
-- page_title _('Enter admin mode')
+- page_title _('Enter Admin Mode')
 
 .row.justify-content-center
   .col-6.new-session-forms-container
diff --git a/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml b/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml
index f707c6585ec58eea8775d777f32c4fd3c7f92031..d4999798c1931989cb1aa794941639f03c8b05ef 100644
--- a/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml
+++ b/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml
@@ -3,6 +3,6 @@
 - label = local_assigns.fetch(:label)
 
 = link_to clusterable.new_path(provider: provider), class: 'btn gl-button btn-outline flex-fill d-inline-flex flex-column mr-3 justify-content-center align-items-center' do
-  = image_tag logo_path, alt: label, class: 'gl-w-13 gl-h-13'
+  .svg-content= image_tag logo_path, alt: label, class: 'gl-w-13 gl-h-13'
   %span
     = label
diff --git a/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml b/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml
index 24506205243831e2eb34ad8093ed770165183f80..7a93a7604f5ad8012964bd6d1ef46168f1a4126e 100644
--- a/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml
+++ b/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml
@@ -6,6 +6,6 @@
     = create_cluster_label
   .d-flex
     = render partial: 'clusters/clusters/cloud_providers/cloud_provider_button',
-      locals: { provider: 'gke', label: gke_label, logo_path: '' }
+      locals: { provider: 'eks', label: eks_label, logo_path: 'illustrations/logos/amazon_eks.svg' }
     = render partial: 'clusters/clusters/cloud_providers/cloud_provider_button',
-      locals: { provider: 'eks', label: eks_label, logo_path: '' }
+      locals: { provider: 'gke', label: gke_label, logo_path: 'illustrations/logos/google_gke.svg' }
diff --git a/app/views/clusters/clusters/eks/_index.html.haml b/app/views/clusters/clusters/eks/_index.html.haml
index 0e9334948ab8f6912fdbdcac03eb83882bf6f66d..db64698a7f2a67c85d5d85fcec07ca391093e3f2 100644
--- a/app/views/clusters/clusters/eks/_index.html.haml
+++ b/app/views/clusters/clusters/eks/_index.html.haml
@@ -1 +1,2 @@
-.js-create-eks-cluster-form-container{ data: { 'gitlab-managed-cluster-help-path' => help_page_path('user/project/clusters/index.md', anchor: 'gitlab-managed-clusters') } }
+.js-create-eks-cluster-form-container{ data: { 'gitlab-managed-cluster-help-path' => help_page_path('user/project/clusters/index.md', anchor: 'gitlab-managed-clusters'),
+'kubernetes-integration-help-path' => help_page_path('user/project/clusters/index') } }
diff --git a/app/views/clusters/clusters/gcp/_form.html.haml b/app/views/clusters/clusters/gcp/_form.html.haml
index 196ad422766928787f5b42cba614b8906c2e0925..cca16ce7eda3a27f8b2a25dfd957e20ce8137505 100644
--- a/app/views/clusters/clusters/gcp/_form.html.haml
+++ b/app/views/clusters/clusters/gcp/_form.html.haml
@@ -3,13 +3,12 @@
 - zones_link_url = 'https://cloud.google.com/compute/docs/regions-zones/regions-zones'
 - machine_type_link_url = 'https://cloud.google.com/compute/docs/machine-types'
 - pricing_link_url = 'https://cloud.google.com/compute/pricing#machinetype'
+- kubernetes_integration_url = help_page_path('user/project/clusters/index')
 - help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
 - help_link_end = ' %{external_link_icon}</a>'.html_safe % { external_link_icon: external_link_icon }
 
 %p
-  - link_to_help_page = link_to(s_('ClusterIntegration|help page'),
-    help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
-  = s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page }
+  = s_('ClusterIntegration|Read our %{link_start}help page%{link_end} on Kubernetes cluster integration.').html_safe % { link_start: help_link_start % { url: kubernetes_integration_url }, link_end: '</a>'.html_safe }
 
 %p= link_to('Select a different Google account', @authorize_url)
 
diff --git a/app/views/clusters/clusters/gcp/_gcp_not_configured.html.haml b/app/views/clusters/clusters/gcp/_gcp_not_configured.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..b57e45e9812dcc1113d343689d5b0499d46292b0
--- /dev/null
+++ b/app/views/clusters/clusters/gcp/_gcp_not_configured.html.haml
@@ -0,0 +1,3 @@
+- documentation_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path("integration/google") }
+- link_end = '<a/>'.html_safe
+= s_('Google authentication is not %{link_start}property configured%{link_end}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_start: documentation_link_start, link_end: link_end }
diff --git a/app/views/clusters/clusters/gcp/_signin_with_google_button.html.haml b/app/views/clusters/clusters/gcp/_signin_with_google_button.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..65cfa6552b17954b45891cc902d995ef6bdf004a
--- /dev/null
+++ b/app/views/clusters/clusters/gcp/_signin_with_google_button.html.haml
@@ -0,0 +1,4 @@
+.signin-with-google
+  - create_account_link = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral' }
+  = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px', alt: _('Sign in with Google')), @authorize_url)
+  = s_('or %{link_start}create a new Google account%{link_end}').html_safe % { link_start: create_account_link, link_end: '</a>'.html_safe }
diff --git a/app/views/clusters/clusters/new.html.haml b/app/views/clusters/clusters/new.html.haml
index fb182d99ff02d65df9735eda4201745b9b074320..2c23426aaf90280ccd3ff16985df8a212c8f51a9 100644
--- a/app/views/clusters/clusters/new.html.haml
+++ b/app/views/clusters/clusters/new.html.haml
@@ -2,7 +2,9 @@
 - page_title _('Kubernetes Cluster')
 - create_eks_enabled = Feature.enabled?(:create_eks_clusters)
 - active_tab = local_assigns.fetch(:active_tab, 'create')
-- link_end = '<a/>'.html_safe
+- create_on_gke_tab_label = s_('ClusterIntegration|Create new Cluster on GKE')
+- create_on_eks_tab_label = s_('ClusterIntegration|Create new Cluster on EKS')
+- create_new_cluster_label = s_('ClusterIntegration|Create new Cluster')
 = javascript_include_tag 'https://apis.google.com/js/api.js'
 
 = render_gcp_signup_offer
@@ -14,7 +16,16 @@
     %ul.nav-links.nav-tabs.gitlab-tabs.nav{ role: 'tablist' }
       %li.nav-item{ role: 'presentation' }
         %a.nav-link{ href: '#create-cluster-pane', id: 'create-cluster-tab', class: active_when(active_tab == 'create'), data: { toggle: 'tab' }, role: 'tab' }
-          %span Create new Cluster on GKE
+          %span
+            - if create_eks_enabled
+              - if @gke_selected
+                = create_on_gke_tab_label
+              - elsif @eks_selected
+                = create_on_eks_tab_label
+              - else
+                = create_new_cluster_label
+            - else
+              = create_on_gke_tab_label
       %li.nav-item{ role: 'presentation' }
         %a.nav-link{ href: '#add-cluster-pane', id: 'add-cluster-tab', class: active_when(active_tab == 'add'), data: { toggle: 'tab' }, role: 'tab' }
           %span Add existing cluster
@@ -22,9 +33,14 @@
     .tab-content.gitlab-tab-content
       - if create_eks_enabled
         .tab-pane{ id: 'create-cluster-pane', class: active_when(active_tab == 'create'), role: 'tabpanel' }
-          - if @gke_selected && @valid_gcp_token
+          - if @gke_selected
             = render 'clusters/clusters/gcp/header'
-            = render 'clusters/clusters/gcp/form'
+            - if @valid_gcp_token
+              = render 'clusters/clusters/gcp/form'
+            - elsif @authorize_url
+              = render 'clusters/clusters/gcp/signin_with_google_button'
+            - else
+              = render 'clusters/clusters/gcp/gcp_not_configured'
           - elsif @eks_selected
             = render 'clusters/clusters/eks/index'
           - else
@@ -35,13 +51,9 @@
           - if @valid_gcp_token
             = render 'clusters/clusters/gcp/form'
           - elsif @authorize_url
-            .signin-with-google
-              - create_account_link = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral' }
-              = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px', alt: _('Sign in with Google')), @authorize_url)
-              = s_('or %{link_start}create a new Google account%{link_end}').html_safe % { link_start: create_account_link, link_end: link_end }
+            = render 'clusters/clusters/gcp/signin_with_google_button'
           - else
-            - documentation_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path("integration/google") }
-            = s_('Google authentication is not %{link_start}property configured%{link_end}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_start: documentation_link_start, link_end: link_end }
+            = render 'clusters/clusters/gcp/gcp_not_configured'
 
       .tab-pane{ id: 'add-cluster-pane', class: active_when(active_tab == 'add'), role: 'tabpanel' }
         = render 'clusters/clusters/user/header'
diff --git a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
index c50b20a83dc760e296a888f3f51ecf84a6946a42..6e7ec1264eafbd1c4d9430e9cbe6d3e791a5d3ce 100644
--- a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
+++ b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
@@ -1,41 +1,40 @@
 .blank-state-row
-  = link_to new_project_path, class: "blank-state-link" do
-    .blank-state
+  - if has_start_trial?
+    = render_if_exists "dashboard/projects/blank_state_ee_trial"
+
+  = link_to new_project_path, class: "blank-state blank-state-link" do
+    .blank-state-icon
+      = image_tag("illustrations/welcome/add_new_project")
+    .blank-state-body
+      %h3.blank-state-title
+        Create a project
+      %p.blank-state-text
+        Projects are where you store your code, access issues, wiki and other features of GitLab.
+
+  - if current_user.can_create_group?
+    = link_to new_group_path, class: "blank-state blank-state-link" do
       .blank-state-icon
-        = custom_icon("add_new_project", size: 50)
+        = image_tag("illustrations/welcome/add_new_group")
       .blank-state-body
         %h3.blank-state-title
-          Create a project
+          Create a group
         %p.blank-state-text
-          Projects are where you store your code, access issues, wiki and other features of GitLab.
+          Groups are a great way to organize projects and people.
 
-  - if current_user.can_create_group?
-    = link_to new_group_path, class: "blank-state-link" do
-      .blank-state
-        .blank-state-icon
-          = custom_icon("add_new_group", size: 50)
-        .blank-state-body
-          %h3.blank-state-title
-            Create a group
-          %p.blank-state-text
-            Groups are a great way to organize projects and people.
-
-    = link_to new_admin_user_path, class: "blank-state-link" do
-      .blank-state
-        .blank-state-icon
-          = custom_icon("add_new_user", size: 50)
-        .blank-state-body
-          %h3.blank-state-title
-            Add people
-          %p.blank-state-text
-            Add your team members and others to GitLab.
-
-  = link_to admin_root_path, class: "blank-state-link" do
-    .blank-state
+    = link_to new_admin_user_path, class: "blank-state blank-state-link" do
       .blank-state-icon
-        = custom_icon("configure_server", size: 50)
+        = image_tag("illustrations/welcome/add_new_user")
       .blank-state-body
         %h3.blank-state-title
-          Configure GitLab
+          Add people
         %p.blank-state-text
-          Make adjustments to how your GitLab instance is set up.
+          Add your team members and others to GitLab.
+
+  = link_to admin_root_path, class: "blank-state blank-state-link" do
+    .blank-state-icon
+      = image_tag("illustrations/welcome/configure_server")
+    .blank-state-body
+      %h3.blank-state-title
+        Configure GitLab
+      %p.blank-state-text
+        Make adjustments to how your GitLab instance is set up.
diff --git a/app/views/dashboard/projects/_blank_state_welcome.html.haml b/app/views/dashboard/projects/_blank_state_welcome.html.haml
index 8d5bddbb288d844772c940d56652a420e4e91db2..e3af3405b7649d4c95eda78ae55c975544ac1d9f 100644
--- a/app/views/dashboard/projects/_blank_state_welcome.html.haml
+++ b/app/views/dashboard/projects/_blank_state_welcome.html.haml
@@ -2,19 +2,18 @@
 
 .blank-state-row
   - if current_user.can_create_project?
-    = link_to new_project_path, class: "blank-state-link" do
-      .blank-state
-        .blank-state-icon
-          = custom_icon("add_new_project", size: 50)
-        .blank-state-body
-          %h3.blank-state-title
-            Create a project
-          %p.blank-state-text
-            Projects are where you store your code, access issues, wiki and other features of GitLab.
+    = link_to new_project_path, class: "blank-state blank-state-link" do
+      .blank-state-icon
+        = image_tag("illustrations/welcome/add_new_project")
+      .blank-state-body
+        %h3.blank-state-title
+          Create a project
+        %p.blank-state-text
+          Projects are where you store your code, access issues, wiki and other features of GitLab.
   - else
     .blank-state
       .blank-state-icon
-        = custom_icon("add_new_project", size: 50)
+        = image_tag("illustrations/welcome/add_new_project")
       .blank-state-body
         %h3.blank-state-title
           Create a project
@@ -22,37 +21,34 @@
           If you are added to a project, it will be displayed here.
 
   - if current_user.can_create_group?
-    = link_to new_group_path, class: "blank-state-link" do
-      .blank-state
-        .blank-state-icon
-          = custom_icon("add_new_group", size: 50)
-        .blank-state-body
-          %h3.blank-state-title
-            Create a group
-          %p.blank-state-text
-            Groups are the best way to manage projects and members.
+    = link_to new_group_path, class: "blank-state blank-state-link" do
+      .blank-state-icon
+        = image_tag("illustrations/welcome/add_new_group")
+      .blank-state-body
+        %h3.blank-state-title
+          Create a group
+        %p.blank-state-text
+          Groups are the best way to manage projects and members.
 
   - if public_project_count > 0
-    = link_to trending_explore_projects_path, class: "blank-state-link" do
-      .blank-state
-        .blank-state-icon
-          = custom_icon("globe", size: 50)
-        .blank-state-body
-          %h3.blank-state-title
-            Explore public projects
-          %p.blank-state-text
-            There are
-            = number_with_delimiter(public_project_count)
-            public projects on this server.
-            Public projects are an easy way to allow
-            everyone to have read-only access.
-
-  = link_to "https://docs.gitlab.com/", class: "blank-state-link" do
-    .blank-state
+    = link_to trending_explore_projects_path, class: "blank-state blank-state-link" do
       .blank-state-icon
-        = custom_icon("lightbulb", size: 50)
+        = image_tag("illustrations/welcome/globe")
       .blank-state-body
         %h3.blank-state-title
-          Learn more about GitLab
+          Explore public projects
         %p.blank-state-text
-          Take a look at the documentation to discover all of GitLab's capabilities.
+          There are
+          = number_with_delimiter(public_project_count)
+          public projects on this server.
+          Public projects are an easy way to allow
+          everyone to have read-only access.
+
+  = link_to "https://docs.gitlab.com/", class: "blank-state blank-state-link" do
+    .blank-state-icon
+      = image_tag("illustrations/welcome/lightbulb")
+    .blank-state-body
+      %h3.blank-state-title
+        Learn more about GitLab
+      %p.blank-state-text
+        Take a look at the documentation to discover all of GitLab's capabilities.
diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
index eff68f817bb45e82ed9988be689351c9ef4dc2c4..a2b1f0d9298efa410765a695458c069b9a5db962 100644
--- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml
+++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
@@ -1,4 +1,4 @@
-.blank-state-parent-container{ class: ('has-start-trial-container' if has_start_trial?) }
+.blank-state-parent-container
   .section-container.section-welcome{ class: "#{ 'section-admin-welcome' if current_user.admin? }" }
     .container.section-body
       .row
@@ -7,12 +7,7 @@
             = _('Welcome to GitLab')
           %p.blank-state-text
             = _('Faster releases. Better code. Less pain.')
-        .blank-state-row
-          %div{ class: ('column-large' if has_start_trial?) }
-            - if current_user.admin?
-              = render "blank_state_admin_welcome"
-            - else
-              = render "blank_state_welcome"
-          - if has_start_trial?
-            .column-small
-              = render_if_exists "blank_state_ee_trial"
+        - if current_user.admin?
+          = render "blank_state_admin_welcome"
+        - else
+          = render "blank_state_welcome"
diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml
index dfdf7429dc54fb1d2383f832e2f0cacea0624214..5f85235e8fa718e0e6e20e10dfec1a86c7b8e2fd 100644
--- a/app/views/devise/registrations/new.html.haml
+++ b/app/views/devise/registrations/new.html.haml
@@ -1,5 +1,5 @@
 - page_title "Sign up"
-- if use_experimental_separate_sign_up_flow?
+- if experiment_enabled?(:signup_flow)
   = render 'devise/shared/experimental_separate_sign_up_flow_box'
 - else
   = render 'devise/shared/signup_box'
diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml
index 17e5da501f53e7e1b2c4b80f1506763c30e53ac6..8f6c3ecbe584ddef5c79625f58a04d4f52c7cc50 100644
--- a/app/views/devise/sessions/new.html.haml
+++ b/app/views/devise/sessions/new.html.haml
@@ -4,7 +4,7 @@
   - if form_based_providers.any?
     = render 'devise/shared/tabs_ldap'
   - else
-    - unless use_experimental_separate_sign_up_flow?
+    - unless experiment_enabled?(:signup_flow)
       = render 'devise/shared/tabs_normal'
   .tab-content
     - if password_authentication_enabled_for_web? || ldap_enabled? || crowd_enabled?
diff --git a/app/views/devise/shared/_experimental_separate_sign_up_flow_box.html.haml b/app/views/devise/shared/_experimental_separate_sign_up_flow_box.html.haml
index f92c29da5e63e52acd1704ac15b12bdb05001ab1..5d163d03c735882c12fd1a1897c95c23e5395701 100644
--- a/app/views/devise/shared/_experimental_separate_sign_up_flow_box.html.haml
+++ b/app/views/devise/shared/_experimental_separate_sign_up_flow_box.html.haml
@@ -1,4 +1,4 @@
-- max_name_length = 128
+- content_for(:page_title, _('Register for GitLab'))
 - max_username_length = 255
 .signup-box.p-3.mb-2
   .signup-body
@@ -6,9 +6,6 @@
       .devise-errors.mt-0
         = render "devise/shared/error_messages", resource: resource
       = invisible_captcha
-      .name.form-group
-        = f.label :name, _('Full name'), class: 'label-bold'
-        = f.text_field :name, class: "form-control top js-block-emoji js-validate-length", :data => { :max_length => max_name_length, :max_length_message => s_("SignUp|Name is too long (maximum is %{max_length} characters).") % { max_length: max_name_length }, :qa_selector => 'new_user_name_field' }, required: true, title: _("This field is required.")
       .username.form-group
         = f.label :username, class: 'label-bold'
         = f.text_field :username, class: "form-control middle js-block-emoji js-validate-length js-validate-username", :data => { :max_length => max_username_length, :max_length_message => s_("SignUp|Username is too long (maximum is %{max_length} characters).") % { max_length: max_username_length }, :qa_selector => 'new_user_username_field' }, pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: _("Please create a username with only alphanumeric characters.")
diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml
index deaceccbfc7eae8df851b79f9486f1526a50f2dc..746d43edbadb765f145d05811b5aa80c3658880c 100644
--- a/app/views/devise/shared/_signin_box.html.haml
+++ b/app/views/devise/shared/_signin_box.html.haml
@@ -23,7 +23,7 @@
     .login-body
       = render 'devise/sessions/new_base'
 
-- if use_experimental_separate_sign_up_flow?
+- if experiment_enabled?(:signup_flow)
   %p.light.mt-2
     = _("Don't have an account yet?")
     = link_to _("Register now"), new_registration_path(:user)
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 21c418cb0e4feb7303e4643d3e3e52224ba0d2bf..b9e88f3fc47a749a85d1ea7f2de3953bb6607f9a 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -6,11 +6,13 @@
 
 .event-title.d-flex.flex-wrap
   = inline_event_icon(event)
-  %span.event-type.d-inline-block.append-right-4.pushed #{event.action_name} #{event.ref_type}
-  %span.append-right-4
-    - commits_link = project_commits_path(project, event.ref_name)
-    - should_link = event.tag? ? project.repository.tag_exists?(event.ref_name) : project.repository.branch_exists?(event.ref_name)
-    = link_to_if should_link, event.ref_name, commits_link, class: 'ref-name'
+  - many_refs = event.ref_count.to_i > 1
+  %span.event-type.d-inline-block.append-right-4.pushed= many_refs ? "#{event.action_name} #{event.ref_count} #{event.ref_type.pluralize}" : "#{event.action_name} #{event.ref_type}"
+  - unless many_refs
+    %span.append-right-4
+      - commits_link = project_commits_path(project, event.ref_name)
+      - should_link = event.tag? ? project.repository.tag_exists?(event.ref_name) : project.repository.branch_exists?(event.ref_name)
+      = link_to_if should_link, event.ref_name, commits_link, class: 'ref-name'
 
   = render "events/event_scope", event: event
 
diff --git a/app/views/groups/registry/repositories/index.html.haml b/app/views/groups/registry/repositories/index.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..e85b07132308badd8125545316d8f76956cba8d4
--- /dev/null
+++ b/app/views/groups/registry/repositories/index.html.haml
@@ -0,0 +1,12 @@
+- page_title _("Container Registry")
+
+%section
+  .row.registry-placeholder.prepend-bottom-10
+    .col-12
+      #js-vue-registry-images{ data: { endpoint: group_container_registries_path(@group, format: :json),
+        "help_page_path" => help_page_path('user/packages/container_registry/index'),
+        "no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
+        "containers_error_image" => image_path('illustrations/docker-error-state.svg'),
+        "repository_url" => "",
+        is_group_page: true,
+        character_error: @character_error.to_s } }
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 0e6c16f0f06a403ccbd36080a79cae6a90670813..457d05b4a97f68af21c63de683e25fd2e2f9c50a 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -5,6 +5,8 @@
   = auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")
 
 %div{ class: [("limit-container-width" unless fluid_layout)] }
+  = render_if_exists 'trials/banner', namespace: @group
+
   = render 'groups/home_panel'
 
   .groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } }
diff --git a/app/views/groups/sidebar/_packages.html.haml b/app/views/groups/sidebar/_packages.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..16b902a18b9365b93025fa166539478b83eb884d
--- /dev/null
+++ b/app/views/groups/sidebar/_packages.html.haml
@@ -0,0 +1,16 @@
+- if group_container_registry_nav?
+  = nav_link(path: group_packages_nav_link_paths) do
+    = link_to group_container_registries_path(@group), title: _('Container Registry') do
+      .nav-icon-container
+        = sprite_icon('package')
+      %span.nav-item-name
+        = _('Packages')
+    %ul.sidebar-sub-level-items
+      = nav_link(controller: [:packages, :repositories], html_options: { class: "fly-out-top-item" } ) do
+        = link_to group_container_registries_path(@group), title: _('Container Registry') do
+          %strong.fly-out-top-item-name
+            = _('Packages')
+      %li.divider.fly-out-top-item
+      = nav_link(controller: 'groups/container_registries') do
+        = link_to group_container_registries_path(@group), title: _('Container Registry') do
+          %span= _('Container Registry')
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 1efd8647a674ed6f05f44d4c5c6704eec952895e..b8c9f0ae1e821d534c544d2ca879439891d4878a 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -5,7 +5,7 @@
 -# anything from this page beforehand.
 -# Part of an experiment to build a new sign up flow. Will be removed again with
 -# https://gitlab.com/gitlab-org/growth/engineering/issues/64
-- if use_experimental_separate_sign_up_flow? && current_path?("sessions#new")
+- if experiment_enabled?(:signup_flow) && current_path?("sessions#new")
   = javascript_tag nonce: true do
     :plain
       if (window.location.hash === '#register-pane') {
diff --git a/app/views/layouts/devise_experimental_separate_sign_up_flow.html.haml b/app/views/layouts/devise_experimental_separate_sign_up_flow.html.haml
index b10145b62af3da340db6d45805493803ab7d9fda..2f05717fc0ea8cb2ce9718212db658b483ba20ef 100644
--- a/app/views/layouts/devise_experimental_separate_sign_up_flow.html.haml
+++ b/app/views/layouts/devise_experimental_separate_sign_up_flow.html.haml
@@ -14,7 +14,8 @@
               = render_if_exists 'layouts/devise_help_text'
               .text-center.signup-heading.mt-3.mb-3
                 = image_tag(image_url('logo.svg'), class: 'gitlab-logo', alt: 'GitLab Logo')
-                %h2= _('Register for GitLab.com')
+                - if content_for?(:page_title)
+                  %h2= yield :page_title
               = yield
       %hr.footer-fixed
       .footer-container
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 5122c2517aa30554d843d66e107b1dacef1a8391..d339751848b929f2a550c4e7d46205e90c914723 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -55,15 +55,15 @@
             = nav_link(controller: 'admin/dashboard') do
               = link_to admin_root_path, class: 'admin-icon qa-admin-area-link d-xl-none' do
                 = _('Admin Area')
-            - if Feature.enabled?(:user_mode_in_session)
-              - if header_link?(:admin_mode)
-                = nav_link(controller: 'admin/sessions') do
-                  = link_to destroy_admin_session_path, class: 'd-lg-none lock-open-icon' do
-                    = _('Leave admin mode')
-              - elsif current_user.admin?
-                = nav_link(controller: 'admin/sessions') do
-                  = link_to new_admin_session_path, class: 'd-lg-none lock-icon' do
-                    = _('Enter admin mode')
+          - if Feature.enabled?(:user_mode_in_session)
+            - if header_link?(:admin_mode)
+              = nav_link(controller: 'admin/sessions') do
+                = link_to destroy_admin_session_path, class: 'd-lg-none lock-open-icon' do
+                  = _('Leave Admin Mode')
+            - elsif current_user.admin?
+              = nav_link(controller: 'admin/sessions') do
+                = link_to new_admin_session_path, class: 'd-lg-none lock-icon' do
+                  = _('Enter Admin Mode')
           - if Gitlab::Sherlock.enabled?
             %li
               = link_to sherlock_transactions_path, class: 'admin-icon' do
@@ -74,6 +74,15 @@
       = link_to admin_root_path, class: 'admin-icon qa-admin-area-link', title: _('Admin Area'), aria: { label: _('Admin Area') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
         = sprite_icon('admin', size: 18)
 
+  - if Feature.enabled?(:user_mode_in_session)
+    - if header_link?(:admin_mode)
+      = nav_link(controller: 'admin/sessions', html_options: { class: "d-none d-lg-block d-xl-block"}) do
+        = link_to destroy_admin_session_path, title: _('Leave Admin Mode'), aria: { label: _('Leave Admin Mode') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
+          = sprite_icon('lock-open', size: 18)
+    - elsif current_user.admin?
+      = nav_link(controller: 'admin/sessions', html_options: { class: "d-none d-lg-block d-xl-block"}) do
+        = link_to new_admin_session_path, title: _('Enter Admin Mode'), aria: { label: _('Enter Admin Mode') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
+          = sprite_icon('lock', size: 18)
 
   -# Shortcut to Dashboard > Projects
   - if dashboard_nav_link?(:projects)
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 7cc7d1783c42e69a6b720087f4ee4fe9851873b7..4930c6cf5f776bfaf86be9a53c5f4972e0917bdd 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -118,7 +118,7 @@
                 %strong.fly-out-top-item-name
                   = _('Kubernetes')
 
-      = render_if_exists 'groups/sidebar/packages' # EE-specific
+      = render_if_exists 'groups/sidebar/packages'
 
       - if group_sidebar_link?(:group_members)
         = nav_link(path: 'group_members#index') do
diff --git a/app/views/notify/new_release_email.html.haml b/app/views/notify/new_release_email.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..45e99f3c07a7fe221beb8a7860092e34ef82c448
--- /dev/null
+++ b/app/views/notify/new_release_email.html.haml
@@ -0,0 +1,18 @@
+- release_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: @target_url }
+- description_details = { tag: @release.tag, name: @project.name, release_link_start: release_link_start, release_link_end: '</a>'.html_safe }
+
+%div{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+  %p
+    = _("A new Release %{tag} for %{name} was published. Visit the %{release_link_start}Releases page%{release_link_end} to read more about it.").html_safe % description_details
+
+  %p
+    %h4= _("Assets:")
+    %ul
+      - @release.links.each do |link|
+        %li= link_to(link.name, link.url)
+      - @release.sources.each do |source|
+        %li= link_to(_("Download %{format}") % { format: source.format }, source.url)
+
+  %p
+    %h4= _("Release notes:")
+    = markdown_field(@release, :description)
diff --git a/app/views/notify/new_release_email.text.erb b/app/views/notify/new_release_email.text.erb
new file mode 100644
index 0000000000000000000000000000000000000000..e03cf2d5fd1af48fec0b2fc6b7dc077fbe0045ed
--- /dev/null
+++ b/app/views/notify/new_release_email.text.erb
@@ -0,0 +1,12 @@
+<%= _("A new Release %{tag} for %{name} was published. Visit the Releases page to read more about it:").html_safe % { tag: @release.tag, name: @project.name } %> <%= @target_url %>
+
+<%= _("Assets:") %>
+<% @release.links.each do |link| -%>
+  - <%= link.name %>: <%= link.url %>
+<% end -%>
+<% @release.sources.each do |source| -%>
+  - <%= _("Download %{format}:") % { format: source.format } %> <%= source.url %>
+<% end -%>
+
+<%= _("Release notes:") %>
+<%= @release.description %>
diff --git a/app/views/profiles/notifications/_group_settings.html.haml b/app/views/profiles/notifications/_group_settings.html.haml
index 1776d260e19c33a15413ae083c5d3079797b6bab..33b0aa93d843b0eebdda81f4fd15609a36db864e 100644
--- a/app/views/profiles/notifications/_group_settings.html.haml
+++ b/app/views/profiles/notifications/_group_settings.html.haml
@@ -12,5 +12,5 @@
     = render 'shared/notifications/button', notification_setting: setting, emails_disabled: emails_disabled
 
   .table-section.section-30
-    = form_for @user.notification_settings.find { |ns| ns.source == group }, url: profile_notifications_group_path(group), method: :put, html: { class: 'update-notifications' } do |f|
+    = form_for setting, url: profile_notifications_group_path(group), method: :put, html: { class: 'update-notifications' } do |f|
       = f.select :notification_email, @user.all_emails, { include_blank: 'Global notification email' }, class: 'select2 js-group-notification-email'
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 0576f51fa83621c4009e33490ecf6119ea922a32..68b7efc6fb4735c5f23bfe62cd058ca7f85e509d 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -94,6 +94,7 @@
         - else
           = f.text_field :name, label: s_('Profiles|Full name'), required: true, title: s_("Profiles|Using emojis in names seems fun, but please try to set a status message instead"), wrapper: { class: 'col-md-9 qa-full-name rspec-full-name' }, help: s_("Profiles|Enter your name, so people you know can recognize you")
         = f.text_field :id, readonly: true, label: s_('Profiles|User ID'), wrapper: { class: 'col-md-3' }
+      = f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, {}, class: 'input-md'
 
       = render_if_exists 'profiles/email_settings', form: f
       = f.text_field :skype, class: 'input-md', placeholder: s_("Profiles|username")
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index e4129a91daf30c6357df3d0e3309cb5b70e7d420..2e00632892b79f23274c51976a5383fa237b87c4 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -14,7 +14,7 @@
           %li= desc
       %p= _('The following items will NOT be exported:')
       %ul
-        %li= _('Job traces and artifacts')
+        %li= _('Job logs and artifacts')
         %li= _('Container registry images')
         %li= _('CI variables')
         %li= _('Webhooks')
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index 688b8f001c34967c3eb7edecfde0ec69dc15c7aa..7c73bbc7479ecbc39a97e3a046373c9138a80268 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -1,6 +1,6 @@
 - breadcrumb_title "Repository"
 - page_title @blob.path, @ref
-- signatures_path = namespace_project_signatures_path(namespace_id: @project.namespace.full_path, project_id: @project.path, id: @last_commit)
+- signatures_path = namespace_project_signatures_path(namespace_id: @project.namespace.full_path, project_id: @project.path, id: @last_commit, limit: 1)
 
 .js-signature-container{ data: { 'signatures-path': signatures_path } }
 
diff --git a/app/views/projects/blob/viewers/_audio.html.haml b/app/views/projects/blob/viewers/_audio.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..dbdf243c36bd4ef6658a3246036dbeb1ec23ec1f
--- /dev/null
+++ b/app/views/projects/blob/viewers/_audio.html.haml
@@ -0,0 +1,2 @@
+.file-content.audio
+  %audio{ src: blob_raw_path, controls: true, data: { setup: '{}' } }
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index ef2ab4c698e0e77d96727b721a7ebab5e73b1426..8270477ed3f7e1487bf34c97b16aba40e55c6222 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -1,31 +1,49 @@
 .gl-responsive-table-row.deployment{ role: 'row' }
+  .table-section.section-15{ role: 'gridcell' }
+    .table-mobile-header{ role: 'rowheader' }= _("Status")
+    .table-mobile-content
+      = render_deployment_status(deployment)
+
   .table-section.section-10{ role: 'gridcell' }
     .table-mobile-header{ role: 'rowheader' }= _("ID")
     %strong.table-mobile-content ##{deployment.iid}
 
-  .table-section.section-30{ role: 'gridcell' }
+  .table-section.section-10{ role: 'gridcell' }
+    .table-mobile-header{ role: 'rowheader' }= _("Triggerer")
+    .table-mobile-content
+      - if deployment.deployed_by
+        = user_avatar(user: deployment.deployed_by, size: 26, css_class: "mr-0 float-none")
+
+  .table-section.section-25{ role: 'gridcell' }
     .table-mobile-header{ role: 'rowheader' }= _("Commit")
     = render 'projects/deployments/commit', deployment: deployment
 
-  .table-section.section-25.build-column{ role: 'gridcell' }
+  .table-section.section-10.build-column{ role: 'gridcell' }
     .table-mobile-header{ role: 'rowheader' }= _("Job")
     - if deployment.deployable
       .table-mobile-content
         .flex-truncate-parent
           .flex-truncate-child
-            = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do
+            = link_to deployment_path(deployment), class: 'build-link' do
               #{deployment.deployable.name} (##{deployment.deployable.id})
-        - if deployment.deployed_by
-          %div
-            by
-            = user_avatar(user: deployment.deployed_by, size: 20, css_class: "mr-0 float-none")
+    - else
+      .badge.badge-info.suggestion-help-hover{ title: s_('Deployment|This deployment was created using the API') }
+        = s_('Deployment|API')
 
-  .table-section.section-15{ role: 'gridcell' }
+  .table-section.section-10{ role: 'gridcell' }
     .table-mobile-header{ role: 'rowheader' }= _("Created")
+    %span.table-mobile-content.flex-truncate-parent
+      %span.flex-truncate-child
+        = time_ago_with_tooltip(deployment.created_at)
+
+  .table-section.section-10{ role: 'gridcell' }
+    .table-mobile-header{ role: 'rowheader' }= _("Deployed")
     - if deployment.deployed_at
-      %span.table-mobile-content= time_ago_with_tooltip(deployment.deployed_at)
+      %span.table-mobile-content.flex-truncate-parent
+        %span.flex-truncate-child
+          = time_ago_with_tooltip(deployment.deployed_at)
 
-  .table-section.section-20.table-button-footer{ role: 'gridcell' }
+  .table-section.section-10.table-button-footer{ role: 'gridcell' }
     .btn-group.table-action-buttons
       = render 'projects/deployments/actions', deployment: deployment
       = render 'projects/deployments/rollback', deployment: deployment
diff --git a/app/views/projects/deployments/_rollback.haml b/app/views/projects/deployments/_rollback.haml
index d6bf8d564deefcbe78cad131c22d2b133e360f76..dffa5e4ba405a3fc2519d7f9626f5c3d474cf343 100644
--- a/app/views/projects/deployments/_rollback.haml
+++ b/app/views/projects/deployments/_rollback.haml
@@ -1,4 +1,4 @@
-- if can?(current_user, :create_deployment, deployment)
+- if deployment.deployable && can?(current_user, :create_deployment, deployment)
   - tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment')
   = button_tag class: 'btn btn-default btn-build has-tooltip', type: 'button', data: { toggle: 'modal', target: "#confirm-rollback-modal-#{deployment.id}" }, title: tooltip do
     - if deployment.last?
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 75da151f3296b3f1729b4bac5c8e7353a9dabd47..c4c39c227c6c85260edba06dfa58128140f50b83 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -60,10 +60,13 @@
     .table-holder
       .ci-table.environments{ role: 'grid' }
         .gl-responsive-table-row.table-row-header{ role: 'row' }
+          .table-section.section-15{ role: 'columnheader' }= _('Status')
           .table-section.section-10{ role: 'columnheader' }= _('ID')
-          .table-section.section-30{ role: 'columnheader' }= _('Commit')
-          .table-section.section-25{ role: 'columnheader' }= _('Job')
-          .table-section.section-15{ role: 'columnheader' }= _('Created')
+          .table-section.section-10{ role: 'columnheader' }= _('Triggerer')
+          .table-section.section-25{ role: 'columnheader' }= _('Commit')
+          .table-section.section-10{ role: 'columnheader' }= _('Job')
+          .table-section.section-10{ role: 'columnheader' }= _('Created')
+          .table-section.section-10{ role: 'columnheader' }= _('Deployed')
 
         = render @deployments
 
diff --git a/app/views/projects/issues/import_csv/_button.html.haml b/app/views/projects/issues/import_csv/_button.html.haml
index acc2c50294fd7bee4069873d588b8acbaaeffb52..fe89d2fb748d72299814d5443780c30e6804fcd9 100644
--- a/app/views/projects/issues/import_csv/_button.html.haml
+++ b/app/views/projects/issues/import_csv/_button.html.haml
@@ -3,7 +3,7 @@
 %button.csv-import-button.btn{ title: _('Import CSV'), class: ('has-tooltip' if type == :icon),
   data: { toggle: 'modal', target: '.issues-import-modal' } }
   - if type == :icon
-    = sprite_icon('upload')
+    = sprite_icon('import')
   - else
     = _('Import CSV')
 
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 49e482ff1dfd4f9f722d8c7526b1c385d9303d23..2633a3899f7f7f5d77d8b810ab863575e74b43af 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -20,4 +20,5 @@
     - if new_issue_email
       = render 'projects/issuable_by_email', email: new_issue_email, issuable_type: 'issue'
 - else
-  = render 'shared/empty_states/issues', button_path: new_project_issue_path(@project), show_import_button: true
+  - new_project_issue_button_path = @project.archived? ? false : new_project_issue_path(@project)
+  = render 'shared/empty_states/issues', new_project_issue_button_path: new_project_issue_button_path, show_import_button: true
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 094cbf755e1e9d949c983c6bcdbc6d10780c3616..4eec81c9125f5afac4fe82c27b33997f61c4d0db 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -59,3 +59,9 @@
     %span.js-details-content.hide
       = link_to @pipeline.sha, project_commit_path(@project, @pipeline.sha), class: "commit-sha commit-hash-full"
     = clipboard_button(text: @pipeline.sha, title: _("Copy commit SHA"))
+
+  .well-segment.related-merge-request-info
+    .icon-container
+      = sprite_icon("git-merge")
+    %span.related-merge-requests
+      = @pipeline.all_related_merge_request_text
diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml
index ff8dae08ad0530516abf7c1960bcf99d11d44b20..9dff251101b25e65639b8a5ee4c5c02c7e3356f6 100644
--- a/app/views/projects/protected_branches/shared/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml
@@ -16,9 +16,7 @@
       %thead
         %tr
           %th
-            = s_("ProtectedBranch|Protected branch (%{protected_branches_count})") % { protected_branches_count: @protected_branches_count }
-          %th
-            = s_("ProtectedBranch|Last commit")
+            = s_("ProtectedBranch|Branch")
           %th
             = s_("ProtectedBranch|Allowed to merge")
           %th
diff --git a/app/views/projects/protected_branches/shared/_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_protected_branch.html.haml
index 2768e4ac5a546c92742ebe9d68d3c430e95b0bd6..4ca6ebe9c789035621d3a01d2184bee199ce017b 100644
--- a/app/views/projects/protected_branches/shared/_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/shared/_protected_branch.html.haml
@@ -5,17 +5,14 @@
     %span.ref-name= protected_branch.name
 
     - if @project.root_ref?(protected_branch.name)
-      %span.badge.badge-info.prepend-left-5 default
-  %td
-    - if protected_branch.wildcard?
-      - matching_branches = protected_branch.matching(repository.branches)
-      = link_to pluralize(matching_branches.count, "matching branch"), namespace_project_protected_branch_path(@project.namespace, @project, protected_branch)
-    - else
-      - if commit = protected_branch.commit
-        = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit-sha')
-        = time_ago_with_tooltip(commit.committed_date)
-      - else
-        (branch was deleted from repository)
+      %span.badge.badge-info.d-inline default
+
+    %div
+      - if protected_branch.wildcard?
+        - matching_branches = protected_branch.matching(repository.branches)
+        = link_to pluralize(matching_branches.count, "matching branch"), namespace_project_protected_branch_path(@project.namespace, @project, protected_branch)
+      - elsif !protected_branch.commit
+        %span.text-muted Branch was deleted.
 
   = yield
 
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index ddb19cb6b65de752c331d77665bd8d623e2e34a6..b2e160e37bc714cdcb396e809c464b556a0d749e 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -5,7 +5,10 @@
     .col-12
       #js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json),
         "help_page_path" => help_page_path('user/packages/container_registry/index'),
+        "two_factor_auth_help_link" => help_page_path('user/profile/account/two_factor_authentication'),
+        "personal_access_tokens_help_link" => help_page_path('user/profile/personal_access_tokens'),
         "no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
         "containers_error_image" => image_path('illustrations/docker-error-state.svg'),
         "repository_url" => escape_once(@project.container_registry_url),
+        "registry_host_url_with_port" => escape_once(registry_config.host_port),
         character_error: @character_error.to_s } }
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..88ca64f2af05472c05889f1c0836c0d53e1aa79f
--- /dev/null
+++ b/app/views/projects/releases/edit.html.haml
@@ -0,0 +1,3 @@
+- page_title _('Edit Release')
+
+#js-edit-release-page{ data: data_for_edit_release_page }
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index 959a2423e0240e3170b6dd713f4b39d2a4e42b83..582f3d6fce48b982e34241ae21aeb415ba546c0f 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -6,7 +6,6 @@
         - hide_class = 'd-none' if @service.activated? != value
         %span.js-service-active-status{ class: hide_class, data: { value: value.to_s } }
           = boolean_to_icon value
-    %p= #{@service.description}.
 
     - if @service.respond_to?(:detailed_description)
       %p= @service.detailed_description
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index a674136e7910eafbe7ee4fbdd37bdcd1dca2e671..ea815be23c1781b1d32c8fc337d717c2b84dc008 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -51,10 +51,10 @@
 
         %hr
         .form-group
-          = f.label :ci_config_path, _('Custom CI config path'), class: 'label-bold'
+          = f.label :ci_config_path, _('Custom CI configuration path'), class: 'label-bold'
           = f.text_field :ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml'
           %p.form-text.text-muted
-            = _("The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>").html_safe
+            = _("The path to the CI configuration file. Defaults to <code>.gitlab-ci.yml</code>").html_safe
             = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-config-path'), target: '_blank'
 
         %hr
@@ -98,7 +98,7 @@
             %span.input-group-append
               .input-group-text /
           %p.form-text.text-muted
-            = _("A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable")
+            = _("A regular expression that will be used to find the test coverage output in the job log. Leave blank to disable")
             = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing'), target: '_blank'
           .bs-callout.bs-callout-info
             %p= _("Below are examples of regex for existing tools:")
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index b58af545439eb4358b2e47002af683d180b7f18a..c5653c3dd5acc7a784cd5c3d44570d127529c091 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -6,7 +6,7 @@
 
 = render partial: 'flash_messages', locals: { project: @project }
 
-- if !@project.empty_repo? && can?(current_user, :download_code, @project)
+- if !@project.empty_repo? && can?(current_user, :download_code, @project) && !vue_file_list_enabled?
   - signatures_path = project_signatures_path(@project, @project.default_branch)
   .js-signature-container{ data: { 'signatures-path': signatures_path } }
 
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 3f6cd628d642c18e0599051186a9f9d3aa5a4d3f..c7bd0262c5419d5fdf3bd1a804d58c72ff7b785b 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -24,7 +24,7 @@
       .text-secondary
         = icon('rocket')
         = _("Release")
-        = link_to release.name, project_releases_path(@project, anchor: release.tag), class: 'default-link-color'
+        = link_to release.name, project_releases_path(@project, anchor: release.tag), class: 'tag-release-link'
       - if release.description.present?
         .description.md.prepend-top-default
           = markdown_field(release, :description)
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 41cd044a5b0a968a72da4b1dd2b7578e62d03361..38422d4533d92941178e34d952efbbc88ce7113b 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -41,7 +41,7 @@
                   %li
                     = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do
                       #{ _('New directory') }
-                - elsif can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project)
+                - elsif can_create_mr_from_fork
                   %li
                     - continue_params = { to: project_new_blob_path(@project, @id),
                                           notice: edit_in_new_fork_notice,
@@ -81,10 +81,15 @@
 
   = render 'projects/find_file_link'
 
-  - if can_collaborate
+  - if can_create_mr_from_fork
     = succeed " " do
-      = link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default qa-web-ide-button' do
-        = _('Web IDE')
+      - if can_collaborate || current_user&.already_forked?(@project)
+        = link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default qa-web-ide-button' do
+          = _('Web IDE')
+      - else
+        = link_to '#modal-confirm-fork', class: 'btn btn-default qa-web-ide-button', data: { target: '#modal-confirm-fork', toggle: 'modal'} do
+          = _('Web IDE')
+        = render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path)
 
   - if show_xcode_link?(@project)
     .project-action-button.project-xcode.inline
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 39b29a20df6232310e98a0847811b7327dbeeb12..65f5bc31d2e1341b86a294c596237c79bf64cebf 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -6,7 +6,8 @@
 = content_for :meta_tags do
   = auto_discovery_link_tag(:atom, project_commits_url(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
 
-.js-signature-container{ data: { 'signatures-path': signatures_path } }
+- unless vue_file_list_enabled?
+  .js-signature-container{ data: { 'signatures-path': signatures_path } }
 
 = render 'projects/last_push'
 = render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
diff --git a/app/views/registrations/welcome.html.haml b/app/views/registrations/welcome.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..02ab974ecc0a90d69671f4ebd2a1e34bdd0022a5
--- /dev/null
+++ b/app/views/registrations/welcome.html.haml
@@ -0,0 +1,17 @@
+- content_for(:page_title, _('Welcome to GitLab<br>%{username}!' % { username: html_escape(current_user.username) }).html_safe)
+- max_name_length = 128
+.text-center.mb-3
+  = _('In order to tailor your experience with GitLab<br>we would like to know a bit more about you.').html_safe
+.signup-box.p-3.mb-2
+  .signup-body
+    = form_for(current_user, url: users_sign_up_update_role_path, html: { class: 'new_new_user gl-show-field-errors', 'aria-live' => 'assertive' }) do |f|
+      .devise-errors.mt-0
+        = render 'devise/shared/error_messages', resource: current_user
+      .name.form-group
+        = f.label :name, _('Full name'), class: 'label-bold'
+        = f.text_field :name, class: 'form-control top js-block-emoji js-validate-length', :data => { :max_length => max_name_length, :max_length_message => s_('Name is too long (maximum is %{max_length} characters).') % { max_length: max_name_length }, :qa_selector => 'new_user_name_field' }, required: true, title: _('This field is required.')
+      .form-group
+        = f.label :role, _('Role'), class: 'label-bold'
+        = f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, {}, class: 'form-control'
+      .submit-container.mt-3
+        = f.submit _('Get started!'), class: 'btn-register btn btn-block mb-0 p-2'
diff --git a/app/views/shared/_allow_request_access.html.haml b/app/views/shared/_allow_request_access.html.haml
index 2b24bde9e59c6059abb3204bc48da84ac26b08b3..ca82f2f3377101651cdce5ecf3cf934e25474b8c 100644
--- a/app/views/shared/_allow_request_access.html.haml
+++ b/app/views/shared/_allow_request_access.html.haml
@@ -3,6 +3,4 @@
 .form-check
   = form.check_box :request_access_enabled, class: 'form-check-input', data: { qa_selector: 'request_access_checkbox' }
   = form.label :request_access_enabled, class: 'form-check-label' do
-    %span{ class: label_class }= _('Allow users to request access')
-    %br
-    %span.text-muted= _('Allow users to request access if visibility is public or internal.')
+    %span{ class: label_class }= _('Allow users to request access (if visibility is public or internal)')
diff --git a/app/views/shared/_confirm_fork_modal.html.haml b/app/views/shared/_confirm_fork_modal.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..db50ea4138752118ee44d7c8519777ce09446973
--- /dev/null
+++ b/app/views/shared/_confirm_fork_modal.html.haml
@@ -0,0 +1,12 @@
+#modal-confirm-fork.modal.qa-confirm-fork-modal
+  .modal-dialog
+    .modal-content
+      .modal-header
+        %h3.page-title= _('Fork project?')
+        %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
+          %span{ "aria-hidden": true } &times;
+      .modal-body.p-3
+        %p= _("You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.") % { tag_start: '', tag_end: ''}
+      .modal-footer
+        = link_to _('Cancel'), '#', class: "btn btn-cancel", "data-dismiss" => "modal"
+        = link_to _('Fork project'), fork_path, class: 'btn btn-success', method: :post
diff --git a/app/views/shared/boards/_switcher.html.haml b/app/views/shared/boards/_switcher.html.haml
index 791186307629142088ffac80c2398a483b2b2296..09a365a290ac87772b973db7f64c2feac08fd2e8 100644
--- a/app/views/shared/boards/_switcher.html.haml
+++ b/app/views/shared/boards/_switcher.html.haml
@@ -1,4 +1,4 @@
-- parent = board.parent
+- parent = board.resource_parent
 - milestone_filter_opts = { format: :json }
 - milestone_filter_opts = milestone_filter_opts.merge(only_group_milestones: true) if board.group_board?
 - weights = Gitlab.ee? ? ([Issue::WEIGHT_ANY] + Issue.weight_options) : []
diff --git a/app/views/shared/deploy_keys/_form.html.haml b/app/views/shared/deploy_keys/_form.html.haml
index bc0dc7f9631f3c88d1d41e8662ecdc1decd8f4b6..1944c293be1258ab31927f73ff211d3d09ac5f1d 100644
--- a/app/views/shared/deploy_keys/_form.html.haml
+++ b/app/views/shared/deploy_keys/_form.html.haml
@@ -6,7 +6,7 @@
 
 .form-group
   = form.label :title, class: 'col-form-label col-sm-2'
-  .col-sm-10= form.text_field :title, class: 'form-control'
+  .col-sm-10= form.text_field :title, class: 'form-control', readonly: ('readonly' unless can?(current_user, :update_deploy_key, deploy_key))
 
 .form-group
   - if deploy_key.new_record?
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index 9173b802dd42a1f8eadfbde3d375e34afc62b8bd..325e01bb5c8dbc96b5eb25975cccd09d8e6dad16 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -1,4 +1,4 @@
-- button_path = local_assigns.fetch(:button_path, false)
+- button_path = local_assigns.fetch(:new_project_issue_button_path, false)
 - project_select_button = local_assigns.fetch(:project_select_button, false)
 - show_import_button = local_assigns.fetch(:show_import_button, false) && can?(current_user, :import_issues, @project)
 - has_button = button_path || project_select_button
@@ -56,4 +56,3 @@
 
 - if show_import_button
   = render 'projects/issues/import_csv/modal'
-
diff --git a/app/views/shared/issuable/_board_create_list_dropdown.html.haml b/app/views/shared/issuable/_board_create_list_dropdown.html.haml
index 416b4a34651c207acbfabc347ca47221ea9643a5..ae0e5e45afed2c0f8b46a87c39f7ccc435d12d4e 100644
--- a/app/views/shared/issuable/_board_create_list_dropdown.html.haml
+++ b/app/views/shared/issuable/_board_create_list_dropdown.html.haml
@@ -3,6 +3,6 @@
     Add list
   .dropdown-menu.dropdown-extended-height.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable.js-tab-container-labels
     = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
-    - if can?(current_user, :admin_label, board.parent)
+    - if can?(current_user, :admin_label, board.resource_parent)
       = render partial: "shared/issuable/label_page_create", locals: { show_add_list: true, add_list: true, add_list_class: 'd-none' }
     = dropdown_loading
diff --git a/app/views/shared/issuable/_feed_buttons.html.haml b/app/views/shared/issuable/_feed_buttons.html.haml
index 83f60fa6fe2bf77ac526369c404a2888d8b1a4b9..4fed95e260712be90c7a3b9a8adeb8c82e270b70 100644
--- a/app/views/shared/issuable/_feed_buttons.html.haml
+++ b/app/views/shared/issuable/_feed_buttons.html.haml
@@ -1,4 +1,4 @@
-= link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: _('Subscribe to RSS feed') do
-  = icon('rss')
+= link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip js-rss-button', data: { container: 'body' }, title: _('Subscribe to RSS feed') do
+  = sprite_icon('rss')
 = link_to safe_params.merge(calendar_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: _('Subscribe to calendar') do
-  = custom_icon('icon_calendar')
+  = sprite_icon('calendar')
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index c9458475aa5cf4d6eedaf2ab5947bb87d4986a53..9d580930fb803a1cc953e20bc6ea6dd561bbc3f8 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -2,7 +2,7 @@
 - board = local_assigns.fetch(:board, nil)
 - is_not_boards_modal_or_productivity_analytics = type != :boards_modal && type != :productivity_analytics
 - block_css_class = is_not_boards_modal_or_productivity_analytics ? 'row-content-block second-block' : ''
-- user_can_admin_list = board && can?(current_user, :admin_list, board.parent)
+- user_can_admin_list = board && can?(current_user, :admin_list, board.resource_parent)
 
 .issues-filters{ class: ("w-100" if type == :boards_modal) }
   .issues-details-filters.filtered-search-block.d-flex.flex-column.flex-md-row{ class: block_css_class, "v-pre" => type == :boards_modal }
@@ -17,12 +17,11 @@
       .issues-other-filters.filtered-search-wrapper.d-flex.flex-column.flex-md-row
         .filtered-search-box
           - if type != :boards_modal && type != :boards
-            = dropdown_tag(custom_icon('icon_history'),
+            = dropdown_tag(_('Recent searches'),
               options: { wrapper_class: "filtered-search-history-dropdown-wrapper",
               toggle_class: "filtered-search-history-dropdown-toggle-button",
               dropdown_class: "filtered-search-history-dropdown",
-              content_class: "filtered-search-history-dropdown-content",
-              title: "Recent searches" }) do
+              content_class: "filtered-search-history-dropdown-content" }) do
               .js-filtered-search-history-dropdown{ data: { full_path: search_history_storage_prefix } }
           .filtered-search-box-input-container.droplab-dropdown
             .scroll-container
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index a55b7fc530a1fe372e1c1b88fb005c05c0f0fc28..c8b2adcf084633959467648859773c1d282b49ee 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -5,174 +5,178 @@
 - signed_in = !!issuable_sidebar.dig(:current_user, :id)
 - can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit)
 
-%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
-  .issuable-sidebar
-    .block.issuable-sidebar-header
-      - if signed_in
-        %span.issuable-header-text.hide-collapsed.float-left
-          = _('To Do')
-      %a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
-        = sidebar_gutter_toggle_icon
-      - if signed_in
-        = render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar
-
-    = form_for issuable_type, url: issuable_sidebar[:issuable_json_path], remote: true, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
-      - if signed_in
-        .block.todo.hide-expanded
-          = render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar, is_collapsed: true
-      .block.assignee.qa-assignee-block
-        = render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees
-
-      = render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar
-
-      - milestone = issuable_sidebar[:milestone] || {}
-      .block.milestone
-        .sidebar-collapsed-icon.has-tooltip{ title: sidebar_milestone_tooltip_label(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
-          = icon('clock-o', 'aria-hidden': 'true')
-          %span.milestone-title.collapse-truncated-title
-            - if milestone.present?
-              = milestone[:title]
-            - else
-              = _('None')
-        .title.hide-collapsed
-          = _('Milestone')
-          = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
-          - if can_edit_issuable
-            = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "milestone", track_event: "click_edit_button", track_value: "" }
-        .value.hide-collapsed
-          - if milestone.present?
-            = link_to milestone[:title], milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport', qa_selector: 'milestone_link' }
-          - else
-            %span.no-value
-              = _('None')
-
-        .selectbox.hide-collapsed
-          = f.hidden_field 'milestone_id', value: milestone[:id], id: nil
-          = dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable_type}[milestone_id]", project_id: issuable_sidebar[:project_id], issuable_id: issuable_sidebar[:id], milestones: issuable_sidebar[:project_milestones_path], ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], use_id: true, default_no: true, selected: milestone[:title], null_default: true, display: 'static' }})
-
-      #issuable-time-tracker.block
-        // Fallback while content is loading
-        .title.hide-collapsed
-          = _('Time tracking')
-          = icon('spinner spin', 'aria-hidden': 'true')
-
-      - if issuable_sidebar.has_key?(:due_date)
-        .block.due_date
-          .sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable_sidebar[:due_date]) }
-            = icon('calendar', 'aria-hidden': 'true')
-            %span.js-due-date-sidebar-value
-              = issuable_sidebar[:due_date].try(:to_s, :medium) || 'None'
+- if Feature.enabled?(:vue_issuable_sidebar, @project.group)
+  %aside#js-vue-issuable-sidebar{ data: { signed_in: signed_in,
+    sidebar_status_class: sidebar_gutter_collapsed_class } }
+- else
+  %aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
+    .issuable-sidebar
+      .block.issuable-sidebar-header
+        - if signed_in
+          %span.issuable-header-text.hide-collapsed.float-left
+            = _('To Do')
+        %a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
+          = sidebar_gutter_toggle_icon
+        - if signed_in
+          = render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar
+
+      = form_for issuable_type, url: issuable_sidebar[:issuable_json_path], remote: true, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
+        - if signed_in
+          .block.todo.hide-expanded
+            = render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar, is_collapsed: true
+        .block.assignee.qa-assignee-block
+          = render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees
+
+        = render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar
+
+        - milestone = issuable_sidebar[:milestone] || {}
+        .block.milestone
+          .sidebar-collapsed-icon.has-tooltip{ title: sidebar_milestone_tooltip_label(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
+            = icon('clock-o', 'aria-hidden': 'true')
+            %span.milestone-title.collapse-truncated-title
+              - if milestone.present?
+                = milestone[:title]
+              - else
+                = _('None')
           .title.hide-collapsed
-            = _('Due date')
+            = _('Milestone')
             = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
             - if can_edit_issuable
-              = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "due_date", track_event: "click_edit_button", track_value: "" }
+              = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "milestone", track_event: "click_edit_button", track_value: "" }
           .value.hide-collapsed
-            %span.value-content
-              - if issuable_sidebar[:due_date]
-                %span.bold= issuable_sidebar[:due_date].to_s(:medium)
-              - else
-                %span.no-value
-                  = _('None')
+            - if milestone.present?
+              = link_to milestone[:title], milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport', qa_selector: 'milestone_link' }
+            - else
+              %span.no-value
+                = _('None')
+
+          .selectbox.hide-collapsed
+            = f.hidden_field 'milestone_id', value: milestone[:id], id: nil
+            = dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable_type}[milestone_id]", project_id: issuable_sidebar[:project_id], issuable_id: issuable_sidebar[:id], milestones: issuable_sidebar[:project_milestones_path], ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], use_id: true, default_no: true, selected: milestone[:title], null_default: true, display: 'static' }})
+
+        #issuable-time-tracker.block
+          // Fallback while content is loading
+          .title.hide-collapsed
+            = _('Time tracking')
+            = icon('spinner spin', 'aria-hidden': 'true')
+
+        - if issuable_sidebar.has_key?(:due_date)
+          .block.due_date
+            .sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable_sidebar[:due_date]) }
+              = icon('calendar', 'aria-hidden': 'true')
+              %span.js-due-date-sidebar-value
+                = issuable_sidebar[:due_date].try(:to_s, :medium) || 'None'
+            .title.hide-collapsed
+              = _('Due date')
+              = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
+              - if can_edit_issuable
+                = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "due_date", track_event: "click_edit_button", track_value: "" }
+            .value.hide-collapsed
+              %span.value-content
+                - if issuable_sidebar[:due_date]
+                  %span.bold= issuable_sidebar[:due_date].to_s(:medium)
+                - else
+                  %span.no-value
+                    = _('None')
+              - if can_edit_issuable
+                %span.no-value.js-remove-due-date-holder{ class: ("hidden" if issuable_sidebar[:due_date].nil?) }
+                  \-
+                  %a.js-remove-due-date{ href: "#", role: "button" }
+                    = _('remove due date')
             - if can_edit_issuable
-              %span.no-value.js-remove-due-date-holder{ class: ("hidden" if issuable_sidebar[:due_date].nil?) }
-                \-
-                %a.js-remove-due-date{ href: "#", role: "button" }
-                  = _('remove due date')
-          - if can_edit_issuable
-            .selectbox.hide-collapsed
-              = f.hidden_field :due_date, value: issuable_sidebar[:due_date].try(:strftime, 'yy-mm-dd')
-              .dropdown
-                %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable_type}[due_date]", ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], display: 'static' } }
-                  %span.dropdown-toggle-text
-                    = _('Due date')
-                  = icon('chevron-down', 'aria-hidden': 'true')
-                .dropdown-menu.dropdown-menu-due-date
-                  = dropdown_title(_('Due date'))
-                  = dropdown_content do
-                    .js-due-date-calendar
-
-      - selected_labels = issuable_sidebar[:labels]
-      .block.labels
-        .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(selected_labels), data: { placement: "left", container: "body", boundary: 'viewport' } }
-          = icon('tags', 'aria-hidden': 'true')
-          %span
-            = selected_labels.size
-        .title.hide-collapsed
-          = _('Labels')
-          = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
-          - if can_edit_issuable
-            = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link qa-edit-link-labels float-right', data: { track_label: "right_sidebar", track_property: "labels", track_event: "click_edit_button", track_value: "" }
-        .value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) }
-          - if selected_labels.any?
-            - selected_labels.each do |label_hash|
-              = render_label(label_from_hash(label_hash).present(issuable_subject: nil), link: sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label_hash[:title]))
-          - else
-            %span.no-value
-              = _('None')
-        .selectbox.hide-collapsed
-          - selected_labels.each do |label|
-            = hidden_field_tag "#{issuable_type}[label_names][]", label[:id], id: nil
-          .dropdown
-            %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: sidebar_label_dropdown_data(issuable_type, issuable_sidebar) }
-              %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
-                = multi_label_name(selected_labels, "Labels")
-              = icon('chevron-down', 'aria-hidden': 'true')
-            .dropdown-menu.dropdown-select.dropdown-menu-paging.qa-dropdown-menu-labels.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height
-              = render partial: "shared/issuable/label_page_default"
-              - if issuable_sidebar.dig(:current_user, :can_admin_label)
-                = render partial: "shared/issuable/label_page_create"
-
-      = render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar
-
-      - if issuable_sidebar.has_key?(:confidential)
+              .selectbox.hide-collapsed
+                = f.hidden_field :due_date, value: issuable_sidebar[:due_date].try(:strftime, 'yy-mm-dd')
+                .dropdown
+                  %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable_type}[due_date]", ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], display: 'static' } }
+                    %span.dropdown-toggle-text
+                      = _('Due date')
+                    = icon('chevron-down', 'aria-hidden': 'true')
+                  .dropdown-menu.dropdown-menu-due-date
+                    = dropdown_title(_('Due date'))
+                    = dropdown_content do
+                      .js-due-date-calendar
+
+        - selected_labels = issuable_sidebar[:labels]
+        .block.labels
+          .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(selected_labels), data: { placement: "left", container: "body", boundary: 'viewport' } }
+            = icon('tags', 'aria-hidden': 'true')
+            %span
+              = selected_labels.size
+          .title.hide-collapsed
+            = _('Labels')
+            = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
+            - if can_edit_issuable
+              = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link qa-edit-link-labels float-right', data: { track_label: "right_sidebar", track_property: "labels", track_event: "click_edit_button", track_value: "" }
+          .value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) }
+            - if selected_labels.any?
+              - selected_labels.each do |label_hash|
+                = render_label(label_from_hash(label_hash).present(issuable_subject: nil), link: sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label_hash[:title]))
+            - else
+              %span.no-value
+                = _('None')
+          .selectbox.hide-collapsed
+            - selected_labels.each do |label|
+              = hidden_field_tag "#{issuable_type}[label_names][]", label[:id], id: nil
+            .dropdown
+              %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: sidebar_label_dropdown_data(issuable_type, issuable_sidebar) }
+                %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
+                  = multi_label_name(selected_labels, "Labels")
+                = icon('chevron-down', 'aria-hidden': 'true')
+              .dropdown-menu.dropdown-select.dropdown-menu-paging.qa-dropdown-menu-labels.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height
+                = render partial: "shared/issuable/label_page_default"
+                - if issuable_sidebar.dig(:current_user, :can_admin_label)
+                  = render partial: "shared/issuable/label_page_create"
+
+        = render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar
+
+        - if issuable_sidebar.has_key?(:confidential)
+          -# haml-lint:disable InlineJavaScript
+          %script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: issuable_sidebar[:confidential], is_editable: can_edit_issuable }.to_json.html_safe
+          #js-confidential-entry-point
+
         -# haml-lint:disable InlineJavaScript
-        %script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: issuable_sidebar[:confidential], is_editable: can_edit_issuable }.to_json.html_safe
-        #js-confidential-entry-point
+        %script#js-lock-issue-data{ type: "application/json" }= { is_locked: !!issuable_sidebar[:discussion_locked], is_editable: can_edit_issuable }.to_json.html_safe
+        #js-lock-entry-point
+
+        .js-sidebar-participants-entry-point
+
+        - if signed_in
+          - if issuable_sidebar[:project_emails_disabled]
+            .block.js-emails-disabled
+              .sidebar-collapsed-icon.has-tooltip{ title: notification_description(:owner_disabled), data: { placement: "left", container: "body", boundary: 'viewport' } }
+                = notification_setting_icon
+              .hide-collapsed= notification_description(:owner_disabled)
+          - else
+            .js-sidebar-subscriptions-entry-point
+
+        - project_ref = issuable_sidebar[:reference]
+        .block.project-reference
+          .sidebar-collapsed-icon.dont-change-state
+            = clipboard_button(text: project_ref, title: _('Copy reference'), placement: "left", boundary: 'viewport')
+          .cross-project-reference.hide-collapsed
+            %span
+              = _('Reference:')
+              %cite{ title: project_ref }
+                = project_ref
+            = clipboard_button(text: project_ref, title: _('Copy reference'), placement: "left", boundary: 'viewport')
+
+        - if issuable_sidebar.dig(:current_user, :can_move)
+          .block.js-sidebar-move-issue-block
+            .sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') }
+              = custom_icon('icon_arrow_right')
+            .dropdown.sidebar-move-issue-dropdown.hide-collapsed
+              %button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button',
+                data: { toggle: 'dropdown', display: 'static', track_label: "right_sidebar", track_property: "move_issue", track_event: "click_button", track_value: "" } }
+                = _('Move issue')
+              .dropdown-menu.dropdown-menu-selectable.dropdown-extended-height
+                = dropdown_title(_('Move issue'))
+                = dropdown_filter(_('Search project'), search_id: 'sidebar-move-issue-dropdown-search')
+                = dropdown_content
+                = dropdown_loading
+                = dropdown_footer add_content_class: true do
+                  %button.btn.btn-success.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ type: 'button', disabled: true }
+                    = _('Move')
+                    = icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon')
 
       -# haml-lint:disable InlineJavaScript
-      %script#js-lock-issue-data{ type: "application/json" }= { is_locked: !!issuable_sidebar[:discussion_locked], is_editable: can_edit_issuable }.to_json.html_safe
-      #js-lock-entry-point
-
-      .js-sidebar-participants-entry-point
-
-      - if signed_in
-        - if issuable_sidebar[:project_emails_disabled]
-          .block.js-emails-disabled
-            .sidebar-collapsed-icon.has-tooltip{ title: notification_description(:owner_disabled), data: { placement: "left", container: "body", boundary: 'viewport' } }
-              = notification_setting_icon
-            .hide-collapsed= notification_description(:owner_disabled)
-        - else
-          .js-sidebar-subscriptions-entry-point
-
-      - project_ref = issuable_sidebar[:reference]
-      .block.project-reference
-        .sidebar-collapsed-icon.dont-change-state
-          = clipboard_button(text: project_ref, title: _('Copy reference'), placement: "left", boundary: 'viewport')
-        .cross-project-reference.hide-collapsed
-          %span
-            = _('Reference:')
-            %cite{ title: project_ref }
-              = project_ref
-          = clipboard_button(text: project_ref, title: _('Copy reference'), placement: "left", boundary: 'viewport')
-
-      - if issuable_sidebar.dig(:current_user, :can_move)
-        .block.js-sidebar-move-issue-block
-          .sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') }
-            = custom_icon('icon_arrow_right')
-          .dropdown.sidebar-move-issue-dropdown.hide-collapsed
-            %button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button',
-              data: { toggle: 'dropdown', display: 'static', track_label: "right_sidebar", track_property: "move_issue", track_event: "click_button", track_value: "" } }
-              = _('Move issue')
-            .dropdown-menu.dropdown-menu-selectable.dropdown-extended-height
-              = dropdown_title(_('Move issue'))
-              = dropdown_filter(_('Search project'), search_id: 'sidebar-move-issue-dropdown-search')
-              = dropdown_content
-              = dropdown_loading
-              = dropdown_footer add_content_class: true do
-                %button.btn.btn-success.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ type: 'button', disabled: true }
-                  = _('Move')
-                  = icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon')
-
-    -# haml-lint:disable InlineJavaScript
-    %script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable_sidebar).to_json.html_safe
+      %script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable_sidebar).to_json.html_safe
diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb
index f69e74b2674882cb8ceb834a6f61c25be2e3880a..be05d2a67529f1f76005aacace467c86d7601d2b 100644
--- a/app/workers/admin_email_worker.rb
+++ b/app/workers/admin_email_worker.rb
@@ -4,6 +4,8 @@ class AdminEmailWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category_not_owned!
+
   def perform
     send_repository_check_mail if Gitlab::CurrentSettings.repository_checks_enabled
   end
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index a33afd436b00b36ac2399c81f77ce25275534f5f..b161cc65602bb509b8dd46cb54e19fe40e3fcfc8 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -119,6 +119,8 @@
 - container_repository:delete_container_repository
 - container_repository:cleanup_container_repository
 
+- notifications:new_release
+
 - default
 - mailers # ActionMailer::DeliveryJob.queue_name
 
@@ -173,3 +175,4 @@
 - delete_stored_files
 - import_issues_csv
 - project_daily_statistics
+- create_evidence
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
index c9ddeb08613dcfe8744ba3baacb8281292507722..577c439f4a25002ac946bd02d1ceac201fc6b271 100644
--- a/app/workers/authorized_projects_worker.rb
+++ b/app/workers/authorized_projects_worker.rb
@@ -4,6 +4,8 @@ class AuthorizedProjectsWorker
   include ApplicationWorker
   prepend WaitableWorker
 
+  feature_category :authentication_and_authorization
+
   # This is a workaround for a Ruby 2.3.7 bug. rspec-mocks cannot restore the
   # visibility of prepended modules. See https://github.com/rspec/rspec-mocks/issues/1231
   # for more details.
diff --git a/app/workers/auto_merge_process_worker.rb b/app/workers/auto_merge_process_worker.rb
index cd81cdbc60c9a99cbadd18763ab28ebc242e4bde..e4dccb891ce3ef22abe70c0a3215a7e2f0abebd8 100644
--- a/app/workers/auto_merge_process_worker.rb
+++ b/app/workers/auto_merge_process_worker.rb
@@ -4,6 +4,7 @@ class AutoMergeProcessWorker
   include ApplicationWorker
 
   queue_namespace :auto_merge
+  feature_category :continuous_delivery
 
   def perform(merge_request_id)
     MergeRequest.find_by_id(merge_request_id).try do |merge_request|
diff --git a/app/workers/background_migration_worker.rb b/app/workers/background_migration_worker.rb
index b83412b5e6ea938b6bcf8f63819a16427b89f96b..20e2cdd7f9631c134b72e960377520ab5d45005d 100644
--- a/app/workers/background_migration_worker.rb
+++ b/app/workers/background_migration_worker.rb
@@ -3,6 +3,8 @@
 class BackgroundMigrationWorker
   include ApplicationWorker
 
+  feature_category_not_owned!
+
   # The minimum amount of time between processing two jobs of the same migration
   # class.
   #
diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb
index b0c3676714c0cda9ffd1fb907f916cdeb5d209ed..15b31acf3e5e916b44aa9f070c3cbf9db3d4904a 100644
--- a/app/workers/build_hooks_worker.rb
+++ b/app/workers/build_hooks_worker.rb
@@ -5,6 +5,7 @@ class BuildHooksWorker
   include PipelineQueue
 
   queue_namespace :pipeline_hooks
+  feature_category :continuous_integration
 
   # rubocop: disable CodeReuse/ActiveRecord
   def perform(build_id)
diff --git a/app/workers/build_queue_worker.rb b/app/workers/build_queue_worker.rb
index 67d5b0f5f5bbce0712d9da6a164c6a89f6c09987..6584fba4c65761a2aa6617be9a36af79d7a7f66f 100644
--- a/app/workers/build_queue_worker.rb
+++ b/app/workers/build_queue_worker.rb
@@ -5,6 +5,7 @@ class BuildQueueWorker
   include PipelineQueue
 
   queue_namespace :pipeline_processing
+  feature_category :continuous_integration
 
   # rubocop: disable CodeReuse/ActiveRecord
   def perform(build_id)
diff --git a/app/workers/chat_notification_worker.rb b/app/workers/chat_notification_worker.rb
index 25a306e94d810c2f6986aa57a86e5f8e2bdb49d8..3bc2edad62ca944050cdfeb70a9545968136259d 100644
--- a/app/workers/chat_notification_worker.rb
+++ b/app/workers/chat_notification_worker.rb
@@ -3,6 +3,8 @@
 class ChatNotificationWorker
   include ApplicationWorker
 
+  feature_category :chatops
+
   RESCHEDULE_INTERVAL = 2.seconds
 
   # rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/workers/ci/archive_traces_cron_worker.rb b/app/workers/ci/archive_traces_cron_worker.rb
index ad7a29719acf5144bedfcfaffe5617b1d288bd6e..74f389175b92d27b7c66d1025c96d193ea0d2fa0 100644
--- a/app/workers/ci/archive_traces_cron_worker.rb
+++ b/app/workers/ci/archive_traces_cron_worker.rb
@@ -5,6 +5,8 @@ class ArchiveTracesCronWorker
     include ApplicationWorker
     include CronjobQueue
 
+    feature_category :continuous_integration
+
     # rubocop: disable CodeReuse/ActiveRecord
     def perform
       # Archive stale live traces which still resides in redis or database
diff --git a/app/workers/ci/build_prepare_worker.rb b/app/workers/ci/build_prepare_worker.rb
index 1a35a74ae53e4f5be4019ae265d168fc1ad7995a..20208c18d0318dcaf48dc95a393de75b51cba3b5 100644
--- a/app/workers/ci/build_prepare_worker.rb
+++ b/app/workers/ci/build_prepare_worker.rb
@@ -6,6 +6,7 @@ class BuildPrepareWorker
     include PipelineQueue
 
     queue_namespace :pipeline_processing
+    feature_category :continuous_integration
 
     def perform(build_id)
       Ci::Build.find_by_id(build_id).try do |build|
diff --git a/app/workers/ci/build_schedule_worker.rb b/app/workers/ci/build_schedule_worker.rb
index da219adffc6def3713aea495453a4e68dcdbed8d..f22ec4c78102cb8eae9b8a390f31efd06f9140cd 100644
--- a/app/workers/ci/build_schedule_worker.rb
+++ b/app/workers/ci/build_schedule_worker.rb
@@ -6,6 +6,7 @@ class BuildScheduleWorker
     include PipelineQueue
 
     queue_namespace :pipeline_processing
+    feature_category :continuous_integration
 
     def perform(build_id)
       ::Ci::Build.find_by_id(build_id).try do |build|
diff --git a/app/workers/cleanup_container_repository_worker.rb b/app/workers/cleanup_container_repository_worker.rb
index 0331fc7b01c95bd8a80ff111edae4de19e52c3e0..83fb3e58d29722cb38b2fd119e4f8b449852f9c5 100644
--- a/app/workers/cleanup_container_repository_worker.rb
+++ b/app/workers/cleanup_container_repository_worker.rb
@@ -4,6 +4,7 @@ class CleanupContainerRepositoryWorker
   include ApplicationWorker
 
   queue_namespace :container_repository
+  feature_category :container_registry
 
   attr_reader :container_repository, :current_user
 
diff --git a/app/workers/concerns/application_worker.rb b/app/workers/concerns/application_worker.rb
index 2b36ccb83048f5eb0ddf52587f6668e50d36d217..62748808ff18ea5e1e50d0c6d7311f67f1b8ac54 100644
--- a/app/workers/concerns/application_worker.rb
+++ b/app/workers/concerns/application_worker.rb
@@ -8,6 +8,7 @@ module ApplicationWorker
   extend ActiveSupport::Concern
 
   include Sidekiq::Worker # rubocop:disable Cop/IncludeSidekiqWorker
+  include WorkerAttributes
 
   included do
     set_queue
diff --git a/app/workers/concerns/auto_devops_queue.rb b/app/workers/concerns/auto_devops_queue.rb
index aba928ccaab3f2422bda94244ac25175fc6224f0..61e3c1544bd5647a47800b353071f5250399863f 100644
--- a/app/workers/concerns/auto_devops_queue.rb
+++ b/app/workers/concerns/auto_devops_queue.rb
@@ -5,5 +5,6 @@ module AutoDevopsQueue
 
   included do
     queue_namespace :auto_devops
+    feature_category :auto_devops
   end
 end
diff --git a/app/workers/concerns/chaos_queue.rb b/app/workers/concerns/chaos_queue.rb
index e406509d12dcc08af388461ee42950fb5d44789d..c5db10491f2998ac9489baa6b5049b491eed7231 100644
--- a/app/workers/concerns/chaos_queue.rb
+++ b/app/workers/concerns/chaos_queue.rb
@@ -5,5 +5,6 @@ module ChaosQueue
 
   included do
     queue_namespace :chaos
+    feature_category :chaos_engineering
   end
 end
diff --git a/app/workers/concerns/cluster_queue.rb b/app/workers/concerns/cluster_queue.rb
index e44b40c36c9f56112c0d0151a99aacfa19092cdc..180b86b0124e5638c377cb857a084caa018fcf95 100644
--- a/app/workers/concerns/cluster_queue.rb
+++ b/app/workers/concerns/cluster_queue.rb
@@ -8,5 +8,6 @@ module ClusterQueue
 
   included do
     queue_namespace :gcp_cluster
+    feature_category :kubernetes_configuration
   end
 end
diff --git a/app/workers/concerns/gitlab/github_import/object_importer.rb b/app/workers/concerns/gitlab/github_import/object_importer.rb
index eeeff6e93a067e3750e491ae807e8a7d76c7c3d3..b856a9329dde0dd556e3ad4ac58a6dfca5cf62e4 100644
--- a/app/workers/concerns/gitlab/github_import/object_importer.rb
+++ b/app/workers/concerns/gitlab/github_import/object_importer.rb
@@ -12,6 +12,8 @@ module ObjectImporter
         include GithubImport::Queue
         include ReschedulingMethods
         include NotifyUponDeath
+
+        feature_category :importers
       end
 
       # project - An instance of `Project` to import the data into.
diff --git a/app/workers/concerns/gitlab/github_import/queue.rb b/app/workers/concerns/gitlab/github_import/queue.rb
index 59b621f16abde15634caca35e04c859e5471175b..7cc23dd7c0bb3b46da1100f1fcb6058c98cd7a00 100644
--- a/app/workers/concerns/gitlab/github_import/queue.rb
+++ b/app/workers/concerns/gitlab/github_import/queue.rb
@@ -7,6 +7,7 @@ module Queue
 
       included do
         queue_namespace :github_importer
+        feature_category :importers
 
         # If a job produces an error it may block a stage from advancing
         # forever. To prevent this from happening we prevent jobs from going to
diff --git a/app/workers/concerns/object_pool_queue.rb b/app/workers/concerns/object_pool_queue.rb
index 5b648df9c72e4110725801b2fe857edc24600f05..c2e84470fba5a77ab2b971b2fa621ecc5a45f1be 100644
--- a/app/workers/concerns/object_pool_queue.rb
+++ b/app/workers/concerns/object_pool_queue.rb
@@ -8,5 +8,6 @@ module ObjectPoolQueue
 
   included do
     queue_namespace :object_pool
+    feature_category :gitaly
   end
 end
diff --git a/app/workers/concerns/pipeline_background_queue.rb b/app/workers/concerns/pipeline_background_queue.rb
index bbb8ad0c982f07638b3bb12ee2dc78a9243b4d89..0a23780b8073e3c8b2b433d84f8231c6f0964178 100644
--- a/app/workers/concerns/pipeline_background_queue.rb
+++ b/app/workers/concerns/pipeline_background_queue.rb
@@ -8,5 +8,6 @@ module PipelineBackgroundQueue
 
   included do
     queue_namespace :pipeline_background
+    feature_category :continuous_integration
   end
 end
diff --git a/app/workers/concerns/pipeline_queue.rb b/app/workers/concerns/pipeline_queue.rb
index 3aaed4669e5fe649849859ba3e3bb144329e70d6..27cbf6eb61c8e3118bef5547dde67398255f6e0d 100644
--- a/app/workers/concerns/pipeline_queue.rb
+++ b/app/workers/concerns/pipeline_queue.rb
@@ -8,5 +8,6 @@ module PipelineQueue
 
   included do
     queue_namespace :pipeline_default
+    feature_category :continuous_integration
   end
 end
diff --git a/app/workers/concerns/repository_check_queue.rb b/app/workers/concerns/repository_check_queue.rb
index 216d67e5dbcce629302931e64e0dd64c3b7028ce..76f6e1c2e911f750e7d9bb85349abb33a833efe2 100644
--- a/app/workers/concerns/repository_check_queue.rb
+++ b/app/workers/concerns/repository_check_queue.rb
@@ -6,7 +6,7 @@ module RepositoryCheckQueue
 
   included do
     queue_namespace :repository_check
-
     sidekiq_options retry: false
+    feature_category :source_code_management
   end
 end
diff --git a/app/workers/concerns/todos_destroyer_queue.rb b/app/workers/concerns/todos_destroyer_queue.rb
index 8e2b1d30579e37b2e2f21f2811ee8e7994bce053..1bbccbfb1f9e6364b04b03d56090703f1d822e30 100644
--- a/app/workers/concerns/todos_destroyer_queue.rb
+++ b/app/workers/concerns/todos_destroyer_queue.rb
@@ -8,5 +8,6 @@ module TodosDestroyerQueue
 
   included do
     queue_namespace :todos_destroyer
+    feature_category :issue_tracking
   end
 end
diff --git a/app/workers/create_evidence_worker.rb b/app/workers/create_evidence_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..027dbd2f101c1dc40f4c2e1e446d543d78a1ebad
--- /dev/null
+++ b/app/workers/create_evidence_worker.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class CreateEvidenceWorker
+  include ApplicationWorker
+
+  feature_category :release_governance
+
+  def perform(release_id)
+    release = Release.find_by_id(release_id)
+    return unless release
+
+    Evidence.create!(release: release)
+  end
+end
diff --git a/app/workers/create_gpg_signature_worker.rb b/app/workers/create_gpg_signature_worker.rb
index e3fb5d479ae3b71bbf910ba058d6d2796f3759fa..fc36a2adccdd9b84ff82b5aeeae4eb4e1abc5695 100644
--- a/app/workers/create_gpg_signature_worker.rb
+++ b/app/workers/create_gpg_signature_worker.rb
@@ -3,6 +3,8 @@
 class CreateGpgSignatureWorker
   include ApplicationWorker
 
+  feature_category :source_code_management
+
   # rubocop: disable CodeReuse/ActiveRecord
   def perform(commit_shas, project_id)
     # Older versions of Git::BranchPushService may push a single commit ID on
diff --git a/app/workers/create_note_diff_file_worker.rb b/app/workers/create_note_diff_file_worker.rb
index 0850250f7e3ec9ce682975ee37fbc4d63b9ab321..ca200bd17b48a40e7581d95b680e9e8c3d5ac018 100644
--- a/app/workers/create_note_diff_file_worker.rb
+++ b/app/workers/create_note_diff_file_worker.rb
@@ -3,6 +3,8 @@
 class CreateNoteDiffFileWorker
   include ApplicationWorker
 
+  feature_category :source_code_management
+
   def perform(diff_note_id)
     diff_note = DiffNote.find(diff_note_id)
 
diff --git a/app/workers/create_pipeline_worker.rb b/app/workers/create_pipeline_worker.rb
index 037b4a57d4b24fa4a0e38ae259652c52a80e1e6d..70412ffd0951dc6c95c405c6dbfb803d935bbfb2 100644
--- a/app/workers/create_pipeline_worker.rb
+++ b/app/workers/create_pipeline_worker.rb
@@ -5,6 +5,7 @@ class CreatePipelineWorker
   include PipelineQueue
 
   queue_namespace :pipeline_creation
+  feature_category :continuous_integration
 
   def perform(project_id, user_id, ref, source, params = {})
     project = Project.find(project_id)
diff --git a/app/workers/delete_container_repository_worker.rb b/app/workers/delete_container_repository_worker.rb
index 42e66513ff1d59b082bac61e0fb150f802aacf8c..e70b4fb0a58b64d7310f922e145d4d828739eece 100644
--- a/app/workers/delete_container_repository_worker.rb
+++ b/app/workers/delete_container_repository_worker.rb
@@ -5,6 +5,7 @@ class DeleteContainerRepositoryWorker
   include ExclusiveLeaseGuard
 
   queue_namespace :container_repository
+  feature_category :container_registry
 
   LEASE_TIMEOUT = 1.hour
 
diff --git a/app/workers/delete_diff_files_worker.rb b/app/workers/delete_diff_files_worker.rb
index f518dfe871c5817506a091df3d7a7e9fd01e67fe..e0c1724f1f7a7078bdfe8084eeb19040d2830b61 100644
--- a/app/workers/delete_diff_files_worker.rb
+++ b/app/workers/delete_diff_files_worker.rb
@@ -3,6 +3,8 @@
 class DeleteDiffFilesWorker
   include ApplicationWorker
 
+  feature_category :source_code_management
+
   # rubocop: disable CodeReuse/ActiveRecord
   def perform(merge_request_diff_id)
     merge_request_diff = MergeRequestDiff.find(merge_request_diff_id)
diff --git a/app/workers/delete_merged_branches_worker.rb b/app/workers/delete_merged_branches_worker.rb
index 017d7fd1cb033d50fe89b1bbdb46d7f5df0683d8..44b3db30d0db0a36745d6a5f0fffc798d0f960d2 100644
--- a/app/workers/delete_merged_branches_worker.rb
+++ b/app/workers/delete_merged_branches_worker.rb
@@ -3,6 +3,8 @@
 class DeleteMergedBranchesWorker
   include ApplicationWorker
 
+  feature_category :source_code_management
+
   def perform(project_id, user_id)
     begin
       project = Project.find(project_id)
diff --git a/app/workers/delete_stored_files_worker.rb b/app/workers/delete_stored_files_worker.rb
index ff7931849d8f8f42c11f35bb9baffe93b2578b19..8a693a64055a256c5e6b0dae0370d6b75bc90d04 100644
--- a/app/workers/delete_stored_files_worker.rb
+++ b/app/workers/delete_stored_files_worker.rb
@@ -3,6 +3,8 @@
 class DeleteStoredFilesWorker
   include ApplicationWorker
 
+  feature_category_not_owned!
+
   def perform(class_name, keys)
     klass = begin
       class_name.constantize
diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb
index efa8794b2148c30aead2f2c86819de8f22be5d03..0e49e787d8ac60ee8c7ec63ea053a51ef7d293a4 100644
--- a/app/workers/delete_user_worker.rb
+++ b/app/workers/delete_user_worker.rb
@@ -3,6 +3,8 @@
 class DeleteUserWorker
   include ApplicationWorker
 
+  feature_category :authentication_and_authorization
+
   def perform(current_user_id, delete_user_id, options = {})
     delete_user  = User.find(delete_user_id)
     current_user = User.find(current_user_id)
diff --git a/app/workers/deployments/finished_worker.rb b/app/workers/deployments/finished_worker.rb
index c9d448d5d18030f7bdcd9d918405945f8cad5058..79a1caccc92cfd8b8393c1c25fe0f2dbaf20553a 100644
--- a/app/workers/deployments/finished_worker.rb
+++ b/app/workers/deployments/finished_worker.rb
@@ -5,6 +5,7 @@ class FinishedWorker
     include ApplicationWorker
 
     queue_namespace :deployment
+    feature_category :continuous_delivery
 
     def perform(deployment_id)
       Deployment.find_by_id(deployment_id).try(:execute_hooks)
diff --git a/app/workers/deployments/success_worker.rb b/app/workers/deployments/success_worker.rb
index da517f3fb2622ea2204d83c7c7a2b40b4d5242b9..f6520307186121273d3b946e3880d7896d10a53d 100644
--- a/app/workers/deployments/success_worker.rb
+++ b/app/workers/deployments/success_worker.rb
@@ -5,12 +5,13 @@ class SuccessWorker
     include ApplicationWorker
 
     queue_namespace :deployment
+    feature_category :continuous_delivery
 
     def perform(deployment_id)
       Deployment.find_by_id(deployment_id).try do |deployment|
         break unless deployment.success?
 
-        UpdateDeploymentService.new(deployment).execute
+        Deployments::AfterCreateService.new(deployment).execute
       end
     end
   end
diff --git a/app/workers/detect_repository_languages_worker.rb b/app/workers/detect_repository_languages_worker.rb
index 838c3be78f0ed25b2bd4b1b83a1c90128f14f02a..954d0f9336bd827165d019f0b38797d887a1e28c 100644
--- a/app/workers/detect_repository_languages_worker.rb
+++ b/app/workers/detect_repository_languages_worker.rb
@@ -6,6 +6,7 @@ class DetectRepositoryLanguagesWorker
   include ExclusiveLeaseGuard
 
   sidekiq_options retry: 1
+  feature_category :source_code_management
 
   LEASE_TIMEOUT = 300
 
diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb
index e70bf17d5a9b3fa9d95c0e93ea3b2bfbefc9172a..c82728be3299da226203d460514af02b207b90c7 100644
--- a/app/workers/email_receiver_worker.rb
+++ b/app/workers/email_receiver_worker.rb
@@ -3,6 +3,8 @@
 class EmailReceiverWorker
   include ApplicationWorker
 
+  feature_category :issue_tracking
+
   def perform(raw)
     return unless Gitlab::IncomingEmail.enabled?
 
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index ed3e354e4c27d85314758f4f67f37a4be8d25351..2231c91a720f68459e9768b70c88836f6b4a3e3f 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -5,6 +5,8 @@ class EmailsOnPushWorker
 
   attr_reader :email, :skip_premailer
 
+  feature_category :source_code_management
+
   def perform(project_id, recipients, push_data, options = {})
     options.symbolize_keys!
     options.reverse_merge!(
diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb
index 6f0e0fd33f7204e1040503054a713984e3aa8567..9545227fa31e2e1e4ef998279c06018108657f6c 100644
--- a/app/workers/expire_build_artifacts_worker.rb
+++ b/app/workers/expire_build_artifacts_worker.rb
@@ -4,6 +4,8 @@ class ExpireBuildArtifactsWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :continuous_integration
+
   def perform
     if Feature.enabled?(:ci_new_expire_job_artifacts_service, default_enabled: true)
       perform_efficient_artifacts_removal
diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb
index 71e61dcb87897f1859e1b65f3b395bfdbb5a6ec1..db5240d5c8e5606303460debc870cbe2e15af9e9 100644
--- a/app/workers/expire_build_instance_artifacts_worker.rb
+++ b/app/workers/expire_build_instance_artifacts_worker.rb
@@ -3,6 +3,8 @@
 class ExpireBuildInstanceArtifactsWorker
   include ApplicationWorker
 
+  feature_category :continuous_integration
+
   # rubocop: disable CodeReuse/ActiveRecord
   def perform(build_id)
     build = Ci::Build
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index 5499e12e49b618f343266fc654b8c0d3dfdd9e34..ad119917774d1fd35b44e1b3f968d33100dc8209 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -4,6 +4,7 @@ class GitGarbageCollectWorker
   include ApplicationWorker
 
   sidekiq_options retry: false
+  feature_category :gitaly
 
   # Timeout set to 24h
   LEASE_TIMEOUT = 86400
diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb
index 0b3437a8a33f2e8bd40da66491356e5b474f1bd0..44e69e48694adc98959cf532d6ceec70310d59da 100644
--- a/app/workers/gitlab/github_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/github_import/advance_stage_worker.rb
@@ -10,6 +10,7 @@ class AdvanceStageWorker
       include ApplicationWorker
 
       sidekiq_options dead: false
+      feature_category :importers
 
       INTERVAL = 30.seconds.to_i
 
diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb
index 0e4d40acc5c89d61a07e152ee244c3bee2b1dfed..9766331cf4b03b736dd4cf7bef833e6377d7b413 100644
--- a/app/workers/gitlab_shell_worker.rb
+++ b/app/workers/gitlab_shell_worker.rb
@@ -4,6 +4,8 @@ class GitlabShellWorker
   include ApplicationWorker
   include Gitlab::ShellAdapter
 
+  feature_category :source_code_management
+
   def perform(action, *arg)
     gitlab_shell.__send__(action, *arg) # rubocop:disable GitlabSecurity/PublicSend
   end
diff --git a/app/workers/gitlab_usage_ping_worker.rb b/app/workers/gitlab_usage_ping_worker.rb
index a5e22f88a3b7948c3891fea0f0fb50c8899dc741..ad8302a844a0b94402b36cbe614af957aacb5565 100644
--- a/app/workers/gitlab_usage_ping_worker.rb
+++ b/app/workers/gitlab_usage_ping_worker.rb
@@ -6,6 +6,8 @@ class GitlabUsagePingWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category_not_owned!
+
   # Retry for up to approximately three hours then give up.
   sidekiq_options retry: 10, dead: false
 
diff --git a/app/workers/group_destroy_worker.rb b/app/workers/group_destroy_worker.rb
index b4a3ddcae51a5a49112b0d8875db733a69c4c397..553fd359bafda9c68e829dab3333a30388adcb8d 100644
--- a/app/workers/group_destroy_worker.rb
+++ b/app/workers/group_destroy_worker.rb
@@ -4,6 +4,8 @@ class GroupDestroyWorker
   include ApplicationWorker
   include ExceptionBacktrace
 
+  feature_category :groups
+
   def perform(group_id, user_id)
     begin
       group = Group.find(group_id)
diff --git a/app/workers/hashed_storage/base_worker.rb b/app/workers/hashed_storage/base_worker.rb
index 237e278c537c79f369249372d18d0df243ec4c87..1ab2108f6bb1e2f3569858d42abb41c2b6ab1128 100644
--- a/app/workers/hashed_storage/base_worker.rb
+++ b/app/workers/hashed_storage/base_worker.rb
@@ -3,6 +3,9 @@
 module HashedStorage
   class BaseWorker
     include ExclusiveLeaseGuard
+    include WorkerAttributes
+
+    feature_category :source_code_management
 
     LEASE_TIMEOUT = 30.seconds.to_i
     LEASE_KEY_SEGMENT = 'project_migrate_hashed_storage_worker'
diff --git a/app/workers/hashed_storage/migrator_worker.rb b/app/workers/hashed_storage/migrator_worker.rb
index 49e347d4060b86b8031f0f34b3deddd0f94c5b8d..72a3faec5f46419e584eaaf3ac4b2da48ddb3f23 100644
--- a/app/workers/hashed_storage/migrator_worker.rb
+++ b/app/workers/hashed_storage/migrator_worker.rb
@@ -5,6 +5,7 @@ class MigratorWorker
     include ApplicationWorker
 
     queue_namespace :hashed_storage
+    feature_category :source_code_management
 
     # @param [Integer] start initial ID of the batch
     # @param [Integer] finish last ID of the batch
diff --git a/app/workers/hashed_storage/rollbacker_worker.rb b/app/workers/hashed_storage/rollbacker_worker.rb
index a4da84437870bdaa09edc4595580db9f38a4070b..8babdcfb96d801a05f3e4a4b4e2a73539676d57e 100644
--- a/app/workers/hashed_storage/rollbacker_worker.rb
+++ b/app/workers/hashed_storage/rollbacker_worker.rb
@@ -5,6 +5,7 @@ class RollbackerWorker
     include ApplicationWorker
 
     queue_namespace :hashed_storage
+    feature_category :source_code_management
 
     # @param [Integer] start initial ID of the batch
     # @param [Integer] finish last ID of the batch
diff --git a/app/workers/import_export_project_cleanup_worker.rb b/app/workers/import_export_project_cleanup_worker.rb
index da3debdeede6c37099f3d367143ae4ed79c5258f..07c29d40b545096ec0176a235cc74e0be4b578c2 100644
--- a/app/workers/import_export_project_cleanup_worker.rb
+++ b/app/workers/import_export_project_cleanup_worker.rb
@@ -4,6 +4,8 @@ class ImportExportProjectCleanupWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :importers
+
   def perform
     ImportExportCleanUpService.new.execute
   end
diff --git a/app/workers/import_issues_csv_worker.rb b/app/workers/import_issues_csv_worker.rb
index b9d7099af71a171a890036084db6485eab34e58a..d98343203183c769f895ea3d92cb0b8dbd9096cf 100644
--- a/app/workers/import_issues_csv_worker.rb
+++ b/app/workers/import_issues_csv_worker.rb
@@ -3,6 +3,8 @@
 class ImportIssuesCsvWorker
   include ApplicationWorker
 
+  feature_category :issue_tracking
+
   sidekiq_retries_exhausted do |job|
     Upload.find(job['args'][2]).destroy
   end
diff --git a/app/workers/invalid_gpg_signature_update_worker.rb b/app/workers/invalid_gpg_signature_update_worker.rb
index fc8a731b427a234420674e8fbd8a41c70bc03b32..573efdf9fb1e1376ab94e9eb4affeaf97790cc66 100644
--- a/app/workers/invalid_gpg_signature_update_worker.rb
+++ b/app/workers/invalid_gpg_signature_update_worker.rb
@@ -3,6 +3,8 @@
 class InvalidGpgSignatureUpdateWorker
   include ApplicationWorker
 
+  feature_category :source_code_management
+
   # rubocop: disable CodeReuse/ActiveRecord
   def perform(gpg_key_id)
     gpg_key = GpgKey.find_by(id: gpg_key_id)
diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb
index 29631c6b7ac68300ac36c123c28f3dde4ee0eb37..a133ed6ed1b9cf59e3b7aa50e1e3da0167db7d91 100644
--- a/app/workers/irker_worker.rb
+++ b/app/workers/irker_worker.rb
@@ -6,6 +6,8 @@
 class IrkerWorker
   include ApplicationWorker
 
+  feature_category :integrations
+
   def perform(project_id, chans, colors, push_data, settings)
     project = Project.find(project_id)
 
diff --git a/app/workers/issue_due_scheduler_worker.rb b/app/workers/issue_due_scheduler_worker.rb
index 476cba47ad70ab5f2b1580776e57de92b61b9250..d4d47659ef01bbd421442f4e0d55cdf82fa98be4 100644
--- a/app/workers/issue_due_scheduler_worker.rb
+++ b/app/workers/issue_due_scheduler_worker.rb
@@ -4,6 +4,8 @@ class IssueDueSchedulerWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :issue_tracking
+
   # rubocop: disable CodeReuse/ActiveRecord
   def perform
     project_ids = Issue.opened.due_tomorrow.group(:project_id).pluck(:project_id).map { |id| [id] }
diff --git a/app/workers/mail_scheduler/issue_due_worker.rb b/app/workers/mail_scheduler/issue_due_worker.rb
index 1e1dde1e8292da45731193fbd37ce75bef60fb62..6df816de71fc5b676debc8c3fec1e23b917be4b2 100644
--- a/app/workers/mail_scheduler/issue_due_worker.rb
+++ b/app/workers/mail_scheduler/issue_due_worker.rb
@@ -5,6 +5,8 @@ class IssueDueWorker
     include ApplicationWorker
     include MailSchedulerQueue
 
+    feature_category :issue_tracking
+
     # rubocop: disable CodeReuse/ActiveRecord
     def perform(project_id)
       Issue.opened.due_tomorrow.in_projects(project_id).preload(:project).find_each do |issue|
diff --git a/app/workers/mail_scheduler/notification_service_worker.rb b/app/workers/mail_scheduler/notification_service_worker.rb
index 421fbf04e281ef32fc39bb4bf5167c17662e3137..0d06dab3b2ef2bdd1f1a466ef28b495ddcd35b7c 100644
--- a/app/workers/mail_scheduler/notification_service_worker.rb
+++ b/app/workers/mail_scheduler/notification_service_worker.rb
@@ -7,6 +7,8 @@ class NotificationServiceWorker
     include ApplicationWorker
     include MailSchedulerQueue
 
+    feature_category :issue_tracking
+
     def perform(meth, *args)
       check_arguments!(args)
 
diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb
index ee864b733cd83c5a348ddd9347cade9e54b57369..70b909afea8ac0d198b0b47e1c947b6c5d0afa0c 100644
--- a/app/workers/merge_worker.rb
+++ b/app/workers/merge_worker.rb
@@ -3,6 +3,8 @@
 class MergeWorker
   include ApplicationWorker
 
+  feature_category :source_code_management
+
   def perform(merge_request_id, current_user_id, params)
     params = params.with_indifferent_access
     current_user = User.find(current_user_id)
diff --git a/app/workers/migrate_external_diffs_worker.rb b/app/workers/migrate_external_diffs_worker.rb
index fe757968d49d4fc8cab525a3a5781f7d1fc8aba7..d248e2b5500758640485c93a3794063cf9643d97 100644
--- a/app/workers/migrate_external_diffs_worker.rb
+++ b/app/workers/migrate_external_diffs_worker.rb
@@ -3,6 +3,8 @@
 class MigrateExternalDiffsWorker
   include ApplicationWorker
 
+  feature_category :source_code_management
+
   def perform(merge_request_diff_id)
     diff = MergeRequestDiff.find_by_id(merge_request_diff_id)
     return unless diff
diff --git a/app/workers/namespaceless_project_destroy_worker.rb b/app/workers/namespaceless_project_destroy_worker.rb
index f6e9874605531796a930e705e2211047ca4feb72..113afc268f2b9f5f6aa04490ed88ce42f1dcc814 100644
--- a/app/workers/namespaceless_project_destroy_worker.rb
+++ b/app/workers/namespaceless_project_destroy_worker.rb
@@ -10,6 +10,8 @@ class NamespacelessProjectDestroyWorker
   include ApplicationWorker
   include ExceptionBacktrace
 
+  feature_category :authentication_and_authorization
+
   def perform(project_id)
     begin
       project = Project.unscoped.find(project_id)
@@ -31,6 +33,6 @@ def perform(project_id)
   def unlink_fork(project)
     merge_requests = project.forked_from_project.merge_requests.opened.from_project(project)
 
-    merge_requests.update_all(state: 'closed')
+    merge_requests.update_all(state_id: MergeRequest.available_states[:closed])
   end
 end
diff --git a/app/workers/namespaces/prune_aggregation_schedules_worker.rb b/app/workers/namespaces/prune_aggregation_schedules_worker.rb
index 4e40feee70247c9de8f1c073c7e1ffe75f2583fe..16259ffbfa6e5d5ea25a9eb21ac4470f9b05e367 100644
--- a/app/workers/namespaces/prune_aggregation_schedules_worker.rb
+++ b/app/workers/namespaces/prune_aggregation_schedules_worker.rb
@@ -5,6 +5,8 @@ class PruneAggregationSchedulesWorker
     include ApplicationWorker
     include CronjobQueue
 
+    feature_category :source_code_management
+
     # Worker to prune pending rows on Namespace::AggregationSchedule
     # It's scheduled to run once a day at 1:05am.
     def perform
diff --git a/app/workers/namespaces/root_statistics_worker.rb b/app/workers/namespaces/root_statistics_worker.rb
index 0c1ca5eb975fe1e2fedc84d6d74ff45ba9b938e5..fd772c8cff681c790f4cbb0035b6af819c31b7b0 100644
--- a/app/workers/namespaces/root_statistics_worker.rb
+++ b/app/workers/namespaces/root_statistics_worker.rb
@@ -5,6 +5,7 @@ class RootStatisticsWorker
     include ApplicationWorker
 
     queue_namespace :update_namespace_statistics
+    feature_category :source_code_management
 
     def perform(namespace_id)
       namespace = Namespace.find(namespace_id)
diff --git a/app/workers/namespaces/schedule_aggregation_worker.rb b/app/workers/namespaces/schedule_aggregation_worker.rb
index b7d580220d6631cd6df730cd93d3037722480533..87e135fbf21385ce0c7a6c0d85a552f4c509447a 100644
--- a/app/workers/namespaces/schedule_aggregation_worker.rb
+++ b/app/workers/namespaces/schedule_aggregation_worker.rb
@@ -5,6 +5,7 @@ class ScheduleAggregationWorker
     include ApplicationWorker
 
     queue_namespace :update_namespace_statistics
+    feature_category :source_code_management
 
     def perform(namespace_id)
       return unless aggregation_schedules_table_exists?
diff --git a/app/workers/new_issue_worker.rb b/app/workers/new_issue_worker.rb
index 85b53973f565fa5510577e262424f63ee8d4d3b8..1b0fec597e70397b0209c45b8c96bfe85a40babb 100644
--- a/app/workers/new_issue_worker.rb
+++ b/app/workers/new_issue_worker.rb
@@ -4,6 +4,8 @@ class NewIssueWorker
   include ApplicationWorker
   include NewIssuable
 
+  feature_category :issue_tracking
+
   def perform(issue_id, user_id)
     return unless objects_found?(issue_id, user_id)
 
diff --git a/app/workers/new_merge_request_worker.rb b/app/workers/new_merge_request_worker.rb
index fa48c1b29a81dcf89c14a17f8822fa97bcaccdbd..0a5b2f8633178de930083597c1f7dfdd3b10d868 100644
--- a/app/workers/new_merge_request_worker.rb
+++ b/app/workers/new_merge_request_worker.rb
@@ -4,6 +4,8 @@ class NewMergeRequestWorker
   include ApplicationWorker
   include NewIssuable
 
+  feature_category :source_code_management
+
   def perform(merge_request_id, user_id)
     return unless objects_found?(merge_request_id, user_id)
 
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index 7648af3a8b9fc22176657ae8a4c1d42e2cb4b460..d0d2a5637388aad9b448ca7fe947c81ea535c301 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -3,6 +3,8 @@
 class NewNoteWorker
   include ApplicationWorker
 
+  feature_category :issue_tracking
+
   # Keep extra parameter to preserve backwards compatibility with
   # old `NewNoteWorker` jobs (can remove later)
   # rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/workers/new_release_worker.rb b/app/workers/new_release_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..28d2517238e488193479dedf269ae56d1bf8c3b6
--- /dev/null
+++ b/app/workers/new_release_worker.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class NewReleaseWorker
+  include ApplicationWorker
+
+  queue_namespace :notifications
+  feature_category :release_orchestration
+
+  def perform(release_id)
+    release = Release.with_project_and_namespace.find_by_id(release_id)
+    return unless release
+
+    NotificationService.new.send_new_release_notifications(release)
+  end
+end
diff --git a/app/workers/object_storage/background_move_worker.rb b/app/workers/object_storage/background_move_worker.rb
index 19ccae7739c33d0a4466ca6286a0c5f93f2e00e6..55f8e1c3ede9188927746159c63dab21563a9a41 100644
--- a/app/workers/object_storage/background_move_worker.rb
+++ b/app/workers/object_storage/background_move_worker.rb
@@ -6,6 +6,7 @@ class BackgroundMoveWorker
     include ObjectStorageQueue
 
     sidekiq_options retry: 5
+    feature_category_not_owned!
 
     def perform(uploader_class_name, subject_class_name, file_field, subject_id)
       uploader_class = uploader_class_name.constantize
diff --git a/app/workers/object_storage/migrate_uploads_worker.rb b/app/workers/object_storage/migrate_uploads_worker.rb
index c9fd19cf9d7fc3c37c95be04bf6e2bd658003471..01e6fdb2d3e3797ac3c246e956494c21f58327cf 100644
--- a/app/workers/object_storage/migrate_uploads_worker.rb
+++ b/app/workers/object_storage/migrate_uploads_worker.rb
@@ -5,6 +5,8 @@ class MigrateUploadsWorker
     include ApplicationWorker
     include ObjectStorageQueue
 
+    feature_category_not_owned!
+
     SanityCheckError = Class.new(StandardError)
 
     class MigrationResult
diff --git a/app/workers/pages_domain_removal_cron_worker.rb b/app/workers/pages_domain_removal_cron_worker.rb
index 79f38e1b89ff2c704e23f0275e5e9f62d969df4c..25e747c78d0ed949941db09e23d800cf515a0713 100644
--- a/app/workers/pages_domain_removal_cron_worker.rb
+++ b/app/workers/pages_domain_removal_cron_worker.rb
@@ -4,6 +4,8 @@ class PagesDomainRemovalCronWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :pages
+
   def perform
     PagesDomain.for_removal.find_each do |domain|
       domain.destroy!
diff --git a/app/workers/pages_domain_ssl_renewal_cron_worker.rb b/app/workers/pages_domain_ssl_renewal_cron_worker.rb
index e5dde07a648e75d121752f4942577f4e60e2c6ab..f7a243e9b3bcd846955851b12e3bf0bb90b95e5d 100644
--- a/app/workers/pages_domain_ssl_renewal_cron_worker.rb
+++ b/app/workers/pages_domain_ssl_renewal_cron_worker.rb
@@ -4,6 +4,8 @@ class PagesDomainSslRenewalCronWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :pages
+
   def perform
     return unless ::Gitlab::LetsEncrypt.enabled?
 
diff --git a/app/workers/pages_domain_ssl_renewal_worker.rb b/app/workers/pages_domain_ssl_renewal_worker.rb
index 87fd80599467dc3e811fdbf787ed219ed85c2300..4db7d22ef7ee5f66c1eacab394d80463a6641f81 100644
--- a/app/workers/pages_domain_ssl_renewal_worker.rb
+++ b/app/workers/pages_domain_ssl_renewal_worker.rb
@@ -3,6 +3,8 @@
 class PagesDomainSslRenewalWorker
   include ApplicationWorker
 
+  feature_category :pages
+
   def perform(domain_id)
     domain = PagesDomain.find_by_id(domain_id)
     return unless domain&.enabled?
diff --git a/app/workers/pages_domain_verification_cron_worker.rb b/app/workers/pages_domain_verification_cron_worker.rb
index 60703c83e9e1ea343641492d5fddcc4a8a2e2138..bb3a7fede9a130bc8f093a49c2c9cc6fe6d68ef3 100644
--- a/app/workers/pages_domain_verification_cron_worker.rb
+++ b/app/workers/pages_domain_verification_cron_worker.rb
@@ -4,6 +4,8 @@ class PagesDomainVerificationCronWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :pages
+
   def perform
     return if Gitlab::Database.read_only?
 
diff --git a/app/workers/pages_domain_verification_worker.rb b/app/workers/pages_domain_verification_worker.rb
index 7817b2ee5fc37d5d5b687da40dafcb6eef497c6a..b0888036498127d5e9377fff7e556a865555007a 100644
--- a/app/workers/pages_domain_verification_worker.rb
+++ b/app/workers/pages_domain_verification_worker.rb
@@ -3,6 +3,8 @@
 class PagesDomainVerificationWorker
   include ApplicationWorker
 
+  feature_category :pages
+
   # rubocop: disable CodeReuse/ActiveRecord
   def perform(domain_id)
     return if Gitlab::Database.read_only?
diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb
index fa0dfa2ff4bc7028878a2127f7d9724c18de2c18..484d90538495de77db991f39a8bad54caf7c69d8 100644
--- a/app/workers/pages_worker.rb
+++ b/app/workers/pages_worker.rb
@@ -4,6 +4,7 @@ class PagesWorker
   include ApplicationWorker
 
   sidekiq_options retry: 3
+  feature_category :pages
 
   def perform(action, *arg)
     send(action, *arg) # rubocop:disable GitlabSecurity/PublicSend
diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb
index 96524d93f8d1685df42ee449b3c26c469b6e53a5..96f3725dbbeda1b32df7b009021fe883d9a7fe4b 100644
--- a/app/workers/pipeline_process_worker.rb
+++ b/app/workers/pipeline_process_worker.rb
@@ -5,6 +5,7 @@ class PipelineProcessWorker
   include PipelineQueue
 
   queue_namespace :pipeline_processing
+  feature_category :continuous_integration
 
   # rubocop: disable CodeReuse/ActiveRecord
   def perform(pipeline_id, build_ids = nil)
diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb
index 9410fd1a7864757990d18a17865db840e1cf7bee..f500ea0835349dea7684662a96071ad7c4c31694 100644
--- a/app/workers/pipeline_schedule_worker.rb
+++ b/app/workers/pipeline_schedule_worker.rb
@@ -4,6 +4,8 @@ class PipelineScheduleWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :continuous_integration
+
   def perform
     Ci::PipelineSchedule.runnable_schedules.preloaded.find_in_batches do |schedules|
       schedules.each do |schedule|
diff --git a/app/workers/plugin_worker.rb b/app/workers/plugin_worker.rb
index c293e28be4a7eb264e0a3d859a3cd0df2ffb6ba6..e708031abdfe2896a1cb7a5cf1d194a25f2175f5 100644
--- a/app/workers/plugin_worker.rb
+++ b/app/workers/plugin_worker.rb
@@ -4,6 +4,7 @@ class PluginWorker
   include ApplicationWorker
 
   sidekiq_options retry: false
+  feature_category :integrations
 
   def perform(file_name, data)
     success, message = Gitlab::Plugin.execute(file_name, data)
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 4f193e95faa8203d8f5cf8e373b60e406997cde9..a3bc7e5b9c99400d88fa67dfa9726547ed7b8cb9 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -3,6 +3,8 @@
 class PostReceive
   include ApplicationWorker
 
+  feature_category :source_code_management
+
   def perform(gl_repository, identifier, changes, push_options = {})
     project, repo_type = Gitlab::GlRepository.parse(gl_repository)
 
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
index f6ebe4ab006597af0057b5dca160d18041fe084f..1e4561fc6eabd1d87fcddab625fde5bf15c2dd40 100644
--- a/app/workers/process_commit_worker.rb
+++ b/app/workers/process_commit_worker.rb
@@ -10,6 +10,8 @@
 class ProcessCommitWorker
   include ApplicationWorker
 
+  feature_category :source_code_management
+
   # project_id - The ID of the project this commit belongs to.
   # user_id - The ID of the user that pushed the commit.
   # commit_hash - Hash containing commit details to use for constructing a
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index e3f1f61991ce39064601091bc937c063532951b2..57a01c0dd8eacb421ddd662760aa7eaf7c86b476 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -5,6 +5,8 @@ class ProjectCacheWorker
   include ApplicationWorker
   LEASE_TIMEOUT = 15.minutes.to_i
 
+  feature_category :source_code_management
+
   # project_id - The ID of the project for which to flush the cache.
   # files - An Array containing extra types of files to refresh such as
   #         `:readme` to flush the README and `:changelog` to flush the
diff --git a/app/workers/project_daily_statistics_worker.rb b/app/workers/project_daily_statistics_worker.rb
index 101f5c28459a6fcad511e86a0c07e03b7319bc3e..19c2fd67763a9787a6871ece81fb2ac36d937847 100644
--- a/app/workers/project_daily_statistics_worker.rb
+++ b/app/workers/project_daily_statistics_worker.rb
@@ -3,6 +3,8 @@
 class ProjectDailyStatisticsWorker
   include ApplicationWorker
 
+  feature_category :source_code_management
+
   def perform(project_id)
     project = Project.find_by_id(project_id)
 
diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb
index 4447e8672400d6f5c5e4c30404368a538539bfa5..1d20837faa2cf61f89fdf945150aeb78924f619a 100644
--- a/app/workers/project_destroy_worker.rb
+++ b/app/workers/project_destroy_worker.rb
@@ -4,6 +4,8 @@ class ProjectDestroyWorker
   include ApplicationWorker
   include ExceptionBacktrace
 
+  feature_category :source_code_management
+
   def perform(project_id, user_id, params)
     project = Project.find(project_id)
     user = User.find(user_id)
diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb
index ed9da39c7c39fb39de5324af454e23fbc0dd9ff5..bbcf3b72718728bdb95098bf500446569bb69691 100644
--- a/app/workers/project_export_worker.rb
+++ b/app/workers/project_export_worker.rb
@@ -5,6 +5,7 @@ class ProjectExportWorker
   include ExceptionBacktrace
 
   sidekiq_options retry: 3
+  feature_category :source_code_management
 
   def perform(current_user_id, project_id, after_export_strategy = {}, params = {})
     current_user = User.find(current_user_id)
diff --git a/app/workers/project_service_worker.rb b/app/workers/project_service_worker.rb
index 25567cec08bb7e07c9934dc257c40f8bb2cb9282..8041404fc7114d3a4d63cd04df43d40d2e7d3625 100644
--- a/app/workers/project_service_worker.rb
+++ b/app/workers/project_service_worker.rb
@@ -4,6 +4,7 @@ class ProjectServiceWorker
   include ApplicationWorker
 
   sidekiq_options dead: false
+  feature_category :integrations
 
   def perform(hook_id, data)
     data = data.with_indifferent_access
diff --git a/app/workers/propagate_service_template_worker.rb b/app/workers/propagate_service_template_worker.rb
index 3ccd76156975bbf821543208099f47a11d3a1b82..73a2b453207f710820c683c25ef489a2b5c728de 100644
--- a/app/workers/propagate_service_template_worker.rb
+++ b/app/workers/propagate_service_template_worker.rb
@@ -4,6 +4,8 @@
 class PropagateServiceTemplateWorker
   include ApplicationWorker
 
+  feature_category :source_code_management
+
   LEASE_TIMEOUT = 4.hours.to_i
 
   # rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/workers/prune_old_events_worker.rb b/app/workers/prune_old_events_worker.rb
index e2d1fb3ed350a7fd45906e5fb716262a5b7fedb2..f421e8dbf59240635ea7ed3a9c338c6d9f2cba8b 100644
--- a/app/workers/prune_old_events_worker.rb
+++ b/app/workers/prune_old_events_worker.rb
@@ -4,6 +4,8 @@ class PruneOldEventsWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category_not_owned!
+
   # rubocop: disable CodeReuse/ActiveRecord
   def perform
     # Contribution calendar shows maximum 12 months of events, we retain 3 years for data integrity.
diff --git a/app/workers/prune_web_hook_logs_worker.rb b/app/workers/prune_web_hook_logs_worker.rb
index 38054069f4e3edd9d0e4cc19a6bd045890bd3c5c..8e48b45fc3492517b6225ab9ee0f403d0d30abd6 100644
--- a/app/workers/prune_web_hook_logs_worker.rb
+++ b/app/workers/prune_web_hook_logs_worker.rb
@@ -6,6 +6,8 @@ class PruneWebHookLogsWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :integrations
+
   # The maximum number of rows to remove in a single job.
   DELETE_LIMIT = 50_000
 
diff --git a/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb
index b30864db802ce6232897827f6cab3ad99d23e819..af4a3def0624ce9be3a0016de5c68239561fe934 100644
--- a/app/workers/reactive_caching_worker.rb
+++ b/app/workers/reactive_caching_worker.rb
@@ -3,6 +3,8 @@
 class ReactiveCachingWorker
   include ApplicationWorker
 
+  feature_category_not_owned!
+
   def perform(class_name, id, *args)
     klass = begin
       class_name.constantize
diff --git a/app/workers/rebase_worker.rb b/app/workers/rebase_worker.rb
index 8d06adcd99375201980a8ab8b1fd2ea90fc19159..7343226fdcd0947bbd6105a63c567c5384a996d1 100644
--- a/app/workers/rebase_worker.rb
+++ b/app/workers/rebase_worker.rb
@@ -5,6 +5,8 @@
 class RebaseWorker
   include ApplicationWorker
 
+  feature_category :source_code_management
+
   def perform(merge_request_id, current_user_id)
     current_user = User.find(current_user_id)
     merge_request = MergeRequest.find(merge_request_id)
diff --git a/app/workers/remote_mirror_notification_worker.rb b/app/workers/remote_mirror_notification_worker.rb
index 368abfeda99255aa4a307af32da1ccee835065dd..8bc19230caf9c43ce64c11fb2425e81b14383510 100644
--- a/app/workers/remote_mirror_notification_worker.rb
+++ b/app/workers/remote_mirror_notification_worker.rb
@@ -3,6 +3,8 @@
 class RemoteMirrorNotificationWorker
   include ApplicationWorker
 
+  feature_category :source_code_management
+
   def perform(remote_mirror_id)
     remote_mirror = RemoteMirror.find_by_id(remote_mirror_id)
 
diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb
index 25128caf72fa6d18356f5ae48cd56b28a844e28b..147b412b7721564d0fe4664dd7da059636c01907 100644
--- a/app/workers/remove_expired_group_links_worker.rb
+++ b/app/workers/remove_expired_group_links_worker.rb
@@ -4,6 +4,8 @@ class RemoveExpiredGroupLinksWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :authentication_and_authorization
+
   def perform
     ProjectGroupLink.expired.destroy_all # rubocop: disable DestroyAll
   end
diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb
index 3497a1f928064a63d2d06fd01074f6b7991f1041..75f06fd9f6b5fd4b7742d5b71a9a662cbcc467d1 100644
--- a/app/workers/remove_expired_members_worker.rb
+++ b/app/workers/remove_expired_members_worker.rb
@@ -4,6 +4,8 @@ class RemoveExpiredMembersWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :authentication_and_authorization
+
   def perform
     Member.expired.find_each do |member|
       Members::DestroyService.new.execute(member, skip_authorization: true)
diff --git a/app/workers/remove_unreferenced_lfs_objects_worker.rb b/app/workers/remove_unreferenced_lfs_objects_worker.rb
index 95e7a9f537f5f7dfc04a06e5b4777e018d0a8287..7f2c23f4685a620a8aa277ce2d6d037877216ace 100644
--- a/app/workers/remove_unreferenced_lfs_objects_worker.rb
+++ b/app/workers/remove_unreferenced_lfs_objects_worker.rb
@@ -4,6 +4,8 @@ class RemoveUnreferencedLfsObjectsWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :source_code_management
+
   def perform
     LfsObject.destroy_unreferenced
   end
diff --git a/app/workers/repository_archive_cache_worker.rb b/app/workers/repository_archive_cache_worker.rb
index c1dff8ced9077bd766fbc9db768e0b242dc61b43..ebc83c1b17a0dc085d2bf2c9ba21c04c6f428a4f 100644
--- a/app/workers/repository_archive_cache_worker.rb
+++ b/app/workers/repository_archive_cache_worker.rb
@@ -4,6 +4,8 @@ class RepositoryArchiveCacheWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :source_code_management
+
   def perform
     RepositoryArchiveCleanUpService.new.execute
   end
diff --git a/app/workers/repository_check/dispatch_worker.rb b/app/workers/repository_check/dispatch_worker.rb
index 0a7d9a14c6a5433d35ceaf3f93f0070224f95708..d2bd5f9b96726aaeb215b8cd2f1f8e857f4380ad 100644
--- a/app/workers/repository_check/dispatch_worker.rb
+++ b/app/workers/repository_check/dispatch_worker.rb
@@ -7,6 +7,8 @@ class DispatchWorker
     include ::EachShardWorker
     include ExclusiveLeaseGuard
 
+    feature_category :source_code_management
+
     LEASE_TIMEOUT = 1.hour
 
     def perform
diff --git a/app/workers/repository_cleanup_worker.rb b/app/workers/repository_cleanup_worker.rb
index aa26c173a72652c07b88058ae287151069fc6935..dd2cbd42d1f27e93cd84782a1b556826fbd66b61 100644
--- a/app/workers/repository_cleanup_worker.rb
+++ b/app/workers/repository_cleanup_worker.rb
@@ -4,6 +4,7 @@ class RepositoryCleanupWorker
   include ApplicationWorker
 
   sidekiq_options retry: 3
+  feature_category :source_code_management
 
   sidekiq_retries_exhausted do |msg, err|
     next if err.is_a?(ActiveRecord::RecordNotFound)
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 35e9c58eb139965eb1170f7e9a0afa98900bb88a..0adf745c7aca16772473dd2cc4624932303d1235 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -6,6 +6,8 @@ class RepositoryForkWorker
   include ProjectStartImport
   include ProjectImportOptions
 
+  feature_category :source_code_management
+
   def perform(*args)
     target_project_id = args.shift
     target_project = Project.find(target_project_id)
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index 85771fa8b3110186326edad30e83f51b392922fc..bc2d0366fddde8333a8cb9d6d599fe3693b37e46 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -6,6 +6,8 @@ class RepositoryImportWorker
   include ProjectStartImport
   include ProjectImportOptions
 
+  feature_category :importers
+
   # technical debt: https://gitlab.com/gitlab-org/gitlab/issues/33991
   sidekiq_options memory_killer_memory_growth_kb: ENV.fetch('MEMORY_KILLER_REPOSITORY_IMPORT_WORKER_MEMORY_GROWTH_KB', 50).to_i
   sidekiq_options memory_killer_max_memory_growth_kb: ENV.fetch('MEMORY_KILLER_REPOSITORY_IMPORT_WORKER_MAX_MEMORY_GROWTH_KB', 300_000).to_i
diff --git a/app/workers/repository_remove_remote_worker.rb b/app/workers/repository_remove_remote_worker.rb
index a85e9fa93948627b271c10507a0c9aaf313717a0..3e55ebc77ed661a2a0adddc836ce333b252a625e 100644
--- a/app/workers/repository_remove_remote_worker.rb
+++ b/app/workers/repository_remove_remote_worker.rb
@@ -4,6 +4,8 @@ class RepositoryRemoveRemoteWorker
   include ApplicationWorker
   include ExclusiveLeaseGuard
 
+  feature_category :source_code_management
+
   LEASE_TIMEOUT = 1.hour
 
   attr_reader :project, :remote_name
diff --git a/app/workers/repository_update_remote_mirror_worker.rb b/app/workers/repository_update_remote_mirror_worker.rb
index d13c7641eb3db22591345daeea751045992cc354..b4d96546fa49791550970d6d1b3d6385c390b5d7 100644
--- a/app/workers/repository_update_remote_mirror_worker.rb
+++ b/app/workers/repository_update_remote_mirror_worker.rb
@@ -7,6 +7,7 @@ class RepositoryUpdateRemoteMirrorWorker
   include Gitlab::ExclusiveLeaseHelpers
 
   sidekiq_options retry: 3, dead: false
+  feature_category :source_code_management
 
   LOCK_WAIT_TIME = 30.seconds
   MAX_TRIES = 3
diff --git a/app/workers/requests_profiles_worker.rb b/app/workers/requests_profiles_worker.rb
index ae022d43e2976bf1c29b0a40e04c49f3f64ed3ce..6ab020afb10f3d2a9c926b5aa5ef8ea7c6309d2f 100644
--- a/app/workers/requests_profiles_worker.rb
+++ b/app/workers/requests_profiles_worker.rb
@@ -4,6 +4,8 @@ class RequestsProfilesWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :source_code_management
+
   def perform
     Gitlab::RequestProfiler.remove_all_profiles
   end
diff --git a/app/workers/run_pipeline_schedule_worker.rb b/app/workers/run_pipeline_schedule_worker.rb
index 659f8b8039772d9b5db49784461ad3568745fdd6..853f774875af9423867778247cd343cf038f89e7 100644
--- a/app/workers/run_pipeline_schedule_worker.rb
+++ b/app/workers/run_pipeline_schedule_worker.rb
@@ -5,6 +5,7 @@ class RunPipelineScheduleWorker
   include PipelineQueue
 
   queue_namespace :pipeline_creation
+  feature_category :continuous_integration
 
   # rubocop: disable CodeReuse/ActiveRecord
   def perform(schedule_id, user_id)
diff --git a/app/workers/schedule_migrate_external_diffs_worker.rb b/app/workers/schedule_migrate_external_diffs_worker.rb
index 04a370f01af93a36b87df96d378ca125521efb71..8abb5922b540dc5c7ebaa675b53faa4c6dd38024 100644
--- a/app/workers/schedule_migrate_external_diffs_worker.rb
+++ b/app/workers/schedule_migrate_external_diffs_worker.rb
@@ -5,6 +5,8 @@ class ScheduleMigrateExternalDiffsWorker
   include CronjobQueue
   include Gitlab::ExclusiveLeaseHelpers
 
+  feature_category :source_code_management
+
   def perform
     in_lock(self.class.name.underscore, ttl: 2.hours, retries: 0) do
       MergeRequests::MigrateExternalDiffsService.enqueue!
diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb
index 7e002d8822c6736fff1726283f5b03393eedbf6a..971edb1f14f9ffa7c35280802a6d694bf786a80a 100644
--- a/app/workers/stuck_ci_jobs_worker.rb
+++ b/app/workers/stuck_ci_jobs_worker.rb
@@ -4,6 +4,8 @@ class StuckCiJobsWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :continuous_integration
+
   EXCLUSIVE_LEASE_KEY = 'stuck_ci_builds_worker_lease'
 
   BUILD_RUNNING_OUTDATED_TIMEOUT = 1.hour
diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb
index a9ff5b22b25b21626147ceeab4706768018879aa..4993cd1220c832e5c2e23cb31b48065d3ae28b4f 100644
--- a/app/workers/stuck_import_jobs_worker.rb
+++ b/app/workers/stuck_import_jobs_worker.rb
@@ -4,6 +4,8 @@ class StuckImportJobsWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :importers
+
   IMPORT_JOBS_EXPIRATION = 15.hours.to_i
 
   def perform
diff --git a/app/workers/stuck_merge_jobs_worker.rb b/app/workers/stuck_merge_jobs_worker.rb
index e840ae4742163f0e64bbe913f54b932e7452a178..024863ab5304fc184904141e82625801d1a85591 100644
--- a/app/workers/stuck_merge_jobs_worker.rb
+++ b/app/workers/stuck_merge_jobs_worker.rb
@@ -4,6 +4,8 @@ class StuckMergeJobsWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :source_code_management
+
   def self.logger
     Rails.logger # rubocop:disable Gitlab/RailsLogger
   end
@@ -31,7 +33,7 @@ def perform
   def apply_current_state!(completed_jids, completed_ids)
     merge_requests = MergeRequest.where(id: completed_ids)
 
-    merge_requests.where.not(merge_commit_sha: nil).update_all(state: :merged)
+    merge_requests.where.not(merge_commit_sha: nil).update_all(state_id: MergeRequest.available_states[:merged])
 
     merge_requests_to_reopen = merge_requests.where(merge_commit_sha: nil)
 
diff --git a/app/workers/system_hook_push_worker.rb b/app/workers/system_hook_push_worker.rb
index 15e369ebcfb1b5f45541fff1acead3a07d7b385d..fc6237f359ac93778f1e8f65980157fbc88f2296 100644
--- a/app/workers/system_hook_push_worker.rb
+++ b/app/workers/system_hook_push_worker.rb
@@ -3,6 +3,8 @@
 class SystemHookPushWorker
   include ApplicationWorker
 
+  feature_category :source_code_management
+
   def perform(push_data, hook_id)
     SystemHooksService.new.execute_hooks(push_data, hook_id)
   end
diff --git a/app/workers/trending_projects_worker.rb b/app/workers/trending_projects_worker.rb
index 55b599ba38fc7cef93d54ce9ea81f0ae12f2fee9..4c8ee1ee4254f97f72c80cfb98578f32c5760d44 100644
--- a/app/workers/trending_projects_worker.rb
+++ b/app/workers/trending_projects_worker.rb
@@ -4,6 +4,8 @@ class TrendingProjectsWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :source_code_management
+
   def perform
     Rails.logger.info('Refreshing trending projects') # rubocop:disable Gitlab/RailsLogger
 
diff --git a/app/workers/update_external_pull_requests_worker.rb b/app/workers/update_external_pull_requests_worker.rb
index c5acfa82ada2deea05b24159b59f8100006f4d46..8b0952528fa9f59a4206d0a2325de807833503af 100644
--- a/app/workers/update_external_pull_requests_worker.rb
+++ b/app/workers/update_external_pull_requests_worker.rb
@@ -3,6 +3,8 @@
 class UpdateExternalPullRequestsWorker
   include ApplicationWorker
 
+  feature_category :source_code_management
+
   def perform(project_id, user_id, ref)
     project = Project.find_by_id(project_id)
     return unless project
diff --git a/app/workers/update_head_pipeline_for_merge_request_worker.rb b/app/workers/update_head_pipeline_for_merge_request_worker.rb
index 4ec2b9d8fbedc426a2f9cc2a76229911f9e3aa89..77859abfea416140bc0fddd2525f557309e64877 100644
--- a/app/workers/update_head_pipeline_for_merge_request_worker.rb
+++ b/app/workers/update_head_pipeline_for_merge_request_worker.rb
@@ -5,6 +5,7 @@ class UpdateHeadPipelineForMergeRequestWorker
   include PipelineQueue
 
   queue_namespace :pipeline_processing
+  feature_category :continuous_integration
 
   def perform(merge_request_id)
     MergeRequest.find_by_id(merge_request_id).try do |merge_request|
diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb
index 6c0e472e05a032130067c9061882bf8106aaaeb1..8e1703cdd0b954212054b12ef25378c01bf46174 100644
--- a/app/workers/update_merge_requests_worker.rb
+++ b/app/workers/update_merge_requests_worker.rb
@@ -3,6 +3,8 @@
 class UpdateMergeRequestsWorker
   include ApplicationWorker
 
+  feature_category :source_code_management
+
   LOG_TIME_THRESHOLD = 90 # seconds
 
   # rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/workers/update_project_statistics_worker.rb b/app/workers/update_project_statistics_worker.rb
index 3abb7e34a9ddccbcf2a14864fe62082e85647b14..e36cebf6f4f41adb8e651673f59690dbdc1b01a1 100644
--- a/app/workers/update_project_statistics_worker.rb
+++ b/app/workers/update_project_statistics_worker.rb
@@ -4,6 +4,8 @@
 class UpdateProjectStatisticsWorker
   include ApplicationWorker
 
+  feature_category :source_code_management
+
   # project_id - The ID of the project for which to flush the cache.
   # statistics - An Array containing columns from ProjectStatistics to
   #              refresh, if empty all columns will be refreshed
diff --git a/app/workers/upload_checksum_worker.rb b/app/workers/upload_checksum_worker.rb
index 834dcaa435d133dc7e6327f5e630f4e4a39a4fb7..d35367145b80931d91a6df2c7fcd879051de1f82 100644
--- a/app/workers/upload_checksum_worker.rb
+++ b/app/workers/upload_checksum_worker.rb
@@ -3,6 +3,8 @@
 class UploadChecksumWorker
   include ApplicationWorker
 
+  feature_category :geo_replication
+
   def perform(upload_id)
     upload = Upload.find(upload_id)
     upload.calculate_checksum!
diff --git a/app/workers/web_hook_worker.rb b/app/workers/web_hook_worker.rb
index 09219a24a16c2d29fdc63707258b442902df63a2..fd7ca93683e5a5db8d276c5f5ae22dbb404ed281 100644
--- a/app/workers/web_hook_worker.rb
+++ b/app/workers/web_hook_worker.rb
@@ -3,6 +3,7 @@
 class WebHookWorker
   include ApplicationWorker
 
+  feature_category :integrations
   sidekiq_options retry: 4, dead: false
 
   def perform(hook_id, data, hook_name)
diff --git a/changelogs/unreleased/12819-remove-feature-flag.yml b/changelogs/unreleased/12819-remove-feature-flag.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0096a0d00ac23f250f9f6a675bcab4a555e820e2
--- /dev/null
+++ b/changelogs/unreleased/12819-remove-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Fix pod logs failure when pod contains more than 1 container
+merge_request: 18574
+author:
+type: fixed
diff --git a/changelogs/unreleased/13360-fix-epics-api.yml b/changelogs/unreleased/13360-fix-epics-api.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ab89f2a8b5c4d19a8a1082d0d12295710681d7e3
--- /dev/null
+++ b/changelogs/unreleased/13360-fix-epics-api.yml
@@ -0,0 +1,5 @@
+---
+title: Fix creating epics with dates from api
+merge_request: 18393
+author:
+type: fixed
diff --git a/changelogs/unreleased/13426-disable-design-mutation-abilities-when-issue-moved-or-locked.yml b/changelogs/unreleased/13426-disable-design-mutation-abilities-when-issue-moved-or-locked.yml
new file mode 100644
index 0000000000000000000000000000000000000000..21c2dbff7e91cd839d08291bbb0c60effab36afe
--- /dev/null
+++ b/changelogs/unreleased/13426-disable-design-mutation-abilities-when-issue-moved-or-locked.yml
@@ -0,0 +1,5 @@
+---
+title: Make designs read-only if the issue has been moved, or if its discussion has been locked
+merge_request: 18551
+author:
+type: changed
diff --git a/changelogs/unreleased/14064-commit-status-on-any-pipelines.yml b/changelogs/unreleased/14064-commit-status-on-any-pipelines.yml
new file mode 100644
index 0000000000000000000000000000000000000000..db55148cf656aacdfa857e26e12fe9adfb7a5f1d
--- /dev/null
+++ b/changelogs/unreleased/14064-commit-status-on-any-pipelines.yml
@@ -0,0 +1,5 @@
+---
+title: Make commit status created for any pipelines
+merge_request: 17524
+author: Aufar Gilbran
+type: changed
diff --git a/changelogs/unreleased/14945-fix-jira-api-url-parsing.yml b/changelogs/unreleased/14945-fix-jira-api-url-parsing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c7323080e954bed6bdfe6356c96d36689b2ba713
--- /dev/null
+++ b/changelogs/unreleased/14945-fix-jira-api-url-parsing.yml
@@ -0,0 +1,5 @@
+---
+title: 'JIRA Integration API URL works having a trailing slash'
+merge_request: 18526
+author:
+type: fixed
diff --git a/changelogs/unreleased/17970-preserve-leading-whitespace.yml b/changelogs/unreleased/17970-preserve-leading-whitespace.yml
new file mode 100644
index 0000000000000000000000000000000000000000..84085b8054710a0b66351fc407c9dd9c3ec2856c
--- /dev/null
+++ b/changelogs/unreleased/17970-preserve-leading-whitespace.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent the slash command parser from removing leading whitespace from content that is unrelated to slash commands
+merge_request: 18589
+author: Jared Deckard
+type: fixed
diff --git a/changelogs/unreleased/18217-request-access-to-project-should-be-on-by-default.yml b/changelogs/unreleased/18217-request-access-to-project-should-be-on-by-default.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3fb1b874e772f15a4a26d04267ab046f4cd1b1e0
--- /dev/null
+++ b/changelogs/unreleased/18217-request-access-to-project-should-be-on-by-default.yml
@@ -0,0 +1,5 @@
+---
+title: Enable Request Access functionality by default for new projects and groups
+merge_request: 17662
+author:
+type: changed
diff --git a/changelogs/unreleased/19822-audio-preview-in-repo.yml b/changelogs/unreleased/19822-audio-preview-in-repo.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9b165479f095e71cb6551f521b5829f392cb6aac
--- /dev/null
+++ b/changelogs/unreleased/19822-audio-preview-in-repo.yml
@@ -0,0 +1,5 @@
+---
+title: Users can preview audio files in a repository.
+merge_request: 18354
+author: Jesse Hall @jessehall3
+type: added
diff --git a/changelogs/unreleased/20-add-signup-step-2.yml b/changelogs/unreleased/20-add-signup-step-2.yml
new file mode 100644
index 0000000000000000000000000000000000000000..cfaa57cea97c03859465522cb122fc0abcfdf882
--- /dev/null
+++ b/changelogs/unreleased/20-add-signup-step-2.yml
@@ -0,0 +1,5 @@
+---
+title: Add step 2 of the experimental signup flow
+merge_request: 16583
+author:
+type: changed
diff --git a/changelogs/unreleased/20829-extend-mr-attributes-returned-by-graphql.yml b/changelogs/unreleased/20829-extend-mr-attributes-returned-by-graphql.yml
new file mode 100644
index 0000000000000000000000000000000000000000..86e34bcdab13d3f620495697f5336d6c97b058d5
--- /dev/null
+++ b/changelogs/unreleased/20829-extend-mr-attributes-returned-by-graphql.yml
@@ -0,0 +1,5 @@
+---
+title: Extend graphql query endpoint for merge requests to return more attributes to support sidebar implementation
+merge_request: 17813
+author:
+type: other
diff --git a/changelogs/unreleased/21800-parse-mentioned-users-group-projects-from-markdown.yml b/changelogs/unreleased/21800-parse-mentioned-users-group-projects-from-markdown.yml
new file mode 100644
index 0000000000000000000000000000000000000000..463d8a0ab98796fd6fd62c54026d648697e06c3d
--- /dev/null
+++ b/changelogs/unreleased/21800-parse-mentioned-users-group-projects-from-markdown.yml
@@ -0,0 +1,5 @@
+---
+title: Adds separate parsers for mentions of users, groups, projects in markdown content
+merge_request: 18318
+author:
+type: added
diff --git a/changelogs/unreleased/23079-write-permission-global-deploy-keys.yml b/changelogs/unreleased/23079-write-permission-global-deploy-keys.yml
new file mode 100644
index 0000000000000000000000000000000000000000..04f9dfc904347bc8c98d577dc895d4addfdad8f9
--- /dev/null
+++ b/changelogs/unreleased/23079-write-permission-global-deploy-keys.yml
@@ -0,0 +1,5 @@
+---
+title: Allow maintainers to toggle write permission for public deploy keys
+merge_request: 17210
+author:
+type: fixed
diff --git a/changelogs/unreleased/23315-group-level-container-registry-browser.yml b/changelogs/unreleased/23315-group-level-container-registry-browser.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4340c565a881eda0361c5ac9d941a09542078264
--- /dev/null
+++ b/changelogs/unreleased/23315-group-level-container-registry-browser.yml
@@ -0,0 +1,5 @@
+---
+title: Group level Container Registry browser
+merge_request: 17615
+author:
+type: added
diff --git a/changelogs/unreleased/2358-elasticsearch-project-snippets.yml b/changelogs/unreleased/2358-elasticsearch-project-snippets.yml
new file mode 100644
index 0000000000000000000000000000000000000000..28324c1827d80f91919ed64aec8a2ff07f38b30b
--- /dev/null
+++ b/changelogs/unreleased/2358-elasticsearch-project-snippets.yml
@@ -0,0 +1,5 @@
+---
+title: Support ES searches for project snippets
+merge_request: 18459
+author:
+type: fixed
diff --git a/changelogs/unreleased/26001-notification-release-be.yml b/changelogs/unreleased/26001-notification-release-be.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f3e81a60dc94570924421f432065bcd9d7aef113
--- /dev/null
+++ b/changelogs/unreleased/26001-notification-release-be.yml
@@ -0,0 +1,5 @@
+---
+title: Add 'New release' to the project custom notifications
+merge_request: 17877
+author:
+type: added
diff --git a/changelogs/unreleased/26019-evidence-collection.yml b/changelogs/unreleased/26019-evidence-collection.yml
new file mode 100644
index 0000000000000000000000000000000000000000..439a4b55900744de501ca94ce9133eadc40e33f2
--- /dev/null
+++ b/changelogs/unreleased/26019-evidence-collection.yml
@@ -0,0 +1,5 @@
+---
+title: Creation of Evidence collection of new releases.
+merge_request: 17217
+author:
+type: added
diff --git a/changelogs/unreleased/27715-fix-unrenderable-notes.yml b/changelogs/unreleased/27715-fix-unrenderable-notes.yml
new file mode 100644
index 0000000000000000000000000000000000000000..329f9cbb30c48e9833c1b9b7021095987efa897c
--- /dev/null
+++ b/changelogs/unreleased/27715-fix-unrenderable-notes.yml
@@ -0,0 +1,5 @@
+---
+title: Fix showing diff when it has legacy diff notes
+merge_request: 18510
+author:
+type: fixed
diff --git a/changelogs/unreleased/28211-check-if-mapping-is-empty-before-caching.yml b/changelogs/unreleased/28211-check-if-mapping-is-empty-before-caching.yml
new file mode 100644
index 0000000000000000000000000000000000000000..efdd47bb061497724f94212b7caa978a8a7f992b
--- /dev/null
+++ b/changelogs/unreleased/28211-check-if-mapping-is-empty-before-caching.yml
@@ -0,0 +1,5 @@
+---
+title: Check if mapping is empty before caching in File Collections
+merge_request: 18290
+author: briankabiro
+type: performance
diff --git a/changelogs/unreleased/28243-check-for-docker-images-before-renaming-group.yml b/changelogs/unreleased/28243-check-for-docker-images-before-renaming-group.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8717d59b1bceabb1cdd3274cbe6d2e2b27b876f4
--- /dev/null
+++ b/changelogs/unreleased/28243-check-for-docker-images-before-renaming-group.yml
@@ -0,0 +1,6 @@
+---
+title: Prevents a group path change when a project inside the group has container
+  registry images
+merge_request: 17583
+author:
+type: fixed
diff --git a/changelogs/unreleased/29121-rename-trace.yml b/changelogs/unreleased/29121-rename-trace.yml
new file mode 100644
index 0000000000000000000000000000000000000000..14c724e83562accf0ceb6c985e310ae784204c49
--- /dev/null
+++ b/changelogs/unreleased/29121-rename-trace.yml
@@ -0,0 +1,5 @@
+---
+title: Replace wording trace with log
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/29215-500-error-when-deleting-group-web-hook-activerecord-statementinvali.yml b/changelogs/unreleased/29215-500-error-when-deleting-group-web-hook-activerecord-statementinvali.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f1b826204188599a0786b26a9f8382e20c9ea1aa
--- /dev/null
+++ b/changelogs/unreleased/29215-500-error-when-deleting-group-web-hook-activerecord-statementinvali.yml
@@ -0,0 +1,5 @@
+---
+title: Use cascading deletes for deleting logs upon deleting a webhook
+merge_request: 18642
+author:
+type: performance
diff --git a/changelogs/unreleased/29477-notification-settings-display-all-groups.yml b/changelogs/unreleased/29477-notification-settings-display-all-groups.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a4cb6fe16438f2b75c4f53017fd4a76ccf1cdae6
--- /dev/null
+++ b/changelogs/unreleased/29477-notification-settings-display-all-groups.yml
@@ -0,0 +1,5 @@
+---
+title: Show all groups user belongs to in Notification settings
+merge_request: 17303
+author:
+type: fixed
diff --git a/changelogs/unreleased/29513-continue-improvements-for-time-window-filtering-on-metrics-dashboar.yml b/changelogs/unreleased/29513-continue-improvements-for-time-window-filtering-on-metrics-dashboar.yml
new file mode 100644
index 0000000000000000000000000000000000000000..668e25f474955026c394c7a18e1132344a505f18
--- /dev/null
+++ b/changelogs/unreleased/29513-continue-improvements-for-time-window-filtering-on-metrics-dashboar.yml
@@ -0,0 +1,5 @@
+---
+title: Improve time window filtering on metrics dashboard
+merge_request: 17554
+author:
+type: added
diff --git a/changelogs/unreleased/29835-webide-fork.yml b/changelogs/unreleased/29835-webide-fork.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1849b414a2df331f3f536f35f486e4a1c7f7e055
--- /dev/null
+++ b/changelogs/unreleased/29835-webide-fork.yml
@@ -0,0 +1,6 @@
+---
+title: Web IDE button should fork and open forked project when selected from read-only
+  project
+merge_request: 17672
+author:
+type: added
diff --git a/changelogs/unreleased/29881-fix-ide-delete-and-readd.yml b/changelogs/unreleased/29881-fix-ide-delete-and-readd.yml
new file mode 100644
index 0000000000000000000000000000000000000000..91445ca791bc4ffe7ea386503dff30e948c2df19
--- /dev/null
+++ b/changelogs/unreleased/29881-fix-ide-delete-and-readd.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Web IDE tree not updating modified status
+merge_request: 18647
+author:
+type: fixed
diff --git a/changelogs/unreleased/30525-iframe_jaeger.yml b/changelogs/unreleased/30525-iframe_jaeger.yml
new file mode 100644
index 0000000000000000000000000000000000000000..19fdccf63333b56946b46b9eb23bf8f09dee597d
--- /dev/null
+++ b/changelogs/unreleased/30525-iframe_jaeger.yml
@@ -0,0 +1,5 @@
+---
+title: Embed Jaeger in Gitlab UI
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/30619-make-recent-searches-more-visible.yml b/changelogs/unreleased/30619-make-recent-searches-more-visible.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c57806fcdd98138a44458547821bfffe43833b96
--- /dev/null
+++ b/changelogs/unreleased/30619-make-recent-searches-more-visible.yml
@@ -0,0 +1,5 @@
+---
+title: Use text instead of icon for recent searches dropdown
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/30877-optimize-explore-snippets.yml b/changelogs/unreleased/30877-optimize-explore-snippets.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7ca52876609e654e44d947425662fb640f45326a
--- /dev/null
+++ b/changelogs/unreleased/30877-optimize-explore-snippets.yml
@@ -0,0 +1,5 @@
+---
+title: Show only personal snippets on explore page
+merge_request: 18092
+author:
+type: performance
diff --git a/changelogs/unreleased/31007-limit-activity-events.yml b/changelogs/unreleased/31007-limit-activity-events.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d5ad588af33ae1f2b1617e56578c552bb082ecc6
--- /dev/null
+++ b/changelogs/unreleased/31007-limit-activity-events.yml
@@ -0,0 +1,5 @@
+---
+title: Aggregate push events when there are too many
+merge_request: 18239
+author:
+type: changed
diff --git a/changelogs/unreleased/31009-limit-project-hooks-services.yml b/changelogs/unreleased/31009-limit-project-hooks-services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dc1e046156708c5edc5e3e1f26ffaf2de1deb192
--- /dev/null
+++ b/changelogs/unreleased/31009-limit-project-hooks-services.yml
@@ -0,0 +1,5 @@
+---
+title: Don't execute webhooks/services when above limit
+merge_request: 17874
+author:
+type: performance
diff --git a/changelogs/unreleased/31441-make-it-easy-for-includes-to-add-jobs-at-beginning-end-of-pipeline.yml b/changelogs/unreleased/31441-make-it-easy-for-includes-to-add-jobs-at-beginning-end-of-pipeline.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e909c56983bca22654dc60bf90df9f36943fa164
--- /dev/null
+++ b/changelogs/unreleased/31441-make-it-easy-for-includes-to-add-jobs-at-beginning-end-of-pipeline.yml
@@ -0,0 +1,5 @@
+---
+title: Add two new predefined stages to pipelines
+merge_request: 18205
+author:
+type: added
diff --git a/changelogs/unreleased/31573-cross-project-pipeline-triggering-does-not-work-in-core.yml b/changelogs/unreleased/31573-cross-project-pipeline-triggering-does-not-work-in-core.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1638746ea7266369527cbb54f774eabfdba700fa
--- /dev/null
+++ b/changelogs/unreleased/31573-cross-project-pipeline-triggering-does-not-work-in-core.yml
@@ -0,0 +1,5 @@
+---
+title: Allow cross-project pipeline triggering with CI_JOB_TOKEN in core
+merge_request: 17251
+author:
+type: added
diff --git a/changelogs/unreleased/31678-update-cluster-link-text.yml b/changelogs/unreleased/31678-update-cluster-link-text.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fc75974938042470683c56a2f6e8b7e558c51d38
--- /dev/null
+++ b/changelogs/unreleased/31678-update-cluster-link-text.yml
@@ -0,0 +1,5 @@
+---
+title: Update cluster link text
+merge_request: 18322
+author:
+type: changed
diff --git a/changelogs/unreleased/31914-graphql-todos-query-pd.yml b/changelogs/unreleased/31914-graphql-todos-query-pd.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e39bcda1ff6dd7e031374fbfb5b8347f7d7df1ed
--- /dev/null
+++ b/changelogs/unreleased/31914-graphql-todos-query-pd.yml
@@ -0,0 +1,5 @@
+---
+title: Add ability to query todos using GraphQL
+merge_request: 18218
+author:
+type: added
diff --git a/changelogs/unreleased/31923-Snowplow-custom-events-Monitor.yml b/changelogs/unreleased/31923-Snowplow-custom-events-Monitor.yml
new file mode 100644
index 0000000000000000000000000000000000000000..161fb59ca6edc8cc11a13ed8b0faeda4fb0f73f4
--- /dev/null
+++ b/changelogs/unreleased/31923-Snowplow-custom-events-Monitor.yml
@@ -0,0 +1,5 @@
+---
+title: 'Snowplow custom events for Monitor: Health Product Categories'
+merge_request: 18157
+author:
+type: added
diff --git a/changelogs/unreleased/32380-update-issue-list-icons.yml b/changelogs/unreleased/32380-update-issue-list-icons.yml
new file mode 100644
index 0000000000000000000000000000000000000000..42ad9b1eb998969d02500b488f90bef55150fe28
--- /dev/null
+++ b/changelogs/unreleased/32380-update-issue-list-icons.yml
@@ -0,0 +1,5 @@
+---
+title: Use correct icons for issue actions
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/32919-inform-the-user-that-removing-the-last-tag-of-an-image-it-will-remo.yml b/changelogs/unreleased/32919-inform-the-user-that-removing-the-last-tag-of-an-image-it-will-remo.yml
new file mode 100644
index 0000000000000000000000000000000000000000..76da5fcebc87bc3477a3495b3ca0d9386ad0b0b6
--- /dev/null
+++ b/changelogs/unreleased/32919-inform-the-user-that-removing-the-last-tag-of-an-image-it-will-remo.yml
@@ -0,0 +1,6 @@
+---
+title: Add more specific message to clarify the role of empty images in container
+  registry
+merge_request: 32919
+author:
+type: changed
diff --git a/changelogs/unreleased/32930-matching-branch-code-owner-approval.yml b/changelogs/unreleased/32930-matching-branch-code-owner-approval.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b2e1d6d595801f447f4bbdc1f99b94863bf918c4
--- /dev/null
+++ b/changelogs/unreleased/32930-matching-branch-code-owner-approval.yml
@@ -0,0 +1,5 @@
+---
+title: Add matching branch info to branch column
+merge_request: 18352
+author:
+type: added
diff --git a/changelogs/unreleased/33533-go-to-root-if-no-path-on-branch.yml b/changelogs/unreleased/33533-go-to-root-if-no-path-on-branch.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c85faa9e7a2769f24e78dd3ddc4f6f0e3618f08e
--- /dev/null
+++ b/changelogs/unreleased/33533-go-to-root-if-no-path-on-branch.yml
@@ -0,0 +1,6 @@
+---
+title: When a user views a file's blame or blob and switches to a branch where the
+  current file does not exist, they will now be redirected to the root of the repository.
+merge_request: 18169
+author: Jesse Hall @jessehall3
+type: changed
diff --git a/changelogs/unreleased/33876-ensure-proper-access-level-check-on-pa.yml b/changelogs/unreleased/33876-ensure-proper-access-level-check-on-pa.yml
new file mode 100644
index 0000000000000000000000000000000000000000..686382c7caf3a7264d8b2c483d6c61866223146b
--- /dev/null
+++ b/changelogs/unreleased/33876-ensure-proper-access-level-check-on-pa.yml
@@ -0,0 +1,5 @@
+---
+title: Allow to view productivity analytics page without a license
+merge_request: 33876
+author:
+type: fixed
diff --git a/changelogs/unreleased/34032-container-registry-bug-on-modal-delete-button-and-title-text.yml b/changelogs/unreleased/34032-container-registry-bug-on-modal-delete-button-and-title-text.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a4d3e62a48a559b818d5541d975604843cf6ad96
--- /dev/null
+++ b/changelogs/unreleased/34032-container-registry-bug-on-modal-delete-button-and-title-text.yml
@@ -0,0 +1,5 @@
+---
+title: Fix container registry delete tag modal title and button
+merge_request: 34032
+author:
+type: fixed
diff --git a/changelogs/unreleased/34120-design-system-notes-icon-does-not-appear.yml b/changelogs/unreleased/34120-design-system-notes-icon-does-not-appear.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8533067a408658fd94c2a4cc2e2b6a1ccd58e5ff
--- /dev/null
+++ b/changelogs/unreleased/34120-design-system-notes-icon-does-not-appear.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve missing design system notes icons
+merge_request: 18693
+author:
+type: fixed
diff --git a/changelogs/unreleased/34299-enable-color-chip-asciidoc.yml b/changelogs/unreleased/34299-enable-color-chip-asciidoc.yml
new file mode 100644
index 0000000000000000000000000000000000000000..546e6bc6b635c00fb10fa058291f769f2db6a0e0
--- /dev/null
+++ b/changelogs/unreleased/34299-enable-color-chip-asciidoc.yml
@@ -0,0 +1,5 @@
+---
+title: Enable the color chip in AsciiDoc documents
+merge_request: 18723
+author:
+type: added
diff --git a/changelogs/unreleased/34320-error-when-uploading-a-few-designs-in-a-row.yml b/changelogs/unreleased/34320-error-when-uploading-a-few-designs-in-a-row.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b727bc7f85ea8de9d6747510a736f7e1ce84199f
--- /dev/null
+++ b/changelogs/unreleased/34320-error-when-uploading-a-few-designs-in-a-row.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Error when uploading a few designs in a row
+merge_request: 18811
+author:
+type: fixed
diff --git a/changelogs/unreleased/45797-welcome-screen.yml b/changelogs/unreleased/45797-welcome-screen.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4f0868c484f10fc4393af8316ce00e7ab93d0fa2
--- /dev/null
+++ b/changelogs/unreleased/45797-welcome-screen.yml
@@ -0,0 +1,5 @@
+---
+title: Fix formatting welcome screen external users
+merge_request: 16667
+author:
+type: fixed
diff --git a/changelogs/unreleased/46686-add-aws-cluster-data-model.yml b/changelogs/unreleased/46686-add-aws-cluster-data-model.yml
new file mode 100644
index 0000000000000000000000000000000000000000..130c4c0c855e2dd6a00a2ed7b526ded73759a6d2
--- /dev/null
+++ b/changelogs/unreleased/46686-add-aws-cluster-data-model.yml
@@ -0,0 +1,5 @@
+---
+title: Add database tables to store AWS roles and cluster providers
+merge_request: 17057
+author:
+type: added
diff --git a/changelogs/unreleased/64837-persist-refs-over-browser-tabs.yml b/changelogs/unreleased/64837-persist-refs-over-browser-tabs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..68042383ed6070319afa1259d590f5308aa8064a
--- /dev/null
+++ b/changelogs/unreleased/64837-persist-refs-over-browser-tabs.yml
@@ -0,0 +1,5 @@
+---
+title: persist the refs when open the link of refs in a new tab of browser
+merge_request: 31998
+author: minghuan lei
+type: added
diff --git a/changelogs/unreleased/ab-iid-unnecessary-locks.yml b/changelogs/unreleased/ab-iid-unnecessary-locks.yml
new file mode 100644
index 0000000000000000000000000000000000000000..cbdef4ffa87c9a45259b7ca0c5128bbbac329f8b
--- /dev/null
+++ b/changelogs/unreleased/ab-iid-unnecessary-locks.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid unnecessary locks on internal_ids
+merge_request: 18328
+author:
+type: performance
diff --git a/changelogs/unreleased/ab-replace-index.yml b/changelogs/unreleased/ab-replace-index.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3e8586d2ad1c0a72e48e8c88de0ffd9ebeb873be
--- /dev/null
+++ b/changelogs/unreleased/ab-replace-index.yml
@@ -0,0 +1,5 @@
+---
+title: Replace index on ci_triggers
+merge_request: 18652
+author:
+type: performance
diff --git a/changelogs/unreleased/ac-fix-only-os-uplods.yml b/changelogs/unreleased/ac-fix-only-os-uplods.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d63ddc059b6db8c06ef45f63b4581ef968947d3e
--- /dev/null
+++ b/changelogs/unreleased/ac-fix-only-os-uplods.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid dumping files on disk when direct_upload is enabled
+merge_request: 18135
+author:
+type: performance
diff --git a/changelogs/unreleased/add-ansi2json-log-parser.yml b/changelogs/unreleased/add-ansi2json-log-parser.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1aec5d36fbe8ad072689a6ee3e06da8a8fb98e10
--- /dev/null
+++ b/changelogs/unreleased/add-ansi2json-log-parser.yml
@@ -0,0 +1,5 @@
+---
+title: Introduce new Ansi2json parser to convert job logs to JSON
+merge_request: 18133
+author:
+type: added
diff --git a/changelogs/unreleased/allow-api-lookup-of-inherited-member-by-id.yml b/changelogs/unreleased/allow-api-lookup-of-inherited-member-by-id.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f266d197c6c8abc04475e9b9f4c83196c5f661eb
--- /dev/null
+++ b/changelogs/unreleased/allow-api-lookup-of-inherited-member-by-id.yml
@@ -0,0 +1,5 @@
+---
+title: Add individual inherited member lookup API
+merge_request: 17744
+author:
+type: added
diff --git a/changelogs/unreleased/an-sidekiq-job-feature-attribution.yml b/changelogs/unreleased/an-sidekiq-job-feature-attribution.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6f5832dfef2d98059197e97af677a2d186084540
--- /dev/null
+++ b/changelogs/unreleased/an-sidekiq-job-feature-attribution.yml
@@ -0,0 +1,5 @@
+---
+title: Attribute each Sidekiq worker to a feature category
+merge_request: 18462
+author:
+type: other
diff --git a/changelogs/unreleased/deployments-api.yml b/changelogs/unreleased/deployments-api.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dce1763bdf1ca90bbde44360a26ef35a13134c39
--- /dev/null
+++ b/changelogs/unreleased/deployments-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add API for manually creating and updating deployments
+merge_request: 17620
+author:
+type: added
diff --git a/changelogs/unreleased/dz-improve-groups-list-ui.yml b/changelogs/unreleased/dz-improve-groups-list-ui.yml
new file mode 100644
index 0000000000000000000000000000000000000000..36460eb911a8d298bcd32678c4d92bff0226064a
--- /dev/null
+++ b/changelogs/unreleased/dz-improve-groups-list-ui.yml
@@ -0,0 +1,5 @@
+---
+title: Increase group avatar size to 40px
+merge_request: 18654
+author:
+type: changed
diff --git a/changelogs/unreleased/eb-missing-dependencies-custom-callout-message.yml b/changelogs/unreleased/eb-missing-dependencies-custom-callout-message.yml
new file mode 100644
index 0000000000000000000000000000000000000000..eda37da11b4999fafe513fc3a2adc9abac339343
--- /dev/null
+++ b/changelogs/unreleased/eb-missing-dependencies-custom-callout-message.yml
@@ -0,0 +1,6 @@
+---
+title: Include in the callout message a list of jobs that caused missing dependencies
+  failure.
+merge_request: 18219
+author:
+type: added
diff --git a/changelogs/unreleased/expose-artifacts-to-merge-request-widget.yml b/changelogs/unreleased/expose-artifacts-to-merge-request-widget.yml
new file mode 100644
index 0000000000000000000000000000000000000000..24113325feba5c31b125276f95395971f264e9c4
--- /dev/null
+++ b/changelogs/unreleased/expose-artifacts-to-merge-request-widget.yml
@@ -0,0 +1,5 @@
+---
+title: Expose arbitrary job artifacts in Merge Request widget
+merge_request: 18385
+author:
+type: added
diff --git a/changelogs/unreleased/feature-add-copyable-login-with-copy-to-empty-container-registry-view.yml b/changelogs/unreleased/feature-add-copyable-login-with-copy-to-empty-container-registry-view.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6d7a773120b5f1aa306f90badb2a223a457a163c
--- /dev/null
+++ b/changelogs/unreleased/feature-add-copyable-login-with-copy-to-empty-container-registry-view.yml
@@ -0,0 +1,5 @@
+---
+title: Adds login input with copy box and supporting copy to empty container registry view
+merge_request: 18244
+author: nate geslin
+type: added
diff --git a/changelogs/unreleased/fix-admin-mode-ui-buttons-missing-on-small-screens.yml b/changelogs/unreleased/fix-admin-mode-ui-buttons-missing-on-small-screens.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4014b5e7ab2f8d824b2e0e0218d7d1722532bf80
--- /dev/null
+++ b/changelogs/unreleased/fix-admin-mode-ui-buttons-missing-on-small-screens.yml
@@ -0,0 +1,5 @@
+---
+title: Fix missing admin mode UI buttons on bigger screen sizes
+merge_request: 18585
+author: Diego Louzán
+type: fixed
diff --git a/changelogs/unreleased/geo-mk-add-custom-http-clone-url-root.yml b/changelogs/unreleased/geo-mk-add-custom-http-clone-url-root.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5bd7cc1761d9dde56912180750d49ad8294abf93
--- /dev/null
+++ b/changelogs/unreleased/geo-mk-add-custom-http-clone-url-root.yml
@@ -0,0 +1,5 @@
+---
+title: Add "Custom HTTP Git clone URL root" setting
+merge_request: 18422
+author:
+type: added
diff --git a/changelogs/unreleased/graphql-epic-mutate.yml b/changelogs/unreleased/graphql-epic-mutate.yml
new file mode 100644
index 0000000000000000000000000000000000000000..322c069aa46a3e555ab671bbcb501a28f84fcb3a
--- /dev/null
+++ b/changelogs/unreleased/graphql-epic-mutate.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for epic update through GraphQL API.
+merge_request: 18440
+author:
+type: added
diff --git a/changelogs/unreleased/id-cleanup-anny-approver-migrations.yml b/changelogs/unreleased/id-cleanup-anny-approver-migrations.yml
new file mode 100644
index 0000000000000000000000000000000000000000..979250d47625b816319316d60b18f348733c5c6f
--- /dev/null
+++ b/changelogs/unreleased/id-cleanup-anny-approver-migrations.yml
@@ -0,0 +1,5 @@
+---
+title: Cleanup background migrations for any approval rules
+merge_request: 18256
+author:
+type: changed
diff --git a/changelogs/unreleased/id-fix-nplus1-for-signatures.yml b/changelogs/unreleased/id-fix-nplus1-for-signatures.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e060c771227e324d3fb6fb212e273b71b9b27677
--- /dev/null
+++ b/changelogs/unreleased/id-fix-nplus1-for-signatures.yml
@@ -0,0 +1,5 @@
+---
+title: Remove N+1 for fetching commits signatures
+merge_request: 18389
+author:
+type: performance
diff --git a/changelogs/unreleased/introduce-feature-flag-api.yml b/changelogs/unreleased/introduce-feature-flag-api.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fa6c3be302fab3f44a2ce0f76431793dfd54b357
--- /dev/null
+++ b/changelogs/unreleased/introduce-feature-flag-api.yml
@@ -0,0 +1,5 @@
+---
+title: Support Create/Read/Destroy operations in Feature Flag API
+merge_request: 18198
+author:
+type: added
diff --git a/changelogs/unreleased/issue_11240.yml b/changelogs/unreleased/issue_11240.yml
new file mode 100644
index 0000000000000000000000000000000000000000..751440d1e8c71010198e4767ea7d36a97d76b243
--- /dev/null
+++ b/changelogs/unreleased/issue_11240.yml
@@ -0,0 +1,5 @@
+---
+title: Expose subscribed attribute for epic on API
+merge_request: 18475
+author:
+type: added
diff --git a/changelogs/unreleased/issue_28457.yml b/changelogs/unreleased/issue_28457.yml
new file mode 100644
index 0000000000000000000000000000000000000000..51e19660ffce42a915d75007bad778183d2ef317
--- /dev/null
+++ b/changelogs/unreleased/issue_28457.yml
@@ -0,0 +1,5 @@
+---
+title: Deprecate usage of state column for issues and merge requests
+merge_request: 18099
+author:
+type: changed
diff --git a/changelogs/unreleased/mfluharty-add-mr-links-to-pipeline-view.yml b/changelogs/unreleased/mfluharty-add-mr-links-to-pipeline-view.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e3bb00bc5bdadd73af93e0f49d1b86a88d5f29fe
--- /dev/null
+++ b/changelogs/unreleased/mfluharty-add-mr-links-to-pipeline-view.yml
@@ -0,0 +1,5 @@
+---
+title: Show related merge requests in pipeline view
+merge_request: 18697
+author:
+type: added
diff --git a/changelogs/unreleased/mk-remove-flag-geo_object_storage_replication.yml b/changelogs/unreleased/mk-remove-flag-geo_object_storage_replication.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8e2c6d4b0939da7e89f1dfb5b56cfebecc5b2dfc
--- /dev/null
+++ b/changelogs/unreleased/mk-remove-flag-geo_object_storage_replication.yml
@@ -0,0 +1,5 @@
+---
+title: 'Geo: Enable replicating uploads, LFS objects, and artifacts in Object Storage'
+merge_request: 18482
+author:
+type: added
diff --git a/changelogs/unreleased/nfriend-add-edit-button-to-release-blocks.yml b/changelogs/unreleased/nfriend-add-edit-button-to-release-blocks.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ac0439f9f6372fc45d10ab2e8a69f8f668decd15
--- /dev/null
+++ b/changelogs/unreleased/nfriend-add-edit-button-to-release-blocks.yml
@@ -0,0 +1,5 @@
+---
+title: Add edit button to release blocks on Releases page
+merge_request: 18411
+author:
+type: added
diff --git a/changelogs/unreleased/nfriend-add-edit-release-page.yml b/changelogs/unreleased/nfriend-add-edit-release-page.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5369ab6b19c979a0fe4a47ea12fe3f01b1cd9429
--- /dev/null
+++ b/changelogs/unreleased/nfriend-add-edit-release-page.yml
@@ -0,0 +1,5 @@
+---
+title: Add "Edit Release" page
+merge_request: 18033
+author:
+type: added
diff --git a/changelogs/unreleased/nfriend-fix-lin.yml b/changelogs/unreleased/nfriend-fix-lin.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0b16eb9c1f424c7b48bfef94f160a9439327f2e4
--- /dev/null
+++ b/changelogs/unreleased/nfriend-fix-lin.yml
@@ -0,0 +1,5 @@
+---
+title: Fix button link foreground color
+merge_request: 18669
+author:
+type: fixed
diff --git a/changelogs/unreleased/pokstad1-gitaly-1-70-0.yml b/changelogs/unreleased/pokstad1-gitaly-1-70-0.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3ced037b74a080766aa1a6bb46d75ecb70e1fe66
--- /dev/null
+++ b/changelogs/unreleased/pokstad1-gitaly-1-70-0.yml
@@ -0,0 +1,5 @@
+---
+title: Bump Gitaly to 1.70.0 and remove cache invalidation feature flag
+merge_request: 18766
+author:
+type: other
diff --git a/changelogs/unreleased/psi-indy-embed-zooms.yml b/changelogs/unreleased/psi-indy-embed-zooms.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a05977664f3b2280a6af15869b9a82e5e1346b11
--- /dev/null
+++ b/changelogs/unreleased/psi-indy-embed-zooms.yml
@@ -0,0 +1,5 @@
+---
+title: Embed metrics time window scroll no longer affects other embeds
+merge_request: 18109
+author:
+type: fixed
diff --git a/changelogs/unreleased/return-error-message-when-performance-bar-group-is-not-found.yml b/changelogs/unreleased/return-error-message-when-performance-bar-group-is-not-found.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1d00597ba7d3fcc62f2953f385b22e102ef8a741
--- /dev/null
+++ b/changelogs/unreleased/return-error-message-when-performance-bar-group-is-not-found.yml
@@ -0,0 +1,5 @@
+---
+title: Show error message when setting an invalid group ID for the performance bar
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-snippet-visibility-api.yml b/changelogs/unreleased/sh-fix-snippet-visibility-api.yml
new file mode 100644
index 0000000000000000000000000000000000000000..837da277179bca4896f3fbc15b96bb92cd666e4e
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-snippet-visibility-api.yml
@@ -0,0 +1,5 @@
+---
+title: Fix inability to set snippet visibility via API
+merge_request: 18612
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-hide-license-breakdown.yml b/changelogs/unreleased/sh-hide-license-breakdown.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f6b8efdc77361614bbfc77d1dae9904ac7653b6d
--- /dev/null
+++ b/changelogs/unreleased/sh-hide-license-breakdown.yml
@@ -0,0 +1,5 @@
+---
+title: Hide license breakdown in /admin if user count is high
+merge_request: 18825
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-move-mr-diff-after-commit.yml b/changelogs/unreleased/sh-move-mr-diff-after-commit.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7eb1edcfe4f31d500ab04f1269ce8fb99729cf5f
--- /dev/null
+++ b/changelogs/unreleased/sh-move-mr-diff-after-commit.yml
@@ -0,0 +1,5 @@
+---
+title: Reduce idle in transaction time when updating a merge request
+merge_request: 18493
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-upgrade-grpc.yml b/changelogs/unreleased/sh-upgrade-grpc.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d0c3034eb9343d7eada1b114f3ad3a3e5c81b4ea
--- /dev/null
+++ b/changelogs/unreleased/sh-upgrade-grpc.yml
@@ -0,0 +1,5 @@
+---
+title: Update gRPC to v1.24.0
+merge_request: 18837
+author:
+type: other
diff --git a/changelogs/unreleased/sh-use-template-project-id.yml b/changelogs/unreleased/sh-use-template-project-id.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7784007f5368f607f31589e03b06a12d6115efb0
--- /dev/null
+++ b/changelogs/unreleased/sh-use-template-project-id.yml
@@ -0,0 +1,5 @@
+---
+title: Fix incorrect selection of custom templates
+merge_request: 17205
+author:
+type: fixed
diff --git a/changelogs/unreleased/sort-severity-then-confidence.yml b/changelogs/unreleased/sort-severity-then-confidence.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3c7ab63d60b49328c66c0afbca9c2c40e70506dc
--- /dev/null
+++ b/changelogs/unreleased/sort-severity-then-confidence.yml
@@ -0,0 +1,5 @@
+---
+title: Sort vulnerabilities by severity then confidence for dashboard and pipeline views
+merge_request: 18675
+author:
+type: changed
diff --git a/changelogs/unreleased/stop-showing-new-issue-button-when-project-is-archived.yml b/changelogs/unreleased/stop-showing-new-issue-button-when-project-is-archived.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f786df415286ebb40e5addd2385c7cb4ef6ea740
--- /dev/null
+++ b/changelogs/unreleased/stop-showing-new-issue-button-when-project-is-archived.yml
@@ -0,0 +1,5 @@
+---
+title: Do not show new issue button on archived projects
+merge_request: 18590
+author:
+type: changed
diff --git a/changelogs/unreleased/tc-link-geo-unrepl-docs.yml b/changelogs/unreleased/tc-link-geo-unrepl-docs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c4cd42fa7e84bed888d965dc902e828c0290645b
--- /dev/null
+++ b/changelogs/unreleased/tc-link-geo-unrepl-docs.yml
@@ -0,0 +1,5 @@
+---
+title: Cross-link unreplicated Geo types to issues
+merge_request: 18443
+author:
+type: changed
diff --git a/changelogs/unreleased/update-gitlab-shell-10-2.yml b/changelogs/unreleased/update-gitlab-shell-10-2.yml
new file mode 100644
index 0000000000000000000000000000000000000000..cc13c18d63312f941fb0c5636be9fc6bc6305c67
--- /dev/null
+++ b/changelogs/unreleased/update-gitlab-shell-10-2.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Shell to v10.2.0
+merge_request: 18735
+author:
+type: other
diff --git a/changelogs/unreleased/use-ansi2json-for-job-logs.yml b/changelogs/unreleased/use-ansi2json-for-job-logs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1fce00e821c2edacf9de638f41c88f121bba183e
--- /dev/null
+++ b/changelogs/unreleased/use-ansi2json-for-job-logs.yml
@@ -0,0 +1,5 @@
+---
+title: Use new Ansi2json job log converter via feature flag
+merge_request: 18134
+author:
+type: added
diff --git a/config/feature_categories.yml b/config/feature_categories.yml
new file mode 100644
index 0000000000000000000000000000000000000000..59752a81f60a6e87fe67e642838d0b4d40c8b288
--- /dev/null
+++ b/config/feature_categories.yml
@@ -0,0 +1,103 @@
+#
+# This file contains a list of all feature categories in GitLab
+# It is generated from the stages file at https://gitlab.com/gitlab-com/www-gitlab-com/raw/master/data/stages.yml.
+# If you would like to update it, please run
+# `./scripts/update-feature-categories` to generate a new copy
+#
+# PLEASE DO NOT EDIT THIS FILE MANUALLY.
+#
+---
+- accessibility_testing
+- account-management
+- agile_portfolio_management
+- analysis
+- audit_management
+- authentication_and_authorization
+- auto_devops
+- backup_restore
+- behavior_analytics
+- chaos_engineering
+- chatops
+- cloud_native_installation
+- cluster_cost_optimization
+- cluster_monitoring
+- code_analytics
+- code_quality
+- code_review
+- collection
+- container_network_security
+- container_registry
+- container_scanning
+- continuous_delivery
+- continuous_integration
+- data_loss_prevention
+- dependency_proxy
+- dependency_scanning
+- design_management
+- devops_score
+- disaster_recovery
+- dynamic_application_security_testing
+- error_tracking
+- feature_flags
+- fuzzing
+- geo_replication
+- gitaly
+- gitter
+- groups
+- helm_chart_registry
+- importers
+- incident_management
+- incremental_rollout
+- infrastructure_as_code
+- integration_testing
+- integrations
+- interactive_application_security_testing
+- internationalization
+- issue_tracking
+- kanban_boards
+- kubernetes_configuration
+- language_specific
+- license_compliance
+- live_coding
+- load_testing
+- logging
+- metrics
+- omnibus_package
+- package_registry
+- pages
+- quality_management
+- release_governance
+- release_orchestration
+- requirements_management
+- review_apps
+- runbooks
+- runner
+- runtime_application_self_protection
+- sdk
+- search
+- secret_detection
+- secrets_management
+- serverless
+- service_desk
+- snippets
+- source_code_management
+- static_application_security_testing
+- status_page
+- storage_security
+- synthetic_monitoring
+- system_testing
+- templates
+- threat_detection
+- time_tracking
+- tracing
+- unit_testing
+- usability_testing
+- users
+- value_stream_management
+- vulnerability_database
+- vulnerability_management
+- web_firewall
+- web_ide
+- web_performance
+- wiki
+- workflow_policies
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index f2acfef80e9de6d9dd6899e97904af2643dddbb3..f6814262b7ae8a5554e1ce65c5fad0fe1b14d78b 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -1024,6 +1024,12 @@ production: &base
     #  enabled: true
     #  address: localhost
     #  port: 8083
+    #  # blackout_seconds:
+    #  #   defines an interval to block healthcheck,
+    #  #   but continue accepting application requests
+    #  #   this allows Load Balancer to notice service
+    #  #   being shutdown and not interrupt any of the clients
+    #  blackout_seconds: 10
 
   ## Prometheus settings
   # Do not modify these settings here. They should be modified in /etc/gitlab/gitlab.rb
diff --git a/config/helpers/is_ee_env.js b/config/helpers/is_ee_env.js
index 801cf6abc819f68e097f57067e67691b145ca7cd..78f0bd655285168a9f99065b9f8ad781336aa587 100644
--- a/config/helpers/is_ee_env.js
+++ b/config/helpers/is_ee_env.js
@@ -3,12 +3,12 @@ const path = require('path');
 
 const ROOT_PATH = path.resolve(__dirname, '../..');
 
-// The `IS_GITLAB_EE` is always `string` or `nil`
+// The `FOSS_ONLY` is always `string` or `nil`
 // Thus the nil or empty string will result
-// in using default value: true
+// in using default value: false
 //
 // The behavior needs to be synchronised with
 // lib/gitlab.rb: Gitlab.ee?
+const isFossOnly = JSON.parse(process.env.FOSS_ONLY || 'false');
 module.exports =
-  fs.existsSync(path.join(ROOT_PATH, 'ee', 'app', 'models', 'license.rb')) &&
-  (!process.env.IS_GITLAB_EE || JSON.parse(process.env.IS_GITLAB_EE));
+  fs.existsSync(path.join(ROOT_PATH, 'ee', 'app', 'models', 'license.rb')) && !isFossOnly;
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index b22391e3e6322d706592d650c918f944a66d5bc9..7ee4a4e3610990b8a7e6138c45988762a7fd153b 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -668,6 +668,7 @@
 Settings.monitoring.web_exporter['enabled'] ||= false
 Settings.monitoring.web_exporter['address'] ||= 'localhost'
 Settings.monitoring.web_exporter['port'] ||= 8083
+Settings.monitoring.web_exporter['blackout_seconds'] ||= 10
 
 #
 # Testing settings
diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb
index c5cd055f4ff8d01522d1d5abb05bc48fab88ea80..974eff1a5285e9cd00b3c80ec844d2af4b832809 100644
--- a/config/initializers/7_prometheus_metrics.rb
+++ b/config/initializers/7_prometheus_metrics.rb
@@ -34,6 +34,12 @@ def prometheus_default_multiproc_dir
   config.on(:startup) do
     # webserver metrics are cleaned up in config.ru: `warmup` block
     Prometheus::CleanupMultiprocDirService.new.execute
+    # In production, sidekiq is run in a multi-process setup where processes might interfere
+    # with each other cleaning up and reinitializing prometheus database files, which is why
+    # we're re-doing the work every time here.
+    # A cleaner solution would be to run the cleanup pre-fork, and the initialization once
+    # after all workers have forked, but I don't know how at this point.
+    ::Prometheus::Client.reinitialize_on_pid_change(force: true)
 
     Gitlab::Metrics::Exporter::SidekiqExporter.instance.start
   end
@@ -64,9 +70,18 @@ def prometheus_default_multiproc_dir
     Gitlab::Metrics::Exporter::WebExporter.instance.start
   end
 
+  Gitlab::Cluster::LifecycleEvents.on_before_phased_restart do
+    # We need to ensure that before we re-exec server
+    # we do stop the exporter
+    Gitlab::Metrics::Exporter::WebExporter.instance.stop
+  end
+
   Gitlab::Cluster::LifecycleEvents.on_before_master_restart do
     # We need to ensure that before we re-exec server
     # we do stop the exporter
+    #
+    # We do it again, for being extra safe,
+    # but it should not be needed
     Gitlab::Metrics::Exporter::WebExporter.instance.stop
   end
 
@@ -75,7 +90,6 @@ def prometheus_default_multiproc_dir
     # but this does not happen for Ruby fork
     #
     # This does stop server, as it is running on master.
-    # However, ensures that we close the TCPSocket.
     Gitlab::Metrics::Exporter::WebExporter.instance.stop
   end
 end
diff --git a/config/initializers/cluster_events_before_phased_restart.rb b/config/initializers/cluster_events_before_phased_restart.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cbb1dd1a53acdb64ddc6cd02c16e4b8da565517d
--- /dev/null
+++ b/config/initializers/cluster_events_before_phased_restart.rb
@@ -0,0 +1,14 @@
+# Technical debt, this should be ideally upstreamed.
+#
+# However, there's currently no way to hook before doing
+# graceful shutdown today.
+#
+# Follow-up the issue: https://gitlab.com/gitlab-org/gitlab/issues/34107
+
+if defined?(::Puma)
+  Puma::Cluster.prepend(::Gitlab::Cluster::Mixins::PumaCluster)
+end
+
+if defined?(::Unicorn::HttpServer)
+  Unicorn::HttpServer.prepend(::Gitlab::Cluster::Mixins::UnicornHttpServer)
+end
diff --git a/config/initializers/fog_core_patch.rb b/config/initializers/fog_core_patch.rb
index d3d02216d459b8e947079da65a130937e8d3044a..053e0460a197499331e05b28d348d81795bf1b75 100644
--- a/config/initializers/fog_core_patch.rb
+++ b/config/initializers/fog_core_patch.rb
@@ -34,6 +34,7 @@ module ServicesMixin
     # Gems that have not yet updated with the new fog-core namespace
     LEGACY_FOG_PROVIDERS = %w(google rackspace aliyun).freeze
 
+    # rubocop:disable Gitlab/ConstGetInheritFalse
     def service_provider_constant(service_name, provider_name)
       args = service_provider_search_args(service_name, provider_name)
       Fog.const_get(args.first).const_get(*const_get_args(args.second))
@@ -48,5 +49,6 @@ def service_provider_search_args(service_name, provider_name)
         [provider_name, service_name]
       end
     end
+    # rubocop:enable Gitlab/ConstGetInheritFalse
   end
 end
diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb
index 346725e4080e39dc4da0c415e5a2ff6627ea6eda..d5d4c589884491a7c651eaa69e1d547bd9ed7b18 100644
--- a/config/initializers/lograge.rb
+++ b/config/initializers/lograge.rb
@@ -32,6 +32,10 @@
       payload[:response] = event.payload[:response] if event.payload[:response]
       payload[Labkit::Correlation::CorrelationId::LOG_KEY] = Labkit::Correlation::CorrelationId.current_id
 
+      if cpu_s = Gitlab::Metrics::System.thread_cpu_duration(::Gitlab::RequestContext.start_thread_cpu_time)
+        payload[:cpu_s] = cpu_s
+      end
+
       payload
     end
   end
diff --git a/config/initializers/zz_metrics.rb b/config/initializers/zz_metrics.rb
index 501ec8ccc065fa0243f39a0ba8fa9c770f094876..bc28780cc775588127d412e29da005c389a4d11f 100644
--- a/config/initializers/zz_metrics.rb
+++ b/config/initializers/zz_metrics.rb
@@ -13,7 +13,7 @@ def instrument_classes(instrumentation)
   instrumentation.instrument_methods(Gitlab::Git)
 
   Gitlab::Git.constants.each do |name|
-    const = Gitlab::Git.const_get(name)
+    const = Gitlab::Git.const_get(name, false)
 
     next unless const.is_a?(Module)
 
@@ -75,7 +75,7 @@ def instrument_classes(instrumentation)
   instrumentation.instrument_instance_methods(Rouge::Formatters::HTMLGitlab)
 
   [:XML, :HTML].each do |namespace|
-    namespace_mod = Nokogiri.const_get(namespace)
+    namespace_mod = Nokogiri.const_get(namespace, false)
 
     instrumentation.instrument_methods(namespace_mod)
     instrumentation.instrument_methods(namespace_mod::Document)
diff --git a/config/routes.rb b/config/routes.rb
index f3dd43f78a9ca2ad633929e53612ccd22cef45c1..5bfae777f1768d3adf9f102205eb049515501915 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -55,6 +55,10 @@
     get '/autocomplete/project_groups' => 'autocomplete#project_groups'
   end
 
+  # Sign up
+  get 'users/sign_up/welcome' => 'registrations#welcome'
+  patch 'users/sign_up/update_role' => 'registrations#update_role'
+
   # Search
   get 'search' => 'search#show'
   get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 1baac9874a20c4d21463ab085cd02f4cbd970884..093cde64c851d064c6a90968cbaa07c533b22d71 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -77,6 +77,8 @@
         post :pause
       end
     end
+
+    resources :container_registries, only: [:index], controller: 'registry/repositories'
   end
 
   scope(path: '*id',
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 7d51cfd6dee26fabd56a620593d13bbfae77913c..056289a72db7e9179f0b2b3b69005218a3811906 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -274,6 +274,7 @@
           get :discussions, format: :json
           post :rebase
           get :test_reports
+          get :exposed_artifacts
 
           scope constraints: { format: nil }, action: :show do
             get :commits, defaults: { tab: 'commits' }
diff --git a/config/settings.rb b/config/settings.rb
index 8756c120645afea33429498fa21a999868bd50b6..767c6c5633734143ca489dc6bef61284c3cc96b1 100644
--- a/config/settings.rb
+++ b/config/settings.rb
@@ -104,10 +104,10 @@ def verify_constant_array(modul, current, default)
 
     # check that `current` (string or integer) is a contant in `modul`.
     def verify_constant(modul, current, default)
-      constant = modul.constants.find { |name| modul.const_get(name) == current }
-      value = constant.nil? ? default : modul.const_get(constant)
+      constant = modul.constants.find { |name| modul.const_get(name, false) == current }
+      value = constant.nil? ? default : modul.const_get(constant, false)
       if current.is_a? String
-        value = modul.const_get(current.upcase) rescue default
+        value = modul.const_get(current.upcase, false) rescue default
       end
 
       value
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 34a8bba498f6fcbe4858dc57dc6981b156c8de4b..b97e8ad67c91926c847f20a46a0b06b44247b32d 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -24,6 +24,7 @@
   - [process_commit, 3]
   - [new_note, 2]
   - [new_issue, 2]
+  - [notifications, 2]
   - [new_merge_request, 2]
   - [pipeline_processing, 5]
   - [pipeline_creation, 4]
@@ -96,6 +97,7 @@
   - [phabricator_import_import_tasks, 1]
   - [update_namespace_statistics, 1]
   - [chaos, 2]
+  - [create_evidence, 2]
 
   # EE-specific queues
   - [ldap_group_sync, 2]
diff --git a/config/webpack.config.js b/config/webpack.config.js
index b5656040da2b55e94edbee0ae3ac8af66f6dec80..25fb6cc5f5ab42c8977efd32c11c42aa067442d2 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -380,7 +380,7 @@ module.exports = {
 
     new webpack.DefinePlugin({
       // This one is used to define window.gon.ee and other things properly in tests:
-      'process.env.IS_GITLAB_EE': JSON.stringify(IS_EE),
+      'process.env.IS_EE': JSON.stringify(IS_EE),
       // This one is used to check against "EE" properly in application code
       IS_EE: IS_EE ? 'window.gon && window.gon.ee' : JSON.stringify(false),
     }),
diff --git a/db/migrate/20190821040941_create_cluster_providers_aws.rb b/db/migrate/20190821040941_create_cluster_providers_aws.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f80559861c4ae392c5f85f59f55d76c8a329b2dc
--- /dev/null
+++ b/db/migrate/20190821040941_create_cluster_providers_aws.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class CreateClusterProvidersAws < ActiveRecord::Migration[5.2]
+  DOWNTIME = false
+
+  def change
+    create_table :cluster_providers_aws do |t|
+      t.references :cluster, null: false, type: :bigint, index: { unique: true }, foreign_key: { on_delete: :cascade }
+      t.references :created_by_user, type: :integer, foreign_key: { on_delete: :nullify, to_table: :users }
+
+      t.integer :num_nodes, null: false
+      t.integer :status, null: false
+
+      t.timestamps_with_timezone null: false
+
+      t.string :key_name, null: false, limit: 255
+      t.string :role_arn, null: false, limit: 2048
+      t.string :region, null: false, limit: 255
+      t.string :vpc_id, null: false, limit: 255
+      t.string :subnet_ids, null: false, array: true, default: [], limit: 255
+      t.string :security_group_id, null: false, limit: 255
+      t.string :instance_type, null: false, limit: 255
+
+      t.string :access_key_id, limit: 255
+      t.string :encrypted_secret_access_key_iv, limit: 255
+      t.text :encrypted_secret_access_key
+      t.text :session_token
+      t.text :status_reason
+
+      t.index [:cluster_id, :status]
+    end
+  end
+end
diff --git a/db/migrate/20190912223232_add_role_to_users.rb b/db/migrate/20190912223232_add_role_to_users.rb
new file mode 100644
index 0000000000000000000000000000000000000000..afbd78ed509782591880c8e6ddd15a5aa6e99c8d
--- /dev/null
+++ b/db/migrate/20190912223232_add_role_to_users.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddRoleToUsers < ActiveRecord::Migration[5.2]
+  DOWNTIME = false
+
+  def change
+    add_column :users, :role, :smallint
+  end
+end
diff --git a/db/migrate/20190919091300_create_evidences.rb b/db/migrate/20190919091300_create_evidences.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3f861ed26bd67ba9b7fd006d05953dd54058116a
--- /dev/null
+++ b/db/migrate/20190919091300_create_evidences.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class CreateEvidences < ActiveRecord::Migration[5.2]
+  DOWNTIME = false
+
+  def change
+    create_table :evidences do |t|
+      t.references :release, foreign_key: { on_delete: :cascade }, null: false
+      t.timestamps_with_timezone
+      t.binary :summary_sha
+      t.jsonb :summary, null: false, default: {}
+    end
+  end
+end
diff --git a/db/migrate/20190925055714_default_request_access_groups.rb b/db/migrate/20190925055714_default_request_access_groups.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ba3efbe56f43705e7e0bde9459f2dcc6db784855
--- /dev/null
+++ b/db/migrate/20190925055714_default_request_access_groups.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class DefaultRequestAccessGroups < ActiveRecord::Migration[5.2]
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def up
+    change_column_default :namespaces, :request_access_enabled, true
+  end
+
+  def down
+    change_column_default :namespaces, :request_access_enabled, false
+  end
+end
diff --git a/db/migrate/20190925055902_default_request_access_projects.rb b/db/migrate/20190925055902_default_request_access_projects.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3ad88d0963def0c6d052cc458fa62017de0cb57f
--- /dev/null
+++ b/db/migrate/20190925055902_default_request_access_projects.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class DefaultRequestAccessProjects < ActiveRecord::Migration[5.2]
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def up
+    change_column_default :projects, :request_access_enabled, true
+  end
+
+  def down
+    change_column_default :projects, :request_access_enabled, false
+  end
+end
diff --git a/db/migrate/20190927055500_create_description_versions.rb b/db/migrate/20190927055500_create_description_versions.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6ad34d4a89e9a5b5b5a50fd4fdbd378736b71870
--- /dev/null
+++ b/db/migrate/20190927055500_create_description_versions.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class CreateDescriptionVersions < ActiveRecord::Migration[5.2]
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def up
+    create_table :description_versions do |t|
+      t.timestamps_with_timezone
+      t.references :issue, index: false, foreign_key: { on_delete: :cascade }, type: :integer
+      t.references :merge_request, index: false, foreign_key: { on_delete: :cascade }, type: :integer
+      t.references :epic, index: false, foreign_key: { on_delete: :cascade }, type: :integer
+      t.text :description
+    end
+
+    add_index :description_versions, :issue_id, where: 'issue_id IS NOT NULL'
+    add_index :description_versions, :merge_request_id, where: 'merge_request_id IS NOT NULL'
+    add_index :description_versions, :epic_id, where: 'epic_id IS NOT NULL'
+
+    add_column :system_note_metadata, :description_version_id, :bigint
+  end
+
+  def down
+    remove_column :system_note_metadata, :description_version_id
+
+    drop_table :description_versions
+  end
+end
diff --git a/db/migrate/20190927055540_add_index_to_sytem_note_metadata_description_version_id.rb b/db/migrate/20190927055540_add_index_to_sytem_note_metadata_description_version_id.rb
new file mode 100644
index 0000000000000000000000000000000000000000..695ba955043f6b0200a18adbf1b55683d7294866
--- /dev/null
+++ b/db/migrate/20190927055540_add_index_to_sytem_note_metadata_description_version_id.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddIndexToSytemNoteMetadataDescriptionVersionId < ActiveRecord::Migration[5.2]
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_index :system_note_metadata, :description_version_id, unique: true, where: 'description_version_id IS NOT NULL'
+    add_concurrent_foreign_key :system_note_metadata, :description_versions, column: :description_version_id, on_delete: :nullify
+  end
+
+  def down
+    remove_foreign_key :system_note_metadata, column: :description_version_id
+    remove_concurrent_index :system_note_metadata, :description_version_id
+  end
+end
diff --git a/db/migrate/20190930082942_add_new_release_to_notification_settings.rb b/db/migrate/20190930082942_add_new_release_to_notification_settings.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2ec5815f542fcd60eee0dc804e66e4bd49efb4d7
--- /dev/null
+++ b/db/migrate/20190930082942_add_new_release_to_notification_settings.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddNewReleaseToNotificationSettings < ActiveRecord::Migration[5.2]
+  DOWNTIME = false
+
+  def change
+    add_column :notification_settings, :new_release, :boolean
+  end
+end
diff --git a/db/migrate/20191003015155_add_self_managed_prometheus_alerts.rb b/db/migrate/20191003015155_add_self_managed_prometheus_alerts.rb
new file mode 100644
index 0000000000000000000000000000000000000000..94d16e921dfa1f5148a4e41fbd93845d8b99d8b7
--- /dev/null
+++ b/db/migrate/20191003015155_add_self_managed_prometheus_alerts.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class AddSelfManagedPrometheusAlerts < ActiveRecord::Migration[5.2]
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = false
+
+  def change
+    create_table :self_managed_prometheus_alert_events do |t|
+      t.references :project, index: false, foreign_key: { on_delete: :cascade }, null: false
+      t.references :environment, index: true, foreign_key: { on_delete: :cascade }
+      t.datetime_with_timezone :started_at, null: false
+      t.datetime_with_timezone :ended_at
+
+      t.integer :status, null: false, limit: 2
+      t.string :title, null: false, limit: 255
+      t.string :query_expression, limit: 255
+      t.string :payload_key, null: false, limit: 255
+      t.index [:project_id, :payload_key], unique: true, name: 'idx_project_id_payload_key_self_managed_prometheus_alert_events'
+    end
+  end
+end
diff --git a/db/migrate/20191003060227_add_push_event_hooks_limit_to_application_settings.rb b/db/migrate/20191003060227_add_push_event_hooks_limit_to_application_settings.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f107181bbde4e145c5f32550578c64ee987c2a8c
--- /dev/null
+++ b/db/migrate/20191003060227_add_push_event_hooks_limit_to_application_settings.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddPushEventHooksLimitToApplicationSettings < ActiveRecord::Migration[5.2]
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    add_column_with_default(:application_settings, :push_event_hooks_limit, :integer, default: 3)
+  end
+
+  def down
+    remove_column(:application_settings, :push_event_hooks_limit)
+  end
+end
diff --git a/db/migrate/20191003064615_create_aws_roles.rb b/db/migrate/20191003064615_create_aws_roles.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ee35953f558d893ea47d659d91b690f1156d0314
--- /dev/null
+++ b/db/migrate/20191003064615_create_aws_roles.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CreateAwsRoles < ActiveRecord::Migration[5.2]
+  DOWNTIME = false
+
+  def change
+    create_table :aws_roles, id: false do |t|
+      t.references :user, primary_key: true, default: nil, type: :integer, index: { unique: true }, foreign_key: { on_delete: :cascade }
+
+      t.timestamps_with_timezone null: false
+
+      t.string :role_arn, null: false, limit: 2048
+      t.string :role_external_id, null: false, limit: 64
+
+      t.index :role_external_id, unique: true
+    end
+  end
+end
diff --git a/db/migrate/20191008013056_add_push_event_activities_limit_to_application_settings.rb b/db/migrate/20191008013056_add_push_event_activities_limit_to_application_settings.rb
new file mode 100644
index 0000000000000000000000000000000000000000..84befc95d0064e9c5436c65a5ef4636559c36dd5
--- /dev/null
+++ b/db/migrate/20191008013056_add_push_event_activities_limit_to_application_settings.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddPushEventActivitiesLimitToApplicationSettings < ActiveRecord::Migration[5.2]
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    add_column_with_default(:application_settings, :push_event_activities_limit, :integer, default: 3)
+  end
+
+  def down
+    remove_column(:application_settings, :push_event_activities_limit)
+  end
+end
diff --git a/db/migrate/20191008142331_add_ref_count_to_push_event_payloads.rb b/db/migrate/20191008142331_add_ref_count_to_push_event_payloads.rb
new file mode 100644
index 0000000000000000000000000000000000000000..72621971dbbfeabc10c1acaaa860bc22b1626a44
--- /dev/null
+++ b/db/migrate/20191008142331_add_ref_count_to_push_event_payloads.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddRefCountToPushEventPayloads < ActiveRecord::Migration[5.2]
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def change
+    add_column :push_event_payloads, :ref_count, :integer
+  end
+end
diff --git a/db/migrate/20191008180203_add_issuable_state_id_indexes.rb b/db/migrate/20191008180203_add_issuable_state_id_indexes.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a9a8b8b635912fbb11530ed6d4a2b2effb8efc0d
--- /dev/null
+++ b/db/migrate/20191008180203_add_issuable_state_id_indexes.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+class AddIssuableStateIdIndexes < ActiveRecord::Migration[5.2]
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    # Creates the same indexes that are currently using state:string column
+    # for issues and merge_requests tables
+    create_indexes_for_issues
+    create_indexes_for_merge_requests
+  end
+
+  def down
+    # Removes indexes for issues
+    remove_concurrent_index_by_name :issues, 'idx_issues_on_state_id'
+    remove_concurrent_index_by_name :issues, 'idx_issues_on_project_id_and_created_at_and_id_and_state_id'
+    remove_concurrent_index_by_name :issues, 'idx_issues_on_project_id_and_due_date_and_id_and_state_id'
+    remove_concurrent_index_by_name :issues, 'idx_issues_on_project_id_and_rel_position_and_state_id_and_id'
+    remove_concurrent_index_by_name :issues, 'idx_issues_on_project_id_and_updated_at_and_id_and_state_id'
+
+    # Removes indexes from merge_requests
+    remove_concurrent_index_by_name :merge_requests, 'idx_merge_requests_on_id_and_merge_jid'
+    remove_concurrent_index_by_name :merge_requests, 'idx_merge_requests_on_source_project_and_branch_state_opened'
+    remove_concurrent_index_by_name :merge_requests, 'idx_merge_requests_on_state_id_and_merge_status'
+    remove_concurrent_index_by_name :merge_requests, 'idx_merge_requests_on_target_project_id_and_iid_opened'
+  end
+
+  def create_indexes_for_issues
+    add_concurrent_index :issues, :state_id, name: 'idx_issues_on_state_id'
+
+    add_concurrent_index :issues,
+                         [:project_id, :created_at, :id, :state_id],
+                         name: 'idx_issues_on_project_id_and_created_at_and_id_and_state_id'
+
+    add_concurrent_index :issues,
+                         [:project_id, :due_date, :id, :state_id],
+                         where: 'due_date IS NOT NULL',
+                         name: 'idx_issues_on_project_id_and_due_date_and_id_and_state_id'
+
+    add_concurrent_index :issues,
+                         [:project_id, :relative_position, :state_id, :id],
+                         order: { id: :desc },
+                         name: 'idx_issues_on_project_id_and_rel_position_and_state_id_and_id'
+
+    add_concurrent_index :issues,
+                         [:project_id, :updated_at, :id, :state_id],
+                         name: 'idx_issues_on_project_id_and_updated_at_and_id_and_state_id'
+  end
+
+  def create_indexes_for_merge_requests
+    add_concurrent_index :merge_requests,
+                         [:id, :merge_jid],
+                         where: 'merge_jid IS NOT NULL and state_id = 4',
+                         name: 'idx_merge_requests_on_id_and_merge_jid'
+
+    add_concurrent_index :merge_requests,
+                         [:source_project_id, :source_branch],
+                         where: 'state_id = 1',
+                         name: 'idx_merge_requests_on_source_project_and_branch_state_opened'
+
+    add_concurrent_index :merge_requests,
+                         [:state_id, :merge_status],
+                         where: "state_id = 1 AND merge_status = 'can_be_merged'",
+                         name: 'idx_merge_requests_on_state_id_and_merge_status'
+
+    add_concurrent_index :merge_requests,
+                         [:target_project_id, :iid],
+                         where: 'state_id = 1',
+                         name: 'idx_merge_requests_on_target_project_id_and_iid_opened'
+  end
+end
diff --git a/db/migrate/20191008200204_add_state_id_default_value.rb b/db/migrate/20191008200204_add_state_id_default_value.rb
new file mode 100644
index 0000000000000000000000000000000000000000..15a80163ca837a8ab770b9fdc56d0284feb5926a
--- /dev/null
+++ b/db/migrate/20191008200204_add_state_id_default_value.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddStateIdDefaultValue < ActiveRecord::Migration[5.2]
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def up
+    change_column_default :issues, :state_id, 1
+    change_column_null :issues, :state_id, false
+    change_column_default :merge_requests, :state_id, 1
+    change_column_null :merge_requests, :state_id, false
+  end
+
+  def down
+    change_column_default :issues, :state_id, nil
+    change_column_null :issues, :state_id, true
+    change_column_default :merge_requests, :state_id, nil
+    change_column_null :merge_requests, :state_id, true
+  end
+end
diff --git a/db/migrate/20191009110124_add_has_exposed_artifacts_to_ci_builds_metadata.rb b/db/migrate/20191009110124_add_has_exposed_artifacts_to_ci_builds_metadata.rb
new file mode 100644
index 0000000000000000000000000000000000000000..86c3c540e5e37e2fb2fb7a7bd3bdfec928ae2cfd
--- /dev/null
+++ b/db/migrate/20191009110124_add_has_exposed_artifacts_to_ci_builds_metadata.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddHasExposedArtifactsToCiBuildsMetadata < ActiveRecord::Migration[5.2]
+  DOWNTIME = false
+
+  def up
+    add_column :ci_builds_metadata, :has_exposed_artifacts, :boolean
+  end
+
+  def down
+    remove_column :ci_builds_metadata, :has_exposed_artifacts
+  end
+end
diff --git a/db/migrate/20191009110757_add_index_to_ci_builds_metadata_has_exposed_artifacts.rb b/db/migrate/20191009110757_add_index_to_ci_builds_metadata_has_exposed_artifacts.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6b8c452a62a2040ca059d8bddfb923735c00ef2b
--- /dev/null
+++ b/db/migrate/20191009110757_add_index_to_ci_builds_metadata_has_exposed_artifacts.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToCiBuildsMetadataHasExposedArtifacts < ActiveRecord::Migration[5.2]
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_index :ci_builds_metadata, [:build_id], where: "has_exposed_artifacts IS TRUE", name: 'index_ci_builds_metadata_on_build_id_and_has_exposed_artifacts'
+  end
+
+  def down
+    remove_concurrent_index_by_name :ci_builds_metadata, 'index_ci_builds_metadata_on_build_id_and_has_exposed_artifacts'
+  end
+end
diff --git a/db/migrate/20191009222222_add_custom_http_clone_url_root_to_application_settings.rb b/db/migrate/20191009222222_add_custom_http_clone_url_root_to_application_settings.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0fa8ff449f747499a61b9c255cb69a54784f7968
--- /dev/null
+++ b/db/migrate/20191009222222_add_custom_http_clone_url_root_to_application_settings.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddCustomHttpCloneUrlRootToApplicationSettings < ActiveRecord::Migration[5.2]
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = false
+
+  def change
+    add_column :application_settings, :custom_http_clone_url_root, :string, limit: 511
+  end
+end
diff --git a/db/migrate/20191014084150_add_index_on_snippets_project_id_and_visibility_level.rb b/db/migrate/20191014084150_add_index_on_snippets_project_id_and_visibility_level.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a8f40953e5dcef2d03bff4c89ba5394263324329
--- /dev/null
+++ b/db/migrate/20191014084150_add_index_on_snippets_project_id_and_visibility_level.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexOnSnippetsProjectIdAndVisibilityLevel < ActiveRecord::Migration[5.2]
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_index :snippets, [:project_id, :visibility_level]
+  end
+
+  def down
+    remove_concurrent_index :snippets, [:project_id, :visibility_level]
+  end
+end
diff --git a/db/migrate/20191014132931_remove_index_on_snippets_project_id.rb b/db/migrate/20191014132931_remove_index_on_snippets_project_id.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a1d3ffdb8c8ed1dd3f3fb6db6196c102452dbb2f
--- /dev/null
+++ b/db/migrate/20191014132931_remove_index_on_snippets_project_id.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class RemoveIndexOnSnippetsProjectId < ActiveRecord::Migration[5.2]
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    remove_concurrent_index :snippets, [:project_id]
+  end
+
+  def down
+    add_concurrent_index :snippets, [:project_id]
+  end
+end
diff --git a/db/migrate/20191016072826_replace_ci_trigger_requests_index.rb b/db/migrate/20191016072826_replace_ci_trigger_requests_index.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d7c9524806e8119bb1fb2675277aac166aa2be60
--- /dev/null
+++ b/db/migrate/20191016072826_replace_ci_trigger_requests_index.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class ReplaceCiTriggerRequestsIndex < ActiveRecord::Migration[5.2]
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_index :ci_trigger_requests, [:trigger_id, :id], order: { id: :desc }
+
+    remove_concurrent_index :ci_trigger_requests, [:trigger_id]
+  end
+
+  def down
+    add_concurrent_index :ci_trigger_requests, [:trigger_id]
+
+    remove_concurrent_index :ci_trigger_requests, [:trigger_id, :id], order: { id: :desc }
+  end
+end
diff --git a/db/migrate/20191016220135_add_join_table_for_self_managed_prometheus_alert_issues.rb b/db/migrate/20191016220135_add_join_table_for_self_managed_prometheus_alert_issues.rb
new file mode 100644
index 0000000000000000000000000000000000000000..68b448f88360914ab3c7ad82b0ff274e28b10abf
--- /dev/null
+++ b/db/migrate/20191016220135_add_join_table_for_self_managed_prometheus_alert_issues.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class AddJoinTableForSelfManagedPrometheusAlertIssues < ActiveRecord::Migration[5.2]
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = false
+
+  def change
+    # Join table to Issues
+    create_table :issues_self_managed_prometheus_alert_events, id: false do |t|
+      t.references :issue, null: false,
+        index: false, # Uses the index below
+        foreign_key: { on_delete: :cascade }
+      t.references :self_managed_prometheus_alert_event, null: false,
+        index: { name: 'issue_id_issues_self_managed_rometheus_alert_events_index' },
+        foreign_key: { on_delete: :cascade }
+
+      t.timestamps_with_timezone
+      t.index [:issue_id, :self_managed_prometheus_alert_event_id],
+        unique: true, name: 'issue_id_self_managed_prometheus_alert_event_id_index'
+    end
+  end
+end
diff --git a/db/post_migrate/20190809072552_set_self_monitoring_project_alerting_token.rb b/db/post_migrate/20190809072552_set_self_monitoring_project_alerting_token.rb
index 0c4faebc54827e97b3e924651b17fe1c59a43dd1..d10887fb5d5500787aaba994a75461718bc70449 100644
--- a/db/post_migrate/20190809072552_set_self_monitoring_project_alerting_token.rb
+++ b/db/post_migrate/20190809072552_set_self_monitoring_project_alerting_token.rb
@@ -3,71 +3,17 @@
 class SetSelfMonitoringProjectAlertingToken < ActiveRecord::Migration[5.2]
   DOWNTIME = false
 
-  module Migratable
-    module Alerting
-      class ProjectAlertingSetting < ApplicationRecord
-        self.table_name = 'project_alerting_settings'
-
-        belongs_to :project
-
-        validates :token, presence: true
-
-        attr_encrypted :token,
-          mode: :per_attribute_iv,
-          key: Settings.attr_encrypted_db_key_base_truncated,
-          algorithm: 'aes-256-gcm'
-
-        before_validation :ensure_token
-
-        private
-
-        def ensure_token
-          self.token ||= generate_token
-        end
-
-        def generate_token
-          SecureRandom.hex
-        end
-      end
-    end
-
-    class Project < ApplicationRecord
-      has_one :alerting_setting, inverse_of: :project, class_name: 'Alerting::ProjectAlertingSetting'
-    end
-
-    class ApplicationSetting < ApplicationRecord
-      self.table_name = 'application_settings'
-
-      belongs_to :instance_administration_project, class_name: 'Project'
-
-      def self.current_without_cache
-        last
-      end
-    end
-  end
-
-  def setup_alertmanager_token(project)
-    return unless License.feature_available?(:prometheus_alerts)
-
-    project.create_alerting_setting!
-  end
-
   def up
-    Gitlab.ee do
-      project = Migratable::ApplicationSetting.current_without_cache&.instance_administration_project
+    # no-op
+    # Converted to no-op in https://gitlab.com/gitlab-org/gitlab/merge_requests/17049.
 
-      if project
-        setup_alertmanager_token(project)
-      end
-    end
+    # This migration has been made a no-op because the pre-requisite migration
+    # which creates the self-monitoring project has already been removed in
+    # https://gitlab.com/gitlab-org/gitlab/merge_requests/16864. As
+    # such, this migration would do nothing.
   end
 
   def down
-    Gitlab.ee do
-      Migratable::ApplicationSetting.current_without_cache
-        &.instance_administration_project
-        &.alerting_setting
-        &.destroy!
-    end
+    # no-op
   end
 end
diff --git a/db/post_migrate/20191007163701_populate_remaining_any_approver_rules_for_merge_requests.rb b/db/post_migrate/20191007163701_populate_remaining_any_approver_rules_for_merge_requests.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e1c0f1d6c0cc7a0efe9b3495ca372225831acc61
--- /dev/null
+++ b/db/post_migrate/20191007163701_populate_remaining_any_approver_rules_for_merge_requests.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class PopulateRemainingAnyApproverRulesForMergeRequests < ActiveRecord::Migration[5.2]
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+  BATCH_SIZE = 10_000
+  MIGRATION = 'PopulateAnyApprovalRuleForMergeRequests'
+
+  disable_ddl_transaction!
+
+  class MergeRequest < ActiveRecord::Base
+    include EachBatch
+
+    self.table_name = 'merge_requests'
+
+    scope :with_approvals_before_merge, -> { where.not(approvals_before_merge: 0) }
+  end
+
+  def up
+    return unless Gitlab.ee?
+
+    add_concurrent_index :merge_requests, :id,
+      name: 'tmp_merge_requests_with_approvals_before_merge',
+      where: 'approvals_before_merge != 0'
+
+    Gitlab::BackgroundMigration.steal(MIGRATION)
+
+    PopulateRemainingAnyApproverRulesForMergeRequests::MergeRequest.with_approvals_before_merge.each_batch(of: BATCH_SIZE) do |batch|
+      range = batch.pluck('MIN(id)', 'MAX(id)').first
+
+      Gitlab::BackgroundMigration::PopulateAnyApprovalRuleForMergeRequests.new.perform(*range)
+    end
+
+    remove_concurrent_index_by_name(:merge_requests, 'tmp_merge_requests_with_approvals_before_merge')
+  end
+
+  def down
+    # no-op
+  end
+end
diff --git a/db/post_migrate/20191007163736_populate_remaining_any_approver_rules_for_projects.rb b/db/post_migrate/20191007163736_populate_remaining_any_approver_rules_for_projects.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fce17ffcf16ce075de3c55711a2c4c4673504ecb
--- /dev/null
+++ b/db/post_migrate/20191007163736_populate_remaining_any_approver_rules_for_projects.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class PopulateRemainingAnyApproverRulesForProjects < ActiveRecord::Migration[5.2]
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+  BATCH_SIZE = 5_000
+  MIGRATION = 'PopulateAnyApprovalRuleForProjects'
+
+  disable_ddl_transaction!
+
+  class Project < ActiveRecord::Base
+    include EachBatch
+
+    self.table_name = 'projects'
+
+    scope :with_approvals_before_merge, -> { where.not(approvals_before_merge: 0) }
+  end
+
+  def up
+    return unless Gitlab.ee?
+
+    add_concurrent_index :projects, :id,
+      name: 'tmp_projects_with_approvals_before_merge',
+      where: 'approvals_before_merge != 0'
+
+    Gitlab::BackgroundMigration.steal(MIGRATION)
+
+    PopulateRemainingAnyApproverRulesForProjects::Project.with_approvals_before_merge.each_batch(of: BATCH_SIZE) do |batch|
+      range = batch.pluck('MIN(id)', 'MAX(id)').first
+
+      Gitlab::BackgroundMigration::PopulateAnyApprovalRuleForProjects.new.perform(*range)
+    end
+
+    remove_concurrent_index_by_name(:projects, 'tmp_projects_with_approvals_before_merge')
+  end
+
+  def down
+    # no-op
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index afcffea887708954b186af7e613663440a8294da..109f9e8e038110c9de446e16a60896c767c60ddb 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2019_10_04_133612) do
+ActiveRecord::Schema.define(version: 2019_10_16_220135) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "pg_trgm"
@@ -338,6 +338,9 @@
     t.boolean "throttle_incident_management_notification_enabled", default: false, null: false
     t.integer "throttle_incident_management_notification_period_in_seconds", default: 3600
     t.integer "throttle_incident_management_notification_per_period", default: 3600
+    t.integer "push_event_hooks_limit", default: 3, null: false
+    t.integer "push_event_activities_limit", default: 3, null: false
+    t.string "custom_http_clone_url_root", limit: 511
     t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id"
     t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id"
     t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id"
@@ -466,6 +469,15 @@
     t.index ["user_id", "name"], name: "index_award_emoji_on_user_id_and_name"
   end
 
+  create_table "aws_roles", primary_key: "user_id", id: :integer, default: nil, force: :cascade do |t|
+    t.datetime_with_timezone "created_at", null: false
+    t.datetime_with_timezone "updated_at", null: false
+    t.string "role_arn", limit: 2048, null: false
+    t.string "role_external_id", limit: 64, null: false
+    t.index ["role_external_id"], name: "index_aws_roles_on_role_external_id", unique: true
+    t.index ["user_id"], name: "index_aws_roles_on_user_id", unique: true
+  end
+
   create_table "badges", id: :serial, force: :cascade do |t|
     t.string "link_url", null: false
     t.string "image_url", null: false
@@ -679,7 +691,9 @@
     t.boolean "interruptible"
     t.jsonb "config_options"
     t.jsonb "config_variables"
+    t.boolean "has_exposed_artifacts"
     t.index ["build_id"], name: "index_ci_builds_metadata_on_build_id", unique: true
+    t.index ["build_id"], name: "index_ci_builds_metadata_on_build_id_and_has_exposed_artifacts", where: "(has_exposed_artifacts IS TRUE)"
     t.index ["build_id"], name: "index_ci_builds_metadata_on_build_id_and_interruptible", where: "(interruptible = true)"
     t.index ["project_id"], name: "index_ci_builds_metadata_on_project_id"
   end
@@ -906,7 +920,7 @@
     t.datetime "updated_at"
     t.integer "commit_id"
     t.index ["commit_id"], name: "index_ci_trigger_requests_on_commit_id"
-    t.index ["trigger_id"], name: "index_ci_trigger_requests_on_trigger_id"
+    t.index ["trigger_id", "id"], name: "index_ci_trigger_requests_on_trigger_id_and_id", order: { id: :desc }
   end
 
   create_table "ci_triggers", id: :serial, force: :cascade do |t|
@@ -967,6 +981,30 @@
     t.index ["project_id"], name: "index_cluster_projects_on_project_id"
   end
 
+  create_table "cluster_providers_aws", force: :cascade do |t|
+    t.bigint "cluster_id", null: false
+    t.integer "created_by_user_id"
+    t.integer "num_nodes", null: false
+    t.integer "status", null: false
+    t.datetime_with_timezone "created_at", null: false
+    t.datetime_with_timezone "updated_at", null: false
+    t.string "key_name", limit: 255, null: false
+    t.string "role_arn", limit: 2048, null: false
+    t.string "region", limit: 255, null: false
+    t.string "vpc_id", limit: 255, null: false
+    t.string "subnet_ids", limit: 255, default: [], null: false, array: true
+    t.string "security_group_id", limit: 255, null: false
+    t.string "instance_type", limit: 255, null: false
+    t.string "access_key_id", limit: 255
+    t.string "encrypted_secret_access_key_iv", limit: 255
+    t.text "encrypted_secret_access_key"
+    t.text "session_token"
+    t.text "status_reason"
+    t.index ["cluster_id", "status"], name: "index_cluster_providers_aws_on_cluster_id_and_status"
+    t.index ["cluster_id"], name: "index_cluster_providers_aws_on_cluster_id", unique: true
+    t.index ["created_by_user_id"], name: "index_cluster_providers_aws_on_created_by_user_id"
+  end
+
   create_table "cluster_providers_gcp", id: :serial, force: :cascade do |t|
     t.integer "cluster_id", null: false
     t.integer "status"
@@ -1230,6 +1268,18 @@
     t.index ["project_id", "status"], name: "index_deployments_on_project_id_and_status"
   end
 
+  create_table "description_versions", force: :cascade do |t|
+    t.datetime_with_timezone "created_at", null: false
+    t.datetime_with_timezone "updated_at", null: false
+    t.integer "issue_id"
+    t.integer "merge_request_id"
+    t.integer "epic_id"
+    t.text "description"
+    t.index ["epic_id"], name: "index_description_versions_on_epic_id", where: "(epic_id IS NOT NULL)"
+    t.index ["issue_id"], name: "index_description_versions_on_issue_id", where: "(issue_id IS NOT NULL)"
+    t.index ["merge_request_id"], name: "index_description_versions_on_merge_request_id", where: "(merge_request_id IS NOT NULL)"
+  end
+
   create_table "design_management_designs", force: :cascade do |t|
     t.integer "project_id", null: false
     t.integer "issue_id"
@@ -1389,6 +1439,15 @@
     t.index ["target_type", "target_id"], name: "index_events_on_target_type_and_target_id"
   end
 
+  create_table "evidences", force: :cascade do |t|
+    t.bigint "release_id", null: false
+    t.datetime_with_timezone "created_at", null: false
+    t.datetime_with_timezone "updated_at", null: false
+    t.binary "summary_sha"
+    t.jsonb "summary", default: {}, null: false
+    t.index ["release_id"], name: "index_evidences_on_release_id"
+  end
+
   create_table "external_pull_requests", force: :cascade do |t|
     t.datetime_with_timezone "created_at", null: false
     t.datetime_with_timezone "updated_at", null: false
@@ -1871,7 +1930,7 @@
     t.boolean "discussion_locked"
     t.datetime_with_timezone "closed_at"
     t.integer "closed_by_id"
-    t.integer "state_id", limit: 2
+    t.integer "state_id", limit: 2, default: 1, null: false
     t.integer "duplicated_to_id"
     t.index ["author_id"], name: "index_issues_on_author_id"
     t.index ["closed_by_id"], name: "index_issues_on_closed_by_id"
@@ -1881,12 +1940,17 @@
     t.index ["milestone_id"], name: "index_issues_on_milestone_id"
     t.index ["moved_to_id"], name: "index_issues_on_moved_to_id", where: "(moved_to_id IS NOT NULL)"
     t.index ["project_id", "created_at", "id", "state"], name: "index_issues_on_project_id_and_created_at_and_id_and_state"
+    t.index ["project_id", "created_at", "id", "state_id"], name: "idx_issues_on_project_id_and_created_at_and_id_and_state_id"
     t.index ["project_id", "due_date", "id", "state"], name: "idx_issues_on_project_id_and_due_date_and_id_and_state_partial", where: "(due_date IS NOT NULL)"
+    t.index ["project_id", "due_date", "id", "state_id"], name: "idx_issues_on_project_id_and_due_date_and_id_and_state_id", where: "(due_date IS NOT NULL)"
     t.index ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true
     t.index ["project_id", "relative_position", "state", "id"], name: "index_issues_on_project_id_and_rel_position_and_state_and_id", order: { id: :desc }
+    t.index ["project_id", "relative_position", "state_id", "id"], name: "idx_issues_on_project_id_and_rel_position_and_state_id_and_id", order: { id: :desc }
     t.index ["project_id", "updated_at", "id", "state"], name: "index_issues_on_project_id_and_updated_at_and_id_and_state"
+    t.index ["project_id", "updated_at", "id", "state_id"], name: "idx_issues_on_project_id_and_updated_at_and_id_and_state_id"
     t.index ["relative_position"], name: "index_issues_on_relative_position"
     t.index ["state"], name: "index_issues_on_state"
+    t.index ["state_id"], name: "idx_issues_on_state_id"
     t.index ["title"], name: "index_issues_on_title_trigram", opclass: :gin_trgm_ops, using: :gin
     t.index ["updated_at"], name: "index_issues_on_updated_at"
     t.index ["updated_by_id"], name: "index_issues_on_updated_by_id", where: "(updated_by_id IS NOT NULL)"
@@ -1901,6 +1965,15 @@
     t.index ["prometheus_alert_event_id"], name: "issue_id_issues_prometheus_alert_events_index"
   end
 
+  create_table "issues_self_managed_prometheus_alert_events", id: false, force: :cascade do |t|
+    t.bigint "issue_id", null: false
+    t.bigint "self_managed_prometheus_alert_event_id", null: false
+    t.datetime_with_timezone "created_at", null: false
+    t.datetime_with_timezone "updated_at", null: false
+    t.index ["issue_id", "self_managed_prometheus_alert_event_id"], name: "issue_id_self_managed_prometheus_alert_event_id_index", unique: true
+    t.index ["self_managed_prometheus_alert_event_id"], name: "issue_id_issues_self_managed_rometheus_alert_events_index"
+  end
+
   create_table "jira_connect_installations", force: :cascade do |t|
     t.string "client_key"
     t.string "encrypted_shared_secret"
@@ -2222,23 +2295,27 @@
     t.boolean "discussion_locked"
     t.integer "latest_merge_request_diff_id"
     t.boolean "allow_maintainer_to_push"
-    t.integer "state_id", limit: 2
+    t.integer "state_id", limit: 2, default: 1, null: false
     t.string "rebase_jid"
     t.index ["assignee_id"], name: "index_merge_requests_on_assignee_id"
     t.index ["author_id"], name: "index_merge_requests_on_author_id"
     t.index ["created_at"], name: "index_merge_requests_on_created_at"
     t.index ["description"], name: "index_merge_requests_on_description_trigram", opclass: :gin_trgm_ops, using: :gin
     t.index ["head_pipeline_id"], name: "index_merge_requests_on_head_pipeline_id"
+    t.index ["id", "merge_jid"], name: "idx_merge_requests_on_id_and_merge_jid", where: "((merge_jid IS NOT NULL) AND (state_id = 4))"
     t.index ["id", "merge_jid"], name: "index_merge_requests_on_id_and_merge_jid", where: "((merge_jid IS NOT NULL) AND ((state)::text = 'locked'::text))"
     t.index ["latest_merge_request_diff_id"], name: "index_merge_requests_on_latest_merge_request_diff_id"
     t.index ["merge_user_id"], name: "index_merge_requests_on_merge_user_id", where: "(merge_user_id IS NOT NULL)"
     t.index ["milestone_id"], name: "index_merge_requests_on_milestone_id"
     t.index ["source_branch"], name: "index_merge_requests_on_source_branch"
+    t.index ["source_project_id", "source_branch"], name: "idx_merge_requests_on_source_project_and_branch_state_opened", where: "(state_id = 1)"
     t.index ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_and_branch_state_opened", where: "((state)::text = 'opened'::text)"
     t.index ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch"
     t.index ["state", "merge_status"], name: "index_merge_requests_on_state_and_merge_status", where: "(((state)::text = 'opened'::text) AND ((merge_status)::text = 'can_be_merged'::text))"
+    t.index ["state_id", "merge_status"], name: "idx_merge_requests_on_state_id_and_merge_status", where: "((state_id = 1) AND ((merge_status)::text = 'can_be_merged'::text))"
     t.index ["target_branch"], name: "index_merge_requests_on_target_branch"
     t.index ["target_project_id", "created_at"], name: "index_merge_requests_target_project_id_created_at"
+    t.index ["target_project_id", "iid"], name: "idx_merge_requests_on_target_project_id_and_iid_opened", where: "(state_id = 1)"
     t.index ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true
     t.index ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid_opened", where: "((state)::text = 'opened'::text)"
     t.index ["target_project_id", "merge_commit_sha", "id"], name: "index_merge_requests_on_tp_id_and_merge_commit_sha_and_id"
@@ -2333,7 +2410,7 @@
     t.boolean "membership_lock", default: false
     t.boolean "share_with_group_lock", default: false
     t.integer "visibility_level", default: 20, null: false
-    t.boolean "request_access_enabled", default: false, null: false
+    t.boolean "request_access_enabled", default: true, null: false
     t.string "ldap_sync_status", default: "ready", null: false
     t.string "ldap_sync_error"
     t.datetime "ldap_sync_last_update_at"
@@ -2456,6 +2533,7 @@
     t.boolean "issue_due"
     t.boolean "new_epic"
     t.string "notification_email"
+    t.boolean "new_release"
     t.index ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type"
     t.index ["user_id", "source_id", "source_type"], name: "index_notifications_on_user_id_and_source_id_and_source_type", unique: true
     t.index ["user_id"], name: "index_notification_settings_on_user_id"
@@ -2922,7 +3000,7 @@
     t.boolean "has_external_issue_tracker"
     t.string "repository_storage", default: "default", null: false
     t.boolean "repository_read_only"
-    t.boolean "request_access_enabled", default: false, null: false
+    t.boolean "request_access_enabled", default: true, null: false
     t.boolean "has_external_wiki"
     t.string "ci_config_path"
     t.boolean "lfs_enabled"
@@ -3124,6 +3202,7 @@
     t.binary "commit_to"
     t.text "ref"
     t.string "commit_title", limit: 70
+    t.integer "ref_count"
     t.index ["event_id"], name: "index_push_event_payloads_on_event_id", unique: true
   end
 
@@ -3273,6 +3352,19 @@
     t.index ["group_id", "token_encrypted"], name: "index_scim_oauth_access_tokens_on_group_id_and_token_encrypted", unique: true
   end
 
+  create_table "self_managed_prometheus_alert_events", force: :cascade do |t|
+    t.bigint "project_id", null: false
+    t.bigint "environment_id"
+    t.datetime_with_timezone "started_at", null: false
+    t.datetime_with_timezone "ended_at"
+    t.integer "status", limit: 2, null: false
+    t.string "title", limit: 255, null: false
+    t.string "query_expression", limit: 255
+    t.string "payload_key", limit: 255, null: false
+    t.index ["environment_id"], name: "index_self_managed_prometheus_alert_events_on_environment_id"
+    t.index ["project_id", "payload_key"], name: "idx_project_id_payload_key_self_managed_prometheus_alert_events", unique: true
+  end
+
   create_table "sent_notifications", id: :serial, force: :cascade do |t|
     t.integer "project_id"
     t.integer "noteable_id"
@@ -3359,7 +3451,7 @@
     t.index ["author_id"], name: "index_snippets_on_author_id"
     t.index ["content"], name: "index_snippets_on_content_trigram", opclass: :gin_trgm_ops, using: :gin
     t.index ["file_name"], name: "index_snippets_on_file_name_trigram", opclass: :gin_trgm_ops, using: :gin
-    t.index ["project_id"], name: "index_snippets_on_project_id"
+    t.index ["project_id", "visibility_level"], name: "index_snippets_on_project_id_and_visibility_level"
     t.index ["title"], name: "index_snippets_on_title_trigram", opclass: :gin_trgm_ops, using: :gin
     t.index ["updated_at"], name: "index_snippets_on_updated_at"
     t.index ["visibility_level"], name: "index_snippets_on_visibility_level"
@@ -3425,6 +3517,8 @@
     t.string "action"
     t.datetime "created_at", null: false
     t.datetime "updated_at", null: false
+    t.bigint "description_version_id"
+    t.index ["description_version_id"], name: "index_system_note_metadata_on_description_version_id", unique: true, where: "(description_version_id IS NOT NULL)"
     t.index ["note_id"], name: "index_system_note_metadata_on_note_id", unique: true
   end
 
@@ -3688,6 +3782,7 @@
     t.string "first_name", limit: 255
     t.string "last_name", limit: 255
     t.string "static_object_token", limit: 255
+    t.integer "role", limit: 2
     t.index "lower((name)::text)", name: "index_on_users_name_lower"
     t.index ["accepted_term_id"], name: "index_users_on_accepted_term_id"
     t.index ["admin"], name: "index_users_on_admin"
@@ -3739,24 +3834,24 @@
     t.bigint "author_id", null: false
     t.bigint "updated_by_id"
     t.bigint "last_edited_by_id"
-    t.bigint "start_date_sourcing_milestone_id"
-    t.bigint "due_date_sourcing_milestone_id"
-    t.bigint "closed_by_id"
+    t.date "start_date"
+    t.date "due_date"
     t.datetime_with_timezone "last_edited_at"
     t.datetime_with_timezone "created_at", null: false
     t.datetime_with_timezone "updated_at", null: false
+    t.string "title", limit: 255, null: false
+    t.text "title_html", null: false
+    t.text "description"
+    t.text "description_html"
+    t.bigint "start_date_sourcing_milestone_id"
+    t.bigint "due_date_sourcing_milestone_id"
+    t.bigint "closed_by_id"
     t.datetime_with_timezone "closed_at"
-    t.date "start_date"
-    t.date "due_date"
     t.integer "state", limit: 2, default: 1, null: false
     t.integer "severity", limit: 2, null: false
-    t.integer "confidence", limit: 2, null: false
     t.boolean "severity_overridden", default: false
+    t.integer "confidence", limit: 2, null: false
     t.boolean "confidence_overridden", default: false
-    t.string "title", limit: 255, null: false
-    t.text "title_html", null: false
-    t.text "description"
-    t.text "description_html"
     t.index ["author_id"], name: "index_vulnerabilities_on_author_id"
     t.index ["closed_by_id"], name: "index_vulnerabilities_on_closed_by_id"
     t.index ["due_date_sourcing_milestone_id"], name: "index_vulnerabilities_on_due_date_sourcing_milestone_id"
@@ -3944,6 +4039,7 @@
   add_foreign_key "approval_project_rules_users", "users", on_delete: :cascade
   add_foreign_key "approvals", "merge_requests", name: "fk_310d714958", on_delete: :cascade
   add_foreign_key "approver_groups", "namespaces", column: "group_id", on_delete: :cascade
+  add_foreign_key "aws_roles", "users", on_delete: :cascade
   add_foreign_key "badges", "namespaces", column: "group_id", on_delete: :cascade
   add_foreign_key "badges", "projects", on_delete: :cascade
   add_foreign_key "board_assignees", "boards", on_delete: :cascade
@@ -4007,6 +4103,8 @@
   add_foreign_key "cluster_platforms_kubernetes", "clusters", on_delete: :cascade
   add_foreign_key "cluster_projects", "clusters", on_delete: :cascade
   add_foreign_key "cluster_projects", "projects", on_delete: :cascade
+  add_foreign_key "cluster_providers_aws", "clusters", on_delete: :cascade
+  add_foreign_key "cluster_providers_aws", "users", column: "created_by_user_id", on_delete: :nullify
   add_foreign_key "cluster_providers_gcp", "clusters", on_delete: :cascade
   add_foreign_key "clusters", "projects", column: "management_project_id", name: "fk_f05c5e5a42", on_delete: :nullify
   add_foreign_key "clusters", "users", on_delete: :nullify
@@ -4029,6 +4127,9 @@
   add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade
   add_foreign_key "deployments", "clusters", name: "fk_289bba3222", on_delete: :nullify
   add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade
+  add_foreign_key "description_versions", "epics", on_delete: :cascade
+  add_foreign_key "description_versions", "issues", on_delete: :cascade
+  add_foreign_key "description_versions", "merge_requests", on_delete: :cascade
   add_foreign_key "design_management_designs", "issues", on_delete: :cascade
   add_foreign_key "design_management_designs", "projects", on_delete: :cascade
   add_foreign_key "design_management_designs_versions", "design_management_designs", column: "design_id", name: "fk_03c671965c", on_delete: :cascade
@@ -4052,6 +4153,7 @@
   add_foreign_key "events", "namespaces", column: "group_id", name: "fk_61fbf6ca48", on_delete: :cascade
   add_foreign_key "events", "projects", on_delete: :cascade
   add_foreign_key "events", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade
+  add_foreign_key "evidences", "releases", on_delete: :cascade
   add_foreign_key "external_pull_requests", "projects", on_delete: :cascade
   add_foreign_key "fork_network_members", "fork_networks", on_delete: :cascade
   add_foreign_key "fork_network_members", "projects", column: "forked_from_project_id", name: "fk_b01280dae4", on_delete: :nullify
@@ -4113,6 +4215,8 @@
   add_foreign_key "issues", "users", column: "updated_by_id", name: "fk_ffed080f01", on_delete: :nullify
   add_foreign_key "issues_prometheus_alert_events", "issues", on_delete: :cascade
   add_foreign_key "issues_prometheus_alert_events", "prometheus_alert_events", on_delete: :cascade
+  add_foreign_key "issues_self_managed_prometheus_alert_events", "issues", on_delete: :cascade
+  add_foreign_key "issues_self_managed_prometheus_alert_events", "self_managed_prometheus_alert_events", on_delete: :cascade
   add_foreign_key "jira_connect_subscriptions", "jira_connect_installations", on_delete: :cascade
   add_foreign_key "jira_connect_subscriptions", "namespaces", on_delete: :cascade
   add_foreign_key "jira_tracker_data", "services", on_delete: :cascade
@@ -4252,6 +4356,8 @@
   add_foreign_key "reviews", "users", column: "author_id", on_delete: :nullify
   add_foreign_key "saml_providers", "namespaces", column: "group_id", on_delete: :cascade
   add_foreign_key "scim_oauth_access_tokens", "namespaces", column: "group_id", on_delete: :cascade
+  add_foreign_key "self_managed_prometheus_alert_events", "environments", on_delete: :cascade
+  add_foreign_key "self_managed_prometheus_alert_events", "projects", on_delete: :cascade
   add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade
   add_foreign_key "slack_integrations", "services", on_delete: :cascade
   add_foreign_key "smartcard_identities", "users", on_delete: :cascade
@@ -4260,6 +4366,7 @@
   add_foreign_key "software_license_policies", "software_licenses", on_delete: :cascade
   add_foreign_key "subscriptions", "projects", on_delete: :cascade
   add_foreign_key "suggestions", "notes", on_delete: :cascade
+  add_foreign_key "system_note_metadata", "description_versions", name: "fk_fbd87415c9", on_delete: :nullify
   add_foreign_key "system_note_metadata", "notes", name: "fk_d83a918cb1", on_delete: :cascade
   add_foreign_key "term_agreements", "application_setting_terms", column: "term_id"
   add_foreign_key "term_agreements", "users", on_delete: :cascade
diff --git a/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md b/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md
index d8a1d469726ff080fab00fa4722b0604d93966ef..743893d984ab3ef9b73b11ba7245a8e8085d4635 100644
--- a/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md
+++ b/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md
@@ -8,7 +8,7 @@ Managing a large number of users in GitLab can become a burden for system admini
 
 In this guide we will focus on configuring GitLab with Active Directory. [Active Directory](https://en.wikipedia.org/wiki/Active_Directory) is a popular LDAP compatible directory service provided by Microsoft, included in all modern Windows Server operating systems.
 
-GitLab has supported LDAP integration since [version 2.2](https://about.gitlab.com/2012/02/22/gitlab-version-2-2/). With GitLab LDAP [group syncing](../how_to_configure_ldap_gitlab_ee/index.html#group-sync) being added to GitLab Enterprise Edition in [version 6.0](https://about.gitlab.com/2013/08/20/gitlab-6-dot-0-released/). LDAP integration has become one of the most popular features in GitLab.
+GitLab has supported LDAP integration since [version 2.2](https://about.gitlab.com/blog/2012/02/22/gitlab-version-2-2/). With GitLab LDAP [group syncing](../how_to_configure_ldap_gitlab_ee/index.html#group-sync) being added to GitLab Enterprise Edition in [version 6.0](https://about.gitlab.com/blog/2013/08/20/gitlab-6-dot-0-released/). LDAP integration has become one of the most popular features in GitLab.
 
 ## Getting started
 
diff --git a/doc/administration/geo/disaster_recovery/planned_failover.md b/doc/administration/geo/disaster_recovery/planned_failover.md
index 75e07bcf86393eb217cfcfe4dcced232b5654d86..8fee172ec64116dad0256b6d296b636e04bd32d6 100644
--- a/doc/administration/geo/disaster_recovery/planned_failover.md
+++ b/doc/administration/geo/disaster_recovery/planned_failover.md
@@ -43,23 +43,14 @@ will go smoothly.
 
 ### Object storage
 
-Some classes of non-repository data can use object storage in preference to
-file storage. Geo [does not replicate data in object storage](../replication/object_storage.md),
-leaving that task up to the object store itself. For a planned failover, this
-means you can decouple the replication of this data from the failover of the
-GitLab service.
-
-If you're already using object storage, simply verify that your **secondary**
-node has access to the same data as the **primary** node - they must either they share the
-same object storage configuration, or the **secondary** node should be configured to
-access a [geographically-replicated][os-repl] copy provided by the object store
-itself.
-
 If you have a large GitLab installation or cannot tolerate downtime, consider
 [migrating to Object Storage][os-conf] **before** scheduling a planned failover.
 Doing so reduces both the length of the maintenance window, and the risk of data
 loss as a result of a poorly executed planned failover.
 
+In GitLab 12.4, you can optionally allow GitLab to manage replication of Object Storage for
+**secondary** nodes. For more information, see [Object Storage replication][os-conf].
+
 ### Review the configuration of each **secondary** node
 
 Database settings are automatically replicated to the **secondary**  node, but the
@@ -224,5 +215,4 @@ Don't forget to remove the broadcast message after failover is complete.
 [background-verification]: background_verification.md
 [limitations]: ../replication/index.md#current-limitations
 [moving-repositories]: ../../operations/moving_repositories.md
-[os-conf]: ../replication/object_storage.md#configuration
-[os-repl]: ../replication/object_storage.md#replication
+[os-conf]: ../replication/object_storage.md
diff --git a/doc/administration/geo/replication/faq.md b/doc/administration/geo/replication/faq.md
index 43782b7fc3e97ba4df5283bd1c44045605990804..b07b518d3b158cdaa6e2516313cc243da41b5b99 100644
--- a/doc/administration/geo/replication/faq.md
+++ b/doc/administration/geo/replication/faq.md
@@ -45,7 +45,7 @@ query.
 
 ## Can I `git push` to a **secondary** node?
 
-Yes!  Pushing directly to a **secondary** node (for both HTTP and SSH, including Git LFS) was [introduced](https://about.gitlab.com/2018/09/22/gitlab-11-3-released/) in [GitLab Premium](https://about.gitlab.com/pricing/#self-managed) 11.3.
+Yes!  Pushing directly to a **secondary** node (for both HTTP and SSH, including Git LFS) was [introduced](https://about.gitlab.com/blog/2018/09/22/gitlab-11-3-released/) in [GitLab Premium](https://about.gitlab.com/pricing/#self-managed) 11.3.
 
 ## How long does it take to have a commit replicated to a **secondary** node?
 
diff --git a/doc/administration/geo/replication/index.md b/doc/administration/geo/replication/index.md
index bcad820531cc8bb53ab562f333c92217d0fe10a6..1fef2e85ce6987330e0764641942f671d939f31c 100644
--- a/doc/administration/geo/replication/index.md
+++ b/doc/administration/geo/replication/index.md
@@ -63,7 +63,7 @@ Keep in mind that:
   - Get user data for logins (API).
   - Replicate repositories, LFS Objects, and Attachments (HTTPS + JWT).
 - Since GitLab Premium 10.0, the **primary** node no longer talks to **secondary** nodes to notify for changes (API).
-- Pushing directly to a **secondary** node (for both HTTP and SSH, including Git LFS) was [introduced](https://about.gitlab.com/2018/09/22/gitlab-11-3-released/) in [GitLab Premium](https://about.gitlab.com/pricing/#self-managed) 11.3.
+- Pushing directly to a **secondary** node (for both HTTP and SSH, including Git LFS) was [introduced](https://about.gitlab.com/blog/2018/09/22/gitlab-11-3-released/) in [GitLab Premium](https://about.gitlab.com/pricing/#self-managed) 11.3.
 - There are [limitations](#current-limitations) in the current implementation.
 
 ### Architecture
@@ -255,37 +255,58 @@ This list of limitations only reflects the latest version of GitLab. If you are
 The following table lists the GitLab features along with their replication
 and verification status on a **secondary** node.
 
-You can keep track of the progress to include the missing items in:
-
-- [ee-893](https://gitlab.com/groups/gitlab-org/-/epics/893).
-- [ee-1430](https://gitlab.com/groups/gitlab-org/-/epics/1430).
-
-| Feature | Replicated | Verified |
-|-----------|------------|----------|
-| All database content (e.g. snippets, epics, issues, merge requests, groups, and project metadata)  | Yes | Yes |
-| Project repository | Yes | Yes |
-| Project wiki repository | Yes | Yes |
-| Project designs repository | No | No |
-| Uploads (e.g. attachments to issues, merge requests, epics, and avatars) | Yes | Yes, only on transfer, or manually (1) |
-| LFS Objects | Yes | Yes, only on transfer, or manually (1) |
-| CI job artifacts (other than traces) | Yes | No, only manually (1) |
-| Archived traces | Yes | Yes, only on transfer, or manually (1) |
-| Personal snippets | Yes | Yes |
-| Version-controlled personal snippets ([unsupported](https://gitlab.com/gitlab-org/gitlab-foss/issues/13426)) | No | No |
-| Project snippets | Yes | Yes |
-| Version-controlled project snippets ([unsupported](https://gitlab.com/gitlab-org/gitlab-foss/issues/13426)) | No | No |
-| Object pools for forked project deduplication | No | No |
-| [Server-side Git Hooks](../../custom_hooks.md) | No | No |
-| [Elasticsearch integration](../../../integration/elasticsearch.md) | No | No |
-| [GitLab Pages](../../pages/index.md) | No | No |
-| [Container Registry](../../packages/container_registry.md) | Yes | No |
-| [NPM Registry](../../../user/packages/npm_registry/index.md) | No | No |
-| [Maven Packages](../../../user/packages/maven_repository/index.md) | No | No |
-| [Conan Packages](../../../user/packages/conan_repository/index.md) | No | No |
-| [External merge request diffs](../../merge_request_diffs.md) | No, if they are on-disk | No |
-| Content in object storage ([track progress](https://gitlab.com/groups/gitlab-org/-/epics/1526)) | No | No |
-
-1. The integrity can be verified manually using [Integrity Check Rake Task](../../raketasks/check.md) on both nodes and comparing the output between them.
+You can keep track of the progress to implement the missing items in
+these epics/issues:
+
+- [Unreplicated Data Types](https://gitlab.com/groups/gitlab-org/-/epics/893)
+- [Verify all replicated data](https://gitlab.com/groups/gitlab-org/-/epics/1430)
+
+| Feature                                             | Replicated               | Verified                    | Notes                                      |
+|-----------------------------------------------------|--------------------------|-----------------------------|--------------------------------------------|
+| All database content                                | **Yes**                  | **Yes**                     |                                            |
+| Project repository                                  | **Yes**                  | **Yes**                     |                                            |
+| Project wiki repository                             | **Yes**                  | **Yes**                     |                                            |
+| Project designs repository                          | [No][design-replication] | [No][design-verification]   |                                            |
+| Uploads                                             | **Yes**                  | [No][upload-verification]   | Verified only on transfer, or manually (1) |
+| LFS Objects                                         | **Yes**                  | [No][lfs-verification]      | Verified only on transfer, or manually (1) |
+| CI job artifacts (other than traces)                | **Yes**                  | [No][artifact-verification] | Verified only manually (1)                 |
+| Archived traces                                     | **Yes**                  | [No][artifact-verification] | Verified only on transfer, or manually (1) |
+| Personal snippets                                   | **Yes**                  | **Yes**                     |                                            |
+| Version-controlled personal snippets                | No                       | No                          | [Not yet supported][unsupported-snippets]  |
+| Project snippets                                    | **Yes**                  | **Yes**                     |                                            |
+| Version-controlled project snippets                 | No                       | No                          | [Not yet supported][unsupported-snippets]  |
+| Object pools for forked project deduplication       | **Yes**                  | No                          |                                            |
+| [Server-side Git Hooks][custom-hooks]               | No                       | No                          |                                            |
+| [Elasticsearch integration][elasticsearch]          | No                       | No                          |                                            |
+| [GitLab Pages][gitlab-pages]                        | [No][pages-replication]  | No                          |                                            |
+| [Container Registry][container-registry]            | **Yes**                  | No                          |                                            |
+| [NPM Registry][npm-registry]                        | No                       | No                          |                                            |
+| [Maven Repository][maven-repository]                | No                       | No                          |                                            |
+| [Conan Repository][conan-repository]                | No                       | No                          |                                            |
+| [External merge request diffs][merge-request-diffs] | [No][diffs-replication]  | No                          |                                            |
+| Content in object storage                           | **Yes**                  | No                          |                                            |
+
+[design-replication]: https://gitlab.com/groups/gitlab-org/-/epics/1633
+[design-verification]: https://gitlab.com/gitlab-org/gitlab/issues/32467
+[upload-verification]: https://gitlab.com/groups/gitlab-org/-/epics/1817
+[lfs-verification]: https://gitlab.com/gitlab-org/gitlab/issues/8922
+[artifact-verification]: https://gitlab.com/gitlab-org/gitlab/issues/8923
+[diffs-replication]: https://gitlab.com/gitlab-org/gitlab/issues/33817
+[pages-replication]: https://gitlab.com/groups/gitlab-org/-/epics/589
+
+[unsupported-snippets]: https://gitlab.com/gitlab-org/gitlab/issues/14228
+[custom-hooks]: ../../custom_hooks.md
+[elasticsearch]: ../../../integration/elasticsearch.md
+[gitlab-pages]: ../../pages/index.md
+[container-registry]: ../../packages/container_registry.md
+[npm-registry]: ../../../user/packages/npm_registry/index.md
+[maven-repository]: ../../../user/packages/maven_repository/index.md
+[conan-repository]: ../../../user/packages/conan_repository/index.md
+[merge-request-diffs]: ../../merge_request_diffs.md
+
+1. The integrity can be verified manually using
+[Integrity Check Rake Task](../../raketasks/check.md)
+on both nodes and comparing the output between them.
 
 DANGER: **DANGER**
 Features not on this list, or with **No** in the **Replicated** column,
diff --git a/doc/administration/geo/replication/location_aware_git_url.md b/doc/administration/geo/replication/location_aware_git_url.md
index b8ed3d07bddd93e830a4f06d39edf191d4250004..6183a0ad1194758afd335f09f82bb61563461574 100644
--- a/doc/administration/geo/replication/location_aware_git_url.md
+++ b/doc/administration/geo/replication/location_aware_git_url.md
@@ -95,13 +95,12 @@ on the external URL of the current host. For example:
 
 ![Clone panel](img/single_git_clone_panel.png)
 
-However, you can customize the SSH remote URL to use the location-aware
-`git.example.com`. To do so, change the SSH remote URL's host by setting
-`gitlab_rails['gitlab_ssh_host']` in `gitlab.rb` of web nodes.
+You can customize the:
 
-Unfortunately the means to specify a custom HTTP clone URL is not yet
-implemented. The feature request can be found at
-[Customizable Git HTTP clone root URL](https://gitlab.com/gitlab-org/gitlab/issues/31949).
+- SSH remote URL to use the location-aware `git.example.com`. To do so, change the SSH remote URL's
+  host by setting `gitlab_rails['gitlab_ssh_host']` in `gitlab.rb` of web nodes.
+- HTTP remote URL as shown in
+  [Custom Git clone URL for HTTP(S)](../../../user/admin_area/settings/visibility_and_access_controls.md#custom-git-clone-url-for-https).
 
 ## Example Git request handling behavior
 
diff --git a/doc/administration/geo/replication/object_storage.md b/doc/administration/geo/replication/object_storage.md
index 878b67a8f8ede572b4a8f777befa99830b084a9b..a9087abcbd9507b129bec4f23b8dc9be52794267 100644
--- a/doc/administration/geo/replication/object_storage.md
+++ b/doc/administration/geo/replication/object_storage.md
@@ -1,16 +1,33 @@
 # Geo with Object storage **(PREMIUM ONLY)**
 
-Geo can be used in combination with Object Storage (AWS S3, or
-other compatible object storage).
+Geo can be used in combination with Object Storage (AWS S3, or other compatible object storage).
 
-## Configuration
+Currently, **secondary** nodes can use either:
 
-At this time it is required that if object storage is enabled on the
-**primary** node, it must also be enabled on each **secondary** node.
+- The same storage bucket as the **primary** node.
+- A replicated storage bucket.
 
-**Secondary** nodes can use the same storage bucket as the **primary** node, or
-they can use a replicated storage bucket. At this time GitLab does not
-take care of content replication in object storage.
+To have:
+
+- GitLab manage replication, follow [Enabling GitLab replication](#enabling-gitlab-managed-object-storage-replication).
+- Third-party services manage replication, follow [Third-party replication services](#third-party-replication-services).
+
+## Enabling GitLab managed object storage replication
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/10586) in GitLab 12.4.
+
+CAUTION: **Caution:**
+This is a [**beta** feature](https://about.gitlab.com/handbook/product/#beta) and is not ready yet for production use at any scale.
+
+**Secondary** nodes can replicate files stored on the **primary** node regardless of
+whether they are stored on the local filesystem or in object storage.
+
+To enable GitLab replication, you must:
+
+1. Go to **Admin Area > Geo**.
+1. Press **Edit** on the **secondary** node.
+1. Enable the **Allow this secondary node to replicate content on Object Storage**
+   checkbox.
 
 For LFS, follow the documentation to
 [set up LFS object storage](../../../workflow/lfs/lfs_administration.md#storing-lfs-objects-in-remote-object-storage).
@@ -20,12 +37,21 @@ For CI job artifacts, there is similar documentation to configure
 
 For user uploads, there is similar documentation to configure [upload object storage](../../uploads.md#using-object-storage-core-only)
 
-You should enable and configure object storage on both **primary** and **secondary**
-nodes. Migrating existing data to object storage should be performed on the
-**primary** node only. **Secondary** nodes will automatically notice that the migrated
-files are now in object storage.
+If you want to migrate the **primary** node's files to object storage, you can
+configure the **secondary** in a few ways:
+
+- Use the exact same object storage.
+- Use a separate object store but leverage your object storage solution's built-in
+  replication.
+- Use a separate object store and enable the **Allow this secondary node to replicate
+  content on Object Storage** setting.
+
+GitLab does not currently support the case where both:
+
+- The **primary** node uses local storage.
+- A **secondary** node uses object storage.
 
-## Replication
+## Third-party replication services
 
 When using Amazon S3, you can use
 [CRR](https://docs.aws.amazon.com/AmazonS3/latest/dev/crr.html) to
diff --git a/doc/administration/geo/replication/using_a_geo_server.md b/doc/administration/geo/replication/using_a_geo_server.md
index fd61e3258e9da3e44b3f535ccfc1b679a5b65f0b..55c7e78da9299299ba8236922298e1b1a2db0a41 100644
--- a/doc/administration/geo/replication/using_a_geo_server.md
+++ b/doc/administration/geo/replication/using_a_geo_server.md
@@ -4,7 +4,7 @@
 
 After you set up the [database replication and configure the Geo nodes][req], use your closest GitLab node as you would a normal standalone GitLab instance.
 
-Pushing directly to a **secondary** node (for both HTTP, SSH including Git LFS) was [introduced](https://about.gitlab.com/2018/09/22/gitlab-11-3-released/) in [GitLab Premium](https://about.gitlab.com/pricing/#self-managed) 11.3.
+Pushing directly to a **secondary** node (for both HTTP, SSH including Git LFS) was [introduced](https://about.gitlab.com/blog/2018/09/22/gitlab-11-3-released/) in [GitLab Premium](https://about.gitlab.com/pricing/#self-managed) 11.3.
 
 Example of the output you will see when pushing to a **secondary** node:
 
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index 5dcdf0e85e9e3bebd09f4ed8162e6889bb7a69d1..5037e5034c85b2c0b2c1d0f674aed7d8a6e29e0c 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -86,7 +86,8 @@ Below we describe how to configure two Gitaly servers one at
 `gitaly1.internal` and the other at `gitaly2.internal`
 with secret token `abc123secret`. We assume
 your GitLab installation has three repository storages: `default`,
-`storage1` and `storage2`.
+`storage1` and `storage2`. You can use as little as just one server with one
+repository storage if desired.
 
 ### 1. Installation
 
@@ -129,7 +130,7 @@ Configure a token on the instance that runs the GitLab Rails application.
 Next, on the Gitaly servers, you need to configure storage paths, enable
 the network listener and configure the token.
 
-NOTE: **Note:** if you want to reduce the risk of downtime when you enable
+NOTE: **Note:** If you want to reduce the risk of downtime when you enable
 authentication you can temporarily disable enforcement, see [the
 documentation on configuring Gitaly
 authentication](https://gitlab.com/gitlab-org/gitaly/blob/master/doc/configuration/README.md#authentication)
@@ -177,20 +178,19 @@ Check the directory layout on your Gitaly server to be sure.
    # Don't forget to copy `/etc/gitlab/gitlab-secrets.json` from web server to Gitaly server.
    gitlab_rails['internal_api_url'] = 'https://gitlab.example.com'
 
+   # Authentication token to ensure only authorized servers can communicate with
+   # Gitaly server
+   gitaly['auth_token'] = 'abc123secret'
+
    # Make Gitaly accept connections on all network interfaces. You must use
    # firewalls to restrict access to this address/port.
+   # Comment out following line if you only want to support TLS connections
    gitaly['listen_addr'] = "0.0.0.0:8075"
-   gitaly['auth_token'] = 'abc123secret'
-
-   # To use TLS for Gitaly you need to add
-   gitaly['tls_listen_addr'] = "0.0.0.0:9999"
-   gitaly['certificate_path'] = "path/to/cert.pem"
-   gitaly['key_path'] = "path/to/key.pem"
    ```
 
 1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server:
 
-   For `gitaly1.internal`:
+   On `gitaly1.internal`:
 
    ```
    gitaly['storage'] = [
@@ -199,7 +199,7 @@ Check the directory layout on your Gitaly server to be sure.
    ]
    ```
 
-   For `gitaly2.internal`:
+   On `gitaly2.internal`:
 
    ```
    gitaly['storage'] = [
@@ -219,19 +219,19 @@ Check the directory layout on your Gitaly server to be sure.
 
    ```toml
    listen_addr = '0.0.0.0:8075'
-   tls_listen_addr = '0.0.0.0:9999'
-
-   [tls]
-   certificate_path = /path/to/cert.pem
-   key_path = /path/to/key.pem
 
    [auth]
    token = 'abc123secret'
+
+   [logging]
+   format = 'json'
+   level = 'info'
+   dir = '/var/log/gitaly'
    ```
 
 1. Append the following to `/home/git/gitaly/config.toml` for each respective server:
 
-   For `gitaly1.internal`:
+   On `gitaly1.internal`:
 
    ```toml
    [[storage]]
@@ -241,7 +241,7 @@ Check the directory layout on your Gitaly server to be sure.
    name = 'storage1'
    ```
 
-   For `gitaly2.internal`:
+   On `gitaly2.internal`:
 
    ```toml
    [[storage]]
@@ -369,11 +369,12 @@ To disable Gitaly on a client node:
 > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/22602) in GitLab 11.8.
 
 Gitaly supports TLS encryption. To be able to communicate
-with a Gitaly instance that listens for secure connections you will need to use `tls://` url
+with a Gitaly instance that listens for secure connections you will need to use `tls://` URL
 scheme in the `gitaly_address` of the corresponding storage entry in the GitLab configuration.
 
 You will need to bring your own certificates as this isn't provided automatically.
-The certificate to be used needs to be installed on all Gitaly nodes and on all
+The certificate to be used needs to be installed on all Gitaly nodes, and the
+certificate (or CA of certificate) on all
 client nodes that communicate with it following the procedure described in
 [GitLab custom certificate configuration](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates).
 
@@ -395,7 +396,7 @@ To configure Gitaly with TLS:
 
 **For Omnibus GitLab**
 
-1. On the client nodes, edit `/etc/gitlab/gitlab.rb`:
+1. On the client node(s), edit `/etc/gitlab/gitlab.rb` as follows:
 
    ```ruby
    git_data_dirs({
@@ -407,20 +408,38 @@ To configure Gitaly with TLS:
    gitlab_rails['gitaly_token'] = 'abc123secret'
    ```
 
-1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
-1. On the Gitaly server nodes, edit `/etc/gitlab/gitlab.rb`:
+1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) on client node(s).
+1. Create the `/etc/gitlab/ssl` directory and copy your key and certificate there:
+
+   ```sh
+   sudo mkdir -p /etc/gitlab/ssl
+   sudo chmod 700 /etc/gitlab/ssl
+   sudo cp key.pem cert.pem /etc/gitlab/ssl/
+   ```
+
+1. On the Gitaly server node(s), edit `/etc/gitlab/gitlab.rb` and add:
+
+   <!--
+   updates to following example must also be made at
+   https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/advanced/external-gitaly/external-omnibus-gitaly.md#configure-omnibus-gitlab
+   -->
 
    ```ruby
    gitaly['tls_listen_addr'] = "0.0.0.0:9999"
-   gitaly['certificate_path'] = "path/to/cert.pem"
-   gitaly['key_path'] = "path/to/key.pem"
+   gitaly['certificate_path'] = "/etc/gitlab/ssl/cert.pem"
+   gitaly['key_path'] = "/etc/gitlab/ssl/key.pem"
    ```
 
-1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
+1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) on Gitaly server node(s).
+1. (Optional) After [verifying that all Gitaly traffic is being served over TLS](#observe-type-of-gitaly-connections),
+   you can improve security by disabling non-TLS connections by commenting out
+   or deleting `gitaly['listen_addr']` in `/etc/gitlab/gitlab.rb`, saving the file,
+   and [reconfiguring GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure)
+   on Gitaly server node(s).
 
 **For installations from source**
 
-1. On the client nodes, edit `/home/git/gitlab/config/gitlab.yml`:
+1. On the client node(s), edit `/home/git/gitlab/config/gitlab.yml` as follows:
 
    ```yaml
    gitlab:
@@ -445,18 +464,33 @@ To configure Gitaly with TLS:
    data will be stored in this folder. This will no longer be necessary after
    [this issue](https://gitlab.com/gitlab-org/gitaly/issues/1282) is resolved.
 
-1. Save the file and [restart GitLab](../restart_gitlab.md#installations-from-source).
-1. On the Gitaly server nodes, edit `/home/git/gitaly/config.toml`:
+1. Save the file and [restart GitLab](../restart_gitlab.md#installations-from-source) on client node(s).
+1. Create the `/etc/gitlab/ssl` directory and copy your key and certificate there:
+
+   ```sh
+   sudo mkdir -p /etc/gitlab/ssl
+   sudo chmod 700 /etc/gitlab/ssl
+   sudo cp key.pem cert.pem /etc/gitlab/ssl/
+   ```
+
+1. On the Gitaly server node(s), edit `/home/git/gitaly/config.toml` and add:
 
    ```toml
    tls_listen_addr = '0.0.0.0:9999'
 
    [tls]
-   certificate_path = '/path/to/cert.pem'
-   key_path = '/path/to/key.pem'
+   certificate_path = '/etc/gitlab/ssl/cert.pem'
+   key_path = '/etc/gitlab/ssl/key.pem'
    ```
 
-1. Save the file and [restart GitLab](../restart_gitlab.md#installations-from-source).
+1. Save the file and [restart GitLab](../restart_gitlab.md#installations-from-source) on Gitaly server node(s).
+1. (Optional) After [verifying that all Gitaly traffic is being served over TLS](#observe-type-of-gitaly-connections),
+   you can improve security by disabling non-TLS connections by commenting out
+   or deleting `listen_addr` in `/home/git/gitaly/config.toml`, saving the file,
+   and [restarting GitLab](../restart_gitlab.md#installations-from-source)
+   on Gitaly server node(s).
+
+### Observe type of Gitaly connections
 
 To observe what type of connections are actually being used in a
 production environment you can use the following Prometheus query:
@@ -520,7 +554,7 @@ a few things that you need to do:
 1. Configure [database lookup of SSH keys](../operations/fast_ssh_key_lookup.md)
    to eliminate the need for a shared authorized_keys file.
 1. Configure [object storage for job artifacts](../job_artifacts.md#using-object-storage)
-   including [live tracing](../job_traces.md#new-live-trace-architecture).
+   including [incremental logging](../job_logs.md#new-incremental-logging-architecture).
 1. Configure [object storage for LFS objects](../../workflow/lfs/lfs_administration.md#storing-lfs-objects-in-remote-object-storage).
 1. Configure [object storage for uploads](../uploads.md#using-object-storage-core-only).
 
diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md
index 9e47f7767fe461fdd1665b5e3bf5c09281a9df43..9038675a28fc617e6ba3f154d876a0531e71fac3 100644
--- a/doc/administration/gitaly/praefect.md
+++ b/doc/administration/gitaly/praefect.md
@@ -68,20 +68,26 @@ sidekiq['enable'] = false
 gitlab_workhorse['enable'] = false
 gitaly['enable'] = false
 
+# virtual_storage_name must match the same storage name given to praefect in git_data_dirs
+praefect['virtual_storage_name'] = 'praefect'
+praefect['auth_token'] = 'super_secret_abc'
 praefect['enable'] = true
 praefect['storage_nodes'] = [
   {
     'storage' => 'praefect-git-1',
     'address' => 'tcp://praefect-git-1.internal',
+    'token'   => 'token1',
     'primary' => true
   },
   {
     'storage' => 'praefect-git-2',
-    'address' => 'tcp://praefect-git-2.internal'
+    'address' => 'tcp://praefect-git-2.internal',
+    'token'   => 'token2'
   },
   {
     'storage' => 'praefect-git-3',
-    'address' => 'tcp://praefect-git-3.internal'
+    'address' => 'tcp://praefect-git-3.internal',
+    'token'   => 'token3'
   }
 ]
 ```
diff --git a/doc/administration/gitaly/reference.md b/doc/administration/gitaly/reference.md
index a3bb4f8a5095c725b63a3bb7ec131c444793d28e..fe88ef13958753f7dba4759c6bf490c5aee25861 100644
--- a/doc/administration/gitaly/reference.md
+++ b/doc/administration/gitaly/reference.md
@@ -134,7 +134,7 @@ A lot of Gitaly RPCs need to look up Git objects from repositories.
 Most of the time we use `git cat-file --batch` processes for that. For
 better performance, Gitaly can re-use these `git cat-file` processes
 across RPC calls. Previously used processes are kept around in a
-["git cat-file cache"](https://about.gitlab.com/2019/07/08/git-performance-on-nfs/#enter-cat-file-cache).
+["git cat-file cache"](https://about.gitlab.com/blog/2019/07/08/git-performance-on-nfs/#enter-cat-file-cache).
 In order to control how much system resources this uses, we have a maximum number
 of cat-file processes that can go into the cache.
 
diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md
index fc2986380f32f37deba894c5a474afd1001beb51..199944a160c7d3d42b5dd6ace4604702f1d1d673 100644
--- a/doc/administration/high_availability/README.md
+++ b/doc/administration/high_availability/README.md
@@ -198,6 +198,11 @@ separately:
 
 These reference architecture examples rely on the general rule that approximately 2 requests per second (RPS) of load is generated for every 100 users.
 
+The specifications here were performance tested against a specific coded
+workload. Your exact needs may be more, depending on your workload. Your
+workload is influenced by factors such as - but not limited to - how active your
+users are, how much automation you use, mirroring, and repo/change size.
+
 ### 10,000 User Configuration
 
 - **Supported Users (approximate):** 10,000
@@ -211,33 +216,39 @@ environment that supports about 10,000 users. The specifications below are a
 representation of the work so far. The specifications may be adjusted in the
 future based on additional testing and iteration.
 
-NOTE: **Note:** The specifications here were performance tested against a
-specific coded workload. Your exact needs may be more, depending on your
-workload. Your workload is influenced by factors such as - but not limited to -
-how active your users are, how much automation you use, mirroring, and
-repo/change size.
-
-- 3 PostgreSQL - 4 CPU, 16GiB memory per node
-- 1 PgBouncer - 2 CPU, 4GiB memory
-- 2 Redis - 2 CPU, 8GiB memory per node
-- 3 Consul/Sentinel - 2 CPU, 2GiB memory per node
-- 4 Sidekiq - 4 CPU, 16GiB memory per node
-- 5 GitLab application nodes - 16 CPU, 64GiB memory per node
-- 1 Gitaly - 16 CPU, 64GiB memory
-- 1 Monitoring node - 2 CPU, 8GiB memory, 100GiB local storage
+| Service                       | Configuration           | GCP type       |
+| ------------------------------|-------------------------|----------------|
+| 3 GitLab Rails <br> - Puma workers on each node set to 90% of available CPUs with 16 threads | 32 vCPU, 28.8GB Memory | n1-highcpu-32 |
+| 3 PostgreSQL                  | 4 vCPU, 15GB Memory     | n1-standard-4  |
+| 1 PgBouncer                   | 2 vCPU, 1.8GB Memory    | n1-highcpu-2   |
+| X Gitaly[^1] <br> - Gitaly Ruby workers on each node set to 90% of available CPUs with 16 threads | 16 vCPU, 60GB Memory   | n1-standard-16 |
+| 3 Redis Cache + Sentinel <br> - Cache maxmemory set to 90% of available memory | 4 vCPU, 15GB Memory | n1-standard-4 |
+| 3 Redis Persistent + Sentinel | 4 vCPU, 15GB Memory     | n1-standard-4  |
+| 4 Sidekiq                     | 4 vCPU, 15GB Memory     | n1-standard-4  |
+| 3 Consul                      | 2 vCPU, 1.8GB Memory    | n1-highcpu-2   |
+| 1 NFS Server                  | 16 vCPU, 14.4GB Memory  | n1-highcpu-16  |
+| 1 Monitoring node             | 4 CPU, 3.6GB Memory     | n1-highcpu-4   |
+| 1 Load Balancing node[^2] .   | 2 vCPU, 1.8GB Memory    | n1-highcpu-2   |
 
 ### 25,000 User Configuration
 
 - **Supported Users (approximate):** 25,000
 - **RPS:** 500 requests per second
-- **Status:** Work-in-progress
-- **Related Issue:** See the [related issue](https://gitlab.com/gitlab-org/quality/performance/issues/57) for more information.
+- **Known Issues:** The slow API endpoints that were discovered during testing
+  the 10,000 user architecture also affect the 25,000 user architecture. For
+  details, see the related issues list in
+  [this issue](https://gitlab.com/gitlab-org/gitlab-foss/issues/64335).
 
-The Support and Quality teams are in the process of building and performance
-testing an environment that will support around 25,000 users. The specifications
-below are a work-in-progress representation of the work so far. The Quality team
-will be certifying this environment in late 2019. The specifications may be
-adjusted prior to certification based on performance testing.
+The GitLab Support and Quality teams built, performance tested, and validated an
+environment that supports around 25,000 users. The specifications below are a
+representation of the work so far. The specifications may be adjusted in the
+future based on additional testing and iteration.
+
+NOTE: **Note:** The specifications here were performance tested against a
+specific coded workload. Your exact needs may be more, depending on your
+workload. Your workload is influenced by factors such as - but not limited to -
+how active your users are, how much automation you use, mirroring, and
+repo/change size.
 
 | Service                       | Configuration           | GCP type       |
 | ------------------------------|-------------------------|----------------|
@@ -249,7 +260,7 @@ adjusted prior to certification based on performance testing.
 | 3 Redis Persistent + Sentinel | 4 vCPU, 15GB Memory     | n1-standard-4  |
 | 4 Sidekiq                     | 4 vCPU, 15GB Memory     | n1-standard-4  |
 | 3 Consul                      | 2 vCPU, 1.8GB Memory    | n1-highcpu-2   |
-| 1 NFS Server                  | 2 vCPU, 1.8GB Memory    | n1-highcpu-2   |
+| 1 NFS Server                  | 16 vCPU, 14.4GB Memory  | n1-highcpu-16  |
 | 1 Monitoring node             | 4 CPU, 3.6GB Memory     | n1-highcpu-4   |
 | 1 Load Balancing node[^2] .   | 2 vCPU, 1.8GB Memory    | n1-highcpu-2   |
 
@@ -277,15 +288,15 @@ testing.
 | 3 Redis Persistent + Sentinel | 4 vCPU, 15GB Memory     | n1-standard-4  |
 | 4 Sidekiq                     | 4 vCPU, 15GB Memory     | n1-standard-4  |
 | 3 Consul                      | 2 vCPU, 1.8GB Memory    | n1-highcpu-2   |
-| 1 NFS Server                  | 2 vCPU, 1.8GB Memory    | n1-highcpu-2   |
+| 1 NFS Server                  | 16 vCPU, 14.4GB Memory  | n1-highcpu-16  |
 | 1 Monitoring node             | 4 CPU, 3.6GB Memory     | n1-highcpu-4   |
 | 1 Load Balancing node[^2] .   | 2 vCPU, 1.8GB Memory    | n1-highcpu-2   |
 
 [^1]: Gitaly node requirements are dependent on customer data. We recommend 2
-      nodes as an absolute minimum for performance at the 25,000 user scale and
-      4 nodes as an absolute minimum at the 50,000 user scale, but additional
-      nodes should be considered in conjunction with a review of project counts
-      and sizes.
+      nodes as an absolute minimum for performance at the 10,000 and 25,000 user
+      scale and 4 nodes as an absolute minimum at the 50,000 user scale, but
+      additional nodes should be considered in conjunction with a review of
+      project counts and sizes.
 
 [^2]: HAProxy is the only tested and recommended load balancer. Additional
       options may be supported in the future.
diff --git a/doc/administration/high_availability/consul.md b/doc/administration/high_availability/consul.md
index aacc2c5cc400f30161de499fd628892ff056047a..b01419200cc2863658b7e7ce959fe5d47cb82bad 100644
--- a/doc/administration/high_availability/consul.md
+++ b/doc/administration/high_availability/consul.md
@@ -158,7 +158,7 @@ To fix this:
 
 ### Outage recovery
 
-If you lost enough server agents in the cluster to break quorum, then the cluster is considered failed, and it will not function without manual intervenetion.
+If you lost enough server agents in the cluster to break quorum, then the cluster is considered failed, and it will not function without manual intervention.
 
 #### Recreate from scratch
 
diff --git a/doc/administration/high_availability/gitlab.md b/doc/administration/high_availability/gitlab.md
index 0a8343605eb2398e035ce379c4040e7df7136a1f..71ab169a8013d850fa937f92dab0675c557c0a5f 100644
--- a/doc/administration/high_availability/gitlab.md
+++ b/doc/administration/high_availability/gitlab.md
@@ -99,14 +99,14 @@ these additional steps before proceeding with GitLab installation.
 
 ## First GitLab application server
 
-As a final step, run the setup rake task **only on** the first GitLab application server.
-Do not run this on additional application servers.
+On the first application server, run:
 
-1. Initialize the database by running `sudo gitlab-rake gitlab:setup`.
-1. Run `sudo gitlab-ctl reconfigure` to compile the configuration.
+```sh
+sudo gitlab-ctl reconfigure
+```
 
-   CAUTION: **WARNING:** Only run this setup task on **NEW** GitLab instances because it
-   will wipe any existing data.
+This should compile the configuration and initialize the database. Do
+not run this on additional application servers until the next step.
 
 ## Extra configuration for additional GitLab application servers
 
diff --git a/doc/administration/index.md b/doc/administration/index.md
index fb6c1af991b323082292a8c21a9d486f147d031e..e0f8ab8d855f5f2e5cf58affd8f114d13717bbce 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -68,11 +68,10 @@ Learn how to install, configure, update, and maintain your GitLab instance.
 
 #### Customizing GitLab's appearance
 
-- [Header logo](../customization/branded_page_and_email_header.md): Change the logo on all pages and email headers.
-- [Favicon](../customization/favicon.md): Change the default favicon to your own logo.
-- [Branded login page](../customization/branded_login_page.md): Customize the login page with your own logo, title, and description.
-- [Welcome message](../customization/welcome_message.md): Add a custom welcome message to the sign-in page.
-- ["New Project" page](../customization/new_project_page.md): Customize the text to be displayed on the page that opens whenever your users create a new project.
+- [Header logo](../user/admin_area/appearance.md#navigation-bar): Change the logo on all pages and email headers.
+- [Favicon](../user/admin_area/appearance.md#favicon): Change the default favicon to your own logo.
+- [Branded login page](../user/admin_area/appearance.md#sign-in--sign-up-pages): Customize the login page with your own logo, title, and description.
+- ["New Project" page](../user/admin_area/appearance.md#new-project-pages): Customize the text to be displayed on the page that opens whenever your users create a new project.
 - [Additional custom email text](../user/admin_area/settings/email.md#custom-additional-text-premium-only): Add additional custom text to emails sent from GitLab. **(PREMIUM ONLY)**
 
 ### Maintaining GitLab
@@ -105,7 +104,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
 ## User settings and permissions
 
 - [Creating users](../user/profile/account/create_accounts.md): Create users manually or through authentication integrations.
-- [Libravatar](../customization/libravatar.md): Use Libravatar instead of Gravatar for user avatars.
+- [Libravatar](libravatar.md): Use Libravatar instead of Gravatar for user avatars.
 - [Sign-up restrictions](../user/admin_area/settings/sign_up_restrictions.md): block email addresses of specific domains, or whitelist only specific domains.
 - [Access restrictions](../user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols): Define which Git access protocols can be used to talk to GitLab (SSH, HTTP, HTTPS).
 - [Authentication and Authorization](auth/README.md): Configure external authentication with LDAP, SAML, CAS and additional providers.
@@ -154,7 +153,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
 - [Enable/disable GitLab CI/CD](../ci/enable_or_disable_ci.md#site-wide-admin-setting): Enable or disable GitLab CI/CD for your instance.
 - [GitLab CI/CD admin settings](../user/admin_area/settings/continuous_integration.md): Enable or disable Auto DevOps site-wide and define the artifacts' max size and expiration time.
 - [Job artifacts](job_artifacts.md): Enable, disable, and configure job artifacts (a set of files and directories which are outputted by a job when it completes successfully).
-- [Job traces](job_traces.md): Information about the job traces (logs).
+- [Job logs](job_logs.md): Information about the job logs.
 - [Register Shared and specific Runners](../ci/runners/README.md#registering-a-shared-runner): Learn how to register and configure Shared and specific Runners to your own instance.
 - [Shared Runners pipelines quota](../user/admin_area/settings/continuous_integration.md#shared-runners-pipeline-minutes-quota-starter-only): Limit the usage of pipeline minutes for Shared Runners. **(STARTER ONLY)**
 - [Enable/disable Auto DevOps](../topics/autodevops/index.md#enablingdisabling-auto-devops): Enable or disable Auto DevOps for your instance.
diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md
index 86b18e673a5f2c705ad850ef68d15141faf866dd..ec2f40700f5bc3c5ee30cee9a6a92a4b8c534290 100644
--- a/doc/administration/job_artifacts.md
+++ b/doc/administration/job_artifacts.md
@@ -90,7 +90,7 @@ This configuration relies on valid AWS credentials to be configured already.
 Use an object storage option like AWS S3 to store job artifacts.
 
 DANGER: **Danger:**
-If you're enabling S3 in [GitLab HA](high_availability/README.md), you will need to have an [NFS mount set up for CI traces and artifacts](high_availability/nfs.md#a-single-nfs-mount) or enable [live tracing](job_traces.md#new-live-trace-architecture). If these settings are not set, you will risk job traces disappearing or not being saved.
+If you're enabling S3 in [GitLab HA](high_availability/README.md), you will need to have an [NFS mount set up for CI logs and artifacts](high_availability/nfs.md#a-single-nfs-mount) or enable [incremental logging](job_logs.md#new-incremental-logging-architecture). If these settings are not set, you will risk job logs disappearing or not being saved.
 
 #### Object Storage Settings
 
diff --git a/doc/administration/job_logs.md b/doc/administration/job_logs.md
new file mode 100644
index 0000000000000000000000000000000000000000..d6d56515ac64b55b1e9c39d6ecc766e3f872987f
--- /dev/null
+++ b/doc/administration/job_logs.md
@@ -0,0 +1,169 @@
+# Job logs
+
+> [Renamed from Job Traces to Job logs](https://gitlab.com/gitlab-org/gitlab/issues/29121) in 12.4.
+
+Job logs (traces) are sent by GitLab Runner while it's processing a job. You can see
+logs in job pages, pipelines, email notifications, etc.
+
+## Data flow
+
+In general, there are two states for job logs: `log` and `archived log`.
+In the following table you can see the phases a log goes through:
+
+| Phase          | State        | Condition               | Data flow                                | Stored path |
+| -------------- | ------------ | ----------------------- | -----------------------------------------| ----------- |
+| 1: patching    | log          | When a job is running   | GitLab Runner => Unicorn => file storage | `#{ROOT_PATH}/gitlab-ci/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log` |
+| 2: overwriting | log          | When a job is finished  | GitLab Runner => Unicorn => file storage | `#{ROOT_PATH}/gitlab-ci/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log` |
+| 3: archiving   | archived log | After a job is finished | Sidekiq moves log to artifacts folder    | `#{ROOT_PATH}/gitlab-rails/shared/artifacts/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/job.log` |
+| 4: uploading   | archived log | After a log is archived | Sidekiq moves archived log to [object storage](#uploading-logs-to-object-storage) (if configured) | `#{bucket_name}/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/job.log` |
+
+The `ROOT_PATH` varies per environment. For Omnibus GitLab it
+would be `/var/opt/gitlab`, and for installations from source
+it would be `/home/git/gitlab`.
+
+## Changing the job logs local location
+
+To change the location where the job logs will be stored, follow the steps below.
+
+**In Omnibus installations:**
+
+1. Edit `/etc/gitlab/gitlab.rb` and add or amend the following line:
+
+   ```ruby
+   gitlab_ci['builds_directory'] = '/mnt/to/gitlab-ci/builds'
+   ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**In installations from source:**
+
+1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines:
+
+   ```yaml
+   gitlab_ci:
+     # The location where build logs are stored (default: builds/).
+     # Relative paths are relative to Rails.root.
+     builds_path: path/to/builds/
+   ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+
+[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
+[restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab"
+
+## Uploading logs to object storage
+
+Archived logs are considered as [job artifacts](job_artifacts.md).
+Therefore, when you [set up the object storage integration](job_artifacts.md#object-storage-settings),
+job logs are automatically migrated to it along with the other job artifacts.
+
+See "Phase 4: uploading" in [Data flow](#data-flow) to learn about the process.
+
+## How to remove job logs
+
+There isn't a way to automatically expire old job logs, but it's safe to remove
+them if they're taking up too much space. If you remove the logs manually, the
+job output in the UI will be empty.
+
+## New incremental logging architecture
+
+> [Introduced][ce-18169] in GitLab 10.4.
+> [Announced as General availability][ce-46097] in GitLab 11.0.
+
+NOTE: **Note:**
+This feature is off by default. See below for how to [enable or disable](#enabling-incremental-logging) it.
+
+By combining the process with object storage settings, we can completely bypass
+the local file storage. This is a useful option if GitLab is installed as
+cloud-native, for example on Kubernetes.
+
+The data flow is the same as described in the [data flow section](#data-flow)
+with one change: _the stored path of the first two phases is different_. This incremental
+log architecture stores chunks of logs in Redis and a persistent store (object storage or database) instead of
+file storage. Redis is used as first-class storage, and it stores up-to 128KB
+of data. Once the full chunk is sent, it is flushed to a persistent store, either object storage(temporary directory) or database.
+After a while, the data in Redis and a persitent store will be archived to [object storage](#uploading-logs-to-object-storage).
+
+The data are stored in the following Redis namespace: `Gitlab::Redis::SharedState`.
+
+Here is the detailed data flow:
+
+1. GitLab Runner picks a job from GitLab
+1. GitLab Runner sends a piece of log to GitLab
+1. GitLab appends the data to Redis
+1. Once the data in Redis reach 128KB, the data is flushed to a persistent store (object storage or the database).
+1. The above steps are repeated until the job is finished.
+1. Once the job is finished, GitLab schedules a Sidekiq worker to archive the log.
+1. The Sidekiq worker archives the log to object storage and cleans up the log
+   in Redis and a persistent store (object storage or the database).
+
+### Enabling incremental logging
+
+The following commands are to be issued in a Rails console:
+
+```sh
+# Omnibus GitLab
+gitlab-rails console
+
+# Installation from source
+cd /home/git/gitlab
+sudo -u git -H bin/rails console RAILS_ENV=production
+```
+
+**To check if incremental logging (trace) is enabled:**
+
+```ruby
+Feature.enabled?('ci_enable_live_trace')
+```
+
+**To enable incremental logging (trace):**
+
+```ruby
+Feature.enable('ci_enable_live_trace')
+```
+
+NOTE: **Note:**
+The transition period will be handled gracefully. Upcoming logs will be
+generated with the incremental architecture, and on-going logs will stay with the
+legacy architecture, which means that on-going logs won't be forcibly
+re-generated with the incremental architecture.
+
+**To disable incremental logging (trace):**
+
+```ruby
+Feature.disable('ci_enable_live_trace')
+```
+
+NOTE: **Note:**
+The transition period will be handled gracefully. Upcoming logs will be generated
+with the legacy architecture, and on-going incremental logs will stay with the incremental
+architecture, which means that on-going incremental logs won't be forcibly re-generated
+with the legacy architecture.
+
+### Potential implications
+
+In some cases, having data stored on Redis could incur data loss:
+
+1. **Case 1: When all data in Redis are accidentally flushed**
+   - On going incremental logs could be recovered by re-sending logs (this is
+     supported by all versions of the GitLab Runner).
+   - Finished jobs which have not archived incremental logs will lose the last part
+     (~128KB) of log data.
+
+1. **Case 2: When Sidekiq workers fail to archive (e.g., there was a bug that
+   prevents archiving process, Sidekiq inconsistency, etc.)**
+   - Currently all log data in Redis will be deleted after one week. If the
+     Sidekiq workers can't finish by the expiry date, the part of log data will be lost.
+
+Another issue that might arise is that it could consume all memory on the Redis
+instance. If the number of jobs is 1000, 128MB (128KB * 1000) is consumed.
+
+Also, it could pressure the database replication lag. `INSERT`s are generated to
+indicate that we have log chunk. `UPDATE`s with 128KB of data is issued once we
+receive multiple chunks.
+
+[ce-18169]: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/18169
+[ce-21193]: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/21193
+[ce-46097]: https://gitlab.com/gitlab-org/gitlab-foss/issues/46097
diff --git a/doc/administration/job_traces.md b/doc/administration/job_traces.md
index af60d777932516e77bffbe9531b99302ffbfc359..d0b346a931edb5c5e1805e15ca1632ecc6a9db20 100644
--- a/doc/administration/job_traces.md
+++ b/doc/administration/job_traces.md
@@ -1,167 +1,5 @@
-# Job traces (logs)
-
-Job traces are sent by GitLab Runner while it's processing a job. You can see
-traces in job pages, pipelines, email notifications, etc.
-
-## Data flow
-
-In general, there are two states in job traces: "live trace" and "archived trace".
-In the following table you can see the phases a trace goes through.
-
-| Phase          | State          | Condition                 | Data flow                                       |  Stored path |
-| -----          | -----          | ---------                 | ---------                                       |  ----------- |
-| 1: patching    | Live trace     | When a job is running     | GitLab Runner => Unicorn => file storage        |`#{ROOT_PATH}/gitlab-ci/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`|
-| 2: overwriting | Live trace     | When a job is finished    | GitLab Runner => Unicorn => file storage        |`#{ROOT_PATH}/gitlab-ci/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`|
-| 3: archiving   | Archived trace | After a job is finished   | Sidekiq moves live trace to artifacts folder    |`#{ROOT_PATH}/gitlab-rails/shared/artifacts/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/job.log`|
-| 4: uploading   | Archived trace | After a trace is archived | Sidekiq moves archived trace to [object storage](#uploading-traces-to-object-storage) (if configured)  |`#{bucket_name}/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/job.log`|
-
-The `ROOT_PATH` varies per your environment. For Omnibus GitLab it
-would be `/var/opt/gitlab`, whereas for installations from source
-it would be `/home/git/gitlab`.
-
-## Changing the job traces local location
-
-To change the location where the job logs will be stored, follow the steps below.
-
-**In Omnibus installations:**
-
-1. Edit `/etc/gitlab/gitlab.rb` and add or amend the following line:
-
-   ```ruby
-   gitlab_ci['builds_directory'] = '/mnt/to/gitlab-ci/builds'
-   ```
-
-1. Save the file and [reconfigure GitLab][] for the changes to take effect.
-
+---
+redirect_to: 'job_logs.md'
 ---
 
-**In installations from source:**
-
-1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines:
-
-   ```yaml
-   gitlab_ci:
-     # The location where build traces are stored (default: builds/).
-     # Relative paths are relative to Rails.root.
-     builds_path: path/to/builds/
-   ```
-
-1. Save the file and [restart GitLab][] for the changes to take effect.
-
-[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
-[restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab"
-
-## Uploading traces to object storage
-
-Archived traces are considered as [job artifacts](job_artifacts.md).
-Therefore, when you [set up the object storage integration](job_artifacts.md#object-storage-settings),
-job traces are automatically migrated to it along with the other job artifacts.
-
-See "Phase 4: uploading" in [Data flow](#data-flow) to learn about the process.
-
-## How to remove job traces
-
-There isn't a way to automatically expire old job logs, but it's safe to remove
-them if they're taking up too much space. If you remove the logs manually, the
-job output in the UI will be empty.
-
-## New live trace architecture
-
-> [Introduced][ce-18169] in GitLab 10.4.
-> [Announced as General availability][ce-46097] in GitLab 11.0.
-
-NOTE: **Note:**
-This feature is off by default. Check below how to [enable/disable](#enabling-live-trace) it.
-
-By combining the process with object storage settings, we can completely bypass
-the local file storage. This is a useful option if GitLab is installed as
-cloud-native, for example on Kubernetes.
-
-The data flow is the same as described in the [data flow section](#data-flow)
-with one change: _the stored path of the first two phases is different_. This new live
-trace architecture stores chunks of traces in Redis and a persistent store (object storage or database) instead of
-file storage. Redis is used as first-class storage, and it stores up-to 128KB
-of data. Once the full chunk is sent, it is flushed a persistent store, either object storage(temporary directory) or database.
-After a while, the data in Redis and a persitent store will be archived to [object storage](#uploading-traces-to-object-storage).
-
-The data are stored in the following Redis namespace: `Gitlab::Redis::SharedState`.
-
-Here is the detailed data flow:
-
-1. GitLab Runner picks a job from GitLab
-1. GitLab Runner sends a piece of trace to GitLab
-1. GitLab appends the data to Redis
-1. Once the data in Redis reach 128KB, the data is flushed to a persistent store (object storage or the database).
-1. The above steps are repeated until the job is finished.
-1. Once the job is finished, GitLab schedules a Sidekiq worker to archive the trace.
-1. The Sidekiq worker archives the trace to object storage and cleans up the trace
-   in Redis and a persistent store (object storage or the database).
-
-### Enabling live trace
-
-The following commands are to be issues in a Rails console:
-
-```sh
-# Omnibus GitLab
-gitlab-rails console
-
-# Installation from source
-cd /home/git/gitlab
-sudo -u git -H bin/rails console RAILS_ENV=production
-```
-
-**To check if live trace is enabled:**
-
-```ruby
-Feature.enabled?('ci_enable_live_trace')
-```
-
-**To enable live trace:**
-
-```ruby
-Feature.enable('ci_enable_live_trace')
-```
-
-NOTE: **Note:**
-The transition period will be handled gracefully. Upcoming traces will be
-generated with the new architecture, and on-going live traces will stay with the
-legacy architecture, which means that on-going live traces won't be forcibly
-re-generated with the new architecture.
-
-**To disable live trace:**
-
-```ruby
-Feature.disable('ci_enable_live_trace')
-```
-
-NOTE: **Note:**
-The transition period will be handled gracefully. Upcoming traces will be generated
-with the legacy architecture, and on-going live traces will stay with the new
-architecture, which means that on-going live traces won't be forcibly re-generated
-with the legacy architecture.
-
-### Potential implications
-
-In some cases, having data stored on Redis could incur data loss:
-
-1. **Case 1: When all data in Redis are accidentally flushed**
-   - On going live traces could be recovered by re-sending traces (this is
-     supported by all versions of the GitLab Runner).
-   - Finished jobs which have not archived live traces will lose the last part
-     (~128KB) of trace data.
-
-1. **Case 2: When Sidekiq workers fail to archive (e.g., there was a bug that
-   prevents archiving process, Sidekiq inconsistency, etc.)**
-   - Currently all trace data in Redis will be deleted after one week. If the
-     Sidekiq workers can't finish by the expiry date, the part of trace data will be lost.
-
-Another issue that might arise is that it could consume all memory on the Redis
-instance. If the number of jobs is 1000, 128MB (128KB * 1000) is consumed.
-
-Also, it could pressure the database replication lag. `INSERT`s are generated to
-indicate that we have trace chunk. `UPDATE`s with 128KB of data is issued once we
-receive multiple chunks.
-
-[ce-18169]: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/18169
-[ce-21193]: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/21193
-[ce-46097]: https://gitlab.com/gitlab-org/gitlab-foss/issues/46097
+This document was moved to [another location](job_logs.md).
diff --git a/doc/administration/monitoring/performance/grafana_configuration.md b/doc/administration/monitoring/performance/grafana_configuration.md
index 323f955f59892deab8229da5205b4d4d974093a5..ccba0a554793034fa8b5f2eaf15c14d72b69a0fb 100644
--- a/doc/administration/monitoring/performance/grafana_configuration.md
+++ b/doc/administration/monitoring/performance/grafana_configuration.md
@@ -146,7 +146,7 @@ However, you should **not** reinstate your old data _except_ under one of the fo
 
 If you require access to your old Grafana data but do not meet one of these criteria, you may consider reinstating it temporarily, [exporting the dashboards](https://grafana.com/docs/reference/export_import/#exporting-a-dashboard) you need, then refreshing the data and [re-importing your dashboards](https://grafana.com/docs/reference/export_import/#importing-a-dashboard). Note that this poses a temporary vulnerability while your old Grafana data is in use, and the decision to do so should be weighed carefully with your need to access existing data and dashboards.
 
-For more information and further mitigation details, please refer to our [blog post on the security release](https://about.gitlab.com/2019/08/12/critical-security-release-gitlab-12-dot-1-dot-6-released/).
+For more information and further mitigation details, please refer to our [blog post on the security release](https://about.gitlab.com/blog/2019/08/12/critical-security-release-gitlab-12-dot-1-dot-6-released/).
 
 ---
 
diff --git a/doc/administration/troubleshooting/postgresql.md b/doc/administration/troubleshooting/postgresql.md
index 3bbc3f23d83df0958475a612b6dc414f9b6ea8aa..f427cd88ce05f52ad7a7ef4d9966b435fd1896d1 100644
--- a/doc/administration/troubleshooting/postgresql.md
+++ b/doc/administration/troubleshooting/postgresql.md
@@ -31,30 +31,30 @@ This section is for links to information elsewhere in the GitLab documentation.
   - Destructively reseeding the GitLab database.
   - Guidance around updating packaged PostgreSQL, including how to stop it happening automatically.
 
-- [More about external PostgreSQL](/ee/administration/external_database.html)
+- [More about external PostgreSQL](../external_database.md)
 
-- [Running GEO with external PostgreSQL](/ee/administration/geo/replication/external_database.html)
+- [Running GEO with external PostgreSQL](../geo/replication/external_database.md)
 
 - [Upgrades when running PostgreSQL configured for HA.](https://docs.gitlab.com/omnibus/settings/database.html#upgrading-a-gitlab-ha-cluster)
 
-- Consuming PostgreSQL from [within CI runners](/ee/ci/services/postgres.html)
+- Consuming PostgreSQL from [within CI runners](../../ci/services/postgres.md)
 
-- [Using Slony to update PostgreSQL](/ee/update/upgrading_postgresql_using_slony.html)
+- [Using Slony to update PostgreSQL](../../update/upgrading_postgresql_using_slony.md)
   - Uses replication to handle PostgreSQL upgrades - providing the schemas are the same.
   - Reduces downtime to a short window for swinging over to the newer vewrsion.
 
 - Managing Omnibus PostgreSQL versions [from the development docs](https://docs.gitlab.com/omnibus/development/managing-postgresql-versions.html)
 
-- [PostgreSQL scaling and HA](/ee/administration/high_availability/database.html)
-  - including [troubleshooting](/ee/administration/high_availability/database.html#troubleshooting) gitlab-ctl repmgr-check-master and pgbouncer errors
+- [PostgreSQL scaling and HA](../high_availability/database.md)
+  - including [troubleshooting](../high_availability/database.md#troubleshooting) gitlab-ctl repmgr-check-master and pgbouncer errors
 
-- [Developer database documentation](/ee/development/README.html#database-guides) - some of which is absolutely not for production use. Including:
+- [Developer database documentation](../../development/README.md#database-guides) - some of which is absolutely not for production use. Including:
   - understanding EXPLAIN plans
 
 ### Troubleshooting/Fixes
 
-- [GitLab database requirements](/ee/install/requirements.html#database) including
-  - Support for MySQL was removed in GitLab 12.1; [migrate to PostgreSQL](/ee/update/mysql_to_postgresql.html)
+- [GitLab database requirements](../../install/requirements.md#database) including
+  - Support for MySQL was removed in GitLab 12.1; [migrate to PostgreSQL](../../update/mysql_to_postgresql.md)
   - required extension pg_trgm
   - required extension postgres_fdw for Geo
 
@@ -71,7 +71,7 @@ pg_basebackup: could not create temporary replication slot "pg_basebackup_12345"
 HINT:  Free one or increase max_replication_slots.
 ```
 
-- GEO [replication errors](/ee/administration/geo/replication/troubleshooting.html#fixing-replication-errors) including:
+- GEO [replication errors](../geo/replication/troubleshooting.md#fixing-replication-errors) including:
 
 ```
 ERROR: replication slots can only be used if max_replication_slots > 0
@@ -83,11 +83,11 @@ Command exceeded allowed execution time
 PANIC: could not write to file ‘pg_xlog/xlogtemp.123’: No space left on device
 ```
 
-- [Checking GEO configuration](/ee/administration/geo/replication/troubleshooting.html#checking-configuration) including
+- [Checking GEO configuration](../geo/replication/troubleshooting.md#checking-configuration) including
   - reconfiguring hosts/ports
   - checking and fixing user/password mappings
 
-- [Common GEO errors](/ee/administration/geo/replication/troubleshooting.html#fixing-common-errors)
+- [Common GEO errors](../geo/replication/troubleshooting.md#fixing-common-errors)
 
 ## Support topics
 
diff --git a/doc/api/deployments.md b/doc/api/deployments.md
index df3a98b1dc8bebcbd80b7f4012f308c566975265..27254c42e3a211777804df017b05ff534fa123d3 100644
--- a/doc/api/deployments.md
+++ b/doc/api/deployments.md
@@ -223,3 +223,100 @@ Example of response
   }
 }
 ```
+
+## Create a deployment
+
+```
+POST /projects/:id/deployments
+```
+
+| Attribute        | Type           | Required | Description         |
+|------------------|----------------|----------|---------------------|
+| `id`             | integer/string | yes      | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `environment`    | string         | yes      | The name of the environment to create the deployment for |
+| `sha`            | string         | yes      | The SHA of the commit that is deployed |
+| `ref`            | string         | yes      | The name of the branch or tag that is deployed |
+| `tag`            | boolean        | yes      | A boolean that indicates if the deployed ref is a tag (true) or not (false) |
+| `status`         | string         | yes      | The status of the deployment |
+
+The status can be one of the following values:
+
+- created
+- running
+- success
+- failed
+- canceled
+
+```bash
+curl --data "environment=production&sha=a91957a858320c0e17f3a0eca7cfacbff50ea29a&ref=master&tag=false&status=success" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/deployments"
+```
+
+Example of a response:
+
+```json
+{
+  "id": 42,
+  "iid": 2,
+  "ref": "master",
+  "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+  "created_at": "2016-08-11T11:32:35.444Z",
+  "status": "success",
+  "user": {
+    "name": "Administrator",
+    "username": "root",
+    "id": 1,
+    "state": "active",
+    "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+    "web_url": "http://localhost:3000/root"
+  },
+  "environment": {
+    "id": 9,
+    "name": "production",
+    "external_url": "https://about.gitlab.com"
+  },
+  "deployable": null
+}
+```
+
+## Updating a deployment
+
+```
+PUT /projects/:id/deployments/:deployment_id
+```
+
+| Attribute        | Type           | Required | Description         |
+|------------------|----------------|----------|---------------------|
+| `id`             | integer/string | yes      | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `deployment_id`  | integer        | yes      | The ID of the deployment to update |
+| `status`         | string         | yes      | The new status of the deployment |
+
+```bash
+curl --request PUT --data "status=success" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/deployments/42"
+```
+
+Example of a response:
+
+```json
+{
+  "id": 42,
+  "iid": 2,
+  "ref": "master",
+  "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+  "created_at": "2016-08-11T11:32:35.444Z",
+  "status": "success",
+  "user": {
+    "name": "Administrator",
+    "username": "root",
+    "id": 1,
+    "state": "active",
+    "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+    "web_url": "http://localhost:3000/root"
+  },
+  "environment": {
+    "id": 9,
+    "name": "production",
+    "external_url": "https://about.gitlab.com"
+  },
+  "deployable": null
+}
+```
diff --git a/doc/api/epics.md b/doc/api/epics.md
index c24df6a236f6d9cad21ca14d13e20894ed379870..c7a050f1465fde4dd1cc131fe44cf4368be95491 100644
--- a/doc/api/epics.md
+++ b/doc/api/epics.md
@@ -147,7 +147,8 @@ Example response:
   "closed_at": "2018-08-18T12:22:05.239Z",
   "labels": [],
   "upvotes": 4,
-  "downvotes": 0
+  "downvotes": 0,
+  "subscribed": true
 }
 ```
 
@@ -169,7 +170,7 @@ POST /groups/:id/epics
 | `id`                | integer/string   | yes        | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user                |
 | `title`             | string           | yes        | The title of the epic |
 | `labels`            | string           | no         | The comma separated list of labels |
-| `description`       | string           | no         | The description of the epic. Limited to 1 000 000 characters.  |
+| `description`       | string           | no         | The description of the epic. Limited to 1,048,576 characters.  |
 | `start_date_is_fixed` | boolean        | no         | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) |
 | `start_date_fixed`  | string           | no         | The fixed start date of an epic (since 11.3) |
 | `due_date_is_fixed` | boolean          | no         | Whether due date should be sourced from `due_date_fixed` or from milestones (since 11.3) |
@@ -236,7 +237,7 @@ PUT /groups/:id/epics/:epic_iid
 | `id`                | integer/string   | yes        | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user                |
 | `epic_iid`          | integer/string   | yes        | The internal ID  of the epic  |
 | `title`             | string           | no         | The title of an epic |
-| `description`       | string           | no         | The description of an epic. Limited to 1 000 000 characters.  |
+| `description`       | string           | no         | The description of an epic. Limited to 1,048,576 characters.  |
 | `labels`            | string           | no         | The comma separated list of labels |
 | `start_date_is_fixed` | boolean        | no         | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) |
 | `start_date_fixed`  | string           | no         | The fixed start date of an epic (since 11.3) |
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 1efda2f07ebf97038361a6d12b5476ec0cf02a5b..839289cf6773d09c2c9621476fda08b8bb984f08 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -54,9 +54,87 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
 | `message` | String |  |
 | `authoredDate` | Time |  |
 | `webUrl` | String! |  |
+| `signatureHtml` | String | Rendered html for the commit signature |
 | `author` | User |  |
 | `latestPipeline` | Pipeline | Latest pipeline for this commit |
 
+### CreateDiffNotePayload
+
+| Name  | Type  | Description |
+| ---   |  ---- | ----------  |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
+| `note` | Note | The note after mutation |
+
+### CreateImageDiffNotePayload
+
+| Name  | Type  | Description |
+| ---   |  ---- | ----------  |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
+| `note` | Note | The note after mutation |
+
+### CreateNotePayload
+
+| Name  | Type  | Description |
+| ---   |  ---- | ----------  |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
+| `note` | Note | The note after mutation |
+
+### Design
+
+| Name  | Type  | Description |
+| ---   |  ---- | ----------  |
+| `id` | ID! |  |
+| `project` | Project! |  |
+| `issue` | Issue! |  |
+| `notesCount` | Int! | The total count of user-created notes for this design |
+| `filename` | String! |  |
+| `fullPath` | String! |  |
+| `event` | DesignVersionEvent! | The change that happened to the design at this version |
+| `image` | String! |  |
+| `diffRefs` | DiffRefs! |  |
+
+### DesignCollection
+
+| Name  | Type  | Description |
+| ---   |  ---- | ----------  |
+| `project` | Project! |  |
+| `issue` | Issue! |  |
+
+### DesignManagementDeletePayload
+
+| Name  | Type  | Description |
+| ---   |  ---- | ----------  |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
+| `version` | DesignVersion | The new version in which the designs are deleted |
+
+### DesignManagementUploadPayload
+
+| Name  | Type  | Description |
+| ---   |  ---- | ----------  |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
+| `designs` | Design! => Array | The designs that were uploaded by the mutation |
+| `skippedDesigns` | Design! => Array | Any designs that were skipped from the upload due to there being no change to their content since their last version |
+
+### DesignVersion
+
+| Name  | Type  | Description |
+| ---   |  ---- | ----------  |
+| `id` | ID! |  |
+| `sha` | ID! |  |
+
+### DestroyNotePayload
+
+| Name  | Type  | Description |
+| ---   |  ---- | ----------  |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
+| `note` | Note | The note after mutation |
+
 ### DetailedStatus
 
 | Name  | Type  | Description |
@@ -74,9 +152,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
 
 | Name  | Type  | Description |
 | ---   |  ---- | ----------  |
-| `headSha` | String! | The sha of the head at the time the comment was made |
-| `baseSha` | String | The merge base of the branch the comment was made on |
-| `startSha` | String! | The sha of the branch being compared against |
+| `diffRefs` | DiffRefs! |  |
 | `filePath` | String! | The path of the file that was changed |
 | `oldPath` | String | The path of the file on the start sha. |
 | `newPath` | String | The path of the file on the head sha. |
@@ -88,13 +164,147 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
 | `width` | Int | The total width of the image |
 | `height` | Int | The total height of the image |
 
+### DiffRefs
+
+| Name  | Type  | Description |
+| ---   |  ---- | ----------  |
+| `headSha` | String! | The sha of the head at the time the comment was made |
+| `baseSha` | String! | The merge base of the branch the comment was made on |
+| `startSha` | String! | The sha of the branch being compared against |
+
 ### Discussion
 
 | Name  | Type  | Description |
 | ---   |  ---- | ----------  |
 | `id` | ID! |  |
+| `replyId` | ID! | The ID used to reply to this discussion |
 | `createdAt` | Time! |  |
 
+### Epic
+
+| Name  | Type  | Description |
+| ---   |  ---- | ----------  |
+| `userPermissions` | EpicPermissions! | Permissions for the current user on the resource |
+| `id` | ID! |  |
+| `iid` | ID! |  |
+| `title` | String |  |
+| `description` | String |  |
+| `state` | EpicState! |  |
+| `group` | Group! |  |
+| `parent` | Epic |  |
+| `author` | User! |  |
+| `startDate` | Time |  |
+| `startDateIsFixed` | Boolean |  |
+| `startDateFixed` | Time |  |
+| `startDateFromMilestones` | Time |  |
+| `dueDate` | Time |  |
+| `dueDateIsFixed` | Boolean |  |
+| `dueDateFixed` | Time |  |
+| `dueDateFromMilestones` | Time |  |
+| `closedAt` | Time |  |
+| `createdAt` | Time |  |
+| `updatedAt` | Time |  |
+| `hasChildren` | Boolean! |  |
+| `hasIssues` | Boolean! |  |
+| `webPath` | String! |  |
+| `webUrl` | String! |  |
+| `relativePosition` | Int | The relative position of the epic in the Epic tree |
+| `relationPath` | String |  |
+| `reference` | String! |  |
+| `subscribed` | Boolean! | Boolean flag for whether the currently logged in user is subscribed to this epic |
+
+### EpicIssue
+
+| Name  | Type  | Description |
+| ---   |  ---- | ----------  |
+| `userPermissions` | IssuePermissions! | Permissions for the current user on the resource |
+| `iid` | ID! |  |
+| `title` | String! |  |
+| `titleHtml` | String | The GitLab Flavored Markdown rendering of `title` |
+| `description` | String |  |
+| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
+| `state` | IssueState! |  |
+| `reference` | String! |  |
+| `author` | User! |  |
+| `milestone` | Milestone |  |
+| `dueDate` | Time |  |
+| `confidential` | Boolean! |  |
+| `discussionLocked` | Boolean! |  |
+| `upvotes` | Int! |  |
+| `downvotes` | Int! |  |
+| `userNotesCount` | Int! |  |
+| `webPath` | String! |  |
+| `webUrl` | String! |  |
+| `relativePosition` | Int |  |
+| `timeEstimate` | Int! | The time estimate on the issue |
+| `totalTimeSpent` | Int! | Total time reported as spent on the issue |
+| `closedAt` | Time |  |
+| `createdAt` | Time! |  |
+| `updatedAt` | Time! |  |
+| `taskCompletionStatus` | TaskCompletionStatus! |  |
+| `epic` | Epic | The epic to which issue belongs |
+| `weight` | Int |  |
+| `designs` | DesignCollection |  |
+| `designCollection` | DesignCollection |  |
+| `epicIssueId` | ID! |  |
+| `relationPath` | String |  |
+| `id` | ID | The global id of the epic-issue relation |
+
+### EpicPermissions
+
+| Name  | Type  | Description |
+| ---   |  ---- | ----------  |
+| `readEpic` | Boolean! | Whether or not a user can perform `read_epic` on this resource |
+| `readEpicIid` | Boolean! | Whether or not a user can perform `read_epic_iid` on this resource |
+| `updateEpic` | Boolean! | Whether or not a user can perform `update_epic` on this resource |
+| `destroyEpic` | Boolean! | Whether or not a user can perform `destroy_epic` on this resource |
+| `adminEpic` | Boolean! | Whether or not a user can perform `admin_epic` on this resource |
+| `createEpic` | Boolean! | Whether or not a user can perform `create_epic` on this resource |
+| `createNote` | Boolean! | Whether or not a user can perform `create_note` on this resource |
+| `awardEmoji` | Boolean! | Whether or not a user can perform `award_emoji` on this resource |
+
+### EpicTreeReorderPayload
+
+| Name  | Type  | Description |
+| ---   |  ---- | ----------  |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
+
+### ExtendedIssue
+
+| Name  | Type  | Description |
+| ---   |  ---- | ----------  |
+| `userPermissions` | IssuePermissions! | Permissions for the current user on the resource |
+| `iid` | ID! |  |
+| `title` | String! |  |
+| `titleHtml` | String | The GitLab Flavored Markdown rendering of `title` |
+| `description` | String |  |
+| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
+| `state` | IssueState! |  |
+| `reference` | String! |  |
+| `author` | User! |  |
+| `milestone` | Milestone |  |
+| `dueDate` | Time |  |
+| `confidential` | Boolean! |  |
+| `discussionLocked` | Boolean! |  |
+| `upvotes` | Int! |  |
+| `downvotes` | Int! |  |
+| `userNotesCount` | Int! |  |
+| `webPath` | String! |  |
+| `webUrl` | String! |  |
+| `relativePosition` | Int |  |
+| `timeEstimate` | Int! | The time estimate on the issue |
+| `totalTimeSpent` | Int! | Total time reported as spent on the issue |
+| `closedAt` | Time |  |
+| `createdAt` | Time! |  |
+| `updatedAt` | Time! |  |
+| `taskCompletionStatus` | TaskCompletionStatus! |  |
+| `epic` | Epic | The epic to which issue belongs |
+| `weight` | Int |  |
+| `designs` | DesignCollection |  |
+| `designCollection` | DesignCollection |  |
+| `subscribed` | Boolean! | Boolean flag for whether the currently logged in user is subscribed to this issue |
+
 ### Group
 
 | Name  | Type  | Description |
@@ -109,11 +319,13 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
 | `visibility` | String |  |
 | `lfsEnabled` | Boolean |  |
 | `requestAccessEnabled` | Boolean |  |
-| `rootStorageStatistics` | RootStorageStatistics | The aggregated storage statistics. Only available if the namespace has no parent |
+| `rootStorageStatistics` | RootStorageStatistics | The aggregated storage statistics. Only available for root namespaces |
 | `userPermissions` | GroupPermissions! | Permissions for the current user on the resource |
 | `webUrl` | String! |  |
 | `avatarUrl` | String |  |
 | `parent` | Group |  |
+| `epicsEnabled` | Boolean |  |
+| `epic` | Epic |  |
 
 ### GroupPermissions
 
@@ -144,10 +356,16 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
 | `webPath` | String! |  |
 | `webUrl` | String! |  |
 | `relativePosition` | Int |  |
+| `timeEstimate` | Int! | The time estimate on the issue |
+| `totalTimeSpent` | Int! | Total time reported as spent on the issue |
 | `closedAt` | Time |  |
 | `createdAt` | Time! |  |
 | `updatedAt` | Time! |  |
 | `taskCompletionStatus` | TaskCompletionStatus! |  |
+| `epic` | Epic | The epic to which issue belongs |
+| `weight` | Int |  |
+| `designs` | DesignCollection |  |
+| `designCollection` | DesignCollection |  |
 
 ### IssuePermissions
 
@@ -158,6 +376,9 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
 | `updateIssue` | Boolean! | Whether or not a user can perform `update_issue` on this resource |
 | `createNote` | Boolean! | Whether or not a user can perform `create_note` on this resource |
 | `reopenIssue` | Boolean! | Whether or not a user can perform `reopen_issue` on this resource |
+| `readDesign` | Boolean! | Whether or not a user can perform `read_design` on this resource |
+| `createDesign` | Boolean! | Whether or not a user can perform `create_design` on this resource |
+| `destroyDesign` | Boolean! | Whether or not a user can perform `destroy_design` on this resource |
 
 ### Label
 
@@ -185,6 +406,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
 | `updatedAt` | Time! |  |
 | `sourceProject` | Project |  |
 | `targetProject` | Project! |  |
+| `diffRefs` | DiffRefs |  |
 | `project` | Project! |  |
 | `projectId` | Int! |  |
 | `sourceProjectId` | Int |  |
@@ -213,8 +435,13 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
 | `webUrl` | String |  |
 | `upvotes` | Int! |  |
 | `downvotes` | Int! |  |
-| `subscribed` | Boolean! |  |
 | `headPipeline` | Pipeline |  |
+| `milestone` | Milestone | The milestone this merge request is linked to |
+| `subscribed` | Boolean! | Boolean flag for whether the currently logged in user is subscribed to this MR |
+| `discussionLocked` | Boolean! | Boolean flag determining if comments on the merge request are locked to members only |
+| `timeEstimate` | Int! | The time estimate for the merge request |
+| `totalTimeSpent` | Int! | Total time reported as spent on the merge request |
+| `reference` | String! | Internal merge request reference. Returned in shortened format by default |
 | `taskCompletionStatus` | TaskCompletionStatus! |  |
 
 ### MergeRequestPermissions
@@ -271,6 +498,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
 | `visibility` | String |  |
 | `lfsEnabled` | Boolean |  |
 | `requestAccessEnabled` | Boolean |  |
+| `rootStorageStatistics` | RootStorageStatistics | The aggregated storage statistics. Only available for root namespaces |
 
 ### Note
 
@@ -381,7 +609,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
 | `statistics` | ProjectStatistics |  |
 | `repository` | Repository |  |
 | `mergeRequest` | MergeRequest |  |
-| `issue` | Issue |  |
+| `issue` | ExtendedIssue |  |
 
 ### ProjectPermissions
 
@@ -424,6 +652,10 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
 | `createPages` | Boolean! | Whether or not a user can perform `create_pages` on this resource |
 | `destroyPages` | Boolean! | Whether or not a user can perform `destroy_pages` on this resource |
 | `readPagesContent` | Boolean! | Whether or not a user can perform `read_pages_content` on this resource |
+| `adminOperations` | Boolean! | Whether or not a user can perform `admin_operations` on this resource |
+| `readDesign` | Boolean! | Whether or not a user can perform `read_design` on this resource |
+| `createDesign` | Boolean! | Whether or not a user can perform `create_design` on this resource |
+| `destroyDesign` | Boolean! | Whether or not a user can perform `destroy_design` on this resource |
 
 ### ProjectStatistics
 
@@ -458,12 +690,12 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
 
 | Name  | Type  | Description |
 | ---   |  ---- | ----------  |
-| `storageSize` | Int! | The total storage in Bytes |
-| `repositorySize` | Int! | The Git repository size in Bytes |
-| `lfsObjectsSize` | Int! | The LFS objects size in Bytes |
-| `buildArtifactsSize` | Int! | The CI artifacts size in Bytes |
-| `packagesSize` | Int! | The packages size in Bytes |
-| `wikiSize` | Int! | The wiki size in Bytes |
+| `storageSize` | Int! | The total storage in bytes |
+| `repositorySize` | Int! | The git repository size in bytes |
+| `lfsObjectsSize` | Int! | The LFS objects size in bytes |
+| `buildArtifactsSize` | Int! | The CI artifacts size in bytes |
+| `packagesSize` | Int! | The packages size in bytes |
+| `wikiSize` | Int! | The wiki size in bytes |
 
 ### Submodule
 
@@ -474,6 +706,8 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
 | `type` | EntryType! |  |
 | `path` | String! |  |
 | `flatPath` | String! |  |
+| `webUrl` | String |  |
+| `treeUrl` | String |  |
 
 ### TaskCompletionStatus
 
@@ -482,6 +716,20 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
 | `count` | Int! |  |
 | `completedCount` | Int! |  |
 
+### Todo
+
+| Name  | Type  | Description |
+| ---   |  ---- | ----------  |
+| `id` | ID! | Id of the todo |
+| `project` | Project | The project this todo is associated with |
+| `group` | Group | Group this todo is associated with |
+| `author` | User! | The owner of this todo |
+| `action` | TodoActionEnum! | Action of the todo |
+| `targetType` | TodoTargetEnum! | Target type of the todo |
+| `body` | String! | Body of the todo |
+| `state` | TodoStateEnum! | State of the todo |
+| `createdAt` | Time! | Timestamp this todo was created |
+
 ### ToggleAwardEmojiPayload
 
 | Name  | Type  | Description |
@@ -495,7 +743,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
 
 | Name  | Type  | Description |
 | ---   |  ---- | ----------  |
-| `lastCommit` | Commit |  |
+| `lastCommit` | Commit | Last commit for the tree |
 
 ### TreeEntry
 
@@ -508,6 +756,22 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
 | `flatPath` | String! |  |
 | `webUrl` | String |  |
 
+### UpdateEpicPayload
+
+| Name  | Type  | Description |
+| ---   |  ---- | ----------  |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
+| `epic` | Epic | The epic after mutation |
+
+### UpdateNotePayload
+
+| Name  | Type  | Description |
+| ---   |  ---- | ----------  |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
+| `note` | Note | The note after mutation |
+
 ### User
 
 | Name  | Type  | Description |
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 12a63ce6e243a4aeb9f912d35a7045597f5a6330..0ddbb18ce9218ec4eab56fa433e34a566e3a5ca9 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -58,7 +58,7 @@ GET /issues?confidential=true
 | `updated_after`     | datetime         | no         | Return issues updated on or after the given time                                                                                                    |
 | `updated_before`    | datetime         | no         | Return issues updated on or before the given time                                                                                                   |
 | `confidential`      | Boolean          | no         | Filter confidential or public issues.                                                                                                               |
-| `not`               | Hash             | no         | Return issues that do not match the parameters supplied. Accepts: `labels`, `milestone`, `author_id`, `author_username`, `assignee_id`, `assignee_username`, `my_reaction_emoji`, `search`, `in` |  
+| `not`               | Hash             | no         | Return issues that do not match the parameters supplied. Accepts: `labels`, `milestone`, `author_id`, `author_username`, `assignee_id`, `assignee_username`, `my_reaction_emoji`, `search`, `in` |
 
 ```bash
 curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/issues
@@ -207,7 +207,7 @@ GET /groups/:id/issues?confidential=true
 | `updated_after`     | datetime         | no         | Return issues updated on or after the given time                                                                              |
 | `updated_before`    | datetime         | no         | Return issues updated on or before the given time                                                                             |
 | `confidential`     | Boolean          | no         | Filter confidential or public issues.                                                                                         |
-| `not`               | Hash             | no         | Return issues that do not match the parameters supplied. Accepts: `labels`, `milestone`, `author_id`, `author_username`, `assignee_id`, `assignee_username`, `my_reaction_emoji`, `search`, `in` |  
+| `not`               | Hash             | no         | Return issues that do not match the parameters supplied. Accepts: `labels`, `milestone`, `author_id`, `author_username`, `assignee_id`, `assignee_username`, `my_reaction_emoji`, `search`, `in` |
 
 ```bash
 curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/4/issues
@@ -605,7 +605,7 @@ POST /projects/:id/issues
 | `id`                                      | integer/string | yes      | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
 | `iid`                                     | integer/string | no       | The internal ID of the project's issue (requires admin or project owner rights) |
 | `title`                                   | string         | yes      | The title of an issue |
-| `description`                             | string         | no       | The description of an issue. Limited to 1 000 000 characters. |
+| `description`                             | string         | no       | The description of an issue. Limited to 1,048,576 characters. |
 | `confidential`                            | boolean        | no       | Set an issue to be confidential. Default is `false`.  |
 | `assignee_ids`                            | integer array  | no       | The ID of a user to assign issue |
 | `milestone_id`                            | integer        | no       | The global ID of a milestone to assign issue  |
@@ -615,6 +615,7 @@ POST /projects/:id/issues
 | `merge_request_to_resolve_discussions_of` | integer        | no       | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values.|
 | `discussion_to_resolve`                   | string         | no       | The ID of a discussion to resolve. This will fill in the issue with a default description and mark the discussion as resolved. Use in combination with `merge_request_to_resolve_discussions_of`. |
 | `weight` **(STARTER)**                    | integer        | no       | The weight of the issue. Valid values are greater than or equal to 0. |
+| `epic_iid` **(ULTIMATE)** | integer | no | IID of the epic to add the issue to. Valid values are greater than or equal to 0. |
 
 ```bash
 curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/4/issues?title=Issues%20with%20auth&labels=bug
@@ -706,7 +707,7 @@ PUT /projects/:id/issues/:issue_iid
 | `id`           | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
 | `issue_iid`    | integer | yes      | The internal ID of a project's issue                                                                       |
 | `title`        | string  | no       | The title of an issue                                                                                      |
-| `description`  | string  | no       | The description of an issue. Limited to 1 000 000 characters.        |
+| `description`  | string  | no       | The description of an issue. Limited to 1,048,576 characters.        |
 | `confidential` | boolean | no       | Updates an issue to be confidential                                                                        |
 | `assignee_ids` | integer array | no | The ID of the user(s) to assign the issue to. Set to `0` or provide an empty value to unassign all assignees. |
 | `milestone_id` | integer | no       | The global ID of a milestone to assign the issue to. Set to `0` or provide an empty value to unassign a milestone.|
@@ -716,6 +717,7 @@ PUT /projects/:id/issues/:issue_iid
 | `due_date`     | string  | no       | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11`                                           |
 | `weight` **(STARTER)** | integer | no | The weight of the issue. Valid values are greater than or equal to 0. 0                                                                    |
 | `discussion_locked` | boolean | no  | Flag indicating if the issue's discussion is locked. If the discussion is locked only project members can add or edit comments. |
+| `epic_iid` **(ULTIMATE)** | integer | no | IID of the epic to add the issue to. Valid values are greater than or equal to 0. |
 
 ```bash
 curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/4/issues/85?state_event=close
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index 8ee4facc018bd46284773e56675318659ebd9973..bafcfd110d3237766d5d666c8bb8b4a50959b110 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -537,9 +537,9 @@ Possible response status codes:
 | 400       | Invalid path provided                |
 | 404       | Build not found or no file/artifacts |
 
-## Get a trace file
+## Get a log file
 
-Get a trace of a specific job of a project
+Get a log (trace) of a specific job of a project:
 
 ```
 GET /projects/:id/jobs/:job_id/trace
@@ -556,10 +556,10 @@ curl --location --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.ex
 
 Possible response status codes:
 
-| Status    | Description                       |
-|-----------|-----------------------------------|
-| 200       | Serves the trace file             |
-| 404       | Build not found or no trace file  |
+| Status    | Description                   |
+|-----------|-------------------------------|
+| 200       | Serves the log file           |
+| 404       | Job not found or no log file  |
 
 ## Cancel a job
 
@@ -661,7 +661,7 @@ Example of response
 
 ## Erase a job
 
-Erase a single job of a project (remove job artifacts and a job trace)
+Erase a single job of a project (remove job artifacts and a job log)
 
 ```
 POST /projects/:id/jobs/:job_id/erase
diff --git a/doc/api/members.md b/doc/api/members.md
index da62dc53659093977b0cab21243dfc86ae760325..50dcf86c97273861ddc439bc00f2991a52df719b 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -26,6 +26,7 @@ GET /projects/:id/members
 | --------- | ---- | -------- | ----------- |
 | `id`      | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
 | `query`   | string | no     | A query string to search for members |
+| `user_ids`   | array of integers | no     | Filter the results on the given user IDs |
 
 ```bash
 curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/members
@@ -62,9 +63,8 @@ Example response:
 ## List all members of a group or project including inherited members
 
 Gets a list of group or project members viewable by the authenticated user, including inherited members through ancestor groups.
-When a user is a member of the project/group and of one or more ancestor groups the user is returned only once with the project access_level (if exists)
-or the access_level for the user in the first group which he belongs to in the project groups ancestors chain.
-**Note:** We plan to [change](https://gitlab.com/gitlab-org/gitlab-foss/issues/62284) this behavior to return highest access_level instead.
+When a user is a member of the project/group and of one or more ancestor groups the user is returned only once with the project `access_level` (if exists)
+or the `access_level` for the user in the first group which he belongs to in the project groups ancestors chain.
 
 ```
 GET /groups/:id/members/all
@@ -75,6 +75,7 @@ GET /projects/:id/members/all
 | --------- | ---- | -------- | ----------- |
 | `id`      | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
 | `query`   | string | no     | A query string to search for members |
+| `user_ids`   | array of integers | no     | Filter the results on the given user IDs |
 
 ```bash
 curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/members/all
@@ -120,7 +121,7 @@ Example response:
 
 ## Get a member of a group or project
 
-Gets a member of a group or project.
+Gets a member of a group or project. Returns only direct members and not inherited members through ancestor groups.
 
 ```
 GET /groups/:id/members/:user_id
@@ -152,6 +153,42 @@ Example response:
 }
 ```
 
+## Get a member of a group or project, including inherited members
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/17744) in GitLab 12.4.
+
+Gets a member of a group or project, including members inherited through ancestor groups. See the corresponding [endpoint to list all inherited members](#list-all-members-of-a-group-or-project-including-inherited-members) for details.
+
+```
+GET /groups/:id/members/all/:user_id
+GET /projects/:id/members/all/:user_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`      | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `user_id` | integer | yes   | The user ID of the member |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/members/all/:user_id
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/:id/members/all/:user_id
+```
+
+Example response:
+
+```json
+{
+  "id": 1,
+  "username": "raymond_smith",
+  "name": "Raymond Smith",
+  "state": "active",
+  "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
+  "web_url": "http://192.168.1.8:3000/root",
+  "access_level": 30,
+  "expires_at": null
+}
+```
+
 ## Add a member to a group or project
 
 Adds a member to a group or project.
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 738715d2dbf440ad1120ef8fe12e96d81dbad634..4bc46c3030d1c07171dcd59c71855db91df59d53 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -897,7 +897,7 @@ POST /projects/:id/merge_requests
 | `title`                    | string  | yes      | Title of MR                                                                     |
 | `assignee_id`              | integer | no       | Assignee user ID                                                                |
 | `assignee_ids`             | integer array | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees.  |
-| `description`              | string  | no       | Description of MR. Limited to 1 000 000 characters. |
+| `description`              | string  | no       | Description of MR. Limited to 1,048,576 characters. |
 | `target_project_id`        | integer | no       | The target project (numeric id)                                                 |
 | `labels`                   | string  | no       | Labels for MR as a comma-separated list                                         |
 | `milestone_id`             | integer | no       | The global ID of a milestone                                                           |
@@ -1050,7 +1050,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid
 | `assignee_ids`             | integer array | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees.  |
 | `milestone_id`             | integer | no       | The global ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.|
 | `labels`                   | string  | no       | Comma-separated label names for a merge request. Set to an empty string to unassign all labels.                    |
-| `description`              | string  | no       | Description of MR. Limited to 1 000 000 characters. |
+| `description`              | string  | no       | Description of MR. Limited to 1,048,576 characters. |
 | `state_event`              | string  | no       | New state (close/reopen)                                                        |
 | `remove_source_branch`     | boolean | no       | Flag indicating if a merge request should remove the source branch when merging |
 | `squash`                   | boolean | no       | Squash commits into a single commit when merging |
diff --git a/doc/api/notes.md b/doc/api/notes.md
index 1f5baf7d0e1c0d4fbec6470d4110ee430afa6c22..2cace425ff2dea2370921d21d237f536a6ce2dab 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -113,7 +113,7 @@ Parameters:
 
 - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
 - `issue_iid` (required) - The IID of an issue
-- `body` (required) - The content of a note. Limited to 1 000 000 characters.
+- `body` (required) - The content of a note. Limited to 1,000,000 characters.
 - `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights)
 
 ```bash
@@ -133,7 +133,7 @@ Parameters:
 - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
 - `issue_iid` (required) - The IID of an issue
 - `note_id` (required) - The ID of a note
-- `body` (required) - The content of a note. Limited to 1 000 000 characters.
+- `body` (required) - The content of a note. Limited to 1,000,000 characters.
 
 ```bash
 curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/issues/11/notes?body=note
@@ -231,7 +231,7 @@ Parameters:
 
 - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
 - `snippet_id` (required) - The ID of a snippet
-- `body` (required) - The content of a note. Limited to 1 000 000 characters.
+- `body` (required) - The content of a note. Limited to 1,000,000 characters.
 - `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z
 
 ```bash
@@ -251,7 +251,7 @@ Parameters:
 - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
 - `snippet_id` (required) - The ID of a snippet
 - `note_id` (required) - The ID of a note
-- `body` (required) - The content of a note. Limited to 1 000 000 characters.
+- `body` (required) - The content of a note. Limited to 1,000,000 characters.
 
 ```bash
 curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippets/11/notes?body=note
@@ -354,7 +354,7 @@ Parameters:
 
 - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
 - `merge_request_iid` (required) - The IID of a merge request
-- `body` (required) - The content of a note. Limited to 1 000 000 characters.
+- `body` (required) - The content of a note. Limited to 1,000,000 characters.
 - `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z
 
 ### Modify existing merge request note
@@ -370,7 +370,7 @@ Parameters:
 - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
 - `merge_request_iid` (required) - The IID of a merge request
 - `note_id` (required) - The ID of a note
-- `body` (required) - The content of a note. Limited to 1 000 000 characters.
+- `body` (required) - The content of a note. Limited to 1,000,000 characters.
 
 ```bash
 curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/notes?body=note
@@ -472,7 +472,7 @@ Parameters:
 | --------- | -------------- | -------- | ----------- |
 | `id`      | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
 | `epic_id` | integer | yes  | The ID of an epic |
-| `body`    | string  | yes  | The content of a note. Limited to 1 000 000 characters. |
+| `body`    | string  | yes  | The content of a note. Limited to 1,000,000 characters. |
 
 ```bash
 curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippet/11/notes?body=note
@@ -493,7 +493,7 @@ Parameters:
 | `id`      | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
 | `epic_id` | integer | yes  | The ID of an epic |
 | `note_id` | integer | yes  | The ID of a note |
-| `body`    | string  | yes  | The content of a note. Limited to 1 000 000 characters. |
+| `body`    | string  | yes  | The content of a note. Limited to 1,000,000 characters. |
 
 ```bash
 curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippet/11/notes?body=note
diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md
index c2a39fd2178d7bf797f9a54adcf69dbe7bb4e9cd..7a6c90701baf0e9d111647657c8d3f6f3f0ea2e2 100644
--- a/doc/api/project_snippets.md
+++ b/doc/api/project_snippets.md
@@ -78,7 +78,7 @@ Parameters:
 - `title` (required) - The title of a snippet
 - `file_name` (required) - The name of a snippet file
 - `description` (optional) - The description of a snippet
-- `code` (required) - The content of a snippet
+- `content` (required) - The content of a snippet
 - `visibility` (required) - The snippet's visibility
 
 Example request:
@@ -97,7 +97,7 @@ curl --request POST https://gitlab.com/api/v4/projects/:id/snippets \
   "title" : "Example Snippet Title",
   "description" : "More verbose snippet description",
   "file_name" : "example.txt",
-  "code" : "source code \n with multiple lines\n",
+  "content" : "source code \n with multiple lines\n",
   "visibility" : "private"
 }
 ```
@@ -117,7 +117,7 @@ Parameters:
 - `title` (optional) - The title of a snippet
 - `file_name` (optional) - The name of a snippet file
 - `description` (optional) - The description of a snippet
-- `code` (optional) - The content of a snippet
+- `content` (optional) - The content of a snippet
 - `visibility` (optional) - The snippet's visibility
 
 Example request:
@@ -136,7 +136,7 @@ curl --request PUT https://gitlab.com/api/v4/projects/:id/snippets \
   "title" : "Updated Snippet Title",
   "description" : "More verbose snippet description",
   "file_name" : "new_filename.txt",
-  "code" : "updated source code \n with multiple lines\n",
+  "content" : "updated source code \n with multiple lines\n",
   "visibility" : "private"
 }
 ```
diff --git a/doc/api/releases/index.md b/doc/api/releases/index.md
index 79bc3511bc8ba9a477007d6e8a3679cd2ad07487..ee2df3e4c5d89faec4fce022c065ef4a97dac520 100644
--- a/doc/api/releases/index.md
+++ b/doc/api/releases/index.md
@@ -122,10 +122,6 @@ Example response:
             }
          ]
       },
-      "_links":{
-         "merge_requests_url": "https://gitlab.example.com/root/awesome_app/merge_requests?release_tag=v0.2&scope=all&state=opened",
-         "issues_url": "https://gitlab.example.com/root/awesome_app/issues?release_tag=v0.2&scope=all&state=opened"
-      }
    },
    {
       "tag_name":"v0.1",
@@ -182,10 +178,6 @@ Example response:
 
          ]
       },
-      "_links":{
-         "merge_requests_url": "https://gitlab.example.com/root/awesome_app/merge_requests?release_tag=v0.1&scope=all&state=opened",
-         "issues_url": "https://gitlab.example.com/root/awesome_app/issues?release_tag=v0.1&scope=all&state=opened"
-      }
    }
 ]
 ```
@@ -297,10 +289,6 @@ Example response:
 
       ]
    },
-   "_links":{
-      "merge_requests_url": "https://gitlab.example.com/root/awesome_app/merge_requests?release_tag=v0.1&scope=all&state=opened",
-      "issues_url": "https://gitlab.example.com/root/awesome_app/issues?release_tag=v0.1&scope=all&state=opened"
-   }
 }
 ```
 
@@ -426,10 +414,6 @@ Example response:
          }
       ]
    },
-   "_links":{
-      "merge_requests_url": "https://gitlab.example.com/root/awesome_app/merge_requests?release_tag=v0.3&scope=all&state=opened",
-      "issues_url": "https://gitlab.example.com/root/awesome_app/issues?release_tag=v0.3&scope=all&state=opened"
-   }
 }
 ```
 
@@ -531,10 +515,6 @@ Example response:
 
       ]
    },
-   "_links":{
-      "merge_requests_url": "https://gitlab.example.com/root/awesome_app/merge_requests?release_tag=v0.1&scope=all&state=opened",
-      "issues_url": "https://gitlab.example.com/root/awesome_app/issues?release_tag=v0.1&scope=all&state=opened"
-   }
 }
 ```
 
@@ -617,10 +597,6 @@ Example response:
 
       ]
    },
-   "_links":{
-      "merge_requests_url": "https://gitlab.example.com/root/awesome_app/merge_requests?release_tag=v0.1&scope=all&state=opened",
-      "issues_url": "https://gitlab.example.com/root/awesome_app/issues?release_tag=v0.1&scope=all&state=opened"
-   }
 }
 ```
 
diff --git a/doc/api/settings.md b/doc/api/settings.md
index efb6809794f7245e1c28fd71acbcd5419f86cebb..2d9e435bbb624e9df595413c60f944447f4e4f5a 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -289,6 +289,8 @@ are listed in the descriptions of the relevant settings.
 | `prometheus_metrics_enabled`             | boolean          | no                                   | Enable Prometheus metrics. |
 | `protected_ci_variables`                 | boolean          | no                                   | Environment variables are protected by default. |
 | `pseudonymizer_enabled`                  | boolean          | no                                   | **(PREMIUM)** When enabled, GitLab will run a background job that will produce pseudonymized CSVs of the GitLab database that will be uploaded to your configured object storage directory.
+| `push_event_hooks_limit`                 | integer          | no                                   | Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value. |
+| `push_event_activities_limit`            | integer          | no                                   | Number of changes (branches or tags) in a single push to determine whether individual push events or bulk push events will be created. [Bulk push events will be created](../user/admin_area/settings/push_event_activities_limit.md) if it surpasses that value. |
 | `recaptcha_enabled`                      | boolean          | no                                   | (**If enabled, requires:** `recaptcha_private_key` and `recaptcha_site_key`) Enable reCAPTCHA. |
 | `recaptcha_private_key`                  | string           | required by: `recaptcha_enabled`     | Private key for reCAPTCHA. |
 | `recaptcha_site_key`                     | string           | required by: `recaptcha_enabled`     | Site key for reCAPTCHA. |
diff --git a/doc/ci/README.md b/doc/ci/README.md
index acab433cec222547ff3b9a3eef5633cb531642a0..5286764d178f1e9ddbc338f172a190d09e0e8aa3 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -85,7 +85,7 @@ GitLab CI/CD supports numerous configuration options:
 | [Job artifacts](../user/project/pipelines/job_artifacts.md) | Output, use, and reuse job artifacts. |
 | [Cache dependencies](caching/index.md) | Cache your dependencies for a faster execution. |
 | [Schedule pipelines](../user/project/pipelines/schedules.md) | Schedule pipelines to run as often as you need. |
-| [Custom path for `.gitlab-ci.yml`](../user/project/pipelines/settings.md#custom-ci-config-path) | Define a custom path for the CI/CD configuration file. |
+| [Custom path for `.gitlab-ci.yml`](../user/project/pipelines/settings.md#custom-ci-configuration-path) | Define a custom path for the CI/CD configuration file. |
 | [Git submodules for CI/CD](git_submodules.md) | Configure jobs for using Git submodules.|
 | [SSH keys for CI/CD](ssh_keys/README.md) | Using SSH keys in your CI pipelines. |
 | [Pipelines triggers](triggers/README.md) | Trigger pipelines through the API. |
@@ -161,9 +161,9 @@ See also:
 The following articles explain reasons to use GitLab CI/CD
 for your CI/CD infrastructure:
 
-- [Why we chose GitLab CI for our CI/CD solution](https://about.gitlab.com/2016/10/17/gitlab-ci-oohlala/)
-- [Building our web-app on GitLab CI](https://about.gitlab.com/2016/07/22/building-our-web-app-on-gitlab-ci/)
-- [5 Teams that made the switch to GitLab CI/CD](https://about.gitlab.com/2019/04/25/5-teams-that-made-the-switch-to-gitlab-ci-cd/)
+- [Why we chose GitLab CI for our CI/CD solution](https://about.gitlab.com/blog/2016/10/17/gitlab-ci-oohlala/)
+- [Building our web-app on GitLab CI](https://about.gitlab.com/blog/2016/07/22/building-our-web-app-on-gitlab-ci/)
+- [5 Teams that made the switch to GitLab CI/CD](https://about.gitlab.com/blog/2019/04/25/5-teams-that-made-the-switch-to-gitlab-ci-cd/)
 
 See also the [Why CI/CD?](https://docs.google.com/presentation/d/1OGgk2Tcxbpl7DJaIOzCX4Vqg3dlwfELC3u2jEeCBbDk) presentation.
 
diff --git a/doc/ci/caching/index.md b/doc/ci/caching/index.md
index 6a7b60c2ba5b9b81bdfde0030782a3ff770c98e3..6b8e7fa2ad555e2cb5558fc8fe2c965d7859ac98 100644
--- a/doc/ci/caching/index.md
+++ b/doc/ci/caching/index.md
@@ -491,8 +491,8 @@ job B:
 To fix that, use different `keys` for each job.
 
 In another case, let's assume you have more than one Runners assigned to your
-project, but the distributed cache is not enabled. We want the second time the
-pipeline is run, `job A` and `job B` to re-use their cache (which in this case
+project, but the distributed cache is not enabled. The second time the
+pipeline is run, we want `job A` and `job B` to re-use their cache (which in this case
 will be different):
 
 ```yaml
@@ -518,7 +518,7 @@ job B:
 ```
 
 In that case, even if the `key` is different (no fear of overwriting), you
-might experience the cached files to "get cleaned" before each stage if the
+might experience that the cached files "get cleaned" before each stage if the
 jobs run on different Runners in the subsequent pipelines.
 
 ## Clearing the cache
diff --git a/doc/ci/ci_cd_for_external_repos/index.md b/doc/ci/ci_cd_for_external_repos/index.md
index 257520d149cd23ba4c28f0e07498b5634429390e..35e2117c285918d6231f28f422eff6d90733c60e 100644
--- a/doc/ci/ci_cd_for_external_repos/index.md
+++ b/doc/ci/ci_cd_for_external_repos/index.md
@@ -7,7 +7,7 @@ type: index, howto
 >[Introduced][ee-4642] in [GitLab Premium][eep] 10.6.
 
 NOTE: **Note:**
-This feature [is available for free](https://about.gitlab.com/2019/09/09/ci-cd-github-extended-again/) to
+This feature [is available for free](https://about.gitlab.com/blog/2019/09/09/ci-cd-github-extended-again/) to
 GitLab.com users until March 22nd, 2020.
 
 GitLab CI/CD can be used with:
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 903866b534eb8665bf89f893b9cdfc23468a7483..cef95c8e22a0fcb1075390b727930eee96d308ec 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -748,7 +748,7 @@ Re-using variables defined inside `script` as part of the environment name will
 Below are some links you may find interesting:
 
 - [The `.gitlab-ci.yml` definition of environments](yaml/README.md#environment)
-- [A blog post on Deployments & Environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
+- [A blog post on Deployments & Environments](https://about.gitlab.com/blog/2016/08/26/ci-deployment-and-environments/)
 - [Review Apps - Use dynamic environments to deploy your code for every branch](review_apps/index.md)
 - [Deploy Boards for your applications running on Kubernetes](../user/project/deploy_boards.md) **(PREMIUM)**
 
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index 026837e480ab990f52ef522da548ca14d223d77c..d2333f7e468b8280f7621d6259473e7cd59fdf1c 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -59,10 +59,10 @@ Note that older articles and videos may not reflect the state of the latest GitL
 
 For examples of setting up GitLab CI/CD for cloud-based environments, see:
 
-- [How to set up multi-account AWS SAM deployments with GitLab CI](https://about.gitlab.com/2019/02/04/multi-account-aws-sam-deployments-with-gitlab-ci/)
+- [How to set up multi-account AWS SAM deployments with GitLab CI](https://about.gitlab.com/blog/2019/02/04/multi-account-aws-sam-deployments-with-gitlab-ci/)
 - [Automating Kubernetes Deployments with GitLab CI/CD](https://www.youtube.com/watch?v=wEDRfAz6_Uw)
-- [How to autoscale continuous deployment with GitLab Runner on DigitalOcean](https://about.gitlab.com/2018/06/19/autoscale-continuous-deployment-gitlab-runner-digital-ocean/)
-- [How to create a CI/CD pipeline with Auto Deploy to Kubernetes using GitLab and Helm](https://about.gitlab.com/2017/09/21/how-to-create-ci-cd-pipeline-with-autodeploy-to-kubernetes-using-gitlab-and-helm/)
+- [How to autoscale continuous deployment with GitLab Runner on DigitalOcean](https://about.gitlab.com/blog/2018/06/19/autoscale-continuous-deployment-gitlab-runner-digital-ocean/)
+- [How to create a CI/CD pipeline with Auto Deploy to Kubernetes using GitLab and Helm](https://about.gitlab.com/blog/2017/09/21/how-to-create-ci-cd-pipeline-with-autodeploy-to-kubernetes-using-gitlab-and-helm/)
 - [Demo - Deploying from GitLab to OpenShift Container Cluster](https://youtu.be/EwbhA53Jpp4)
 
 See also the following video overviews:
@@ -74,32 +74,32 @@ See also the following video overviews:
 
 For some customer experiences with GitLab CI/CD, see:
 
-- [How Verizon Connect reduced datacenter deploys from 30 days to under 8 hours with GitLab](https://about.gitlab.com/2019/02/14/verizon-customer-story/)
-- [How Wag! cut their release process from 40 minutes to just 6](https://about.gitlab.com/2019/01/16/wag-labs-blog-post/)
-- [How Jaguar Land Rover embraced CI to speed up their software lifecycle](https://about.gitlab.com/2018/07/23/chris-hill-devops-enterprise-summit-talk/)
+- [How Verizon Connect reduced datacenter deploys from 30 days to under 8 hours with GitLab](https://about.gitlab.com/blog/2019/02/14/verizon-customer-story/)
+- [How Wag! cut their release process from 40 minutes to just 6](https://about.gitlab.com/blog/2019/01/16/wag-labs-blog-post/)
+- [How Jaguar Land Rover embraced CI to speed up their software lifecycle](https://about.gitlab.com/blog/2018/07/23/chris-hill-devops-enterprise-summit-talk/)
 
 ### Getting started
 
 For some examples to help get you started, see:
 
-- [GitLab CI/CD's 2018 highlights](https://about.gitlab.com/2019/01/21/gitlab-ci-cd-features-improvements/)
-- [A beginner's guide to continuous integration](https://about.gitlab.com/2018/01/22/a-beginners-guide-to-continuous-integration/)
+- [GitLab CI/CD's 2018 highlights](https://about.gitlab.com/blog/2019/01/21/gitlab-ci-cd-features-improvements/)
+- [A beginner's guide to continuous integration](https://about.gitlab.com/blog/2018/01/22/a-beginners-guide-to-continuous-integration/)
 
 ### Implementing GitLab CI/CD
 
 For examples of others who have implemented GitLab CI/CD, see:
 
-- [How to streamline interactions between multiple repositories with multi-project pipelines](https://about.gitlab.com/2018/10/31/use-multiproject-pipelines-with-gitlab-cicd/)
-- [How we used GitLab CI to build GitLab faster](https://about.gitlab.com/2018/05/02/using-gitlab-ci-to-build-gitlab-faster/)
-- [Test all the things in GitLab CI with Docker by example](https://about.gitlab.com/2018/02/05/test-all-the-things-gitlab-ci-docker-examples/)
-- [A Craftsman looks at continuous integration](https://about.gitlab.com/2018/01/17/craftsman-looks-at-continuous-integration/)
-- [Go tools and GitLab: How to do continuous integration like a boss](https://about.gitlab.com/2017/11/27/go-tools-and-gitlab-how-to-do-continuous-integration-like-a-boss/)
-- [GitBot – automating boring Git operations with CI](https://about.gitlab.com/2017/11/02/automating-boring-git-operations-gitlab-ci/)
-- [How to use GitLab CI for Vue.js](https://about.gitlab.com/2017/09/12/vuejs-app-gitlab/)
+- [How to streamline interactions between multiple repositories with multi-project pipelines](https://about.gitlab.com/blog/2018/10/31/use-multiproject-pipelines-with-gitlab-cicd/)
+- [How we used GitLab CI to build GitLab faster](https://about.gitlab.com/blog/2018/05/02/using-gitlab-ci-to-build-gitlab-faster/)
+- [Test all the things in GitLab CI with Docker by example](https://about.gitlab.com/blog/2018/02/05/test-all-the-things-gitlab-ci-docker-examples/)
+- [A Craftsman looks at continuous integration](https://about.gitlab.com/blog/2018/01/17/craftsman-looks-at-continuous-integration/)
+- [Go tools and GitLab: How to do continuous integration like a boss](https://about.gitlab.com/blog/2017/11/27/go-tools-and-gitlab-how-to-do-continuous-integration-like-a-boss/)
+- [GitBot – automating boring Git operations with CI](https://about.gitlab.com/blog/2017/11/02/automating-boring-git-operations-gitlab-ci/)
+- [How to use GitLab CI for Vue.js](https://about.gitlab.com/blog/2017/09/12/vuejs-app-gitlab/)
 - Video: [GitLab CI/CD Deep Dive](https://youtu.be/pBe4t1CD8Fc?t=195)
-- [Dockerizing GitLab Review Apps](https://about.gitlab.com/2017/07/11/dockerizing-review-apps/)
-- [Fast and natural continuous integration with GitLab CI](https://about.gitlab.com/2017/05/22/fast-and-natural-continuous-integration-with-gitlab-ci/)
-- [Demo: CI/CD with GitLab in action](https://about.gitlab.com/2017/03/13/ci-cd-demo/)
+- [Dockerizing GitLab Review Apps](https://about.gitlab.com/blog/2017/07/11/dockerizing-review-apps/)
+- [Fast and natural continuous integration with GitLab CI](https://about.gitlab.com/blog/2017/05/22/fast-and-natural-continuous-integration-with-gitlab-ci/)
+- [Demo: CI/CD with GitLab in action](https://about.gitlab.com/blog/2017/03/13/ci-cd-demo/)
 
 ### Migrating to GitLab from third-party CI tools
 
@@ -109,17 +109,17 @@ For examples of others who have implemented GitLab CI/CD, see:
 
 To see how you can integrate GitLab CI/CD with third-party systems, see:
 
-- [Streamline and shorten error remediation with Sentry’s new GitLab integration](https://about.gitlab.com/2019/01/25/sentry-integration-blog-post/)
-- [How to simplify your smart home configuration with GitLab CI/CD](https://about.gitlab.com/2018/08/02/using-the-gitlab-ci-slash-cd-for-smart-home-configuration-management/)
-- [Demo: GitLab + Jira + Jenkins](https://about.gitlab.com/2018/07/30/gitlab-workflow-with-jira-jenkins/)
-- [Introducing Auto Breakfast from GitLab (sort of)](https://about.gitlab.com/2018/06/29/introducing-auto-breakfast-from-gitlab/)
+- [Streamline and shorten error remediation with Sentry’s new GitLab integration](https://about.gitlab.com/blog/2019/01/25/sentry-integration-blog-post/)
+- [How to simplify your smart home configuration with GitLab CI/CD](https://about.gitlab.com/blog/2018/08/02/using-the-gitlab-ci-slash-cd-for-smart-home-configuration-management/)
+- [Demo: GitLab + Jira + Jenkins](https://about.gitlab.com/blog/2018/07/30/gitlab-workflow-with-jira-jenkins/)
+- [Introducing Auto Breakfast from GitLab (sort of)](https://about.gitlab.com/blog/2018/06/29/introducing-auto-breakfast-from-gitlab/)
 
 ### Mobile development
 
 For help with using GitLab CI/CD for mobile application development, see:
 
-- [How to publish Android apps to the Google Play Store with GitLab and fastlane](https://about.gitlab.com/2019/01/28/android-publishing-with-gitlab-and-fastlane/)
-- [Setting up GitLab CI for Android projects](https://about.gitlab.com/2018/10/24/setting-up-gitlab-ci-for-android-projects/)
-- [Working with YAML in GitLab CI from the Android perspective](https://about.gitlab.com/2017/11/20/working-with-yaml-gitlab-ci-android/)
-- [How to use GitLab CI and MacStadium to build your macOS or iOS projects](https://about.gitlab.com/2017/05/15/how-to-use-macstadium-and-gitlab-ci-to-build-your-macos-or-ios-projects/)
-- [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
+- [How to publish Android apps to the Google Play Store with GitLab and fastlane](https://about.gitlab.com/blog/2019/01/28/android-publishing-with-gitlab-and-fastlane/)
+- [Setting up GitLab CI for Android projects](https://about.gitlab.com/blog/2018/10/24/setting-up-gitlab-ci-for-android-projects/)
+- [Working with YAML in GitLab CI from the Android perspective](https://about.gitlab.com/blog/2017/11/20/working-with-yaml-gitlab-ci-android/)
+- [How to use GitLab CI and MacStadium to build your macOS or iOS projects](https://about.gitlab.com/blog/2017/05/15/how-to-use-macstadium-and-gitlab-ci-to-build-your-macos-or-ios-projects/)
+- [Setting up GitLab CI for iOS projects](https://about.gitlab.com/blog/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md
index 1d4c9221cf297e959b8e1d5c11d5fd67a03ecb3f..49f4a14c5ace11094bd4dbb9c098413c29d4d950 100644
--- a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md
+++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md
@@ -16,14 +16,14 @@ description: "Continuous Deployment of a Spring Boot application to Cloud Foundr
 In this article, we'll demonstrate how to deploy a [Spring
 Boot](https://projects.spring.io/spring-boot/) application to [Cloud
 Foundry (CF)](https://www.cloudfoundry.org/) with GitLab CI/CD using the [Continuous
-Deployment](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#continuous-deployment)
+Deployment](https://about.gitlab.com/blog/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#continuous-deployment)
 method.
 
 All the code for this project can be found in this [GitLab
 repo](https://gitlab.com/gitlab-examples/spring-gitlab-cf-deploy-demo).
 
 In case you're interested in deploying Spring Boot applications to Kubernetes
-using GitLab CI/CD, read through the blog post [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/).
+using GitLab CI/CD, read through the blog post [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/blog/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/).
 
 ## Requirements
 
diff --git a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md
index e084d2fc20e5b108f0753311c21e831b33149598..e1c59f3b025b802e064914bac9d88aed48521890 100644
--- a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md
+++ b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md
@@ -254,7 +254,7 @@ pipeline to include running the tests along with the existing build job.
 
 To ensure our changes don't break the build and all tests still pass, we utilize
 Continuous Integration (CI) to run these checks automatically for every push.
-Read through this article to understand [Continuous Integration, Continuous Delivery, and Continuous Deployment](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/),
+Read through this article to understand [Continuous Integration, Continuous Delivery, and Continuous Deployment](https://about.gitlab.com/blog/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/),
 and how these methods are leveraged by GitLab.
 From the [last tutorial](https://ryanhallcs.wordpress.com/2017/03/15/devops-and-game-dev/) we already have a `.gitlab-ci.yml` file set up for building our app from
 every push. We need to set up a new CI job for testing, which GitLab CI/CD will run after the build job using our generated artifacts from gulp.
@@ -390,7 +390,7 @@ We have our codebase built and tested on every push. To complete the full pipeli
 let's set up [free web hosting with AWS S3](https://aws.amazon.com/s/dm/optimization/server-side-test/free-tier/free_np/) and a job through which our build artifacts get
 deployed. GitLab also has a free static site hosting service we could use, [GitLab Pages](https://about.gitlab.com/product/pages/),
 however Dark Nova specifically uses other AWS tools that necessitates using `AWS S3`.
-Read through this article that describes [deploying to both S3 and GitLab Pages](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
+Read through this article that describes [deploying to both S3 and GitLab Pages](https://about.gitlab.com/blog/2016/08/26/ci-deployment-and-environments/)
 and further delves into the principles of GitLab CI/CD than discussed in this article.
 
 ### Set up S3 Bucket
@@ -529,4 +529,4 @@ Here are some ideas to further investigate that can speed up or improve your pip
 - Set up a custom [Docker](../../../ci/docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) image that can preload dependencies and tools (like AWS CLI)
 - Forward a [custom domain](https://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html) to your game's S3 static website
 - Combine jobs if you find it unnecessary for a small project
-- Avoid the queues and set up your own [custom GitLab CI/CD runner](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/)
+- Avoid the queues and set up your own [custom GitLab CI/CD runner](https://about.gitlab.com/blog/2016/03/01/gitlab-runner-with-docker/)
diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
index 7407a8fbc386a69f2c26ab85d7667b3642367bbc..a7ed4ca3514d46bffe35817f5547d0f4dcf93307 100644
--- a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
+++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
@@ -15,7 +15,7 @@ last_updated: 2019-03-06
 
 GitLab features our applications with Continuous Integration, and it is possible to easily deploy the new code changes to the production server whenever we want.
 
-In this tutorial, we'll show you how to initialize a [Laravel](https://laravel.com) application and set up our [Envoy](https://laravel.com/docs/master/envoy) tasks, then we'll jump into see how to test and deploy it with [GitLab CI/CD](../README.md) via [Continuous Delivery](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/).
+In this tutorial, we'll show you how to initialize a [Laravel](https://laravel.com) application and set up our [Envoy](https://laravel.com/docs/master/envoy) tasks, then we'll jump into see how to test and deploy it with [GitLab CI/CD](../README.md) via [Continuous Delivery](https://about.gitlab.com/blog/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/).
 
 We assume you have a basic experience with Laravel, Linux servers,
 and you know how to use GitLab.
@@ -391,7 +391,7 @@ git push origin master
 ## Continuous Integration with GitLab
 
 We have our app ready on GitLab, and we also can deploy it manually.
-But let's take a step forward to do it automatically with [Continuous Delivery](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#continuous-delivery) method.
+But let's take a step forward to do it automatically with [Continuous Delivery](https://about.gitlab.com/blog/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#continuous-delivery) method.
 We need to check every commit with a set of automated tests to become aware of issues at the earliest, and then, we can deploy to the target environment if we are happy with the result of the tests.
 
 [GitLab CI/CD](../../README.md) allows us to use [Docker](https://www.docker.com) engine to handle the process of testing and deploying our app.
@@ -469,7 +469,7 @@ Congratulations! You just pushed the first Docker image to the GitLab Registry,
 ![container registry page with image](img/container_registry_page_with_image.jpg)
 
 >**Note:**
-You can also [use GitLab CI/CD](https://about.gitlab.com/2016/05/23/gitlab-container-registry/#use-with-gitlab-ci) to build and push your Docker images, rather than doing that on your machine.
+You can also [use GitLab CI/CD](https://about.gitlab.com/blog/2016/05/23/gitlab-container-registry/#use-with-gitlab-ci) to build and push your Docker images, rather than doing that on your machine.
 
 We'll use this image further down in the `.gitlab-ci.yml` configuration file to handle the process of testing and deploying our app.
 
@@ -605,7 +605,7 @@ The job `deploy_production` will deploy the app to the production server.
 To deploy our app with Envoy, we had to set up the `$SSH_PRIVATE_KEY` variable as an [SSH private key](../../ssh_keys/README.md#ssh-keys-when-using-the-docker-executor).
 If the SSH keys have added successfully, we can run Envoy.
 
-As mentioned before, GitLab supports [Continuous Delivery](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#continuous-delivery) methods as well.
+As mentioned before, GitLab supports [Continuous Delivery](https://about.gitlab.com/blog/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#continuous-delivery) methods as well.
 The [environment](../../yaml/README.md#environment) keyword tells GitLab that this job deploys to the `production` environment.
 The `url` keyword is used to generate a link to our application on the GitLab Environments page.
 The `only` keyword tells GitLab CI that the job should be executed only when the pipeline is building the `master` branch.
@@ -634,7 +634,7 @@ deploy_production:
     - master
 ```
 
-You may also want to add another job for [staging environment](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/), to final test your application before deploying to production.
+You may also want to add another job for [staging environment](https://about.gitlab.com/blog/2016/08/26/ci-deployment-and-environments/), to final test your application before deploying to production.
 
 ### Turn on GitLab CI/CD
 
diff --git a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md
index ded5dcbf5a210cd703e204d0d05bec6decc4778f..a81568d6cd4883fe886828065aa849465f0293dc 100644
--- a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md
+++ b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md
@@ -412,8 +412,8 @@ other reasons][ci-reasons] to keep using GitLab CI/CD. The benefits to our teams
 [mix-ecto]: https://hexdocs.pm/ecto/Mix.Tasks.Ecto.Create.html "mix and Ecto"
 [iex]: https://elixir-lang.org/getting-started/introduction.html#interactive-mode "Interactive Mode"
 [ci-lint]: https://gitlab.com/ci/lint "CI Lint Tool"
-[ci-reasons]: https://about.gitlab.com/2015/02/03/7-reasons-why-you-should-be-using-ci/ "7 Reasons Why You Should Be Using CI"
-[ci-guide]: https://about.gitlab.com/2015/12/14/getting-started-with-gitlab-and-gitlab-ci/ "Getting Started With GitLab And GitLab CI/CD"
+[ci-reasons]: https://about.gitlab.com/blog/2015/02/03/7-reasons-why-you-should-be-using-ci/ "7 Reasons Why You Should Be Using CI"
+[ci-guide]: https://about.gitlab.com/blog/2015/12/14/getting-started-with-gitlab-and-gitlab-ci/ "Getting Started With GitLab And GitLab CI/CD"
 [ci-docs]: ../../README.md "GitLab CI/CD Documentation"
 [skipping-jobs]: ../../yaml/README.md#skipping-jobs "Skipping Jobs"
 [gitlab-runners]: ../../runners/README.md "GitLab Runners Documentation"
diff --git a/doc/ci/merge_request_pipelines/index.md b/doc/ci/merge_request_pipelines/index.md
index e29a13e87af723dc7bac282fc6a9c196b1eb4377..a49279f19326d07451b752bf31a2ee15333af7f1 100644
--- a/doc/ci/merge_request_pipelines/index.md
+++ b/doc/ci/merge_request_pipelines/index.md
@@ -151,13 +151,13 @@ parent project. This means you cannot completely trust the pipeline result,
 because, technically, external contributors can disguise their pipeline results
 by tweaking their GitLab Runner in the forked project.
 
-There are multiple reasons about why GitLab doesn't allow those pipelines to be
+There are multiple reasons why GitLab doesn't allow those pipelines to be
 created in the parent project, but one of the biggest reasons is security concern.
 External users could steal secret variables from the parent project by modifying
 `.gitlab-ci.yml`, which could be some sort of credentials. This should not happen.
 
 We're discussing a secure solution of running pipelines for merge requests
-that submitted from forked projects,
+that are submitted from forked projects,
 see [the issue about the permission extension](https://gitlab.com/gitlab-org/gitlab-foss/issues/23902).
 
 ## Additional predefined variables
diff --git a/doc/ci/multi_project_pipelines.md b/doc/ci/multi_project_pipelines.md
index 762980a977c3fa7399d74dea8b14034067aa3288..093d334e9377bd5a14d7c5add4f751c21da2e98c 100644
--- a/doc/ci/multi_project_pipelines.md
+++ b/doc/ci/multi_project_pipelines.md
@@ -5,7 +5,7 @@ type: reference
 # Multi-project pipelines **(PREMIUM)**
 
 > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/2121) in
-[GitLab Premium 9.3](https://about.gitlab.com/2017/06/22/gitlab-9-3-released/#multi-project-pipeline-graphs).
+[GitLab Premium 9.3](https://about.gitlab.com/blog/2017/06/22/gitlab-9-3-released/#multi-project-pipeline-graphs).
 
 When you set up [GitLab CI/CD](README.md) across multiple projects, you can visualize
 the entire pipeline, including all cross-project inter-dependencies.
@@ -24,7 +24,7 @@ and when hovering or tapping (on touchscreen devices) they will expand and be sh
 ![Multi-project mini graph](img/multi_pipeline_mini_graph.gif)
 
 Multi-project pipelines are useful for larger products that require cross-project inter-dependencies, such as those
-adopting a [microservices architecture](https://about.gitlab.com/2016/08/16/trends-in-version-control-land-microservices/).
+adopting a [microservices architecture](https://about.gitlab.com/blog/2016/08/16/trends-in-version-control-land-microservices/).
 
 For a demonstration of how cross-functional development teams can use cross-pipeline
 triggering to trigger multiple pipelines for different microservices projects, see
@@ -119,7 +119,8 @@ Use:
 
 - The `project` keyword to specify the full path to a downstream project.
 - The `branch` keyword to specify the name of a branch in the project specified by `project`.
-  Variable expansion is supported.
+  [From GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/issues/10126), variable expansion is
+  supported.
 
 GitLab will use a commit that is currently on the HEAD of the branch when
 creating a downstream pipeline.
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index 0d0a40aceaaf43a705ab68fe3323a4898e1d1e82..e5f2701c6ae1c0f4ceb2d87377c827957eb8c2e1 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -283,11 +283,11 @@ You can also access pipelines for a merge request by navigating to its **Pipelin
 
 When you access a pipeline, you can see the related jobs for that pipeline.
 
-Clicking on an individual job will show you its job trace, and allow you to:
+Clicking on an individual job will show you its job log, and allow you to:
 
 - Cancel the job.
 - Retry the job.
-- Erase the job trace.
+- Erase the job log.
 
 ### Seeing the failure reason for jobs
 
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index f12597abf4304d1dd381a24c9aafd4b34c52c8fe..10a898be900d5737a69f0584c04b11be43ea1b59 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -235,7 +235,7 @@ Visit the [examples README][examples] to see a list of examples using GitLab
 CI with various languages.
 
 [runner-install]: https://docs.gitlab.com/runner/install/
-[blog-ci]: https://about.gitlab.com/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/
+[blog-ci]: https://about.gitlab.com/blog/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/
 [examples]: ../examples/README.md
 [ci]: https://about.gitlab.com/product/continuous-integration/
 [yaml]: ../yaml/README.md
diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md
index 7a1e6e4e1b8bc8d2947fea9fcc9d4fb9c9f0251e..da92fadafc4cb57e06b3dfeccb45c3572605e091 100644
--- a/doc/ci/review_apps/index.md
+++ b/doc/ci/review_apps/index.md
@@ -24,8 +24,8 @@ In the above example:
 
 - A Review App is built every time a commit is pushed to `topic branch`.
 - The reviewer fails two reviews before passing the third review.
-- Once the review as passed, `topic branch` is merged into `master` where it's deploy to staging.
-- After been approved in staging, the changes that were merged into `master` are deployed in to production.
+- Once the review has passed, `topic branch` is merged into `master` where it is deployed to staging.
+- After having been approved in staging, the changes that were merged into `master` are deployed in to production.
 
 ## How Review Apps work
 
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index f7d1a3e88a2511a81d06248d8927b96988a39312..4011ae4df7030f3269cf1755c8497d3db0c4cbb8 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -365,8 +365,8 @@ We're always looking for contributions that can mitigate these
 ### Resetting the registration token for a Project
 
 If you think that registration token for a Project was revealed, you should
-reset them. It's recommended because such token can be used to register another
-Runner to the Project. It may be next used to obtain the values of secret
+reset them. It's recommended because such a token can be used to register another
+Runner to the Project. It may then be used to obtain the values of secret
 variables or clone the project code, that normally may be unavailable for the
 attacker.
 
@@ -379,10 +379,10 @@ To reset the token:
 1. After the page is refreshed, expand the **Runners settings** section
    and check the registration token - it should be changed.
 
-From now on the old token is not valid anymore and will not allow to register
-a new Runner to the project. If you are using any tools to provision and
-register new Runners, you should now update the token that is used to the
-new value.
+From now on the old token is no longer valid and will not register
+any new Runners to the project. If you are using any tools to provision and
+register new Runners, the tokens used in those tools should be updated to reflect the
+value of the new token.
 
 ## Determining the IP address of a Runner
 
diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
index 64dc47f07cf58e5206580e13053063bc67714854..bee1501aed8e2cc226e11ece6dce706105882873 100644
--- a/doc/ci/ssh_keys/README.md
+++ b/doc/ci/ssh_keys/README.md
@@ -35,8 +35,8 @@ with any type of [executor](https://docs.gitlab.com/runner/executors/)
    if you are accessing a private GitLab repository.
 
 NOTE: **Note:**
-The private key will not be displayed in the job trace, unless you enable
-[debug tracing](../variables/README.md#debug-tracing). You might also want to
+The private key will not be displayed in the job log, unless you enable
+[debug logging](../variables/README.md#debug-logging). You might also want to
 check the [visibility of your pipelines](../../user/project/pipelines/settings.md#visibility-of-pipelines).
 
 ## SSH keys when using the Docker executor
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index d2efae8ebef72396ac466defc9088f81c58ab965..82cbd40c4c6b9926295a8e58c505b7e83a002b6d 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -6,7 +6,7 @@ type: tutorial
 
 > **Notes**:
 >
-> - [Introduced](https://about.gitlab.com/2015/08/22/gitlab-7-14-released/) in GitLab 7.14.
+> - [Introduced](https://about.gitlab.com/blog/2015/08/22/gitlab-7-14-released/) in GitLab 7.14.
 > - GitLab 8.12 has a completely redesigned job permissions system. Read all
 >   about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#pipeline-triggers).
 
@@ -157,7 +157,7 @@ curl --request POST \
 You can also benefit by using triggers in your `.gitlab-ci.yml`. Let's say that
 you have two projects, A and B, and you want to trigger a rebuild on the `master`
 branch of project B whenever a tag on project A is created. This is the job you
-need to add in project's A `.gitlab-ci.yml`:
+need to add in project A's `.gitlab-ci.yml`:
 
 ```yaml
 build_docs:
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 34e5bcd9601a4aa3a2d8832b33c83f52e9c89eb6..5d86d382aa8269b42ffa7f08f39fa00dd8b0970c 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -568,7 +568,7 @@ Below you can find supported syntax reference:
    Precedence of operators follows standard Ruby 2.5 operation
    [precedence](https://ruby-doc.org/core-2.5.0/doc/syntax/precedence_rdoc.html).
 
-## Debug tracing
+## Debug logging
 
 > Introduced in GitLab Runner 1.7.
 
@@ -576,24 +576,24 @@ CAUTION: **Warning:**
 Enabling debug tracing can have severe security implications. The
 output **will** contain the content of all your variables and any other
 secrets! The output **will** be uploaded to the GitLab server and made visible
-in job traces!
+in job logs!
 
 By default, GitLab Runner hides most of the details of what it is doing when
-processing a job. This behavior keeps job traces short, and prevents secrets
-from being leaked into the trace unless your script writes them to the screen.
+processing a job. This behavior keeps job logs short, and prevents secrets
+from being leaked into the log unless your script writes them to the screen.
 
 If a job isn't working as expected, this can make the problem difficult to
 investigate; in these cases, you can enable debug tracing in `.gitlab-ci.yml`.
 Available on GitLab Runner v1.7+, this feature enables the shell's execution
-trace, resulting in a verbose job trace listing all commands that were run,
+log, resulting in a verbose job log listing all commands that were run,
 variables that were set, etc.
 
 Before enabling this, you should ensure jobs are visible to
 [team members only](../../user/permissions.md#project-features). You should
-also [erase](../pipelines.md#accessing-individual-jobs) all generated job traces
+also [erase](../pipelines.md#accessing-individual-jobs) all generated job logs
 before making them visible again.
 
-To enable debug traces, set the `CI_DEBUG_TRACE` variable to `true`:
+To enable debug logs (traces), set the `CI_DEBUG_TRACE` variable to `true`:
 
 ```yaml
 job_name:
@@ -601,7 +601,7 @@ job_name:
     CI_DEBUG_TRACE: "true"
 ```
 
-Example truncated output with debug trace set to true:
+Example truncated output with `CI_DEBUG_TRACE` set to `true`:
 
 ```bash
 ...
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index 52f712abfae9cc31a17b0f3423883721d3b17c79..20e70d212b0f95c1e863af394fa3d0ad9055aaa1 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -40,13 +40,14 @@ future GitLab releases.**
 | `CI_COMMIT_TAG`                         | 9.0    | 0.5    | The commit tag name. Present only when building tags. |
 | `CI_COMMIT_TITLE`                       | 10.8   | all    | The title of the commit - the full first line of the message |
 | `CI_CONFIG_PATH`                        | 9.4    | 0.5    | The path to CI config file. Defaults to `.gitlab-ci.yml` |
-| `CI_DEBUG_TRACE`                        | all    | 1.7    | Whether [debug tracing](README.md#debug-tracing) is enabled |
+| `CI_DEBUG_TRACE`                        | all    | 1.7    | Whether [debug logging (tracing)](README.md#debug-logging) is enabled |
 | `CI_DEPLOY_PASSWORD`                    | 10.8   | all    | Authentication password of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
 | `CI_DEPLOY_USER`                        | 10.8   | all    | Authentication username of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
 | `CI_DISPOSABLE_ENVIRONMENT`             | all    | 10.1   | Marks that the job is executed in a disposable environment (something that is created only for this job and disposed of/destroyed after the execution - all executors except `shell` and `ssh`). If the environment is disposable, it is set to true, otherwise it is not defined at all. |
 | `CI_ENVIRONMENT_NAME`                   | 8.15   | all    | The name of the environment for this job. Only present if [`environment:name`](../yaml/README.md#environmentname) is set. |
 | `CI_ENVIRONMENT_SLUG`                   | 8.15   | all    | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. Only present if [`environment:name`](../yaml/README.md#environmentname) is set. |
 | `CI_ENVIRONMENT_URL`                    | 9.3    | all    | The URL of the environment for this job. Only present if [`environment:url`](../yaml/README.md#environmenturl) is set. |
+| `CI_DEFAULT_BRANCH`                     | 12.4   | all    | The name of the default branch for the project. |
 | `CI_JOB_ID`                             | 9.0    | all    | The unique id of the current job that GitLab CI uses internally |
 | `CI_JOB_MANUAL`                         | 8.12   | all    | The flag to indicate that job was manually started |
 | `CI_JOB_NAME`                           | 9.0    | 0.5    | The name of the job as defined in `.gitlab-ci.yml` |
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 1a7c358eb91eccf1820ebd8a549db262b3a73eb6..4569e9ff9b6483bedca642eb79521255397fafac 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -242,10 +242,10 @@ For more information, see see [Available settings for `services`](../docker/usin
 
 `before_script` is used to define the command that should be run before all
 jobs, including deploy jobs, but after the restoration of [artifacts](#artifacts).
-This can be an array or a multi-line string.
+This must be an an array.
 
 `after_script` is used to define the command that will be run after all
-jobs, including failed ones. This has to be an array or a multi-line string.
+jobs, including failed ones. This must be an an array.
 
 Scripts specified in `before_script` are:
 
@@ -318,6 +318,17 @@ There are also two edge cases worth mentioning:
    `test` and `deploy` are allowed to be used as job's stage by default.
 1. If a job doesn't specify a `stage`, the job is assigned the `test` stage.
 
+#### `.pre` and `.post`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/31441) in GitLab 12.4.
+
+The following stages are available to every pipeline:
+
+- `.pre`, which is guaranteed to always be the first stage in a pipeline.
+- `.post`, which is guaranteed to always be the last stage in a pipeline.
+
+User-defined stages are executed after `.pre` and before `.post`.
+
 ### `stage`
 
 `stage` is defined per-job and relies on [`stages`](#stages) which is defined
@@ -330,6 +341,10 @@ stages:
   - test
   - deploy
 
+job 0:
+  stage: .pre
+  script: make something useful before build stage
+
 job 1:
   stage: build
   script: make build dependencies
@@ -345,6 +360,10 @@ job 3:
 job 4:
   stage: deploy
   script: make deploy
+
+job 5:
+  stage: .post
+  script: make something useful at the end of pipeline
 ```
 
 #### Using your own Runners
@@ -1086,13 +1105,53 @@ Manual actions are considered to be write actions, so permissions for
 [protected branches](../../user/project/protected_branches.md) are used when
 a user wants to trigger an action. In other words, in order to trigger a manual
 action assigned to a branch that the pipeline is running for, the user needs to
-have the ability to merge to this branch.
+have the ability to merge to this branch. It is possible to use protected environments
+to more strictly [protect manual deployments](#protecting-manual-jobs-premium) from being
+run by unauthorized users.
 
 NOTE: **Note:**
 Using `when:manual` and `trigger` together results in the error `jobs:#{job-name} when
 should be on_success, on_failure or always`, because `when:manual` prevents triggers
 being used.
 
+##### Protecting manual jobs **(PREMIUM)**
+
+It's possible to use [protected environments](../environments/protected_environments.md)
+to define a precise list of users authorized to run a manual job. By allowing only
+users associated with a protected environment to trigger manual jobs, it is possible
+to implement some special use cases, such as:
+
+- More precisely limiting who can deploy to an environment.
+- Enabling a pipeline to be blocked until an approved user "approves" it.
+
+To do this, you must:
+
+1. Add an `environment` to the job. For example:
+
+   ```yaml
+   deploy_prod:
+     stage: deploy
+     script:
+       - echo "Deploy to production server"
+     environment:
+       name: production
+       url: https://example.com
+     when: manual
+     only:
+       - master
+   ```
+
+1. In the [protected environments settings](../environments/protected_environments.md#protecting-environments),
+   select the environment (`production` in the example above) and add the users, roles or groups
+   that are authorized to trigger the manual job to the **Allowed to Deploy** list. Only those in
+   this list will be able to trigger this manual job, as well as GitLab administrators
+   who are always able to use protected environments.
+
+Additionally, if a manual job is defined as blocking by adding `allow_failure: false`,
+the next stages of the pipeline will not run until the manual job is triggered. This
+can be used as a way to have a defined list of users allowed to "approve" later pipeline
+stages by triggering the blocking manual job.
+
 #### `when:delayed`
 
 > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/21767) in GitLab 11.4.
diff --git a/doc/development/README.md b/doc/development/README.md
index 7e1a563ea025d6ec1b73e2738ff4efa78b1ff9b7..e3ec460a6fe00c8c0bf3e0fad1e29efc602866a5 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -99,7 +99,7 @@ description: 'Learn how to contribute to GitLab.'
 - [Post deployment migrations](post_deployment_migrations.md)
 - [Background migrations](background_migrations.md)
 - [Swapping tables](swapping_tables.md)
-- [Deleting exiting migrations](deleting_migrations.md)
+- [Deleting migrations](deleting_migrations.md)
 
 ### Best practices
 
@@ -151,6 +151,10 @@ description: 'Learn how to contribute to GitLab.'
 - [Frontend tracking guide](event_tracking/frontend.md)
 - [Backend tracking guide](event_tracking/backend.md)
 
+## Experiment Guide
+
+- [Introduction](experiment_guide/index.md)
+
 ## Build guides
 
 - [Building a package for testing purposes](build_test_package.md)
diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md
index 793b1fbb2f5e4051b41daf71a67ced20269d3a78..cdd0e9b2a7b0e58b80c44b2e3cafbd423b21c507 100644
--- a/doc/development/api_graphql_styleguide.md
+++ b/doc/development/api_graphql_styleguide.md
@@ -539,3 +539,8 @@ it 'returns a successful response' do
    expect(graphql_mutation_response(:merge_request_set_wip)['errors']).to be_empty
 end
 ```
+
+## Documentation
+
+For information on generating GraphQL documentation, see
+[Rake tasks related to GraphQL](rake_tasks.md#update-graphql-documentation).
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index ee2a426db97dcea5861d75c70bdfb00555c7b419..ccedb96d27d08824366e0a1fe7933a03f1a46096 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -267,7 +267,7 @@ GitLab CI is the open-source continuous integration service included with GitLab
 - Layer: Core Service (Processor)
 - Process: `gitlab-workhorse`
 
-[GitLab Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse) is a program designed at GitLab to help alleviate pressure from Unicorn. You can read more about the [historical reasons for developing](https://about.gitlab.com/2016/04/12/a-brief-history-of-gitlab-workhorse/). It's designed to act as a smart reverse proxy to help speed up GitLab as a whole.
+[GitLab Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse) is a program designed at GitLab to help alleviate pressure from Unicorn. You can read more about the [historical reasons for developing](https://about.gitlab.com/blog/2016/04/12/a-brief-history-of-gitlab-workhorse/). It's designed to act as a smart reverse proxy to help speed up GitLab as a whole.
 
 #### Grafana
 
diff --git a/doc/development/chatops_on_gitlabcom.md b/doc/development/chatops_on_gitlabcom.md
index a1c07ee2a1e8de46229550b5edcff6b45f6ba433..8a313a120f1a2489c17a71769b4bf42760468e82 100644
--- a/doc/development/chatops_on_gitlabcom.md
+++ b/doc/development/chatops_on_gitlabcom.md
@@ -14,7 +14,7 @@ tasks such as:
 To request access to Chatops on GitLab.com:
 
 1. Log into <https://ops.gitlab.net/users/sign_in> **using the same username** as for GitLab.com (you may have to rename it).
-1. Ask [anyone in the `chatops` project](https://gitlab.com/gitlab-com/chatops/-/project_members) to add you by running `/chatops run member add <username> gitlab-com/chatops --ops`.
+1. Ask [an owner/maintainer in the `chatops` project](https://gitlab.com/gitlab-com/chatops/-/project_members?search=&sort=access_level_desc) to add you by running `/chatops run member add <username> gitlab-com/chatops --ops`.
 
 ## See also
 
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index 988f82118cb35fb51d0679ae0c344df9ac73c343..b6ec7a858fafe6deef605bc1d9021794f7b38b12 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -492,19 +492,50 @@ For other punctuation rules, please refer to the
 
 - Use inline link markdown markup `[Text](https://example.com)`.
   It's easier to read, review, and maintain. **Do not** use `[Text][identifier]`.
-- To link to internal documentation, use relative links, not full URLs. Use `../` to
-  navigate to high-level directories, and always add the file name `file.md` at the
-  end of the link with the `.md` extension, not `.html`.
-  Example: instead of `[text](../../merge_requests/)`, use
-  `[text](../../merge_requests/index.md)` or, `[text](../../ci/README.md)`, or,
-  for anchor links, `[text](../../ci/README.md#examples)`.
-  Using the markdown extension is necessary for the [`/help`](index.md#gitlab-help)
-  section of GitLab.
-- To link from CE to EE-only documentation, use the EE-only doc full URL.
+
 - Use [meaningful anchor texts](https://www.futurehosting.com/blog/links-should-have-meaningful-anchor-text-heres-why/).
   E.g., instead of writing something like `Read more about GitLab Issue Boards [here](LINK)`,
   write `Read more about [GitLab Issue Boards](LINK)`.
 
+### Links to internal documentation
+
+- To link to internal documentation, use relative links, not full URLs.
+  Use `../` to navigate to high-level directories. Links should not refer to root.
+
+  Don't:
+
+  ```md
+  [Geo Troubleshooting](https://docs.gitlab.com/ee/administration/geo/replication/troubleshooting.html)
+  [Geo Troubleshooting](/ee/administration/geo/replication/troubleshooting.md)
+  ```
+
+  Do:
+
+  ```md
+  [Geo Troubleshooting](../../geo/replication/troubleshooting.md)
+  ```
+
+- Always add the file name `file.md` at the end of the link with the `.md` extension, not `.html`.
+
+  Don't:
+
+  ```md
+  [merge requests](../../merge_requests/)
+  [issues](../../issues/tags.html)
+  [issue tags](../../issues/tags.html#stages)
+  ```
+
+  Do:
+
+  ```md
+  [merge requests](../../merge_requests/index.md)
+  [issues](../../issues/tags.md)
+  [issue tags](../../issues/tags.md#stages)
+  ```
+
+- Using the markdown extension is necessary for the [`/help`](index.md#gitlab-help)
+  section of GitLab.
+
 ### Links requiring permissions
 
 Don't link directly to:
@@ -1020,6 +1051,38 @@ In this case:
 - Different highlighting languages are used for each config in the code block.
 - The [GitLab Restart](#gitlab-restart) section is used to explain a required restart/reconfigure of GitLab.
 
+## Feature flags
+
+Sometimes features are shipped with feature flags, either:
+
+- On by default, but providing the option to turn the feature off.
+- Off by default, but providing the option to turn the feature on.
+
+When documenting feature flags for a feature, it's important that users know:
+
+- Why a feature flag is necessary. Some of the reasons are
+  [outlined in the handbook](https://about.gitlab.com/handbook/product/#alpha-beta-ga).
+- That administrative access is required to make a feature flag change.
+- What to ask for when requesting a change to a feature flag's state.
+
+NOTE: **Note:**
+The [Product Manager for the relevant group](https://about.gitlab.com/handbook/product/categories/#devops-stages)
+must review and approve the addition or removal of any mentions of using feature flags before the doc change is merged.
+
+The following is sample text for adding feature flag documentation for a feature:
+
+````md
+### Disabling the feature
+
+This feature comes with the `:feature_flag` feature flag enabled by default. However, in some cases
+this feature is incompatible with old configuration. To turn off the feature while configuration is
+migrated, ask a GitLab administrator with Rails console access to run the following command:
+
+```ruby
+Feature.disable(:feature_flag)
+```
+````
+
 ## API
 
 Here is a list of must-have items. Use them in the exact order that appears
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index f89371f38ce628619aee8d47f39c42c0f09904ac..cc9df4794929369dd8fe6b3e09f31ee438500c64 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -20,9 +20,9 @@ should be added for EE. Licensed features can be stubbed using the
 spec helper `stub_licensed_features` in `EE::LicenseHelpers`.
 
 You can force GitLab to act as CE by either deleting the `ee/` directory or by
-setting the [`IS_GITLAB_EE` environment variable](https://gitlab.com/gitlab-org/gitlab/blob/master/config/helpers/is_ee_env.js)
-to something that evaluates as `false`. The same works for running tests
-(for example `IS_GITLAB_EE=0 yarn jest`).
+setting the [`FOSS_ONLY` environment variable](https://gitlab.com/gitlab-org/gitlab/blob/master/config/helpers/is_ee_env.js)
+to something that evaluates as `true`. The same works for running tests
+(for example `FOSS_ONLY=1 yarn jest`).
 
 [ee-as-ce]: https://gitlab.com/gitlab-org/gitlab/issues/2500
 
diff --git a/doc/development/experiment_guide/index.md b/doc/development/experiment_guide/index.md
new file mode 100644
index 0000000000000000000000000000000000000000..5155433c9ad27c14269b4bd4d09f74be585d0596
--- /dev/null
+++ b/doc/development/experiment_guide/index.md
@@ -0,0 +1,65 @@
+# Experiment Guide
+
+Experiments will be conducted by teams from the [Growth Section](https://about.gitlab.com/handbook/engineering/development/growth/) and are not tied to releases, because they will primarily target GitLab.com.
+
+Experiments will be run as an A/B test and will be behind a feature flag to turn the test on or off. Based on the data the experiment generates, the team decides if the experiment had a positive impact and will be the new default or rolled back.
+
+## Follow-up issue
+
+Each experiment requires a follow-up issue to resolve the experiment. Immediately after an experiment is deployed, the deadline of the issue needs to be set (this depends on the experiment but can be up to a few weeks in the future).
+After the deadline, the issue needs to be resolved and either:
+
+- It was successful and the experiment will be the new default.
+- It was not successful and all code related to the experiment will be removed.
+
+In either case, an outcome of the experiment should be posted to the issue with the reasoning for the decision.
+
+## Code reviews
+
+Since the code of experiments will not be part of the codebase for a long time and we want to iterate fast to retrieve data,the code quality of experiments might sometimes not fulfill our standards but should not negatively impact the availability of GitLab whether the experiment is running or not.
+Experiments will only be deployed to a fraction of users but we still want a flawless experience for those users. Therefore, experiments still require tests.
+
+For reviewers and maintainers: if you find code that would usually not make it through the review, but is temporarily acceptable, please mention your concerns but note that it's not necessary to change.
+The author then adds a comment to this piece of code and adds a link to the issue that resolves the experiment.
+
+## How to create an A/B test
+
+- [ ] Add the experiment to the `Gitlab::Experimentation::EXPERIMENTS` hash in [`experimentation.rb`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib%2Fgitlab%2Fexperimentation.rb):
+
+  ```ruby
+  EXPERIMENTS = {
+    other_experiment: {
+      #...
+    },
+    # Add your experiment here:
+    sign_up_flow: {
+      feature_toggle: :experimental_sign_up_flow, # Feature flag that will be used
+      environment: ::Gitlab.dev_env_or_com?, # Target environment
+      enabled_ratio: 0.1 # Percentage of users that will be part of the experiment. 10% of the users would be part of this experiments.
+    }
+  }.freeze
+  ```
+
+- [ ] Use the experiment in a controller:
+
+  ```ruby
+  class RegistrationController < Applicationcontroller
+   def show
+     # experiment_enabled?(:feature_name) is also available in views and helpers
+     if experiment_enabled?(:sign_up_flow)
+       # render the experiment
+     else
+       # render the original version
+     end
+   end
+  end
+  ```
+
+- [ ] Track necessery events. See the [event tracking guide](../event_tracking/index.md) for details.
+- [ ] After the merge request is merged, use [`chatops`](../../ci/chatops/README.md) to enable the feature flag and start the experiment. For visibility, please run the command in the `#s_growth` channel:
+
+  ```
+  /chatops run feature set --project=gitlab-org/gitlab experimental_sign_up_flow true
+  ```
+
+  If you notice issues with the experiment, you can disable the experiment by setting the feature flag to `false` again.
diff --git a/doc/development/filtering_by_label.md b/doc/development/filtering_by_label.md
index 6aa7e7a529348ce235ccf83590c00e0c51a52779..32df54eafd365ca947437f47bdbb7b40b85ff142 100644
--- a/doc/development/filtering_by_label.md
+++ b/doc/development/filtering_by_label.md
@@ -79,7 +79,7 @@ it did not improve query performance.
 
 ## Attempt B: Denormalize using an array column
 
-Having [removed MySQL support in GitLab 12.1](https://about.gitlab.com/2019/06/27/removing-mysql-support/),
+Having [removed MySQL support in GitLab 12.1](https://about.gitlab.com/blog/2019/06/27/removing-mysql-support/),
 using [Postgres's arrays](https://www.postgresql.org/docs/9.6/arrays.html) became more
 tractable as we didn't have to support two databases. We discussed denormalizing
 the `label_links` table for querying in
diff --git a/doc/development/issuable-like-models.md b/doc/development/issuable-like-models.md
index 27cac825b7f401ad91a7de1008faa19cc8020e28..ce19fd77496ebca16e57eef2836647e5f50fe78a 100644
--- a/doc/development/issuable-like-models.md
+++ b/doc/development/issuable-like-models.md
@@ -11,8 +11,8 @@ There are max length constraints for the most important text fields for `Issuabl
 
 - `title`: 255 chars
 - `title_html`: 800 chars
-- `description`: 16000 chars
-- `description_html`: 48000 chars
+- `description`: 1 megabyte
+- `description_html`: 5 megabytes
 
 [Issue]: https://docs.gitlab.com/ee/user/project/issues
 [Merge Requests]: https://docs.gitlab.com/ee/user/project/merge_requests
diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md
index 6520c7dbbcf87bbf36857167e8f2dedb2d34702f..5954de03db4ba8d1bf14a3f965841a257512fa4a 100644
--- a/doc/development/pipelines.md
+++ b/doc/development/pipelines.md
@@ -102,7 +102,7 @@ These common definitions are:
   `docker.elastic.co/elasticsearch/elasticsearch:5.6.12` services.
 - `.only-ee`: Only creates a job for the `gitlab` project.
 - `.only-ee-as-if-foss`: Same as `.only-ee` but simulate the FOSS project by
-  setting the `IS_GITLAB_EE='0'` environment variable.
+  setting the `FOSS_ONLY='1'` environment variable.
 
 ## Changes detection
 
@@ -115,6 +115,7 @@ from a commit or MR by extending from the following CI definitions:
 - `.only-qa-changes`: Allows a job to only be created upon QA-related changes.
 - `.only-docs-changes`: Allows a job to only be created upon docs-related changes.
 - `.only-code-qa-changes`: Allows a job to only be created upon code-related or QA-related changes.
+- `.only-graphql-changes`: Allows a job to only be created upon graphql-related changes.
 
 **See <https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/global.gitlab-ci.yml>
 for the list of exact patterns.**
@@ -127,7 +128,7 @@ execute jobs out of order for the following jobs:
 ```mermaid
 graph RL;
   A[setup-test-env];
-  B["gitlab:assets:compile<br/>(master only)"];
+  B["gitlab:assets:compile pull-push-cache<br/>(master only)"];
   C[gitlab:assets:compile pull-cache];
   D["cache gems<br/>(master and tags only)"];
   E[review-build-cng];
@@ -136,7 +137,7 @@ graph RL;
   G2["schedule:review-deploy<br/>(master only)"];
   H[karma];
   I[jest];
-  J["compile-assets<br/>(master only)"];
+  J["compile-assets pull-push-cache<br/>(master only)"];
   K[compile-assets pull-cache];
   L[webpack-dev-server];
   M[coverage];
@@ -145,39 +146,42 @@ graph RL;
   P["schedule:package-and-qa<br/>(master schedule only)"];
   Q[package-and-qa];
   R[package-and-qa-manual];
+  S["RSpec<br/>(e.g. rspec unit pg9)"]
+  T[retrieve-tests-metadata];
 
 subgraph "`prepare` stage"
     A
     F
-    J
     K
+    J
+    T
     end
 
 subgraph "`test` stage"
     B --> |needs| A;
     C --> |needs| A;
     D --> |needs| A;
-    H -.-> |depends on| A;
-    H -.-> |depends on| J;
-    H -.-> |depends on| K;
-    I -.-> |depends on| A;
-    I -.-> |depends on| J;
-    I -.-> |depends on| K;
-    L -.-> |depends on| A;
-    L -.-> |depends on| J;
-    L -.-> |depends on| K;
+    H -.-> |needs and depends on| A;
+    H -.-> |needs and depends on| K;
+    I -.-> |needs and depends on| A;
+    I -.-> |needs and depends on| K;
+    L -.-> |needs and depends on| A;
+    L -.-> |needs and depends on| K;
+    O -.-> |needs and depends on| A;
+    O -.-> |needs and depends on| K;
+    S -.-> |needs and depends on| A;
+    S -.-> |needs and depends on| K;
+    S -.-> |needs and depends on| T;
     downtime_check --> |needs and depends on| A;
     db:* --> |needs| A;
     gitlab:setup --> |needs| A;
-    O -.-> |depends on| A;
-    O -.-> |depends on| B;
-    O -.-> |depends on| C;
     downtime_check --> |needs and depends on| A;
+    graphql-docs-verify --> |needs| A;
     end
 
 subgraph "`review-prepare` stage"
     E --> |needs| C;
-    X["schedule:review-build-cng<br/>(master schedule only)"] --> |needs| B;
+    X["schedule:review-build-cng<br/>(master schedule only)"] --> |needs| C;
     end
 
 subgraph "`review` stage"
@@ -190,7 +194,7 @@ subgraph "`qa` stage"
     Q --> |needs| F;
     R --> |needs| C;
     R --> |needs| F;
-    P --> |needs| B;
+    P --> |needs| C;
     P --> |needs| F;
     review-qa-smoke -.-> |needs and depends on| G;
     review-qa-all -.-> |needs and depends on| G;
@@ -209,7 +213,7 @@ subgraph "`post-test` stage"
     end
 
 subgraph "`pages` stage"
-    N -.-> |depends on| B;
+    N -.-> |depends on| C;
     N -.-> |depends on| H;
     N -.-> |depends on| M;
     end
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index 20604cce9c694149bd1ec515b70d77f38e3698de..a6d3c008686bfeefdabb0b5330300bbdb23b31d4 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -220,3 +220,26 @@ bundle exec rake db:obsolete_ignored_columns
 ```
 
 Feel free to remove their definitions from their `ignored_columns` definitions.
+
+## Update GraphQL Documentation
+
+To generate GraphQL documentation based on the GitLab schema, run:
+
+```shell
+bundle exec rake gitlab:graphql:compile_docs
+```
+
+In its current state, the rake task:
+
+- Generates output for GraphQL objects.
+- Places the output at `doc/api/graphql/reference/index.md`.
+
+This uses some features from `graphql-docs` gem like its schema parser and helper methods.
+The docs generator code comes from our side giving us more flexibility, like using Haml templates and generating Markdown files.
+
+To edit the template used, please take a look at `lib/gitlab/graphql/docs/templates/default.md.haml`.
+The actual renderer is at `Gitlab::Graphql::Docs::Renderer`.
+
+`@parsed_schema` is an instance variable that the `graphql-docs` gem expects to have available.
+`Gitlab::Graphql::Docs::Helper` defines the `object` method we currently use. This is also where you
+should implement any new methods for new types you'd like to display.
diff --git a/doc/development/sidekiq_style_guide.md b/doc/development/sidekiq_style_guide.md
index c181b31f069952a71fb1367c1831112ee572c72c..d52a3e652e3ea5aaafce05d487cd2879866b44a7 100644
--- a/doc/development/sidekiq_style_guide.md
+++ b/doc/development/sidekiq_style_guide.md
@@ -61,6 +61,56 @@ the extra jobs will take resources away from jobs from workers that were already
 there, if the resources available to the Sidekiq process handling the namespace
 are not adjusted appropriately.
 
+## Feature Categorization
+
+Each Sidekiq worker, or one of its ancestor classes, must declare a
+`feature_category` attribute. This attribute maps each worker to a feature
+category. This is done for error budgeting, alert routing, and team attribution
+for Sidekiq workers.
+
+The declaration uses the `feature_category` class method, as shown below.
+
+```ruby
+class SomeScheduledTaskWorker
+  include ApplicationWorker
+
+  # Declares that this feature is part of the
+  # `continuous_integration` feature category
+  feature_category :continuous_integration
+
+  # ...
+end
+```
+
+The list of value values can be found in the file `config/feature_categories.yml`.
+This file is, in turn generated from the [`stages.yml` from the GitLab Company Handbook
+source](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/stages.yml).
+
+### Updating `config/feature_categories.yml`
+
+Occassionally new features will be added to GitLab stages. When this occurs, you
+can automatically update `config/feature_categories.yml` by running
+`scripts/update-feature-categories`. This script will fetch and parse
+[`stages.yml`](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/stages.yml)
+and generare a new version of the file, which needs to be checked into source control.
+
+### Excluding Sidekiq workers from feature categorization
+
+A few Sidekiq workers, that are used across all features, cannot be mapped to a
+single category. These should be declared as such using the `feature_category_not_owned!`
+ declaration, as shown below:
+
+```ruby
+class SomeCrossCuttingConcernWorker
+  include ApplicationWorker
+
+  # Declares that this worker does not map to a feature category
+  feature_category_not_owned!
+
+  # ...
+end
+```
+
 ## Tests
 
 Each Sidekiq worker must be tested using RSpec, just like any other class. These
diff --git a/doc/development/testing_guide/end_to_end/quick_start_guide.md b/doc/development/testing_guide/end_to_end/quick_start_guide.md
index db32a9a87cf3ecfbcd12be20cf970bc25418c353..2457d8ada5accd8508b8ab4235adcea98ed7de7f 100644
--- a/doc/development/testing_guide/end_to_end/quick_start_guide.md
+++ b/doc/development/testing_guide/end_to_end/quick_start_guide.md
@@ -38,7 +38,7 @@ The GitLab QA end-to-end tests are organized by the different [stages in the Dev
 
 > There may be sub-directories inside the stages directories, for different features. For example: `.../browser_ui/2_plan/ee_epics/` and `.../browser_ui/2_plan/issues/`.
 
-Now, let's say we want to create tests for the [scoped labels](https://about.gitlab.com/2019/04/22/gitlab-11-10-released/#scoped-labels) feature, available on GitLab EE Premium (this feature is part of the Plan stage.)
+Now, let's say we want to create tests for the [scoped labels](https://about.gitlab.com/blog/2019/04/22/gitlab-11-10-released/#scoped-labels) feature, available on GitLab EE Premium (this feature is part of the Plan stage.)
 
 > Because these tests are for a feature available only on GitLab EE, we need to create them in the [EE repository](https://gitlab.com/gitlab-org/gitlab).
 
diff --git a/doc/development/testing_guide/flaky_tests.md b/doc/development/testing_guide/flaky_tests.md
index 129876f0c8e7faf89e67dd286703680f3fb8cba1..3a96f8204fc8668bce2eb40b808726aee117b207 100644
--- a/doc/development/testing_guide/flaky_tests.md
+++ b/doc/development/testing_guide/flaky_tests.md
@@ -80,6 +80,10 @@ This was originally implemented in: <https://gitlab.com/gitlab-org/gitlab-foss/m
 - [Bis](https://gitlab.com/gitlab-org/gitlab-foss/issues/34609#note_34048715): <https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/12604>
 - [Bis](https://gitlab.com/gitlab-org/gitlab-foss/issues/34698#note_34276286): <https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/12664>
 - [Assert against the underlying database state instead of against a page's content](https://gitlab.com/gitlab-org/gitlab-foss/issues/31437): <https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/10934>
+- In JS tests, shifting elements can cause Capybara to misclick when the element moves at the exact time Capybara sends the click
+  - [Dropdowns rendering upward or downward due to window size and scroll position](https://gitlab.com/gitlab-org/gitlab/merge_requests/17660)
+  - [Lazy loaded images can cause Capybara to misclick](https://gitlab.com/gitlab-org/gitlab/merge_requests/18713)
+- [Triggering JS events before the event handlers are set up](https://gitlab.com/gitlab-org/gitlab/merge_requests/18742)
 
 #### Capybara viewport size related issues
 
diff --git a/doc/development/utilities.md b/doc/development/utilities.md
index 38e416d68e4d8f8afd73a97928eb151b4e34c908..a5f6fec4cd8627d5590f1c46f73e169dedefcdd7 100644
--- a/doc/development/utilities.md
+++ b/doc/development/utilities.md
@@ -1,6 +1,6 @@
 # GitLab utilities
 
-We developed a number of utilities to ease development.
+We have developed a number of utilities to help ease development:
 
 ## `MergeHash`
 
@@ -51,15 +51,15 @@ Refer to: <https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/utils/mer
 
 Refer to <https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/utils/override.rb>:
 
-- This utility could help us check if a particular method would override
-  another method or not. It has the same idea of Java's `@Override` annotation
-  or Scala's `override` keyword. However we only do this check when
+- This utility can help you check if one method would override
+  another or not. It is the same concept as Java's `@Override` annotation
+  or Scala's `override` keyword. However, you should only do this check when
   `ENV['STATIC_VERIFICATION']` is set to avoid production runtime overhead.
-  This is useful to check:
+  This is useful for checking:
 
-  - If we have typos in overriding methods.
-  - If we renamed the overridden methods, making original overriding methods
-    overrides nothing.
+  - If you have typos in overriding methods.
+  - If you renamed the overridden methods, which make the original override methods
+    irrelevant.
 
     Here's a simple example:
 
@@ -100,11 +100,11 @@ Refer to <https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/utils/stro
 
 - Memoize the value even if it is `nil` or `false`.
 
-  We often do `@value ||= compute`, however this doesn't work well if
-  `compute` might eventually give `nil` and we don't want to compute again.
-  Instead we could use `defined?` to check if the value is set or not.
-  However it's tedious to write such pattern, and `StrongMemoize` would
-  help us use such pattern.
+  We often do `@value ||= compute`. However, this doesn't work well if
+  `compute` might eventually give `nil` and you don't want to compute again.
+  Instead you could use `defined?` to check if the value is set or not.
+  It's tedious to write such pattern, and `StrongMemoize` would
+  help you use such pattern.
 
   Instead of writing patterns like this:
 
@@ -118,7 +118,7 @@ Refer to <https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/utils/stro
   end
   ```
 
-  We could write it like:
+  You could write it like:
 
   ``` ruby
   class Find
@@ -151,7 +151,7 @@ and the cache key would be based on the class name, method name,
 optionally customized instance level values, optionally customized
 method level values, and optional method arguments.
 
-A simple example that only uses the instance level customised values:
+A simple example that only uses the instance level customised values is:
 
 ``` ruby
 class UserAccess
@@ -169,8 +169,8 @@ end
 
 This way, the result of `can_push_to_branch?` would be cached in
 `RequestStore.store` based on the cache key. If `RequestStore` is not
-currently active, then it would be stored in a hash saved in an
-instance variable, so the cache logic would be the same.
+currently active, then it would be stored in a hash, and saved in an
+instance variable so the cache logic would be the same.
 
 We can also set different strategies for different methods:
 
diff --git a/doc/install/README.md b/doc/install/README.md
index 619ab34c221c82e3a0b14d0a26a374ec86055431..b906deadca9291cc99bf5f43433462190e3092c4 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -83,7 +83,7 @@ the above methods, provided the cloud provider supports it.
 - [Install GitLab on Google Cloud Platform](google_cloud_platform/index.md): Install Omnibus GitLab on a VM in GCP.
 - [Install GitLab on Azure](azure/index.md): Install Omnibus GitLab from Azure Marketplace.
 - [Install GitLab on OpenShift](https://docs.gitlab.com/charts/installation/cloud/openshift.html): Install GitLab on OpenShift by using GitLab's Helm charts.
-- [Install GitLab on DC/OS](https://d2iq.com/blog/gitlab-dcos): Install GitLab on Mesosphere DC/OS via the [GitLab-Mesosphere integration](https://about.gitlab.com/2016/09/16/announcing-gitlab-and-mesosphere/).
-- [Install GitLab on DigitalOcean](https://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/): Install Omnibus GitLab on DigitalOcean.
+- [Install GitLab on DC/OS](https://d2iq.com/blog/gitlab-dcos): Install GitLab on Mesosphere DC/OS via the [GitLab-Mesosphere integration](https://about.gitlab.com/blog/2016/09/16/announcing-gitlab-and-mesosphere/).
+- [Install GitLab on DigitalOcean](https://about.gitlab.com/blog/2016/04/27/getting-started-with-gitlab-and-digitalocean/): Install Omnibus GitLab on DigitalOcean.
 - _Testing only!_ [DigitalOcean and Docker Machine](digitaloceandocker.md):
   Quickly test any version of GitLab on DigitalOcean using Docker Machine.
diff --git a/doc/install/azure/index.md b/doc/install/azure/index.md
index 0ab8eed677e7eebac0fb35678f11b5e986c49c8b..c789467175a70542ba2a977ce6f32c95d65bed13 100644
--- a/doc/install/azure/index.md
+++ b/doc/install/azure/index.md
@@ -423,7 +423,7 @@ Check out our other [Technical Articles](../../articles/index.md) or browse the
   - [Azure - Properly Shutdown an Azure VM](https://buildazure.com/properly-shutdown-azure-vm-to-save-money/)
 - [SSH], [PuTTY](https://www.putty.org) and [Using SSH in PuTTY][Using-SSH-In-Putty]
 
-[Original-Blog-Post]: https://about.gitlab.com/2016/07/13/how-to-setup-a-gitlab-instance-on-microsoft-azure/ "How to Set up a GitLab Instance on Microsoft Azure"
+[Original-Blog-Post]: https://about.gitlab.com/blog/2016/07/13/how-to-setup-a-gitlab-instance-on-microsoft-azure/ "How to Set up a GitLab Instance on Microsoft Azure"
 [CE]: https://about.gitlab.com/features/
 [EE]: https://about.gitlab.com/features/#ee-starter
 
diff --git a/doc/install/installation.md b/doc/install/installation.md
index ed0997f86a63923ee590fd55a88997ab43ba0ea6..cc91f14045f50d67b0a26dee07c788000f42c80c 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -183,7 +183,7 @@ sudo make prefix=/usr/local install
 # When editing config/gitlab.yml (Step 5), change the git -> bin_path to /usr/local/bin/git
 ```
 
-For the [Custom Favicon](../customization/favicon.md) to work, GraphicsMagick
+For the [Custom Favicon](../user/admin_area/appearance.md#favicon) to work, GraphicsMagick
 needs to be installed.
 
 ```sh
@@ -448,7 +448,7 @@ sudo -u git -H mkdir -p public/uploads/
 # now that files in public/uploads are served by gitlab-workhorse
 sudo chmod 0700 public/uploads
 
-# Change the permissions of the directory where CI job traces are stored
+# Change the permissions of the directory where CI job logs are stored
 sudo chmod -R u+rwX builds/
 
 # Change the permissions of the directory where CI artifacts are stored
diff --git a/doc/install/openshift_and_gitlab/index.md b/doc/install/openshift_and_gitlab/index.md
index 1d0a16ea7b2b8fcdaa5d3163f24d08a34749e7f6..010e56fb0974c90f34c88c1feebd5b3a7f1d346f 100644
--- a/doc/install/openshift_and_gitlab/index.md
+++ b/doc/install/openshift_and_gitlab/index.md
@@ -21,7 +21,7 @@ In this tutorial, we will see how to deploy GitLab in OpenShift using GitLab's
 official Docker image while getting familiar with the web interface and CLI
 tools that will help us achieve our goal.
 
-For a video demonstration on installing GitLab on OpenShift, check the article [In 13 minutes from Kubernetes to a complete application development tool](https://about.gitlab.com/2016/11/14/idea-to-production/).
+For a video demonstration on installing GitLab on OpenShift, check the article [In 13 minutes from Kubernetes to a complete application development tool](https://about.gitlab.com/blog/2016/11/14/idea-to-production/).
 
 ---
 
diff --git a/doc/integration/jenkins.md b/doc/integration/jenkins.md
index d865f9777994eb94ae04d372c6ba13a2ff563fc2..a54f6843c532619d4a508a8888658f2516786690 100644
--- a/doc/integration/jenkins.md
+++ b/doc/integration/jenkins.md
@@ -33,7 +33,7 @@ and [Migrating from Jenkins to GitLab](https://www.youtube.com/watch?v=RlEVGOpYF
   therefore, you opt for keep using Jenkins to build your apps. Show the results of your
   pipelines directly in GitLab.
 
-For a real use case, read the blog post [Continuous integration: From Jenkins to GitLab using Docker](https://about.gitlab.com/2017/07/27/docker-my-precious/).
+For a real use case, read the blog post [Continuous integration: From Jenkins to GitLab using Docker](https://about.gitlab.com/blog/2017/07/27/docker-my-precious/).
 
 NOTE: **Moving from a traditional CI plug-in to a single application for the entire software development lifecycle can decrease hours spent on maintaining toolchains by 10% or more.**
 Visit the ['GitLab vs. Jenkins' comparison page](https://about.gitlab.com/devops-tools/jenkins-vs-gitlab.html) to learn how our built-in CI compares to Jenkins.
diff --git a/doc/policy/maintenance.md b/doc/policy/maintenance.md
index 42e05dd9a5c910e0353fd734202e29f2c5319fd3..d118c2f40cb84f3eb16d7f0472b94ff1a3ef812f 100644
--- a/doc/policy/maintenance.md
+++ b/doc/policy/maintenance.md
@@ -47,7 +47,7 @@ medium-level security issues, we may backport security fixes to the previous two
 monthly releases.
 
 For very serious security issues, there is
-[precedent](https://about.gitlab.com/2016/05/02/cve-2016-4340-patches/)
+[precedent](https://about.gitlab.com/blog/2016/05/02/cve-2016-4340-patches/)
 to backport security fixes to even more monthly releases of GitLab.
 This decision is made on a case-by-case basis.
 
diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md
index ad196e27f53aa7d44b1be651bf485208b7d577c8..9e4a666d4422be947b69e9b50612c61ce0a5a7cd 100644
--- a/doc/topics/authentication/index.md
+++ b/doc/topics/authentication/index.md
@@ -8,8 +8,8 @@ This page gathers all the resources for the topic **Authentication** within GitL
 - [Two-Factor Authentication (2FA)](../../user/profile/account/two_factor_authentication.md#two-factor-authentication)
 - [Why do I keep getting signed out?](../../user/profile/index.md#why-do-i-keep-getting-signed-out)
 - **Articles:**
-  - [Support for Universal 2nd Factor Authentication - YubiKeys](https://about.gitlab.com/2016/06/22/gitlab-adds-support-for-u2f/)
-  - [Security Webcast with Yubico](https://about.gitlab.com/2016/08/31/gitlab-and-yubico-security-webcast/)
+  - [Support for Universal 2nd Factor Authentication - YubiKeys](https://about.gitlab.com/blog/2016/06/22/gitlab-adds-support-for-u2f/)
+  - [Security Webcast with Yubico](https://about.gitlab.com/blog/2016/08/31/gitlab-and-yubico-security-webcast/)
 - **Integrations:**
   - [GitLab as OAuth2 authentication service provider](../../integration/oauth_provider.md#introduction-to-oauth)
   - [GitLab as OpenID Connect identity provider](../../integration/openid_connect_provider.md)
@@ -22,7 +22,7 @@ This page gathers all the resources for the topic **Authentication** within GitL
 - **Articles:**
   - [How to Configure LDAP with GitLab CE](../../administration/auth/how_to_configure_ldap_gitlab_ce/index.md)
   - [How to Configure LDAP with GitLab EE](../../administration/auth/how_to_configure_ldap_gitlab_ee/index.md) **(STARTER)**
-  - [Feature Highlight: LDAP Integration](https://about.gitlab.com/2014/07/10/feature-highlight-ldap-sync/)
+  - [Feature Highlight: LDAP Integration](https://about.gitlab.com/blog/2014/07/10/feature-highlight-ldap-sync/)
   - [Debugging LDAP](https://about.gitlab.com/handbook/support/workflows/debugging_ldap.html)
 - **Integrations:**
   - [OmniAuth](../../integration/omniauth.md)
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index c8cc5b356c04d111c60327a0389a519e35ab5ec3..a1373639a8727e972e1b498491118411fdc05515 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -85,7 +85,7 @@ knowledge of the following:
 Auto DevOps provides great defaults for all the stages; you can, however,
 [customize](#customizing) almost everything to your needs.
 
-For an overview on the creation of Auto DevOps, read the blog post [From 2/3 of the Self-Hosted Git Market, to the Next-Generation CI System, to Auto DevOps](https://about.gitlab.com/2017/06/29/whats-next-for-gitlab-ci/).
+For an overview on the creation of Auto DevOps, read the blog post [From 2/3 of the Self-Hosted Git Market, to the Next-Generation CI System, to Auto DevOps](https://about.gitlab.com/blog/2017/06/29/whats-next-for-gitlab-ci/).
 
 NOTE: **Note**
 Kubernetes clusters can [be used without](../../user/project/clusters/index.md)
@@ -98,7 +98,7 @@ To make full use of Auto DevOps, you will need:
 - **GitLab Runner** (for all stages)
 
   Your Runner needs to be configured to be able to run Docker. Generally this
-  means using the either the [Docker](https://docs.gitlab.com/runner/executors/docker.html)
+  means using either the [Docker](https://docs.gitlab.com/runner/executors/docker.html)
   or [Kubernetes](https://docs.gitlab.com/runner/executors/kubernetes.html) executors, with
   [privileged mode enabled](https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode).
 
@@ -487,6 +487,9 @@ in the first place, and thus not realize that it needs to re-apply the old confi
 
 > Introduced in [GitLab Ultimate][ee] 10.4.
 
+This is an optional step, since it requires a [review app](#auto-review-apps).
+If that requirement is not met, the job will be silently skipped.
+
 Dynamic Application Security Testing (DAST) uses the
 popular open source tool [OWASP ZAProxy](https://github.com/zaproxy/zaproxy)
 to perform an analysis on the current code and checks for potential security
@@ -498,6 +501,29 @@ later download and check out.
 Any security warnings are also shown in the merge request widget. Read how
 [DAST works](../../user/application_security/dast/index.md).
 
+On your default branch, DAST scans an app deployed specifically for that purpose.
+The app is deleted after DAST has run.
+
+On feature branches, DAST scans the [review app](#auto-review-apps).
+
+#### Overriding the DAST target
+
+To use a custom target instead of the auto-deployed review apps,
+set a `DAST_WEBSITE` environment variable to the URL for DAST to scan.
+
+NOTE: **Note:**
+If [DAST Full Scan](../../user/application_security/dast/index.md#full-scan) is enabled, it is strongly advised **not**
+to set `DAST_WEBSITE` to any staging or production environment. DAST Full Scan
+actively attacks the target, which can take down the application and lead to
+data loss or corruption.
+
+#### Disabling Auto DAST
+
+DAST can be disabled:
+
+- On all branches by setting the `DAST_DISABLED` environment variable to `"true"`.
+- Only on the default branch by setting the `DAST_DISABLED_FOR_DEFAULT_BRANCH` environment variable to `"true"`.
+
 ### Auto Browser Performance Testing **(PREMIUM)**
 
 > Introduced in [GitLab Premium][ee] 10.4.
@@ -1169,13 +1195,13 @@ This configuration is deprecated and will be removed in the future.
 TIP: **Tip:**
 You can also set this inside your [project's settings](#deployment-strategy).
 
-This configuration based on
+This configuration is based on
 [incremental rollout to production](#incremental-rollout-to-production-premium).
 
 Everything behaves the same way, except:
 
 - It's enabled by setting the `INCREMENTAL_ROLLOUT_MODE` variable to `timed`.
-- Instead of the standard `production` job, the following jobs with a 5 minute delay between each are created:
+- Instead of the standard `production` job, the following jobs are created with a 5 minute delay between each :
   1. `timed rollout 10%`
   1. `timed rollout 25%`
   1. `timed rollout 50%`
diff --git a/doc/topics/git/index.md b/doc/topics/git/index.md
index bbb42d68a0dd3dbdb74ffeefcc3e2754ca4399a2..d6e1f83b876b27a5e4fc48283e25f2fc5aceab41 100644
--- a/doc/topics/git/index.md
+++ b/doc/topics/git/index.md
@@ -11,7 +11,7 @@ large projects with speed and efficiency.
 [GitLab](https://about.gitlab.com) is a Git-based fully integrated platform for
 software development. Besides Git's functionalities, GitLab has a lot of
 powerful [features](https://about.gitlab.com/features/) to enhance your
-[workflow](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/).
+[workflow](https://about.gitlab.com/blog/2016/10/25/gitlab-workflow-an-overview/).
 
 We've gathered some resources to help you to get the best from Git with GitLab.
 
@@ -39,8 +39,8 @@ The following resources will help you get started with Git:
 The following are resources about version control concepts:
 
 - [Git concepts](../../university/training/user_training.md#git-concepts)
-- [Why Git is Worth the Learning Curve](https://about.gitlab.com/2017/05/17/learning-curve-is-the-biggest-challenge-developers-face-with-git/)
-- [The future of SaaS hosted Git repository pricing](https://about.gitlab.com/2016/05/11/git-repository-pricing/)
+- [Why Git is Worth the Learning Curve](https://about.gitlab.com/blog/2017/05/17/learning-curve-is-the-biggest-challenge-developers-face-with-git/)
+- [The future of SaaS hosted Git repository pricing](https://about.gitlab.com/blog/2016/05/11/git-repository-pricing/)
 - [Git website topic about version control](https://git-scm.com/book/en/v2/Getting-Started-About-Version-Control)
 - [GitLab University presentation about Version Control](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit?usp=sharing)
 
@@ -49,8 +49,8 @@ The following are resources about version control concepts:
 The following resources may help you become more efficient at using Git:
 
 - [Useful Git commands](useful_git_commands.md) collected by the GitLab support team.
-- [Git Tips & Tricks](https://about.gitlab.com/2016/12/08/git-tips-and-tricks/)
-- [Eight Tips to help you work better with Git](https://about.gitlab.com/2015/02/19/8-tips-to-help-you-work-better-with-git/)
+- [Git Tips & Tricks](https://about.gitlab.com/blog/2016/12/08/git-tips-and-tricks/)
+- [Eight Tips to help you work better with Git](https://about.gitlab.com/blog/2015/02/19/8-tips-to-help-you-work-better-with-git/)
 
 ## Troubleshooting Git
 
@@ -63,7 +63,7 @@ If you have problems with Git, the following may help:
 
 - [Git Branching - Branches in a Nutshell](https://git-scm.com/book/en/v2/Git-Branching-Branches-in-a-Nutshell)
 - [Git Branching - Branching Workflows](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows)
-- [GitLab Flow](https://about.gitlab.com/2014/09/29/gitlab-flow/)
+- [GitLab Flow](https://about.gitlab.com/blog/2014/09/29/gitlab-flow/)
 
 ## Advanced use
 
@@ -83,9 +83,9 @@ Git-related queries from GitLab.
 
 The following relate to Git Large File Storage:
 
-- [Getting Started with Git LFS](https://about.gitlab.com/2017/01/30/getting-started-with-git-lfs-tutorial/)
+- [Getting Started with Git LFS](https://about.gitlab.com/blog/2017/01/30/getting-started-with-git-lfs-tutorial/)
 - [Migrate an existing Git repo with Git LFS](migrate_to_git_lfs/index.md)
 - [GitLab Git LFS user documentation](../../workflow/lfs/manage_large_binaries_with_git_lfs.md)
 - [GitLab Git LFS admin documentation](../../workflow/lfs/lfs_administration.md)
 - [git-annex to Git-LFS migration guide](../../workflow/lfs/migrate_from_git_annex_to_git_lfs.md)
-- [Towards a production quality open source Git LFS server](https://about.gitlab.com/2015/08/13/towards-a-production-quality-open-source-git-lfs-server/)
+- [Towards a production quality open source Git LFS server](https://about.gitlab.com/blog/2015/08/13/towards-a-production-quality-open-source-git-lfs-server/)
diff --git a/doc/topics/git/migrate_to_git_lfs/index.md b/doc/topics/git/migrate_to_git_lfs/index.md
index 9de978dd0079a8f1978c51fc246e1672edbbee90..0c30b45c55288a1a9ba5b4901b80113c28a04126 100644
--- a/doc/topics/git/migrate_to_git_lfs/index.md
+++ b/doc/topics/git/migrate_to_git_lfs/index.md
@@ -162,7 +162,7 @@ but commented out to help encourage others to add to it in the future. -->
 
 ## References
 
-- [Getting Started with Git LFS](https://about.gitlab.com/2017/01/30/getting-started-with-git-lfs-tutorial/)
+- [Getting Started with Git LFS](https://about.gitlab.com/blog/2017/01/30/getting-started-with-git-lfs-tutorial/)
 - [Migrate from Git Annex to Git LFS](../../../workflow/lfs/migrate_from_git_annex_to_git_lfs.md)
 - [GitLab's Git LFS user documentation](../../../workflow/lfs/manage_large_binaries_with_git_lfs.md)
 - [GitLab's Git LFS administrator documentation](../../../workflow/lfs/lfs_administration.md)
diff --git a/doc/topics/git/numerous_undo_possibilities_in_git/index.md b/doc/topics/git/numerous_undo_possibilities_in_git/index.md
index 51cafb82cecae6b3b9ccda2519f7f1d7c0cbff77..33da01d35b83ace68d551c09b1c79f5889441ecd 100644
--- a/doc/topics/git/numerous_undo_possibilities_in_git/index.md
+++ b/doc/topics/git/numerous_undo_possibilities_in_git/index.md
@@ -521,5 +521,5 @@ but commented out to help encourage others to add to it in the future. -->
 [git-filters-manual]: https://git-scm.com/docs/git-filter-branch#_options
 [git-official]: https://git-scm.com/
 [gitlab]: https://gitlab.com/gitlab-org/gitlab/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria
-[gitlab-flow]: https://about.gitlab.com/2014/09/29/gitlab-flow/
-[gitlab-git-tips-n-tricks]: https://about.gitlab.com/2016/12/08/git-tips-and-tricks/
+[gitlab-flow]: https://about.gitlab.com/blog/2014/09/29/gitlab-flow/
+[gitlab-git-tips-n-tricks]: https://about.gitlab.com/blog/2016/12/08/git-tips-and-tricks/
diff --git a/doc/university/README.md b/doc/university/README.md
index 8dbf6299e90e9f343c572ad49eedda2afba8b123..8f5a5038bb953c8b7a163def81e00a829410834e 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -46,9 +46,9 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
 
 1. [Repositories, Projects and Groups - Video](https://www.youtube.com/watch?v=4TWfh1aKHHw&index=1&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
 1. [Creating a Project in GitLab - Video](https://www.youtube.com/watch?v=7p0hrpNaJ14)
-1. [How to Create Files and Directories](https://about.gitlab.com/2016/02/10/feature-highlight-create-files-and-directories-from-files-page/)
-1. [GitLab Todos](https://about.gitlab.com/2016/03/02/gitlab-todos-feature-highlight/)
-1. [GitLab's Work in Progress (WIP) Flag](https://about.gitlab.com/2016/01/08/feature-highlight-wip/)
+1. [How to Create Files and Directories](https://about.gitlab.com/blog/2016/02/10/feature-highlight-create-files-and-directories-from-files-page/)
+1. [GitLab Todos](https://about.gitlab.com/blog/2016/03/02/gitlab-todos-feature-highlight/)
+1. [GitLab's Work in Progress (WIP) Flag](https://about.gitlab.com/blog/2016/01/08/feature-highlight-wip/)
 
 ### 1.5. Migrating from other Source Control
 
@@ -61,9 +61,9 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
 
 1. [About GitLab](https://about.gitlab.com/company/)
 1. [GitLab Direction](https://about.gitlab.com/direction/)
-1. [GitLab Master Plan](https://about.gitlab.com/2016/09/13/gitlab-master-plan/)
+1. [GitLab Master Plan](https://about.gitlab.com/blog/2016/09/13/gitlab-master-plan/)
 1. [Making GitLab Great for Everyone - Video](https://www.youtube.com/watch?v=GGC40y4vMx0) - Response to "Dear GitHub" letter
-1. [Using Innersourcing to Improve Collaboration](https://about.gitlab.com/2014/09/05/innersourcing-using-the-open-source-workflow-to-improve-collaboration-within-an-organization/)
+1. [Using Innersourcing to Improve Collaboration](https://about.gitlab.com/blog/2014/09/05/innersourcing-using-the-open-source-workflow-to-improve-collaboration-within-an-organization/)
 1. [The Software Development Market and GitLab - Video](https://www.youtube.com/watch?v=sXlhgPK1NTY&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e&index=6) - [Slides](https://docs.google.com/presentation/d/1vCU-NbZWz8NTNK8Vu3y4zGMAHb5DpC8PE5mHtw1PWfI/edit)
 1. [The GitLab Book Club](bookclub/index.md)
 1. [GitLab Resources](https://about.gitlab.com/resources/)
@@ -75,8 +75,8 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
    - The GitLab IRC channel, Gitter Chat Room, Community Forum and Mailing List
    - Getting Technical Support
    - Being part of our Great Community and Contributing to GitLab
-1. [Getting Started with the GitLab Development Kit (GDK)](https://about.gitlab.com/2016/06/08/getting-started-with-gitlab-development-kit/)
-1. [Contributing Technical Articles to the GitLab Blog](https://about.gitlab.com/2016/01/26/call-for-writers/)
+1. [Getting Started with the GitLab Development Kit (GDK)](https://about.gitlab.com/blog/2016/06/08/getting-started-with-gitlab-development-kit/)
+1. [Contributing Technical Articles to the GitLab Blog](https://about.gitlab.com/blog/2016/01/26/call-for-writers/)
 1. [GitLab Training Workshops](training/end-user/README.md)
 1. [GitLab Professional Services](https://about.gitlab.com/services/)
 
@@ -88,36 +88,36 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
 
 ### 2.1 GitLab Pages
 
-1. [Using any Static Site Generator with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
-1. [Securing GitLab Pages with SSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/)
+1. [Using any Static Site Generator with GitLab Pages](https://about.gitlab.com/blog/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
+1. [Securing GitLab Pages with SSL](https://about.gitlab.com/blog/2016/06/24/secure-gitlab-pages-with-startssl/)
 1. [GitLab Pages Documentation](../user/project/pages/index.md)
 
 ### 2.2. GitLab Issues
 
 1. [Markdown in GitLab](../user/markdown.md)
 1. [Issues and Merge Requests - Video](https://www.youtube.com/watch?v=raXvuwet78M)
-1. [Due Dates and Milestones for GitLab Issues](https://about.gitlab.com/2016/08/05/feature-highlight-set-dates-for-issues/)
-1. [How to Use GitLab Labels](https://about.gitlab.com/2016/08/17/using-gitlab-labels/)
-1. [Applying GitLab Labels Automatically](https://about.gitlab.com/2016/08/19/applying-gitlab-labels-automatically/)
+1. [Due Dates and Milestones for GitLab Issues](https://about.gitlab.com/blog/2016/08/05/feature-highlight-set-dates-for-issues/)
+1. [How to Use GitLab Labels](https://about.gitlab.com/blog/2016/08/17/using-gitlab-labels/)
+1. [Applying GitLab Labels Automatically](https://about.gitlab.com/blog/2016/08/19/applying-gitlab-labels-automatically/)
 1. [GitLab Issue Board - Product Page](https://about.gitlab.com/product/issueboard/)
-1. [An Overview of GitLab Issue Board](https://about.gitlab.com/2016/08/22/announcing-the-gitlab-issue-board/)
-1. [Designing GitLab Issue Board](https://about.gitlab.com/2016/08/31/designing-issue-boards/)
+1. [An Overview of GitLab Issue Board](https://about.gitlab.com/blog/2016/08/22/announcing-the-gitlab-issue-board/)
+1. [Designing GitLab Issue Board](https://about.gitlab.com/blog/2016/08/31/designing-issue-boards/)
 1. [From Idea to Production with GitLab - Video](https://www.youtube.com/watch?v=25pHyknRgEo&index=14&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
 
 ### 2.3. Continuous Integration
 
 1. [Operating Systems, Servers, VMs, Containers and Unix - Video](https://www.youtube.com/watch?v=V61kL6IC-zY&index=8&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
 1. [GitLab CI - Product Page](https://about.gitlab.com/product/continuous-integration/)
-1. [Getting started with GitLab and GitLab CI](https://about.gitlab.com/2015/12/14/getting-started-with-gitlab-and-gitlab-ci/)
-1. [GitLab Container Registry](https://about.gitlab.com/2016/05/23/gitlab-container-registry/)
+1. [Getting started with GitLab and GitLab CI](https://about.gitlab.com/blog/2015/12/14/getting-started-with-gitlab-and-gitlab-ci/)
+1. [GitLab Container Registry](https://about.gitlab.com/blog/2016/05/23/gitlab-container-registry/)
 1. [GitLab and Docker - Video](https://www.youtube.com/watch?v=ugOrCcbdHko&index=12&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
-1. [How we scale GitLab with built in Docker](https://about.gitlab.com/2016/06/21/how-we-scale-gitlab-by-having-docker-built-in/)
-1. [Continuous Integration, Delivery, and Deployment with GitLab](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/)
-1. [Deployments and Environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
-1. [Sequential, Parallel or Custom Pipelines](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/)
-1. [Setting up GitLab Runner For Continuous Integration](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/)
-1. [Setting up GitLab Runner on DigitalOcean](https://about.gitlab.com/2016/04/19/how-to-set-up-gitlab-runner-on-digitalocean/)
-1. [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
+1. [How we scale GitLab with built in Docker](https://about.gitlab.com/blog/2016/06/21/how-we-scale-gitlab-by-having-docker-built-in/)
+1. [Continuous Integration, Delivery, and Deployment with GitLab](https://about.gitlab.com/blog/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/)
+1. [Deployments and Environments](https://about.gitlab.com/blog/2016/08/26/ci-deployment-and-environments/)
+1. [Sequential, Parallel or Custom Pipelines](https://about.gitlab.com/blog/2016/07/29/the-basics-of-gitlab-ci/)
+1. [Setting up GitLab Runner For Continuous Integration](https://about.gitlab.com/blog/2016/03/01/gitlab-runner-with-docker/)
+1. [Setting up GitLab Runner on DigitalOcean](https://about.gitlab.com/blog/2016/04/19/how-to-set-up-gitlab-runner-on-digitalocean/)
+1. [Setting up GitLab CI for iOS projects](https://about.gitlab.com/blog/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
 1. [IBM: Continuous Delivery vs Continuous Deployment - Video](https://www.youtube.com/watch?v=igwFj8PPSnw)
 1. [Amazon: Transition to Continuous Delivery - Video](https://www.youtube.com/watch?v=esEFaY0FDKc)
 1. [TechBeacon: Doing continuous delivery? Focus first on reducing release cycle times](https://techbeacon.com/devops/doing-continuous-delivery-focus-first-reducing-release-cycle-times)
@@ -127,14 +127,14 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
 
 1. [GitLab Flow - Video](https://youtu.be/enMumwvLAug?list=PLFGfElNsQthZnwMUFi6rqkyUZkI00OxIV)
 1. [GitLab Flow vs Forking in GitLab - Video](https://www.youtube.com/watch?v=UGotqAUACZA)
-1. [GitLab Flow Overview](https://about.gitlab.com/2014/09/29/gitlab-flow/)
-1. [Always Start with an Issue](https://about.gitlab.com/2016/03/03/start-with-an-issue/)
+1. [GitLab Flow Overview](https://about.gitlab.com/blog/2014/09/29/gitlab-flow/)
+1. [Always Start with an Issue](https://about.gitlab.com/blog/2016/03/03/start-with-an-issue/)
 1. [GitLab Flow Documentation](../workflow/gitlab_flow.md)
 
 ### 2.5. GitLab Comparisons
 
 1. [GitLab Compared to Other Tools](https://about.gitlab.com/devops-tools/)
-1. [Comparing GitLab Terminology](https://about.gitlab.com/2016/01/27/comparing-terms-gitlab-github-bitbucket/)
+1. [Comparing GitLab Terminology](https://about.gitlab.com/blog/2016/01/27/comparing-terms-gitlab-github-bitbucket/)
 1. [GitLab Compared to Atlassian (Recording 2016-03-03)](https://youtu.be/Nbzp1t45ERo)
 1. [GitLab Position FAQ](https://about.gitlab.com/handbook/positioning-faq/)
 1. [Customer review of GitLab with points on why they prefer GitLab](https://www.enovate.co.uk/blog/2015/11/25/gitlab-review)
@@ -153,8 +153,8 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
 1. [How to Install GitLab with Omnibus - Video](https://www.youtube.com/watch?v=Q69YaOjqNhg)
 1. [Installing GitLab - Online Course](https://courses.platzi.com/classes/57-git-gitlab/2476-part-0/)
 1. [Using a Non-Packaged PostgreSQL Database](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#using-a-non-packaged-postgresql-database-management-server)
-1. [Installing GitLab on Microsoft Azure](https://about.gitlab.com/2016/07/13/how-to-setup-a-gitlab-instance-on-microsoft-azure/)
-1. [Installing GitLab on Digital Ocean](https://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/)
+1. [Installing GitLab on Microsoft Azure](https://about.gitlab.com/blog/2016/07/13/how-to-setup-a-gitlab-instance-on-microsoft-azure/)
+1. [Installing GitLab on Digital Ocean](https://about.gitlab.com/blog/2016/04/27/getting-started-with-gitlab-and-digitalocean/)
 
 ### 3.3. Permissions
 
@@ -180,7 +180,7 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
 
 ### 3.8 Cycle Analytics
 
-1. [GitLab Cycle Analytics Overview](https://about.gitlab.com/2016/09/21/cycle-analytics-feature-highlight/)
+1. [GitLab Cycle Analytics Overview](https://about.gitlab.com/blog/2016/09/21/cycle-analytics-feature-highlight/)
 1. [GitLab Cycle Analytics - Product Page](https://about.gitlab.com/product/cycle-analytics/)
 
 ### 3.9. Integrations
@@ -190,8 +190,8 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
 1. [How to Integrate Jenkins with GitLab](../integration/jenkins.md)
 1. [How to Integrate Bamboo with GitLab](../user/project/integrations/bamboo.md)
 1. [How to Integrate Slack with GitLab](../user/project/integrations/slack.md)
-1. [How to Integrate Convox with GitLab](https://about.gitlab.com/2016/06/09/continuous-delivery-with-gitlab-and-convox/)
-1. [Getting Started with GitLab and Shippable CI](https://about.gitlab.com/2016/05/05/getting-started-gitlab-and-shippable/)
+1. [How to Integrate Convox with GitLab](https://about.gitlab.com/blog/2016/06/09/continuous-delivery-with-gitlab-and-convox/)
+1. [Getting Started with GitLab and Shippable CI](https://about.gitlab.com/blog/2016/05/05/getting-started-gitlab-and-shippable/)
 
 ## 4. External Articles
 
diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md
index cdbc6346f3f5d0c68b000056660c107a9a46f2ca..a1beee404eb409d12c9009bf04b3ce36e18fc632 100644
--- a/doc/user/admin_area/settings/account_and_limit_settings.md
+++ b/doc/user/admin_area/settings/account_and_limit_settings.md
@@ -17,7 +17,7 @@ details.
 
 ## Repository size limit **(STARTER)**
 
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/740) in [GitLab Enterprise Edition 8.12](https://about.gitlab.com/2016/09/22/gitlab-8-12-released/#limit-project-size-ee).
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/740) in [GitLab Enterprise Edition 8.12](https://about.gitlab.com/blog/2016/09/22/gitlab-8-12-released/#limit-project-size-ee).
 > Available in [GitLab Starter](https://about.gitlab.com/pricing/).
 
 Repositories within your GitLab instance can grow quickly, especially if you are
diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md
index 9b07ca13eee7deda7494a6795597f754c2e9dafb..c60b33231056c6251ddc769839514e64ae4c04e5 100644
--- a/doc/user/admin_area/settings/continuous_integration.md
+++ b/doc/user/admin_area/settings/continuous_integration.md
@@ -29,7 +29,12 @@ If you want to disable it for a specific project, you can do so in
 ## Maximum artifacts size **(CORE ONLY)**
 
 The maximum size of the [job artifacts](../../../administration/job_artifacts.md)
-can be set at the project level, group level, and at the instance level. The value is:
+can be set at:
+
+- The instance level.
+- [From GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/issues/21688), the project and group level.
+
+The value is:
 
 - In *MB* and the default is 100MB per job.
 - [Set to 1G](../../gitlab_com/index.md#gitlab-cicd) on GitLab.com.
@@ -54,6 +59,9 @@ To change it at the:
   1. Change the value of **maximum artifacts size (in MB)**.
   1. Press **Save changes** for the changes to take effect.
 
+NOTE: **Note**
+The setting at all levels is only available to GitLab administrators.
+
 ## Default artifacts expiration **(CORE ONLY)**
 
 The default expiration time of the [job artifacts](../../../administration/job_artifacts.md)
diff --git a/doc/user/admin_area/settings/email.md b/doc/user/admin_area/settings/email.md
index 6026f9dc735542aa38fbf811a5bf672213d0114b..4611d5f5c778185d29c56e12d58f4d58e4169cbb 100644
--- a/doc/user/admin_area/settings/email.md
+++ b/doc/user/admin_area/settings/email.md
@@ -8,7 +8,7 @@ You can customize some of the content in emails sent from your GitLab instance.
 
 ## Custom logo
 
-The logo in the header of some emails can be customized, see the [logo customization section](../../../customization/branded_page_and_email_header.md).
+The logo in the header of some emails can be customized, see the [logo customization section](../appearance.md#navigation-bar).
 
 ## Custom additional text **(PREMIUM ONLY)**
 
diff --git a/doc/user/admin_area/settings/img/bulk_push_event_v12_4.png b/doc/user/admin_area/settings/img/bulk_push_event_v12_4.png
new file mode 100644
index 0000000000000000000000000000000000000000..38e666e32ac3e46db6f5c88a97a88a18cbe9b289
Binary files /dev/null and b/doc/user/admin_area/settings/img/bulk_push_event_v12_4.png differ
diff --git a/doc/user/admin_area/settings/img/clone_panel_v12_4.png b/doc/user/admin_area/settings/img/clone_panel_v12_4.png
new file mode 100644
index 0000000000000000000000000000000000000000..8aa0bd2f7d8869fd14739e0d66a47b2b27b1d02b
Binary files /dev/null and b/doc/user/admin_area/settings/img/clone_panel_v12_4.png differ
diff --git a/doc/user/admin_area/settings/img/custom_git_clone_url_for_https_v12_4.png b/doc/user/admin_area/settings/img/custom_git_clone_url_for_https_v12_4.png
new file mode 100644
index 0000000000000000000000000000000000000000..22cdd15cc0c3b31abb5840edc30cd305f4845f46
Binary files /dev/null and b/doc/user/admin_area/settings/img/custom_git_clone_url_for_https_v12_4.png differ
diff --git a/doc/user/admin_area/settings/img/push_event_activities_limit_v12_4.png b/doc/user/admin_area/settings/img/push_event_activities_limit_v12_4.png
new file mode 100644
index 0000000000000000000000000000000000000000..fd3775ac4d79780d5bdbdb9589b7614dcdee96bb
Binary files /dev/null and b/doc/user/admin_area/settings/img/push_event_activities_limit_v12_4.png differ
diff --git a/doc/user/admin_area/settings/index.md b/doc/user/admin_area/settings/index.md
index ff86620dbb2d6bf213031c66fcd70d6902b98219..4ca91ae533982d477c89f7123944347bdb6d0468 100644
--- a/doc/user/admin_area/settings/index.md
+++ b/doc/user/admin_area/settings/index.md
@@ -22,6 +22,7 @@ include:
 - [Custom templates repository](instance_template_repository.md) **(PREMIUM)**
 - [Protected paths](protected_paths.md) **(CORE ONLY)**
 - [Help messages for the `/help` page and the login page](help_page.md)
+- [Push event activities limit and bulk push events](push_event_activities_limit.md)
 
 NOTE: **Note:**
 You can change the [first day of the week](../../profile/preferences.md) for the entire GitLab instance
diff --git a/doc/user/admin_area/settings/push_event_activities_limit.md b/doc/user/admin_area/settings/push_event_activities_limit.md
new file mode 100644
index 0000000000000000000000000000000000000000..9850de0f4b3a04858d0ffc394469819cbde83cb2
--- /dev/null
+++ b/doc/user/admin_area/settings/push_event_activities_limit.md
@@ -0,0 +1,28 @@
+---
+type: reference
+---
+
+# Push event activities limit and bulk push events
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/31007) in GitLab 12.4.
+
+This allows you to set the number of changes (branches or tags) in a single push
+to determine whether individual push events or bulk push event will be created.
+Bulk push events will be created if it surpasses that value.
+
+For example, if 4 branches are pushed and the limit is currently set to 3,
+you'll see the following in the activity feed:
+
+![Bulk push event](img/bulk_push_event_v12_4.png)
+
+With this feature, when a single push includes a lot of changes (e.g. 1,000
+branches), only 1 bulk push event will be created instead of creating 1,000 push
+events. This helps in maintaining good system performance and preventing spam on
+the activity feed.
+
+This setting can be modified in **Admin Area > Settings > Network > Performance Optimization**.
+This can also be configured via the [Application settings API](../../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls)
+as `push_event_activities_limit`. The default value is 3, but it can be greater
+than or equal 0.
+
+![Push event activities limit](img/push_event_activities_limit_v12_4.png)
diff --git a/doc/user/admin_area/settings/visibility_and_access_controls.md b/doc/user/admin_area/settings/visibility_and_access_controls.md
index 81e09e4bb8ae3947972eef61a566d24ca3bc236c..f718e31e8bdac3eda7b4056a9d5b4922e572b143 100644
--- a/doc/user/admin_area/settings/visibility_and_access_controls.md
+++ b/doc/user/admin_area/settings/visibility_and_access_controls.md
@@ -135,6 +135,33 @@ Starting with [GitLab 10.7](https://gitlab.com/gitlab-org/gitlab-ce/merge_reques
 HTTP(S) protocol will be allowed for Git clone or fetch requests done by GitLab Runner
 from CI/CD jobs, even if _Only SSH_ was selected.
 
+## Custom Git clone URL for HTTP(S)
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/18422) in GitLab 12.4.
+
+You can customize project Git clone URLs for HTTP(S). This will affect the clone
+panel:
+
+![Clone panel](img/clone_panel_v12_4.png)
+
+For example, if:
+
+- Your GitLab instance is at `https://example.com`, then project clone URLs are like
+  `https://example.com/foo/bar.git`.
+- You want clone URLs that look like `https://git.example.com/gitlab/foo/bar.git` instead,
+  you can set this setting to `https://git.example.com/gitlab/`.
+
+![Custom Git clone URL for HTTP](img/custom_git_clone_url_for_https_v12_4.png)
+
+To specify a custom Git clone URL for HTTP(S):
+
+1. Enter a root URL for **Custom Git clone URL for HTTP(S)**.
+1. Click on **Save changes**.
+
+NOTE: **Note:**
+SSH clone URLs can be customized in `gitlab.rb` by setting `gitlab_rails['gitlab_ssh_host']` and
+other related settings.
+
 ## RSA, DSA, ECDSA, ED25519 SSH keys
 
 These options specify the permitted types and lengths for SSH keys.
diff --git a/doc/user/analytics/cycle_analytics.md b/doc/user/analytics/cycle_analytics.md
index b4b053ded420563a0b977f259c65ec4ae6549cb1..e17202645d398386c15ec890cd3875e6fdd146fe 100644
--- a/doc/user/analytics/cycle_analytics.md
+++ b/doc/user/analytics/cycle_analytics.md
@@ -170,13 +170,13 @@ For Cycle Analytics functionality introduced in GitLab 12.3 and later:
 Learn more about Cycle Analytics in the following resources:
 
 - [Cycle Analytics feature page](https://about.gitlab.com/product/cycle-analytics/)
-- [Cycle Analytics feature preview](https://about.gitlab.com/2016/09/16/feature-preview-introducing-cycle-analytics/)
-- [Cycle Analytics feature highlight](https://about.gitlab.com/2016/09/21/cycle-analytics-feature-highlight/)
+- [Cycle Analytics feature preview](https://about.gitlab.com/blog/2016/09/16/feature-preview-introducing-cycle-analytics/)
+- [Cycle Analytics feature highlight](https://about.gitlab.com/blog/2016/09/21/cycle-analytics-feature-highlight/)
 
 [ce-5986]: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/5986
 [ce-20975]: https://gitlab.com/gitlab-org/gitlab-foss/issues/20975
 [environment]: ../../ci/yaml/README.md#environment
 [GitLab flow]: ../../workflow/gitlab_flow.md
-[idea to production]: https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab
+[idea to production]: https://about.gitlab.com/blog/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab
 [permissions]: ../permissions.md
 [yml]: ../../ci/yaml/README.md
diff --git a/doc/user/analytics/productivity_analytics.md b/doc/user/analytics/productivity_analytics.md
index bd3cbd62ba094e6f4af013c9f29c63bdce242630..09f83dcff4bbfdca1d3ea03d8d33d4f030fca42c 100644
--- a/doc/user/analytics/productivity_analytics.md
+++ b/doc/user/analytics/productivity_analytics.md
@@ -1,6 +1,6 @@
 # Productivity Analytics **(PREMIUM)**
 
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12079) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3 (enabled by feature flags `productivity_analytics`).
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12079) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3 (enabled by default using the feature flags `productivity_analytics`, `productivity_analytics_scatterplot_enabled`).
 
 Track development velocity with Productivity Analytics.
 
@@ -21,6 +21,7 @@ Productivity Analytics allows GitLab users to:
 - Visualize typical merge request (MR) lifetime and statistics. Use a histogram that shows the distribution of the time elapsed between creating and merging merge requests.
 - Drill down into the most time consuming merge requests, select a number of outliers, and filter down all subsequent charts to investigate potential causes.
 - Filter by group, project, author, label, milestone, or a specific date range. Filter down, for example, to the merge requests of a specific author in a group or project during a milestone or specific date range.
+- Measure velocity over time. Visualize the trends of each metric from the charts above over time in order to observe progress. Zoom in on a particular date range if you notice outliers.
 
 ## Accessing metrics and visualizations
 
@@ -40,6 +41,8 @@ The following metrics and visualizations are available on a project or group lev
   - Number of commits per merge request.
   - Number of lines of code per commit.
   - Number of files touched.
+- Scatterplot showing all MRs merged on a certain date, together with the days it took to complete the action and a 30 day rolling median.
+  - Users can zoom in and out on specific days of interest.
 - Table showing the list of merge requests with their respective time duration metrics.
   - Users can sort by any of the above metrics.
 
diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md
index e90f219337b75bd5e8c7df6cc1c07b77c5d059cf..951c4b9dd732b6230fb2de5270af67d2e08a0ba3 100644
--- a/doc/user/application_security/dast/index.md
+++ b/doc/user/application_security/dast/index.md
@@ -81,8 +81,15 @@ variables:
 
 There are two ways to define the URL to be scanned by DAST:
 
-- Set the `DAST_WEBSITE` [variable](../../../ci/yaml/README.md#variables).
-- Add it in an `environment_url.txt` file at the root of your project.
+1. Set the `DAST_WEBSITE` [variable](../../../ci/yaml/README.md#variables).
+
+1. Add it in an `environment_url.txt` file at the root of your project.
+    This is great for testing in dynamic environments. In order to run DAST against
+    an app that is dynamically created during a Gitlab CI pipeline, have the app
+    persist its domain in an `environment_url.txt` file, and DAST will
+    automatically parse that file to find its scan target.
+    You can see an [example](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml)
+    of this in our Auto DevOps CI YML.
 
 If both values are set, the `DAST_WEBSITE` value will take precedence.
 
diff --git a/doc/user/clusters/environments.md b/doc/user/clusters/environments.md
index bbf01dc27f99ac8726e026ad095a64fca45f8672..f83be85726a27ab78a8b70ae58e1df902ca9aac9 100644
--- a/doc/user/clusters/environments.md
+++ b/doc/user/clusters/environments.md
@@ -1,6 +1,7 @@
 # Cluster Environments **(PREMIUM)**
 
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13392) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13392) for group-level clusters in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/14809) for instance-level clusters in [GitLab Premium](https://about.gitlab.com/pricing/) 12.4.
 
 Cluster environments provide a consolidated view of which CI [environments](../../ci/environments.md) are
 deployed to the Kubernetes cluster and it:
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index 035ec15b453e32a0cbcefdd7b1a9825f7dbf0436..5912fc8e9f9e6b0cca40367db0dc6b9dc7acb45b 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -385,9 +385,9 @@ High Performance TCP/HTTP Load Balancer:
 - [`gitlab-cookbooks` / `gitlab-haproxy` · GitLab](https://gitlab.com/gitlab-cookbooks/gitlab-haproxy)
 
 [autoscale mode]: https://docs.gitlab.com/runner/configuration/autoscale.html "How Autoscale works"
-[runners-post]: https://about.gitlab.com/2016/04/05/shared-runners/ "Shared Runners on GitLab.com"
+[runners-post]: https://about.gitlab.com/blog/2016/04/05/shared-runners/ "Shared Runners on GitLab.com"
 [GitLab Runner]: https://gitlab.com/gitlab-org/gitlab-runner
-[altssh]: https://about.gitlab.com/2016/02/18/gitlab-dot-com-now-supports-an-alternate-git-plus-ssh-port/ "GitLab.com now supports an alternate git+ssh port"
+[altssh]: https://about.gitlab.com/blog/2016/02/18/gitlab-dot-com-now-supports-an-alternate-git-plus-ssh-port/ "GitLab.com now supports an alternate git+ssh port"
 [docker in docker]: https://hub.docker.com/_/docker/ "Docker in Docker at DockerHub"
 [mailgun]: https://www.mailgun.com/ "Mailgun website"
 [unicorn-worker-killer]: https://rubygems.org/gems/unicorn-worker-killer "unicorn-worker-killer"
diff --git a/doc/user/group/epics/img/child_epics_roadmap.png b/doc/user/group/epics/img/child_epics_roadmap.png
deleted file mode 100644
index 819fed5898934b3bdb5adff4c2d4e200e3f95300..0000000000000000000000000000000000000000
Binary files a/doc/user/group/epics/img/child_epics_roadmap.png and /dev/null differ
diff --git a/doc/user/group/epics/img/epic_view.png b/doc/user/group/epics/img/epic_view.png
deleted file mode 100644
index c55d302ec291f8d2b8c0a9be46da4c1ecfe0dc6e..0000000000000000000000000000000000000000
Binary files a/doc/user/group/epics/img/epic_view.png and /dev/null differ
diff --git a/doc/user/group/epics/img/epic_view_roadmap_v12.3.png b/doc/user/group/epics/img/epic_view_roadmap_v12.3.png
new file mode 100755
index 0000000000000000000000000000000000000000..a17c56c618bc865e40dbc71de209df96bbe4d308
Binary files /dev/null and b/doc/user/group/epics/img/epic_view_roadmap_v12.3.png differ
diff --git a/doc/user/group/epics/img/epic_view_v12.3.png b/doc/user/group/epics/img/epic_view_v12.3.png
new file mode 100755
index 0000000000000000000000000000000000000000..79758cf3d5260f7088f8acd44c74ebc4bab60d1c
Binary files /dev/null and b/doc/user/group/epics/img/epic_view_v12.3.png differ
diff --git a/doc/user/group/epics/img/epics_list_view.png b/doc/user/group/epics/img/epics_list_view.png
deleted file mode 100644
index b30608d9d31286132a715f5ba8cbc590ec0c24c7..0000000000000000000000000000000000000000
Binary files a/doc/user/group/epics/img/epics_list_view.png and /dev/null differ
diff --git a/doc/user/group/epics/img/epics_list_view_v12.3.png b/doc/user/group/epics/img/epics_list_view_v12.3.png
new file mode 100755
index 0000000000000000000000000000000000000000..c6817a503e7a914a923ba8e19ea022112eade955
Binary files /dev/null and b/doc/user/group/epics/img/epics_list_view_v12.3.png differ
diff --git a/doc/user/group/epics/index.md b/doc/user/group/epics/index.md
index 51e779cce6a5c40be1ca9cc1bd1e3a1f7f720fd0..f9690d4edfe50c396e4bbfcb7cae773f8bf78491 100644
--- a/doc/user/group/epics/index.md
+++ b/doc/user/group/epics/index.md
@@ -10,13 +10,13 @@ Epics let you manage your portfolio of projects more efficiently and with less
 effort by tracking groups of issues that share a theme, across projects and
 milestones.
 
-![epics list view](img/epics_list_view.png)
+![epics list view](img/epics_list_view_v12.3.png)
 
 ## Use cases
 
 - Suppose your team is working on a large feature that involves multiple discussions throughout different issues created in distinct projects within a [Group](../index.md). With Epics, you can track all the related activities that together contribute to that single feature.
 - Track when the work for the group of issues is targeted to begin, and when it is targeted to end.
-- Discuss and collaborate on feature ideas and scope at a high-level.
+- Discuss and collaborate on feature ideas and scope at a high level.
 
 ## Creating an epic
 
@@ -24,78 +24,114 @@ A paginated list of epics is available in each group from where you can create
 a new epic. The list of epics includes also epics from all subgroups of the
 selected group. From your group page:
 
-1. Go to **Epics**
-1. Click the **New epic** button at the top right
-1. Enter a descriptive title and hit **Create epic**
+1. Go to **Epics**.
+1. Click **New epic**.
+1. Enter a descriptive title and click **Create epic**.
 
-Once created, you will be taken to the view for that newly-created epic where
-you can change its title, description, start date, and due date.
+You will be taken to the new epic where can edit the following details:
 
-![epic view](img/epic_view.png)
+- Title
+- Description
+- Start date
+- Due date
+- Labels
+
+An epic's page contains the following tabs:
+
+- **Epics and Issues**: epics and issues added to this epic. Child epics, and their issues, are shown in a tree view.
+  - Click on the <kbd>></kbd> beside a parent epic to reveal the child epics and issues.
+- **Roadmap**: a roadmap view of child epics which have start and due dates.
+
+![epic view](img/epic_view_v12.3.png)
 
 ## Adding an issue to an epic
 
+Any issue that belongs to a project in the epic's group, or any of the epic's
+subgroups, are eligible to be added.  New issues appear at the top of the list of issues in the **Epics and Issues** tab.
+
 An epic contains a list of issues and an issue can be associated with at most
-one epic. When on an epic, you can add its associated issues:
+one epic. When you add an issue to an epic that is already associated with another epic,
+the issue is automatically removed from the previous epic.
+
+To add an issue to an epic:
 
-1. Click the plus icon (<kbd>+</kbd>) under the epic description.
-1. Paste the link of the issue (you can hit <kbd>Spacebar</kbd> to add more than
-   one issues at a time).
+1. Click **Add an issue**.
+1. Paste the link of the issue.
+   - Press <kbd>Spacebar</kbd> and repeat this step if there are multiple issues.
 1. Click **Add**.
 
-Any issue belonging to a project in the epic's group or any of the epic's
-subgroups are eligible to be added. To remove an issue from an epic, click
-on the <kbd>x</kbd> button in the epic's issue list.
+To remove an issue from an epic:
 
-NOTE: **Note:**
-When you add an issue or an epic to an epic that's already associated with another epic,
-the issue or the epic is automatically removed from the previous epic.
+1. Click on the <kbd>x</kbd> button in the epic's issue list.
+1. Click **Remove** in the **Remove issue** warning message.
 
 ## Multi-level child epics
 
 > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/8333) in GitLab Ultimate 11.7.
 
-Much like adding issues to an epic, an epic can have multiple child epics with
-the maximum depth being 5. To add a child epic:
+Any epic that belongs to a group, or subgroup of the parent epic's group, is
+eligible to be added. New child epics appear at the top of the list of epics in the **Epics and Issues** tab.
+
+When you add a child epic that is already associated with another epic,
+that epic is automatically removed from the previous epic.
 
-1. Click the plus icon (<kbd>+</kbd>) under the epic description.
+An epic can have multiple child epics with
+the maximum depth being 5.
+
+To add a child epic:
+
+1. Click **Add an epic**.
 1. Paste the link of the epic.
+   - Press <kbd>Spacebar</kbd> and repeat this step if there are multiple issues.
 1. Click **Add**.
 
-Any epic that belongs to a group or subgroup of the parent epic's group is
-eligible to be added. To remove a child epic from a parent epic,
-click on the <kbd>x</kbd> button in the parent epic's epic list.
+To remove a child epic from a parent epic:
+
+1. Click on the <kbd>x</kbd> button in the parent epic's list of epics.
+1. Click **Remove** in the **Remove epic** warning message.
 
 ## Start date and due date
 
-For each of the dates in the sidebar of an epic, you can choose to either:
+To set a **Start date** and **Due date** for an epic, you can choose either of the following:
 
-- Enter a fixed value.
-- Inherit a dynamic value called "From milestones".
+- **Fixed**: Enter a fixed value.
+- **From milestones:** Inherit a dynamic value from the issues added to the epic.
 
-If you select "From milestones" for the start date, GitLab will automatically set the
+If you select **From milestones** for the start date, GitLab will automatically set the
 date to be earliest start date across all milestones that are currently assigned
-to the issues that are attached to the epic. Similarly, if you select "From milestones"
+to the issues that are added to the epic. Similarly, if you select "From milestones"
 for the due date, GitLab will set it to be the latest due date across all
 milestones that are currently assigned to those issues.
 
-These are dynamic dates in that if milestones are re-assigned to the issues, if the
-milestone dates change, or if issues are added or removed from the epic, then
-the re-calculation will happen immediately to set a new dynamic date.
+These are dynamic dates which are recalculated immediately if any of the following occur:
+
+- Milestones are re-assigned to the issues.
+- Milestone dates change.
+- Issues are added or removed from the epic.
 
-## Roadmap in epics
+## Roadmap
 
 > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7327) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.10.
 
 If your epic contains one or more [child epics](#multi-level-child-epics) which
-have a [start or due date](#start-date-and-due-date), then you can see a
-[roadmap](../roadmap/index.md) view of the child epics under the parent epic itself.
+have a [start or due date](#start-date-and-due-date), a
+[roadmap](../roadmap/index.md) view of the child epics is listed under the parent epic.
 
-![Child epics roadmap](img/child_epics_roadmap.png)
+![Child epics roadmap](img/epic_view_roadmap_v12.3.png)
 
 ## Reordering issues and child epics
 
-Drag and drop to reorder issues and child epics. New issues and child epics added to an epic appear at the top of the list.
+New issues and child epics are added to the top of their respective lists in the **Epics and Issues** tab. You can reorder the list of issues and the list of child epics. Issues and child epics cannot be intermingled.
+
+To reorder issues assigned to an epic:
+
+1. Go to the **Epics and Issues** tab.
+1. Drag and drop issues into the desired order.
+
+To reorder child epics assigned to an epic:
+
+1. Go to the **Epics and Issues** tab.
+1. Drag and drop epics into the desired order.
 
 ## Updating epics
 
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index 93ad32b3e452111259bb706bd3c956ac2510e979..c4be08c842b9ba88a5086c3df64fc4f206799f07 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -17,7 +17,7 @@ Find your groups by clicking **Groups > Your Groups** in the top navigation.
 
 ![GitLab Groups](img/groups.png)
 
-> The **Groups** dropdown in the top navigation was [introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/36234) in [GitLab 11.1](https://about.gitlab.com/2018/07/22/gitlab-11-1-released/#groups-dropdown-in-navigation).
+> The **Groups** dropdown in the top navigation was [introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/36234) in [GitLab 11.1](https://about.gitlab.com/blog/2018/07/22/gitlab-11-1-released/#groups-dropdown-in-navigation).
 
 The **Groups** page displays:
 
@@ -178,9 +178,9 @@ There are two different ways to add a new project to a group:
 
 ### Default project-creation level
 
-> [Introduced][ee-2534] in [GitLab Premium][ee] 10.5.
-> Brought to [GitLab Starter][ee] in 10.7.
-> [Moved](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/25975) to [GitLab Core](https://about.gitlab.com/pricing/) in 11.10.
+> - [Introduced][ee-2534] in [GitLab Premium][ee] 10.5.
+> - Brought to [GitLab Starter][ee] in 10.7.
+> - [Moved](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/25975) to [GitLab Core](https://about.gitlab.com/pricing/) in 11.10.
 
 By default, [Developers and Maintainers](../permissions.md#group-members-permissions) can create projects under a group.
 
@@ -338,8 +338,7 @@ request to add a new user to a project through API will not be possible.
 
 #### IP access restriction **(ULTIMATE)**
 
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/1985) in
-[GitLab Ultimate and Gold](https://about.gitlab.com/pricing/) 12.0.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/1985) in [GitLab Ultimate and Gold](https://about.gitlab.com/pricing/) 12.0.
 
 To make sure only people from within your organization can access particular
 resources, you have the option to restrict access to groups and their
@@ -351,16 +350,20 @@ Add one or more whitelisted IP subnets using CIDR notation in comma separated fo
 coming from a different IP address won't be able to access the restricted
 content.
 
-Restriction currently applies to UI, API access and Git actions via SSH.
+Restriction currently applies to:
+
+- UI.
+- API access.
+- [From GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/issues/32113), Git actions via SSH.
+
 To avoid accidental lock-out, admins and group owners are are able to access
 the group regardless of the IP restriction.
 
 #### Allowed domain restriction **(PREMIUM)**
 
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7297) in
-[GitLab Premium and Silver](https://about.gitlab.com/pricing/) 12.2.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7297) in [GitLab Premium and Silver](https://about.gitlab.com/pricing/) 12.2.
 
-You can restrict access to groups and their underlying projects by
+You can restrict access to groups by
 allowing only users with email addresses in particular domains to be added to the group.
 
 Add email domains you want to whitelist and users with emails from different
diff --git a/doc/user/group/roadmap/index.md b/doc/user/group/roadmap/index.md
index a72cd990706652be9253ade71820029b14ffa842..bcd79bd04bf9a995a6ba7b62a11d90f62b2fcf9e 100644
--- a/doc/user/group/roadmap/index.md
+++ b/doc/user/group/roadmap/index.md
@@ -26,7 +26,7 @@ Epics in the view can be sorted by:
 Each option contains a button that toggles the sort order between **ascending** and **descending**. The sort option and order will be persisted when browsing Epics,
 including the [epics list view](../epics/index.md).
 
-Roadmaps can also be [visualized inside an epic](../epics/index.md#roadmap-in-epics).
+Roadmaps can also be [visualized inside an epic](../epics/index.md#roadmap).
 
 ## Timeline duration
 
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index d40ddb0039018866220050dd5a3f87d0e7fd7685..ee55d7e2a115693e4cff6fec60a909c1ef0f97bb 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -113,6 +113,36 @@ NOTE: **Note:** GitLab is unable to provide support for IdPs that are not listed
 | OneLogin | [Use the OneLogin SAML Test Connector](https://onelogin.service-now.com/support?id=kb_article&sys_id=93f95543db109700d5505eea4b96198f) |
 | Ping Identity | [Add and configure a new SAML application](https://support.pingidentity.com/s/document-item?bundleId=pingone&topicId=xsh1564020480660-1.html) |
 
+When [configuring your identify provider](#configuring-your-identity-provider), please consider the notes below for specific providers to help avoid common issues and as a guide for terminology used.
+
+### Okta setup notes
+
+| GitLab Setting | Okta Field |
+|--------------|----------------|
+| Identifier | Audience URI |
+| Assertion consumer service URL | Single sign on URL |
+
+Under Okta's **Single sign on URL** field, check the option **Use this for Recipient URL and Destination URL**.
+
+Set attribute statements according to the [assertions table](#assertions).
+
+### OneLogin setup notes
+
+The GitLab app listed in the OneLogin app catalog is for self-managed GitLab instances.
+For GitLab.com, use a generic SAML Test Connector such as the SAML Test Connector (Advanced).
+
+| GitLab Setting | OneLogin Field |
+|--------------|----------------|
+| Identifier | Audience |
+| Assertion consumer service URL | Recipient |
+| Assertion consumer service URL | ACS (Consumer) URL |
+| Assertion consumer service URL (escaped version) | ACS (Consumer) URL Validator |
+| GitLab single sign on URL | Login URL |
+
+Recommended `NameID` value: `OneLogin ID`.
+
+Set parameters according to the [assertions table](#assertions).
+
 ## Linking SAML to your existing GitLab.com account
 
 To link SAML to your existing GitLab.com account:
diff --git a/doc/user/index.md b/doc/user/index.md
index 9b38447a9aea273ff84e3413bf391329bed75f49..ee5d4a0a07b48b12c1b2522323ac0406515f1567 100644
--- a/doc/user/index.md
+++ b/doc/user/index.md
@@ -26,11 +26,11 @@ For more information, see [All GitLab Features](https://about.gitlab.com/feature
 
 To get familiar with the concepts needed to develop code on GitLab, read the following articles:
 
-- [Demo: Mastering Code Review With GitLab](https://about.gitlab.com/2017/03/17/demo-mastering-code-review-with-gitlab/).
-- [GitLab Workflow: An Overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/#gitlab-workflow-use-case-scenario).
-- [Tutorial: It's all connected in GitLab](https://about.gitlab.com/2016/03/08/gitlab-tutorial-its-all-connected/): an overview on code collaboration with GitLab.
-- [Trends in Version Control Land: Microservices](https://about.gitlab.com/2016/08/16/trends-in-version-control-land-microservices/).
-- [Trends in Version Control Land: Innersourcing](https://about.gitlab.com/2016/07/07/trends-version-control-innersourcing/).
+- [Demo: Mastering Code Review With GitLab](https://about.gitlab.com/blog/2017/03/17/demo-mastering-code-review-with-gitlab/).
+- [GitLab Workflow: An Overview](https://about.gitlab.com/blog/2016/10/25/gitlab-workflow-an-overview/#gitlab-workflow-use-case-scenario).
+- [Tutorial: It's all connected in GitLab](https://about.gitlab.com/blog/2016/03/08/gitlab-tutorial-its-all-connected/): an overview on code collaboration with GitLab.
+- [Trends in Version Control Land: Microservices](https://about.gitlab.com/blog/2016/08/16/trends-in-version-control-land-microservices/).
+- [Trends in Version Control Land: Innersourcing](https://about.gitlab.com/blog/2016/07/07/trends-version-control-innersourcing/).
 
 ## Use cases
 
diff --git a/doc/user/packages/maven_repository/index.md b/doc/user/packages/maven_repository/index.md
index 0c0b44b3cd89fba1cc578efcd264ce3056095a33..8ed10c098917829479ac5f8e9d8f79127c16b797 100644
--- a/doc/user/packages/maven_repository/index.md
+++ b/doc/user/packages/maven_repository/index.md
@@ -170,7 +170,7 @@ the `distributionManagement` section:
 <repositories>
   <repository>
     <id>gitlab-maven</id>
-    <url>https://gitlab.com/api/v4/groups/my-group/-/packages/maven</url>
+    <url>https://gitlab.com/api/v4/groups/GROUP_ID/-/packages/maven</url>
   </repository>
 </repositories>
 <distributionManagement>
diff --git a/doc/user/project/canary_deployments.md b/doc/user/project/canary_deployments.md
index 88205093d74a698a86bcbd144654cfad5ad13772..3a19c29b241e3fc05c0ab3ef8933f738632f890d 100644
--- a/doc/user/project/canary_deployments.md
+++ b/doc/user/project/canary_deployments.md
@@ -66,5 +66,5 @@ can easily notice them.
 [ee-1659]: https://gitlab.com/gitlab-org/gitlab/issues/1659
 [kube-canary]: https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#canary-deployments
 [deploy board]: deploy_boards.md
-[cd-blog]: https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/
+[cd-blog]: https://about.gitlab.com/blog/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/
 [kube-net]: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies
diff --git a/doc/user/project/clusters/img/kubernetes_pod_logs.png b/doc/user/project/clusters/img/kubernetes_pod_logs.png
deleted file mode 100644
index e664a47386a7927b4ab5438b895afbdd21405ebf..0000000000000000000000000000000000000000
Binary files a/doc/user/project/clusters/img/kubernetes_pod_logs.png and /dev/null differ
diff --git a/doc/user/project/clusters/img/kubernetes_pod_logs_v12_4.png b/doc/user/project/clusters/img/kubernetes_pod_logs_v12_4.png
new file mode 100644
index 0000000000000000000000000000000000000000..73c2ecd182a78c98a998743c0a849ba16eb23098
Binary files /dev/null and b/doc/user/project/clusters/img/kubernetes_pod_logs_v12_4.png differ
diff --git a/doc/user/project/clusters/kubernetes_pod_logs.md b/doc/user/project/clusters/kubernetes_pod_logs.md
index 82f658ce724c986ae4b11c0d17ec4f8f837615f6..4036eaf0bfb302c946e0423f3e72abd4566cce66 100644
--- a/doc/user/project/clusters/kubernetes_pod_logs.md
+++ b/doc/user/project/clusters/kubernetes_pod_logs.md
@@ -17,8 +17,14 @@ Everything you need to build, test, deploy, and run your app at scale.
 1. On the **Environments** page, you should see the status of the environment's pods with [Deploy Boards](../deploy_boards.md).
 1. When mousing over the list of pods, a tooltip will appear with the exact pod name and status.
    ![Deploy Boards pod list](img/pod_logs_deploy_board.png)
-1. Click on the desired pod to bring up the logs view, which will contain the last 500 lines for that pod. Support for pods with multiple containers is coming [in a future release](https://gitlab.com/gitlab-org/gitlab/issues/6502).
-   ![Deploy Boards pod list](img/kubernetes_pod_logs.png)
+1. Click on the desired pod to bring up the logs view, which will contain the last 500 lines for that pod.
+   You may switch between the following in this view:
+   - Pods.
+   - [From GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/issues/5769), environments.
+
+   Support for pods with multiple containers is coming [in a future release](https://gitlab.com/gitlab-org/gitlab/issues/6502).
+
+   ![Deploy Boards pod list](img/kubernetes_pod_logs_v12_4.png)
 
 ## Requirements
 
diff --git a/doc/user/project/code_owners.md b/doc/user/project/code_owners.md
index 0d422612f026847e248a93bc0bee19f69309654f..476f513480cba90e9658c6addff0f4c64775f157 100644
--- a/doc/user/project/code_owners.md
+++ b/doc/user/project/code_owners.md
@@ -1,8 +1,13 @@
+---
+type: reference
+---
+
 # Code Owners **(STARTER)**
 
 > - [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/6916)
 in [GitLab Starter](https://about.gitlab.com/pricing/) 11.3.
 > - [Support for group namespaces](https://gitlab.com/gitlab-org/gitlab-foss/issues/53182) added in GitLab Starter 12.1.
+> - Code Owners for Merge Request approvals was [introduced](https://gitlab.com/gitlab-org/gitlab/issues/4418) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.9.
 
 You can use a `CODEOWNERS` file to specify users or
 [shared groups](members/share_project_with_groups.md)
@@ -10,9 +15,9 @@ that are responsible for certain files in a repository.
 
 You can choose and add the `CODEOWNERS` file in three places:
 
-- to the root directory of the repository
-- inside the `.gitlab/` directory
-- inside the `docs/` directory
+- To the root directory of the repository
+- Inside the `.gitlab/` directory
+- Inside the `docs/` directory
 
 The `CODEOWNERS` file is scoped to a branch, which means that with the
 introduction of new files, the person adding the new content can
@@ -23,6 +28,18 @@ When a file matches multiple entries in the `CODEOWNERS` file,
 the users from all entries are displayed on the blob page of
 the given file.
 
+## Approvals by Code Owners
+
+Once you've set Code Owners to a project, you can configure it to
+receive approvals:
+
+- As [merge request eligible approvers](merge_requests/merge_request_approvals.md#code-owners-as-eligible-approvers-starter). **(STARTER)**
+- As required approvers for [protected branches](protected_branches.md#protected-branches-approval-by-code-owners-premium). **(PREMIUM)**
+
+Once set, Code Owners are displayed in merge requests widgets:
+
+![MR widget - Code Owners](img/code_owners_mr_widget_v12_4.png)
+
 ## The syntax of Code Owners files
 
 Files can be specified using the same kind of patterns you would use
diff --git a/doc/user/project/img/code_owners_approval_new_protected_branch_v12_4.png b/doc/user/project/img/code_owners_approval_new_protected_branch_v12_4.png
new file mode 100755
index 0000000000000000000000000000000000000000..f813b60dcd923b105705dcafb0bf74b46bdddf05
Binary files /dev/null and b/doc/user/project/img/code_owners_approval_new_protected_branch_v12_4.png differ
diff --git a/doc/user/project/img/code_owners_approval_protected_branch_v12_4.png b/doc/user/project/img/code_owners_approval_protected_branch_v12_4.png
new file mode 100755
index 0000000000000000000000000000000000000000..59da6874d141619fc25e575a299e5bdfd9c84a75
Binary files /dev/null and b/doc/user/project/img/code_owners_approval_protected_branch_v12_4.png differ
diff --git a/doc/user/project/img/code_owners_mr_widget_v12_4.png b/doc/user/project/img/code_owners_mr_widget_v12_4.png
new file mode 100644
index 0000000000000000000000000000000000000000..7f7b15ee017fa9a556e2db9d55875ca0d029ff00
Binary files /dev/null and b/doc/user/project/img/code_owners_mr_widget_v12_4.png differ
diff --git a/doc/user/project/index.md b/doc/user/project/index.md
index ef80c8fc6a36ec0285e55e06e8bad4e644764caa..7ae288996da4fa85945091180c1571b4e326ad0e 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.md
@@ -59,7 +59,7 @@ When you create a project in GitLab, you'll have access to a large number of
 
 **GitLab CI/CD:**
 
-- [GitLab CI/CD](../../ci/README.md): GitLab's built-in [Continuous Integration, Delivery, and Deployment](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) tool
+- [GitLab CI/CD](../../ci/README.md): GitLab's built-in [Continuous Integration, Delivery, and Deployment](https://about.gitlab.com/blog/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) tool
   - [Container Registry](../packages/container_registry/index.md): Build and push Docker
   images out-of-the-box
   - [Auto Deploy](../../topics/autodevops/index.md#auto-deploy): Configure GitLab CI/CD
@@ -155,6 +155,26 @@ when a project is part of a group (under a
 If you choose to leave a project you will no longer be a project
 member, therefore, unable to contribute.
 
+## Project's landing page
+
+The project's landing page shows different information depending on
+the project's visibility settings and user permissions.
+
+For public projects, and to members of internal and private projects
+with [permissions to view the project's code](../permissions.md#project-members-permissions):
+
+- The content of a
+  [`README` or an index file](repository/#repository-readme-and-index-files)
+  is displayed (if any), followed by the list of directories within the
+  project's repository.
+- If the project doesn't contain either of these files, the
+  visitor will see the list of files and directories of the repository.
+
+For users without permissions to view the project's code:
+
+- The wiki homepage is displayed, if any.
+- The list of issues within the project is displayed.
+
 ## Redirects when changing repository paths
 
 When a repository path changes, it is essential to smoothly transition from the
diff --git a/doc/user/project/integrations/generic_alerts.md b/doc/user/project/integrations/generic_alerts.md
index ec43696fdeef4a04fcf128e88a6d8f0f2f47ede0..b5f86c00eb33cfc24164aaea47a573dbf3bce246 100644
--- a/doc/user/project/integrations/generic_alerts.md
+++ b/doc/user/project/integrations/generic_alerts.md
@@ -1,6 +1,6 @@
 # Generic alerts integration **(ULTIMATE)**
 
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13203) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.3.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13203) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.4.
 
 GitLab can accept alerts from any source via a generic webhook receiver.
 When you set up the generic alerts integration, a unique endpoint will
@@ -16,7 +16,7 @@ authored by the GitLab Alert Bot.
 To set up the generic alerts integration:
 
 1. Navigate to **Settings > Integrations** in a project.
-1. Click on **Alert endpoint**.
+1. Click on **Alerts endpoint**.
 1. Toggle the **Active**  alert setting. The `URL` and `Authorization Key` for the webhook configuration can be found there.
 
 ## Customizing the payload
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index 168ec1b15ea83dea4b4ca0ef6cea53a99ec6e441..e385ee536362bcdaada2d272ed9a58e323eb3a6c 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -56,6 +56,16 @@ Click on the service links to see further configuration instructions and details
 | [Redmine](redmine.md) | Redmine issue tracker |
 | [YouTrack](youtrack.md) | YouTrack issue tracker |
 
+## Push hooks limit
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/31009) in GitLab 12.4.
+
+If a single push includes changes to more than three branches or tags, services
+supported by `push_hooks` and `tag_push_hooks` events won't be executed.
+
+The number of branches or tags supported can be changed via
+[`push_event_hooks_limit` application setting](../../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls).
+
 ## Services templates
 
 Services templates is a way to set some predefined values in the Service of
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index de2ede6b208ec71274cce12ee10269dd697aceaa..d0f538a4b525c87789d676938adb147cec812c41 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -53,7 +53,7 @@ Navigate to the webhooks page by going to your project's
   [Slack](https://api.slack.com/incoming-webhooks) every time a job fails.
 - You can [integrate with Twilio to be notified via SMS](https://www.datadoghq.com/blog/send-alerts-sms-customizable-webhooks-twilio/)
   every time an issue is created for a specific project or group within GitLab
-- You can use them to [automatically assign labels to merge requests](https://about.gitlab.com/2016/08/19/applying-gitlab-labels-automatically/).
+- You can use them to [automatically assign labels to merge requests](https://about.gitlab.com/blog/2016/08/19/applying-gitlab-labels-automatically/).
 
 ## Webhook endpoint tips
 
@@ -107,6 +107,9 @@ detailed commit data is expensive. Note that despite only 20 commits being
 present in the `commits` attribute, the `total_commits_count` attribute will
 contain the actual total.
 
+Also, if a single push includes changes for more than three (by default, depending on
+[`push_event_hooks_limit` setting](../../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls)) branches, this hook won't be executed.
+
 **Request header**:
 
 ```
@@ -190,6 +193,10 @@ X-Gitlab-Event: Push Hook
 
 Triggered when you create (or delete) tags to the repository.
 
+NOTE: **Note:**
+If a single push includes changes for more than three (by default, depending on
+[`push_event_hooks_limit` setting](../../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls)) tags, this hook won't be executed.
+
 **Request header**:
 
 ```
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index e9a7a15b630df24614c575e9aabe547e8ace95f6..403972941b2907a61c7a55e4bfe57e5c3f23bc9d 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -1,6 +1,6 @@
 # Issue Boards
 
-> [Introduced][ce-5554] in [GitLab 8.11](https://about.gitlab.com/2016/08/22/gitlab-8-11-released/#issue-board).
+> [Introduced][ce-5554] in [GitLab 8.11](https://about.gitlab.com/blog/2016/08/22/gitlab-8-11-released/#issue-board).
 
 ## Overview
 
@@ -125,9 +125,9 @@ Cards finished by the UX team will automatically appear in the **Frontend** colu
 
 NOTE: **Note:**
 For a broader use case, please see the blog post
-[GitLab Workflow, an Overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/#gitlab-workflow-use-case-scenario).
+[GitLab Workflow, an Overview](https://about.gitlab.com/blog/2016/10/25/gitlab-workflow-an-overview/#gitlab-workflow-use-case-scenario).
 For a real use case example, you can read why
-[Codepen decided to adopt Issue Boards](https://about.gitlab.com/2017/01/27/codepen-welcome-to-gitlab/#project-management-everything-in-one-place)
+[Codepen decided to adopt Issue Boards](https://about.gitlab.com/blog/2017/01/27/codepen-welcome-to-gitlab/#project-management-everything-in-one-place)
 to improve their workflow with multiple boards.
 
 #### Quick assignments
@@ -194,7 +194,7 @@ of the issue card you have selected and drop it in the new list you want.
 
 ### Configurable Issue Boards **(STARTER)**
 
-> Introduced in [GitLab Starter Edition 10.2](https://about.gitlab.com/2017/11/22/gitlab-10-2-released/#issue-boards-configuration).
+> Introduced in [GitLab Starter Edition 10.2](https://about.gitlab.com/blog/2017/11/22/gitlab-10-2-released/#issue-boards-configuration).
 
 An Issue Board can be associated with a GitLab [Milestone](milestones/index.md#milestones),
 [Labels](labels.md), Assignee and Weight
@@ -214,7 +214,7 @@ If you don't have editing permission in a board, you're still able to see the co
 
 ### Focus mode **(STARTER)**
 
-> Introduced in [GitLab Starter 9.1](https://about.gitlab.com/2017/04/22/gitlab-9-1-released/#issue-boards-focus-mode-ees-eep).
+> Introduced in [GitLab Starter 9.1](https://about.gitlab.com/blog/2017/04/22/gitlab-9-1-released/#issue-boards-focus-mode-ees-eep).
 
 Click the button at the top right to toggle focus mode on and off. In focus mode, the navigation UI is hidden, allowing you to focus on issues in the board.
 
@@ -230,7 +230,7 @@ especially in combination with [assignee lists](#assignee-lists-premium).
 
 ### Group Issue Boards **(PREMIUM)**
 
-> Introduced in [GitLab Premium 10.0](https://about.gitlab.com/2017/09/22/gitlab-10-0-released/#group-issue-boards).
+> Introduced in [GitLab Premium 10.0](https://about.gitlab.com/blog/2017/09/22/gitlab-10-0-released/#group-issue-boards).
 
 Accessible at the group navigation level, a group issue board offers the same features as a project-level board,
 but it can display issues from all projects in that
@@ -239,7 +239,7 @@ boards. When updating milestones and labels for an issue through the sidebar upd
 group-level objects are available.
 
 NOTE: **Note:**
-Multiple group issue boards were originally introduced in [GitLab 10.0 Premium](https://about.gitlab.com/2017/09/22/gitlab-10-0-released/#group-issue-boards) and
+Multiple group issue boards were originally introduced in [GitLab 10.0 Premium](https://about.gitlab.com/blog/2017/09/22/gitlab-10-0-released/#group-issue-boards) and
 one group issue board per group was made available in GitLab 10.6 Core.
 
 ![Group issue board](img/group_issue_board.png)
@@ -287,7 +287,7 @@ Different issue board features are available in different [GitLab tiers](https:/
 
 | Tier     | Number of Project Issue Boards | Number of Group Issue Boards | Configurable Issue Boards | Assignee Lists |
 |----------|--------------------------------|------------------------------|---------------------------|----------------|
-| Core / Free     | 1                              | 1                            | No                        | No             |
+| Core / Free     | Multiple                              | 1                            | No                        | No             |
 | Starter / Bronze  | Multiple                       | 1                            | Yes                       | No             |
 | Premium / Silver | Multiple                       | Multiple                     | Yes                       | Yes            |
 | Ultimate / Gold | Multiple                       | Multiple                     | Yes                       | Yes            |
diff --git a/doc/user/project/issues/csv_export.md b/doc/user/project/issues/csv_export.md
index 9321f6120ac740088739e8da20ad90acc27f207d..fb7fdde7b940765a7940084c9bea45d6716be795 100644
--- a/doc/user/project/issues/csv_export.md
+++ b/doc/user/project/issues/csv_export.md
@@ -1,6 +1,6 @@
 # Export Issues to CSV **(STARTER)**
 
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/1126) in [GitLab Starter 9.0](https://about.gitlab.com/2017/03/22/gitlab-9-0-released/#export-issues-ees-eep).
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/1126) in [GitLab Starter 9.0](https://about.gitlab.com/blog/2017/03/22/gitlab-9-0-released/#export-issues-ees-eep).
 
 Issues can be exported as CSV from GitLab and are sent to your default notification email as an attachment.
 
diff --git a/doc/user/project/issues/design_management.md b/doc/user/project/issues/design_management.md
index 3318d2fdd2e642ccc081af9907da93ffa8a95935..169da7049a641581484383a5b8ec2b9d5f3fffa5 100644
--- a/doc/user/project/issues/design_management.md
+++ b/doc/user/project/issues/design_management.md
@@ -47,6 +47,8 @@ to be enabled:
   when an issue is deleted.
 - Design Management
   [isn't supported by Geo](https://gitlab.com/groups/gitlab-org/-/epics/1633) yet.
+- Only the latest version of the designs can be deleted.
+- Deleted designs cannot be recovered but you can see them on previous designs versions.
 
 ## The Design Management page
 
@@ -61,6 +63,9 @@ To upload design images, click the **Upload Designs** button and select images t
 Designs with the same filename as an existing uploaded design will create a new version
 of the design, and will replace the previous version.
 
+Designs cannot be added if the issue has been moved, or its
+[discussion is locked](../../discussions/#lock-discussions).
+
 ## Viewing designs
 
 Images on the Design Management page can be enlarged by clicking on them.
@@ -77,6 +82,34 @@ to help summarize changes between versions.
 | Modified (in the selected version) | ![Design Modified](img/design_modified_v12_3.png) |
 | Added (in the selected version) | ![Design Added](img/design_added_v12_3.png) |
 
+## Deleting designs
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/11089) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.4.
+
+There are two ways to delete designs: manually delete them
+individually, or select a few of them to delete at once,
+as shown below.
+
+To delete a single design, click it to view it enlarged,
+then click the trash icon on the top right corner and confirm
+the deletion by clicking the **Delete** button on the modal window:
+
+![Confirm design deletion](img/confirm_design_deletion_v12_4.png)
+
+To delete multiple designs at once, on the design's list view,
+first select the designs you want to delete:
+
+![Select designs](img/select_designs_v12_4.png)
+
+Once selected, click the **Delete selected** button to confirm the deletion:
+
+![Delete multiple designs](img/delete_multiple_designs_v12_4.png)
+
+NOTE: **Note:**
+Only the latest version of the designs can be deleted.
+Deleted designs are not permanently lost; they can be
+viewed by browsing previous versions.
+
 ## Adding annotations to designs
 
 When a design image is displayed, you can add annotations to it by clicking on
diff --git a/doc/user/project/issues/img/confirm_design_deletion_v12_4.png b/doc/user/project/issues/img/confirm_design_deletion_v12_4.png
new file mode 100644
index 0000000000000000000000000000000000000000..b1a55c639ca74e8ae024bf7c859bee1d09e2ce08
Binary files /dev/null and b/doc/user/project/issues/img/confirm_design_deletion_v12_4.png differ
diff --git a/doc/user/project/issues/img/delete_multiple_designs_v12_4.png b/doc/user/project/issues/img/delete_multiple_designs_v12_4.png
new file mode 100644
index 0000000000000000000000000000000000000000..b421a5577df30a8f3f285fbb8853cc75f9ccd62c
Binary files /dev/null and b/doc/user/project/issues/img/delete_multiple_designs_v12_4.png differ
diff --git a/doc/user/project/issues/img/delete_single_design_v12_4.png b/doc/user/project/issues/img/delete_single_design_v12_4.png
new file mode 100644
index 0000000000000000000000000000000000000000..0ca03b48e7674e854f91e70d044f61c362c7b4e2
Binary files /dev/null and b/doc/user/project/issues/img/delete_single_design_v12_4.png differ
diff --git a/doc/user/project/issues/img/select_all_designs_v12_4.png b/doc/user/project/issues/img/select_all_designs_v12_4.png
new file mode 100644
index 0000000000000000000000000000000000000000..b08b04c1214a4a8e5beaea07e3ff0624a5a3633b
Binary files /dev/null and b/doc/user/project/issues/img/select_all_designs_v12_4.png differ
diff --git a/doc/user/project/issues/img/select_designs_v12_4.png b/doc/user/project/issues/img/select_designs_v12_4.png
new file mode 100644
index 0000000000000000000000000000000000000000..a53bd516300d66ced1f9c4013e8505d671449ad8
Binary files /dev/null and b/doc/user/project/issues/img/select_designs_v12_4.png differ
diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md
index eaf4922bec9c3df3a0cebf11e6b3f03944ca6fc2..6abd6fd7047c6fcb82b793c5436867332935170b 100644
--- a/doc/user/project/issues/index.md
+++ b/doc/user/project/issues/index.md
@@ -20,7 +20,7 @@ you can also view all the issues collectively at the group level.
 - Accepting feature proposals, questions, support requests, or bug reports
 - Elaborating on new code implementations
 
-See also [Always start a discussion with an issue](https://about.gitlab.com/2016/03/03/start-with-an-issue/).
+See also [Always start a discussion with an issue](https://about.gitlab.com/blog/2016/03/03/start-with-an-issue/).
 
 ## Parts of an issue
 
diff --git a/doc/user/project/issues/multiple_assignees_for_issues.md b/doc/user/project/issues/multiple_assignees_for_issues.md
index a1b16457a0d551bca084547b99f4705d6b205f1c..b442f70a061d78beab17b7260fa40fb4b8bed058 100644
--- a/doc/user/project/issues/multiple_assignees_for_issues.md
+++ b/doc/user/project/issues/multiple_assignees_for_issues.md
@@ -2,7 +2,7 @@
 
 > **Note:**
 [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/1904)
-in [GitLab Starter 9.2](https://about.gitlab.com/2017/05/22/gitlab-9-2-released/#multiple-assignees-for-issues).
+in [GitLab Starter 9.2](https://about.gitlab.com/blog/2017/05/22/gitlab-9-2-released/#multiple-assignees-for-issues).
 
 ## Overview
 
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
index 32c8c4d0453cba06899ecb353ab75628dc33dab8..cfd6d4eaf4b31a26ab2c1134212c6b2c734c0cbd 100644
--- a/doc/user/project/labels.md
+++ b/doc/user/project/labels.md
@@ -2,9 +2,9 @@
 
 ## Overview
 
-Labels allow you to categorize issues or merge requests using descriptive titles like
+Labels allow you to categorize epics, issues, and merge requests using descriptive titles like
 `bug`, `feature request`, or `docs`. Each label also has a customizable color. They
-allow you to quickly and dynamically filter and manage issues or merge requests you
+allow you to quickly and dynamically filter and manage epics, issues and merge requests you
 care about, and are visible throughout GitLab in most places where issues and merge
 requests are located.
 
@@ -12,8 +12,8 @@ requests are located.
 
 In GitLab, you can create project and group labels:
 
-- **Project labels** can be assigned to issues or merge requests in that project only.
-- **Group labels** can be assigned to any issue or merge request in any project in
+- **Project labels** can be assigned to epics, issues and merge requests in that project only.
+- **Group labels** can be assigned to any epics, issue and merge request in any project in
   that group, or any subgroups of the group.
 
 ## Scoped labels **(PREMIUM)**
@@ -21,7 +21,7 @@ In GitLab, you can create project and group labels:
 > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/9175) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.10.
 
 Scoped labels allow teams to use the simple and familiar label feature to
-annotate their issues, merge requests, and epics to achieve custom fields and
+annotate their epics, issues, merge requests, and epics to achieve custom fields and
 custom workflow states by leveraging a special label title syntax.
 
 A scoped label is a kind of label defined by a special double-colon syntax
@@ -141,11 +141,8 @@ action cannot be reversed and the changes are permanent.
 
 ## Assigning labels from the sidebar
 
-Every issue and merge request can be assigned any number of labels. The labels are
-visible on every issue and merge request page, in the sidebar. They are also visible on:
-
-- Every issue and merge request page in the sidebar.
-- The issue board.
+Every epic, issue, and merge request can be assigned any number of labels. The labels are
+visible on every epic, issue and merge request page, in the sidebar and on your issue boards.
 
 From the sidebar, you can assign or unassign a label to the object (i.e. label or
 unlabel it). You can also perform this as a [quick action](quick_actions.md),
@@ -166,11 +163,11 @@ GitLab will check both the label titles and descriptions for the search.
 
 ## Filtering by label
 
-The following can be filtered labels:
+The following can be filtered by labels:
 
+- Epic lists **(ULTIMATE)**
 - Issue lists
 - Merge Request lists
-- Epic lists **(ULTIMATE)**
 - Issue Boards
 
 ### Filtering in list pages
@@ -180,7 +177,7 @@ The following can be filtered labels:
   - Group labels (including subgroup ancestors)
   - Project labels
 
-- From the group issue list page and the group merge request list page, you can
+- From the group epic lists page, issue list page and the group merge request list page, you can
   [filter](../search/index.md#issues-and-merge-requests) by:
   - Group labels (including subgroup ancestors and subgroup descendants)
   - Project labels
@@ -214,7 +211,7 @@ The following can be filtered labels:
 
 From the project label list page and the group label list page, you can subscribe
 to [notifications](../../workflow/notifications.md) of a given label, to alert you
-that the label has been assigned to an issue or merge request.
+that the label has been assigned to an epic, issue, and merge request.
 
 ![Labels subscriptions](img/labels_subscriptions_v12_1.png)
 
@@ -226,7 +223,7 @@ that the label has been assigned to an issue or merge request.
 > - Priority sorting is based on the highest priority label only. [This discussion](https://gitlab.com/gitlab-org/gitlab-foss/issues/18554) considers changing this.
 
 Labels can have relative priorities, which are used in the "Label priority" and
-"Priority" sort orders of the issue and merge request list pages.
+"Priority" sort orders of the epic, issue, and merge request list pages.
 
 From the project label list page, star a label to indicate that it has a priority.
 
@@ -242,8 +239,8 @@ on the project label list page.
 
 ![Drag to change label priority](img/labels_drag_priority_v12_1.gif)
 
-On the merge request and issue pages, for both groups and projects, you can sort by `Label priority`
-and `Priority`, which account for objects (issues and merge requests) that have prioritized
+On the epic, merge request and issue pages, for both groups and projects, you can sort by `Label priority`
+and `Priority`, which account for objects (epic, issues, and merge requests) that have prioritized
 labels assigned to them.
 
 If you sort by `Label priority`, GitLab considers this sort comparison order:
diff --git a/doc/user/project/merge_requests/img/mr_approvals_by_code_owners_v12_4.png b/doc/user/project/merge_requests/img/mr_approvals_by_code_owners_v12_4.png
new file mode 100755
index 0000000000000000000000000000000000000000..c704129685ffdc1f8fcd84020d3935a3e9b565d1
Binary files /dev/null and b/doc/user/project/merge_requests/img/mr_approvals_by_code_owners_v12_4.png differ
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 4b13d90074f0b75959bc810f590506f0e2513225..2ab7c3fb15bb27452ea86af56db1b5def9a5d2c8 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -70,7 +70,7 @@ B. Consider you're a web developer writing a webpage for your company's website:
 1. Your changes are previewed with [Review Apps](../../../ci/review_apps/index.md)
 1. You request your web designers for their implementation
 1. You request the [approval](merge_request_approvals.md) from your manager **(STARTER)**
-1. Once approved, your merge request is [squashed and merged](squash_and_merge.md), and [deployed to staging with GitLab Pages](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
+1. Once approved, your merge request is [squashed and merged](squash_and_merge.md), and [deployed to staging with GitLab Pages](https://about.gitlab.com/blog/2016/08/26/ci-deployment-and-environments/)
 1. Your production team [cherry picks](#cherry-pick-changes) the merge commit into production
 
 ## Merge requests per project
diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md
index 8a328c025ffe5104db2bbbdc53de12591db3bccd..2aa92ba2316568a5bb57395ff800c88086a8849f 100644
--- a/doc/user/project/merge_requests/merge_request_approvals.md
+++ b/doc/user/project/merge_requests/merge_request_approvals.md
@@ -4,7 +4,7 @@ type: reference, concepts
 
 # Merge request approvals **(STARTER)**
 
-> Introduced in [GitLab Enterprise Edition 7.12](https://about.gitlab.com/2015/06/22/gitlab-7-12-released/#merge-request-approvers-ee-only).
+> Introduced in [GitLab Enterprise Edition 7.12](https://about.gitlab.com/blog/2015/06/22/gitlab-7-12-released/#merge-request-approvers-ee-only).
 
 Merge request approvals enable enforced code review by requiring specified people
 to approve a merge request before it can be unblocked for merging.
@@ -101,7 +101,7 @@ any [eligible approver](#eligible-approvers) may approve.
 The following can approve merge requests:
 
 - Users being added as approvers at project or merge request level.
-- [Code owners](../code_owners.md) related to the merge request ([introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/7933) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.5).
+- [Code owners](#code-owners-as-eligible-approvers-starter) to the files changed by the merge request.
 
 An individual user can be added as an approver for a project if they are a member of:
 
@@ -119,6 +119,31 @@ if [**Prevent author approval**](#allowing-merge-request-authors-to-approve-thei
 and [**Prevent committers approval**](#prevent-approval-of-merge-requests-by-their-committers) (disabled by default)
 are enabled on the project settings.
 
+### Code Owners as eligible approvers **(STARTER)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/7933) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.5.
+
+Once you've added [Code Owners](../code_owners.md) to your
+repository, the owners to the corresponding files will become
+eligible approvers, together with members with Developer or
+higher permissions.
+
+To enable this merge request approval rule:
+
+1. Navigate to your project's **Settings > General** and expand
+**Merge request approvals**.
+1. Locate **All members with Developer role or higher and code owners (if any)** and click **Edit** to choose the number of approvals required.
+
+![MR approvals by Code Owners](img/mr_approvals_by_code_owners_v12_4.png)
+
+Once set, merge requests can only be merged once approved by the
+number of approvals you've set. GitLab will accept approvals from
+users with Developer or higher permissions, as well as by Code Owners,
+indistinguishably.
+
+Alternatively, you can **require**
+[Code Owner's approvals for Protected Branches](../protected_branches.md#protected-branches-approval-by-code-owners-premium). **(PREMIUM)**
+
 ### Implicit approvers
 
 If the number of required approvals is greater than the number of approvers,
@@ -162,26 +187,6 @@ are other conditions that may block it, such as merge conflicts,
 [pending discussions](../../discussions/index.md#only-allow-merge-requests-to-be-merged-if-all-threads-are-resolved)
 or a [failed CI/CD pipeline](merge_when_pipeline_succeeds.md).
 
-## Code Owners approvals **(PREMIUM)**
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/4418) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.9.
-
-It is possible to require at least one approval for each entry in the
-[`CODEOWNERS` file](../code_owners.md) that matches a file changed in
-the merge request. To enable this feature:
-
-1. Navigate to your project's **Settings > General** and expand
-   **Merge request approvals**.
-1. Tick the **Require approval from code owners** checkbox.
-1. Click **Save changes**.
-
-When this feature is enabled, all merge requests will need approval
-from one code owner per matched rule before it can be merged.
-
-NOTE: **Note:** Only the `CODEOWNERS` file on the default branch is evaluated for
-Merge Request approvals. If `CODEOWNERS` is changed on a non-default branch, those
-changes will not affect approvals until merged to the default branch.
-
 ## Overriding the merge request approvals default settings
 
 > Introduced in GitLab Enterprise Edition 9.4.
diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md
index a5d8de9a6cd1d7d3b59990d53ae48a2fed01331d..326a2d302d29916bbf04f9f991d304c17707c7fa 100644
--- a/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md
+++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md
@@ -135,8 +135,8 @@ If you're using CloudFlare, check
 > - **Do not** add any special chars after the default Pages
   domain. E.g., don't point `subdomain.domain.com` to
   or `namespace.gitlab.io/`. Some domain hosting providers may request a trailling dot (`namespace.gitlab.io.`), though.
-> - GitLab Pages IP on GitLab.com [was changed](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/) in 2017.
-> - GitLab Pages IP on GitLab.com [has changed](https://about.gitlab.com/2018/07/19/gcp-move-update/#gitlab-pages-and-custom-domains)
+> - GitLab Pages IP on GitLab.com [was changed](https://about.gitlab.com/blog/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/) in 2017.
+> - GitLab Pages IP on GitLab.com [has changed](https://about.gitlab.com/blog/2018/07/19/gcp-move-update/#gitlab-pages-and-custom-domains)
   from `52.167.214.135` to `35.185.44.232` in 2018.
 
 #### 4. Verify the domain's ownership
@@ -221,7 +221,7 @@ To secure your custom domain with GitLab Pages you can opt by:
   the part of the encryption keychain that identifies the CA.
   Usually it's combined with the PEM certificate, but there are
   some cases in which you need to add them manually.
-  [CloudFlare certs](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/)
+  [CloudFlare certs](https://about.gitlab.com/blog/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/)
   are one of these cases.
 - **A private key**, it's an encrypted key which validates
   your PEM against your domain.
@@ -238,7 +238,7 @@ To secure your custom domain with GitLab Pages you can opt by:
 1. Add the PEM certificate to its corresponding field.
 1. If your certificate is missing its intermediate, copy
   and paste the root certificate (usually available from your CA website)
-  and paste it in the [same field as your PEM certificate](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/),
+  and paste it in the [same field as your PEM certificate](https://about.gitlab.com/blog/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/),
   just jumping a line between them.
 1. Copy your private key and paste it in the last field.
 
diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/ssl_tls_concepts.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/ssl_tls_concepts.md
index 30dc15b94c499322b441c517f138ab4d25aae9fe..ac0a1f1ceba166cabf26f84b3ec70e9a820997c6 100644
--- a/doc/user/project/pages/custom_domains_ssl_tls_certification/ssl_tls_concepts.md
+++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/ssl_tls_concepts.md
@@ -72,4 +72,4 @@ source, and free to use. See [GitLab Pages integration with Let's Encrypt](../cu
 Similarly popular are [certificates issued by CloudFlare](https://www.cloudflare.com/ssl/),
 which also offers a [free CDN service](https://blog.cloudflare.com/cloudflares-free-cdn-and-you/).
 Their certs are valid up to 15 years. See the tutorial on
-[how to add a CloudFlare Certificate to your GitLab Pages website](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/).
+[how to add a CloudFlare Certificate to your GitLab Pages website](https://about.gitlab.com/blog/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/).
diff --git a/doc/user/project/pages/getting_started_part_four.md b/doc/user/project/pages/getting_started_part_four.md
index a544250eb0eb6efff27d726b77df71f6d50b3466..27bd9da8d18004782117fdf32a1a3cbd976a9572 100644
--- a/doc/user/project/pages/getting_started_part_four.md
+++ b/doc/user/project/pages/getting_started_part_four.md
@@ -385,10 +385,10 @@ to understand how to go even further on your scripts.
 
 - On this blog post, understand the concept of
   [using GitLab CI `environments` to deploy your
-  web app to staging and production](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/).
+  web app to staging and production](https://about.gitlab.com/blog/2016/08/26/ci-deployment-and-environments/).
 - On this post, learn [how to run jobs sequentially,
-  in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/)
+  in parallel, or build a custom pipeline](https://about.gitlab.com/blog/2016/07/29/the-basics-of-gitlab-ci/)
 - On this blog post, we go through the process of
-  [pulling specific directories from different projects](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/)
+  [pulling specific directories from different projects](https://about.gitlab.com/blog/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/)
   to deploy this website you're looking at, <https://docs.gitlab.com>.
-- On this blog post, we teach you [how to use GitLab Pages to produce a code coverage report](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/).
+- On this blog post, we teach you [how to use GitLab Pages to produce a code coverage report](https://about.gitlab.com/blog/2016/11/03/publish-code-coverage-report-with-gitlab-pages/).
diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md
index 9812626269247f612e14335c5943a241deeecbe9..0b1cae9ab4c35aca9d1282cafb622ec60629511a 100644
--- a/doc/user/project/pages/getting_started_part_one.md
+++ b/doc/user/project/pages/getting_started_part_one.md
@@ -97,7 +97,7 @@ _Read on about [Projects for GitLab Pages and URL structure](getting_started_par
 
 ### Further reading
 
-- Read through this technical overview on [Static versus Dynamic Websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/)
-- Understand [how modern Static Site Generators work](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) and what you can add to your static site
-- You can use [any SSG with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
+- Read through this technical overview on [Static versus Dynamic Websites](https://about.gitlab.com/blog/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/)
+- Understand [how modern Static Site Generators work](https://about.gitlab.com/blog/2016/06/10/ssg-overview-gitlab-pages-part-2/) and what you can add to your static site
+- You can use [any SSG with GitLab Pages](https://about.gitlab.com/blog/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
 - Fork an [example project](https://gitlab.com/pages) to build your website based upon
diff --git a/doc/user/project/pages/getting_started_part_two.md b/doc/user/project/pages/getting_started_part_two.md
index cb80bf1c43354c8bc9832cd50d1602bcb5093030..ff75291708717f39d8e413e63cdf2a09a10c3af7 100644
--- a/doc/user/project/pages/getting_started_part_two.md
+++ b/doc/user/project/pages/getting_started_part_two.md
@@ -122,7 +122,7 @@ where you'll find its default URL.
 
 > **Notes:**
 >
-> - GitLab Pages [supports any SSG](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/), but,
+> - GitLab Pages [supports any SSG](https://about.gitlab.com/blog/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/), but,
 >   if you don't find yours among the templates, you'll need
 >   to configure your own `.gitlab-ci.yml`. To do that, please
 >   read through the article [Creating and Tweaking GitLab CI/CD for GitLab Pages](getting_started_part_four.md). New SSGs are very welcome among
diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md
index 41a89a2130d1d6dcf12ffa9018de0bd595be7d2e..7d533c6f9d1dba0eaf3755b40c3367d34dc6634e 100644
--- a/doc/user/project/pages/index.md
+++ b/doc/user/project/pages/index.md
@@ -64,7 +64,7 @@ To publish a website with Pages, you can use any Static Site Generator (SSG),
 such as Jekyll, Hugo, Middleman, Harp, Hexo, and Brunch, just to name a few. You can also
 publish any website written directly in plain HTML, CSS, and JavaScript.</p>
 <p>Pages does <strong>not</strong> support dynamic server-side processing, for instance, as <code>.php</code> and <code>.asp</code> requires. See this article to learn more about
-<a href="https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/">static websites vs dynamic websites</a>.</p>
+<a href="https://about.gitlab.com/blog/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/">static websites vs dynamic websites</a>.</p>
 </div>
 <div class="col-md-3"><img src="img/ssgs_pages.png" alt="Examples of SSGs supported by Pages" class="image-noshadow middle display-block"></div>
 </div>
@@ -146,11 +146,11 @@ To learn more about configuration options for GitLab Pages, read the following:
 |---+---|
 | [Custom domains and SSL/TLS Certificates](custom_domains_ssl_tls_certification/index.md) | How to add custom domains and subdomains to your website, configure DNS records and SSL/TLS certificates. |
 | [Let's Encrypt integration](custom_domains_ssl_tls_certification/lets_encrypt_integration.md) | Secure your Pages sites with Let's Encrypt certificates automatically obtained and renewed by GitLab. |
-| [CloudFlare certificates](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) | Secure your Pages site with CloudFlare certificates. |
+| [CloudFlare certificates](https://about.gitlab.com/blog/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) | Secure your Pages site with CloudFlare certificates. |
 |---+---|
-| [Static vs dynamic websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) | A conceptual overview on static versus dynamic sites. |
-| [Modern static site generators](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) | A conceptual overview on SSGs. |
-| [Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) | An overview on using SSGs for GitLab Pages. |
+| [Static vs dynamic websites](https://about.gitlab.com/blog/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) | A conceptual overview on static versus dynamic sites. |
+| [Modern static site generators](https://about.gitlab.com/blog/2016/06/10/ssg-overview-gitlab-pages-part-2/) | A conceptual overview on SSGs. |
+| [Build any SSG site with GitLab Pages](https://about.gitlab.com/blog/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) | An overview on using SSGs for GitLab Pages. |
 
 ## Advanced use
 
@@ -158,11 +158,11 @@ There are quite some great examples of GitLab Pages websites built for some
 specific reasons. These examples can teach you some advanced techniques
 to use and adapt to your own needs:
 
-- [Posting to your GitLab Pages blog from iOS](https://about.gitlab.com/2016/08/19/posting-to-your-gitlab-pages-blog-from-ios/).
-- [GitLab CI: Run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/).
-- [GitLab CI: Deployment & environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/).
-- [Building a new GitLab docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/).
-- [Publish code coverage reports with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/).
+- [Posting to your GitLab Pages blog from iOS](https://about.gitlab.com/blog/2016/08/19/posting-to-your-gitlab-pages-blog-from-ios/).
+- [GitLab CI: Run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/blog/2016/07/29/the-basics-of-gitlab-ci/).
+- [GitLab CI: Deployment & environments](https://about.gitlab.com/blog/2016/08/26/ci-deployment-and-environments/).
+- [Building a new GitLab docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/blog/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/).
+- [Publish code coverage reports with GitLab Pages](https://about.gitlab.com/blog/2016/11/03/publish-code-coverage-report-with-gitlab-pages/).
 
 ## Admin GitLab Pages for self-managed instances
 
@@ -173,5 +173,5 @@ the [admin guide](../../../administration/pages/index.md).
 
 ## More information about GitLab Pages
 
-- Announcement (2016-12-24): ["We're bringing GitLab Pages to CE"](https://about.gitlab.com/2016/12/24/were-bringing-gitlab-pages-to-community-edition/)
-- Announcement (2017-03-06): ["We are changing the IP of GitLab Pages on GitLab.com"](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/)
+- Announcement (2016-12-24): ["We're bringing GitLab Pages to CE"](https://about.gitlab.com/blog/2016/12/24/were-bringing-gitlab-pages-to-community-edition/)
+- Announcement (2017-03-06): ["We are changing the IP of GitLab Pages on GitLab.com"](https://about.gitlab.com/blog/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/)
diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md
index 85d0abdb51a9487168f9c871a36c6c271c7c71e0..794c3030c6aa28b3b61fd4eb0b3f90a59ab05e94 100644
--- a/doc/user/project/pipelines/job_artifacts.md
+++ b/doc/user/project/pipelines/job_artifacts.md
@@ -50,14 +50,9 @@ For more examples on artifacts, follow the [artifacts reference in
 
 ## Browsing artifacts
 
-> With GitLab 9.2, PDFs, images, videos and other formats can be previewed
-> directly in the job artifacts browser without the need to download them.
-> With [GitLab 10.1][ce-14399], HTML files in a public project can be previewed
-> directly in a new tab without the need to download them when
-> [GitLab Pages](../../../administration/pages/index.md) is enabled.
-> The same holds for textual formats (currently supported extensions: `.txt`, `.json`, and `.log`).
-> With [GitLab 12.4][gitlab-16675], also artifacts in private projects can be previewed
-> when [GitLab Pages access control](../../../administration/pages/index.md#access-control) is enabled.
+> - From GitLab 9.2, PDFs, images, videos and other formats can be previewed directly in the job artifacts browser without the need to download them.
+> - Introduced in [GitLab 10.1][ce-14399], HTML files in a public project can be previewed directly in a new tab without the need to download them when [GitLab Pages](../../../administration/pages/index.md) is enabled. The same applies for textual formats (currently supported extensions: `.txt`, `.json`, and `.log`).
+> - Introduced in [GitLab 12.4][gitlab-16675], artifacts in private projects can be previewed when [GitLab Pages access control](../../../administration/pages/index.md#access-control) is enabled.
 
 After a job finishes, if you visit the job's specific page, there are three
 buttons. You can download the artifacts archive or browse its contents, whereas
@@ -70,7 +65,7 @@ The archive browser shows the name and the actual file size of each file in the
 archive. If your artifacts contained directories, then you are also able to
 browse inside them.
 
-Below you can see how browsing looks like. In this case we have browsed inside
+Below you can see what browsing looks like. In this case we have browsed inside
 the archive and at this point there is one directory, a couple files, and
 one HTML file that you can view directly online when
 [GitLab Pages](../../../administration/pages/index.md) is enabled (opens in a new tab).
diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md
index 59e04907e21c4a1545a2cd77954d6aa07aee94be..6480c7e0af98bd2602c21947eef9bda24c9d7d06 100644
--- a/doc/user/project/pipelines/settings.md
+++ b/doc/user/project/pipelines/settings.md
@@ -65,14 +65,14 @@ Project defined timeout (either specific timeout set by user or the default
 For information about setting a maximum artifact size for a project, see
 [Maximum artifacts size](../../admin_area/settings/continuous_integration.md#maximum-artifacts-size-core-only).
 
-## Custom CI config path
+## Custom CI configuration path
 
 > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/12509) in GitLab 9.4.
 
 By default we look for the `.gitlab-ci.yml` file in the project's root
 directory. If you require a different location **within** the repository,
-you can set a custom filepath that will be used to lookup the config file,
-this filepath should be **relative** to the root.
+you can set a custom path that will be used to look up the configuration file,
+this path should be **relative** to the root.
 
 Here are some valid examples:
 
@@ -85,7 +85,7 @@ The path can be customized at a project level. To customize the path:
 
 1. Go to the project's **Settings > CI / CD**.
 1. Expand the **General pipelines** section.
-1. Provide a value in the **Custom CI config path** field.
+1. Provide a value in the **Custom CI configuration path** field.
 1. Click **Save changes**.
 
 ## Test coverage parsing
diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md
index 1bd272bdd0c0e098638f76faf6c5a0b94024f11b..b7c9faeb1dfe7e5263a1bb14110a06b099fbf43e 100644
--- a/doc/user/project/protected_branches.md
+++ b/doc/user/project/protected_branches.md
@@ -86,20 +86,6 @@ Click **Protect** and the branch will appear in the "Protected branch" list.
 
 ![Roles and users list](img/protected_branches_select_roles_and_users_list.png)
 
-## Code Owners approvals **(PREMIUM)**
-
-It is possible to require at least one approval for each entry in the
-[`CODEOWNERS` file](code_owners.md) that matches a file changed in
-the merge request. To enable this feature:
-
-1. Toggle the **Require approval from code owners** slider.
-
-1. Click **Protect**.
-
-When this feature is enabled, all merge requests need approval
-from one code owner per matched rule before they can be merged. Additionally,
-pushes to the protected branch are denied if a rule is matched.
-
 ## Wildcard protected branches
 
 > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/4665) in GitLab 8.10.
@@ -166,6 +152,35 @@ Deleting a protected branch is only allowed via the web interface, not via Git.
 This means that you can't accidentally delete a protected branch from your
 command line or a Git client application.
 
+## Protected Branches approval by Code Owners **(PREMIUM)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13251) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.4.
+
+It is possible to require at least one approval by a
+[Code Owner](code_owners.md) to a file changed by the
+merge request. You can either set Code Owners approvals
+at the time you protect a new branch, or set it to a branch
+already protected, as described below.
+
+To protect a new branch and enable Code Owner's approval:
+
+1. Navigate to your project's **Settings > Repository** and expand **Protected branches**.
+1. Scroll down to **Protect a branch**, select a **Branch** or wildcard you'd like to protect, select who's **Allowed to merge** and **Allowed to push**, and toggle the **Require approval from code owners** slider.
+1. Click **Protect**.
+
+![Code Owners approval - new protected branch](img/code_owners_approval_new_protected_branch_v12_4.png)
+
+To enable Code Owner's approval to branches already protected:
+
+1. Navigate to your project's **Settings > Repository** and expand **Protected branches**.
+1. Scroll down to **Protected branch** and toggle the **Code owner approval** slider for the chosen branch.
+
+![Code Owners approval - branch already protected](img/code_owners_approval_protected_branch_v12_4.png)
+
+When enabled, all merge requests targeting these branches will require approval
+by a Code Owner per matched rule before they can be merged.
+Additionally, direct pushes to the protected branch are denied if a rule is matched.
+
 ## Running pipelines on protected branches
 
 The permission to merge or push to protected branches is used to define if a user can
diff --git a/doc/user/project/releases/img/custom_notifications_new_release_v12_4.png b/doc/user/project/releases/img/custom_notifications_new_release_v12_4.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b4231d5804d765bee1092e0dfec7dfddf72a276
Binary files /dev/null and b/doc/user/project/releases/img/custom_notifications_new_release_v12_4.png differ
diff --git a/doc/user/project/releases/index.md b/doc/user/project/releases/index.md
index d5ac6f99e7fb0843b9882034606de3e2ae640080..ceb077ab8af75bb3ae1f959508273a2ea126c6f3 100644
--- a/doc/user/project/releases/index.md
+++ b/doc/user/project/releases/index.md
@@ -65,6 +65,18 @@ project.
 
 ![Releases list](img/releases.png)
 
+## Notification for Releases
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/26001) in GitLab 12.4.
+
+You can be notified by email when a new Release is created for your project.
+
+To subscribe to these notifications, navigate to your **Project**'s landing page, then click on the
+bell icon. Choose **Custom** from the dropdown menu. The
+following modal window will be then displayed, from which you can select **New release** to complete your subscription to new Releases notifications.
+
+![Custom notification - New release](img/custom_notifications_new_release_v12_4.png)
+
 <!-- ## Troubleshooting
 
 Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/doc/user/project/service_desk.md b/doc/user/project/service_desk.md
index a2094c056352e1cfc88ed37b1e9e89c466c6d0d4..0ca34c4ed0270fdefe4f9b400e73a7de2a66b1f4 100644
--- a/doc/user/project/service_desk.md
+++ b/doc/user/project/service_desk.md
@@ -1,6 +1,6 @@
 # Service Desk **(PREMIUM)**
 
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/149) in [GitLab Premium 9.1](https://about.gitlab.com/2017/04/22/gitlab-9-1-released/#service-desk-eep).
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/149) in [GitLab Premium 9.1](https://about.gitlab.com/blog/2017/04/22/gitlab-9-1-released/#service-desk-eep).
 
 ## Overview
 
@@ -18,7 +18,7 @@ As Service Desk is built right into GitLab itself, the complexity and inefficien
 of multiple tools and external integrations are eliminated, significantly shortening
 the cycle time from feedback to software update.
 
-For an overview, check the video demonstration on [GitLab Service Desk](https://about.gitlab.com/2017/05/09/demo-service-desk/).
+For an overview, check the video demonstration on [GitLab Service Desk](https://about.gitlab.com/blog/2017/05/09/demo-service-desk/).
 
 ## Use cases
 
diff --git a/doc/workflow/forking_workflow.md b/doc/workflow/forking_workflow.md
index 82c2709143ddb3aff3c2beab58ace6b2ec541814..48be38b2ecae44bb7463e20e0a1b9b727e5e9d86 100644
--- a/doc/workflow/forking_workflow.md
+++ b/doc/workflow/forking_workflow.md
@@ -48,4 +48,4 @@ changes will be added to the repository and branch you're merging into.
 
 ![New merge request](forking/merge_request.png)
 
-[gitlab flow]: https://about.gitlab.com/2014/09/29/gitlab-flow/ "GitLab Flow blog post"
+[gitlab flow]: https://about.gitlab.com/blog/2014/09/29/gitlab-flow/ "GitLab Flow blog post"
diff --git a/doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md b/doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md
index 905d5624688d3e8434bba1581c61f6d738b78f07..8f24929c9dc3f284344daac0ca7cb4525e91bbbd 100644
--- a/doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md
+++ b/doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md
@@ -247,8 +247,8 @@ git annex uninit
 [Git LFS]: https://git-lfs.github.com/
 [install-lfs]: https://git-lfs.github.com/
 [issue-remove-annex]: https://gitlab.com/gitlab-org/gitlab/issues/1648
-[lfs-track]: https://about.gitlab.com/2017/01/30/getting-started-with-git-lfs-tutorial/#tracking-files-with-lfs
-[post-1]: https://about.gitlab.com/2017/01/30/getting-started-with-git-lfs-tutorial/
-[post-2]: https://about.gitlab.com/2015/11/23/announcing-git-lfs-support-in-gitlab/
-[post-3]: https://about.gitlab.com/2015/02/17/gitlab-annex-solves-the-problem-of-versioning-large-binaries-with-git/
+[lfs-track]: https://about.gitlab.com/blog/2017/01/30/getting-started-with-git-lfs-tutorial/#tracking-files-with-lfs
+[post-1]: https://about.gitlab.com/blog/2017/01/30/getting-started-with-git-lfs-tutorial/
+[post-2]: https://about.gitlab.com/blog/2015/11/23/announcing-git-lfs-support-in-gitlab/
+[post-3]: https://about.gitlab.com/blog/2015/02/17/gitlab-annex-solves-the-problem-of-versioning-large-binaries-with-git/
 [uninit]: https://git-annex.branchable.com/git-annex-uninit/
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index 9155402e550a7722cc3c9da6fb0c41b06441a6aa..d619c870c5ede37cc3d66bb098c4c8363c869080 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -84,6 +84,7 @@ Below is the table of events users can be notified of:
 | User added to group          | User                | Sent when user is added to group |
 | Group access level changed   | User                | Sent when user group access level is changed |
 | Project moved                | Project members (1) | (1) not disabled |
+| New release                  | Project members     | Custom notification          |
 
 ### Issue / Epics / Merge request events
 
diff --git a/ee/app/assets/javascripts/analytics/cycle_analytics/components/base.vue b/ee/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
index b504b1a1dae0213586c765d9da33d04e0f9ebf31..9550138eae9d722e13ca2cddf1dd46babe66eee6 100644
--- a/ee/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
+++ b/ee/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
@@ -39,14 +39,6 @@ export default {
     return {
       multiProjectSelect: true,
       dateOptions: [7, 30, 90],
-      groupsQueryParams: {
-        min_access_level: featureAccessLevel.EVERYONE,
-      },
-      projectsQueryParams: {
-        per_page: PROJECTS_PER_PAGE,
-        with_shared: false,
-        order_by: 'last_activity_at',
-      },
     };
   },
   computed: {
@@ -106,7 +98,7 @@ export default {
       'setDateRange',
     ]),
     onGroupSelect(group) {
-      this.setCycleAnalyticsDataEndpoint(group.path);
+      this.setCycleAnalyticsDataEndpoint(group.full_path);
       this.setSelectedGroup(group);
       this.fetchCycleAnalyticsData();
     },
@@ -130,6 +122,14 @@ export default {
       this.setDateRange({ skipFetch: true, startDate, endDate });
     },
   },
+  groupsQueryParams: {
+    min_access_level: featureAccessLevel.EVERYONE,
+  },
+  projectsQueryParams: {
+    per_page: PROJECTS_PER_PAGE,
+    with_shared: false,
+    order_by: 'last_activity_at',
+  },
 };
 </script>
 
@@ -144,7 +144,7 @@ export default {
       >
         <groups-dropdown-filter
           class="js-groups-dropdown-filter dropdown-select"
-          :query-params="groupsQueryParams"
+          :query-params="$options.groupsQueryParams"
           @selected="onGroupSelect"
         />
         <projects-dropdown-filter
@@ -152,7 +152,7 @@ export default {
           :key="selectedGroup.id"
           class="js-projects-dropdown-filter ml-md-1 mt-1 mt-md-0 dropdown-select"
           :group-id="selectedGroup.id"
-          :query-params="projectsQueryParams"
+          :query-params="$options.projectsQueryParams"
           :multi-select="multiProjectSelect"
           @selected="onProjectsSelect"
         />
diff --git a/ee/app/assets/javascripts/analytics/cycle_analytics/store/actions.js b/ee/app/assets/javascripts/analytics/cycle_analytics/store/actions.js
index 61dcbfc4a472286bb915f9b2081cb5fad9875603..ec5e252c511a079cafd9d8de68fff19158b87c01 100644
--- a/ee/app/assets/javascripts/analytics/cycle_analytics/store/actions.js
+++ b/ee/app/assets/javascripts/analytics/cycle_analytics/store/actions.js
@@ -1,12 +1,19 @@
 import dateFormat from 'dateformat';
 import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
+import createFlash, { hideFlash } from '~/flash';
 import { __ } from '~/locale';
 import Api from '~/api';
 import httpStatus from '~/lib/utils/http_status';
 import * as types from './mutation_types';
 import { dateFormats } from '../../shared/constants';
 
+const removeError = () => {
+  const flashEl = document.querySelector('.flash-alert');
+  if (flashEl) {
+    hideFlash(flashEl);
+  }
+};
+
 export const setCycleAnalyticsDataEndpoint = ({ commit }, groupPath) =>
   commit(types.SET_CYCLE_ANALYTICS_DATA_ENDPOINT, groupPath);
 
@@ -58,6 +65,8 @@ export const receiveCycleAnalyticsDataSuccess = ({ state, commit, dispatch }, da
   commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS, data);
   const { stages = [] } = state;
   if (stages && stages.length) {
+    removeError();
+
     const { slug } = stages[0];
     dispatch('setStageDataEndpoint', slug);
     dispatch('fetchStageData');
diff --git a/ee/app/assets/javascripts/design_management/components/delete_button.vue b/ee/app/assets/javascripts/design_management/components/delete_button.vue
new file mode 100644
index 0000000000000000000000000000000000000000..ebdb548658251de6a289322b73a534eab44971db
--- /dev/null
+++ b/ee/app/assets/javascripts/design_management/components/delete_button.vue
@@ -0,0 +1,65 @@
+<script>
+import { GlButton, GlLoadingIcon, GlModal, GlModalDirective } from '@gitlab/ui';
+import _ from 'underscore';
+
+export default {
+  name: 'DeleteButton',
+  components: {
+    GlButton,
+    GlLoadingIcon,
+    GlModal,
+  },
+  directives: {
+    GlModalDirective,
+  },
+  props: {
+    isDeleting: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+    buttonClass: {
+      type: String,
+      required: false,
+      default: '',
+    },
+    buttonVariant: {
+      type: String,
+      required: false,
+      default: '',
+    },
+    hasSelectedDesigns: {
+      type: Boolean,
+      required: false,
+      default: true,
+    },
+  },
+  data() {
+    return {
+      modalId: _.uniqueId('design-deletion-confirmation-'),
+    };
+  },
+};
+</script>
+
+<template>
+  <div>
+    <gl-modal
+      :modal-id="modalId"
+      :title="s__('DesignManagement|Delete designs confirmation')"
+      :ok-title="s__('DesignManagement|Delete')"
+      ok-variant="danger"
+      @ok="$emit('deleteSelectedDesigns')"
+    >
+      <p>{{ s__('DesignManagement|Are you sure you want to delete the selected designs?') }}</p>
+    </gl-modal>
+    <gl-button
+      v-gl-modal-directive="modalId"
+      :variant="buttonVariant"
+      :disabled="isDeleting || !hasSelectedDesigns"
+      :class="buttonClass"
+    >
+      <slot></slot>
+    </gl-button>
+  </div>
+</template>
diff --git a/ee/app/assets/javascripts/design_management/components/design_destroyer.vue b/ee/app/assets/javascripts/design_management/components/design_destroyer.vue
new file mode 100644
index 0000000000000000000000000000000000000000..b41dcd8f70f7723ffa16166d978c0e1d9f2d7118
--- /dev/null
+++ b/ee/app/assets/javascripts/design_management/components/design_destroyer.vue
@@ -0,0 +1,73 @@
+<script>
+import { ApolloMutation } from 'vue-apollo';
+import projectQuery from '../graphql/queries/project.query.graphql';
+import destroyDesignMutation from '../graphql/mutations/destroyDesign.mutation.graphql';
+import {
+  updateStoreAfterDesignsDelete,
+  onDesignDeletionError,
+} from '../utils/design_management_utils';
+
+export default {
+  components: {
+    ApolloMutation,
+  },
+  props: {
+    filenames: {
+      type: Array,
+      required: true,
+    },
+    projectPath: {
+      type: String,
+      required: true,
+    },
+    iid: {
+      type: String,
+      required: true,
+    },
+  },
+  computed: {
+    projectQueryBody() {
+      return {
+        query: projectQuery,
+        variables: { fullPath: this.projectPath, iid: this.iid, atVersion: null },
+      };
+    },
+  },
+  methods: {
+    onError(...args) {
+      onDesignDeletionError(...args);
+    },
+    updateStoreAfterDelete(
+      store,
+      {
+        data: { designManagementDelete },
+      },
+    ) {
+      updateStoreAfterDesignsDelete(
+        store,
+        designManagementDelete,
+        this.projectQueryBody,
+        this.filenames,
+      );
+    },
+  },
+  destroyDesignMutation,
+};
+</script>
+
+<template>
+  <ApolloMutation
+    v-slot="{ mutate, loading, error }"
+    :mutation="$options.destroyDesignMutation"
+    :variables="{
+      filenames,
+      projectPath,
+      iid,
+    }"
+    :update="updateStoreAfterDelete"
+    @error="onError"
+    v-on="$listeners"
+  >
+    <slot v-bind="{ mutate, loading, error }"></slot>
+  </ApolloMutation>
+</template>
diff --git a/ee/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue b/ee/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
index 09fd058fbdaae7ea67e712da3a23fb14626e5f49..b813517c0a40b0b92caeeb1d33d4471377098eff 100644
--- a/ee/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
+++ b/ee/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
@@ -80,6 +80,7 @@ export default {
               __typename: 'NoteEdge',
               node: createNote.note,
             });
+            data.design.notesCount += 1;
             store.writeQuery({ query: getDesignQuery, data });
           },
         })
diff --git a/ee/app/assets/javascripts/design_management/components/list/index.vue b/ee/app/assets/javascripts/design_management/components/list/index.vue
deleted file mode 100644
index 5bd28f29698d528e9763d3491de9411c1f15deb8..0000000000000000000000000000000000000000
--- a/ee/app/assets/javascripts/design_management/components/list/index.vue
+++ /dev/null
@@ -1,30 +0,0 @@
-<script>
-import Design from './item.vue';
-
-export default {
-  components: {
-    Design,
-  },
-  props: {
-    designs: {
-      type: Array,
-      required: true,
-    },
-  },
-};
-</script>
-
-<template>
-  <ol class="list-unstyled row">
-    <li v-for="design in designs" :key="design.id" class="col-md-6 col-lg-4 mb-3">
-      <design
-        :id="design.id"
-        :event="design.event"
-        :notes-count="design.notesCount"
-        :image="design.image"
-        :name="design.filename"
-        :updated-at="design.updatedAt"
-      />
-    </li>
-  </ol>
-</template>
diff --git a/ee/app/assets/javascripts/design_management/components/list/item.vue b/ee/app/assets/javascripts/design_management/components/list/item.vue
index 52071e0150780955239ab6ab3bf5fef734599cf7..af9a2b8cfd91f9b4a087bd603db92d2e8c207931 100644
--- a/ee/app/assets/javascripts/design_management/components/list/item.vue
+++ b/ee/app/assets/javascripts/design_management/components/list/item.vue
@@ -25,7 +25,7 @@ export default {
       type: String,
       required: true,
     },
-    name: {
+    filename: {
       type: String,
       required: true,
     },
@@ -69,7 +69,7 @@ export default {
   <router-link
     :to="{
       name: 'design',
-      params: { id: name },
+      params: { id: filename },
       query: $route.query,
     }"
     class="card cursor-pointer text-plain js-design-list-item design-list-item"
@@ -80,11 +80,13 @@ export default {
           <icon :name="icon.name" :size="18" :class="icon.classes" />
         </span>
       </div>
-      <img :src="image" :alt="name" class="block ml-auto mr-auto mw-100 mh-100 design-img" />
+      <img :src="image" :alt="filename" class="block ml-auto mr-auto mw-100 mh-100 design-img" />
     </div>
     <div class="card-footer d-flex w-100">
       <div class="d-flex flex-column str-truncated-100">
-        <span class="bold str-truncated-100" data-qa-selector="design_file_name">{{ name }}</span>
+        <span class="bold str-truncated-100" data-qa-selector="design_file_name">{{
+          filename
+        }}</span>
         <span v-if="updatedAt" class="str-truncated-100">
           {{ __('Updated') }} <timeago :time="updatedAt" tooltip-placement="bottom" />
         </span>
diff --git a/ee/app/assets/javascripts/design_management/components/toolbar/index.vue b/ee/app/assets/javascripts/design_management/components/toolbar/index.vue
index dfb30549ee74609dca44ca0301379a454ec12421..d44f9fd2f81d21ebc775306d9ce542bf8784056e 100644
--- a/ee/app/assets/javascripts/design_management/components/toolbar/index.vue
+++ b/ee/app/assets/javascripts/design_management/components/toolbar/index.vue
@@ -1,15 +1,17 @@
 <script>
 import { __, sprintf } from '~/locale';
-import { GlLoadingIcon } from '@gitlab/ui';
 import Icon from '~/vue_shared/components/icon.vue';
 import timeagoMixin from '~/vue_shared/mixins/timeago';
 import Pagination from './pagination.vue';
+import DeleteButton from '../delete_button.vue';
+import permissionsQuery from '../../graphql/queries/permissions.query.graphql';
+import appDataQuery from '../../graphql/queries/appData.query.graphql';
 
 export default {
   components: {
-    GlLoadingIcon,
     Icon,
     Pagination,
+    DeleteButton,
   },
   mixins: [timeagoMixin],
   props: {
@@ -17,6 +19,10 @@ export default {
       type: String,
       required: true,
     },
+    isDeleting: {
+      type: Boolean,
+      required: true,
+    },
     name: {
       type: String,
       required: false,
@@ -32,6 +38,39 @@ export default {
       required: false,
       default: () => ({}),
     },
+    isLatestVersion: {
+      type: Boolean,
+      required: true,
+    },
+  },
+  data() {
+    return {
+      permissions: {
+        createDesign: false,
+      },
+      projectPath: '',
+      issueIid: null,
+    };
+  },
+  apollo: {
+    appData: {
+      query: appDataQuery,
+      manual: true,
+      result({ data: { projectPath, issueIid } }) {
+        this.projectPath = projectPath;
+        this.issueIid = issueIid;
+      },
+    },
+    permissions: {
+      query: permissionsQuery,
+      variables() {
+        return {
+          fullPath: this.projectPath,
+          iid: this.issueIid,
+        };
+      },
+      update: data => data.project.issue.userPermissions,
+    },
   },
   computed: {
     updatedText() {
@@ -40,6 +79,9 @@ export default {
         updated_by: this.updatedBy.name,
       });
     },
+    canDeleteDesign() {
+      return this.permissions.createDesign;
+    },
   },
 };
 </script>
@@ -61,5 +103,13 @@ export default {
       <small v-if="updatedAt" class="text-secondary">{{ updatedText }}</small>
     </div>
     <pagination :id="id" class="ml-auto" />
+    <delete-button
+      v-if="isLatestVersion && canDeleteDesign"
+      :is-deleting="isDeleting"
+      button-variant="danger"
+      @deleteSelectedDesigns="$emit('delete')"
+    >
+      <icon :size="18" name="remove" />
+    </delete-button>
   </header>
 </template>
diff --git a/ee/app/assets/javascripts/design_management/components/upload/button.vue b/ee/app/assets/javascripts/design_management/components/upload/button.vue
index 8e6d35ecce0e7d6f2114267d90247c12aa0e9c25..eab14bc65f91cfcaef99b3e63b1e612d3b9194f5 100644
--- a/ee/app/assets/javascripts/design_management/components/upload/button.vue
+++ b/ee/app/assets/javascripts/design_management/components/upload/button.vue
@@ -36,7 +36,7 @@ export default {
         )
       "
       :disabled="isSaving"
-      variant="primary"
+      variant="success"
       @click="openFileUpload"
     >
       {{ s__('DesignManagement|Add designs') }}
diff --git a/ee/app/assets/javascripts/design_management/components/upload/form.vue b/ee/app/assets/javascripts/design_management/components/upload/form.vue
deleted file mode 100644
index ef4d90b7e6470222439d68a7fa32404070ae8940..0000000000000000000000000000000000000000
--- a/ee/app/assets/javascripts/design_management/components/upload/form.vue
+++ /dev/null
@@ -1,35 +0,0 @@
-<script>
-import UploadButton from './button.vue';
-import DesignVersionDropdown from './design_version_dropdown.vue';
-
-export default {
-  components: {
-    UploadButton,
-    DesignVersionDropdown,
-  },
-  props: {
-    isSaving: {
-      type: Boolean,
-      required: true,
-    },
-    canUploadDesign: {
-      type: Boolean,
-      required: true,
-    },
-  },
-  methods: {
-    onFileUploadChange(files) {
-      this.$emit('upload', files);
-    },
-  },
-};
-</script>
-
-<template>
-  <header class="row-content-block border-top-0 p-2 d-flex">
-    <div class="d-flex justify-content-between align-items-center w-100">
-      <design-version-dropdown />
-      <upload-button v-if="canUploadDesign" :is-saving="isSaving" @upload="onFileUploadChange" />
-    </div>
-  </header>
-</template>
diff --git a/ee/app/assets/javascripts/design_management/graphql.js b/ee/app/assets/javascripts/design_management/graphql.js
index 2c576069c2acbdeb3ef937c7bfe1e36b7b8f1033..cc6043c5af685718addf68e673f90753b24ec1c4 100644
--- a/ee/app/assets/javascripts/design_management/graphql.js
+++ b/ee/app/assets/javascripts/design_management/graphql.js
@@ -1,5 +1,6 @@
 import Vue from 'vue';
 import VueApollo from 'vue-apollo';
+import _ from 'underscore';
 import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
 import createDefaultClient from '~/lib/graphql';
 import createFlash from '~/flash';
@@ -46,7 +47,7 @@ const defaultClient = createDefaultClient(
       dataIdFromObject: object => {
         // eslint-disable-next-line no-underscore-dangle, @gitlab/i18n/no-non-i18n-strings
         if (object.__typename === 'Design') {
-          return object.id && object.image ? `${object.id}-${object.image}` : null;
+          return object.id && object.image ? `${object.id}-${object.image}` : _.uniqueId();
         }
         return defaultDataIdFromObject(object);
       },
diff --git a/ee/app/assets/javascripts/design_management/graphql/fragments/version.fragment.graphql b/ee/app/assets/javascripts/design_management/graphql/fragments/version.fragment.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..7eb40b12f5112216eb055a1d26f035743d9757a6
--- /dev/null
+++ b/ee/app/assets/javascripts/design_management/graphql/fragments/version.fragment.graphql
@@ -0,0 +1,4 @@
+fragment VersionListItem on DesignVersion {
+  id
+  sha
+}
diff --git a/ee/app/assets/javascripts/design_management/graphql/mutations/destroyDesign.mutation.graphql b/ee/app/assets/javascripts/design_management/graphql/mutations/destroyDesign.mutation.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..4f4a7a6da71f2b81f2da35705d86f6a481155254
--- /dev/null
+++ b/ee/app/assets/javascripts/design_management/graphql/mutations/destroyDesign.mutation.graphql
@@ -0,0 +1,9 @@
+#import "../fragments/version.fragment.graphql"
+
+mutation destroyDesign($filenames: [String!]!, $projectPath: ID!, $iid: ID!) {
+  designManagementDelete(input: { projectPath: $projectPath, iid: $iid, filenames: $filenames }) {
+    version {
+      ...VersionListItem
+    }
+  }
+}
diff --git a/ee/app/assets/javascripts/design_management/graphql/queries/project.query.graphql b/ee/app/assets/javascripts/design_management/graphql/queries/project.query.graphql
index 770eb9cced0a0d855c5ca34f29de77e800174fd5..bce33e44bb3da992986e6a31eb2ca38ed238c1f1 100644
--- a/ee/app/assets/javascripts/design_management/graphql/queries/project.query.graphql
+++ b/ee/app/assets/javascripts/design_management/graphql/queries/project.query.graphql
@@ -1,4 +1,5 @@
 #import "../fragments/designList.fragment.graphql"
+#import "../fragments/version.fragment.graphql"
 
 query project($fullPath: ID!, $iid: String!, $atVersion: ID) {
   project(fullPath: $fullPath) {
@@ -15,8 +16,7 @@ query project($fullPath: ID!, $iid: String!, $atVersion: ID) {
         versions {
           edges {
             node {
-              id
-              sha
+              ...VersionListItem
             }
           }
         }
diff --git a/ee/app/assets/javascripts/design_management/mixins/all_designs.js b/ee/app/assets/javascripts/design_management/mixins/all_designs.js
index 3364fb5be080c6c6dc96f2f6ab51d33ea428adb2..dfa163faeca910ad7d4788f7a7965eae19d2168b 100644
--- a/ee/app/assets/javascripts/design_management/mixins/all_designs.js
+++ b/ee/app/assets/javascripts/design_management/mixins/all_designs.js
@@ -1,3 +1,4 @@
+import { propertyOf } from 'underscore';
 import createFlash from '~/flash';
 import { s__ } from '~/locale';
 import projectQuery from '../graphql/queries/project.query.graphql';
@@ -16,7 +17,13 @@ export default {
           atVersion: this.designsVersion,
         };
       },
-      update: data => extractNodes(data.project.issue.designs.designs),
+      update: data => {
+        const designEdges = propertyOf(data)(['project', 'issue', 'designs', 'designs']);
+        if (designEdges) {
+          return extractNodes(designEdges);
+        }
+        return [];
+      },
       error() {
         this.error = true;
       },
diff --git a/ee/app/assets/javascripts/design_management/mixins/all_versions.js b/ee/app/assets/javascripts/design_management/mixins/all_versions.js
index 4e74eacb7327ed3c983560b82a1aaecb926b52fe..35fd035c7e41954605d75a8478614592c0ba5077 100644
--- a/ee/app/assets/javascripts/design_management/mixins/all_versions.js
+++ b/ee/app/assets/javascripts/design_management/mixins/all_versions.js
@@ -1,5 +1,6 @@
 import projectQuery from '../graphql/queries/project.query.graphql';
 import appDataQuery from '../graphql/queries/appData.query.graphql';
+import { findVersionId } from '../utils/design_management_utils';
 
 export default {
   apollo: {
@@ -36,6 +37,13 @@ export default {
         ? `gid://gitlab/DesignManagement::Version/${this.$route.query.version}`
         : null;
     },
+    isLatestVersion() {
+      if (this.allVersions.length > 0) {
+        const versionId = findVersionId(this.allVersions[0].node.id);
+        return !this.$route.query.version || this.$route.query.version === versionId;
+      }
+      return true;
+    },
   },
   data() {
     return {
diff --git a/ee/app/assets/javascripts/design_management/pages/design/index.vue b/ee/app/assets/javascripts/design_management/pages/design/index.vue
index 39205a75a3ac72256f00caa7332543822b9481a3..94c4499273e79cdb7c4a40cea36c09355f84457d 100644
--- a/ee/app/assets/javascripts/design_management/pages/design/index.vue
+++ b/ee/app/assets/javascripts/design_management/pages/design/index.vue
@@ -9,6 +9,7 @@ import DesignImage from '../../components/image.vue';
 import DesignOverlay from '../../components/design_overlay.vue';
 import DesignDiscussion from '../../components/design_notes/design_discussion.vue';
 import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
+import DesignDestroyer from '../../components/design_destroyer.vue';
 import getDesignQuery from '../../graphql/queries/getDesign.query.graphql';
 import createImageDiffNoteMutation from '../../graphql/mutations/createImageDiffNote.mutation.graphql';
 import { extractDiscussions } from '../../utils/design_management_utils';
@@ -18,6 +19,7 @@ export default {
     DesignImage,
     DesignOverlay,
     DesignDiscussion,
+    DesignDestroyer,
     Toolbar,
     DesignReplyForm,
     GlLoadingIcon,
@@ -142,6 +144,7 @@ export default {
               },
             };
             data.design.discussions.edges.push(newDiscussion);
+            data.design.notesCount += 1;
             store.writeQuery({ query: getDesignQuery, data });
           },
         })
@@ -193,12 +196,25 @@ export default {
     <gl-loading-icon v-if="isLoading" size="xl" class="align-self-center" />
     <template v-else>
       <div class="d-flex flex-column w-100">
-        <toolbar
-          :id="id"
-          :name="design.filename"
-          :updated-at="design.updatedAt"
-          :updated-by="design.updatedBy"
-        />
+        <design-destroyer
+          :filenames="[design.filename]"
+          :project-path="projectPath"
+          :iid="issueIid"
+          @done="$router.push({ name: 'designs' })"
+          @error="$router.push({ name: 'designs' })"
+        >
+          <template v-slot="{ mutate, loading, error }">
+            <toolbar
+              :id="id"
+              :is-deleting="loading"
+              :name="design.filename"
+              :updated-at="design.updatedAt"
+              :updated-by="design.updatedBy"
+              :is-latest-version="isLatestVersion"
+              @delete="mutate()"
+            />
+          </template>
+        </design-destroyer>
         <div class="d-flex flex-column w-100 h-100 mh-100 position-relative">
           <design-image
             :image="design.image"
diff --git a/ee/app/assets/javascripts/design_management/pages/index.vue b/ee/app/assets/javascripts/design_management/pages/index.vue
index 7821b99abbaaf682ab3c1b911f3731ed634845d0..75f0455fce4af84e0cda4068c83c518a8da636d2 100644
--- a/ee/app/assets/javascripts/design_management/pages/index.vue
+++ b/ee/app/assets/javascripts/design_management/pages/index.vue
@@ -1,25 +1,30 @@
 <script>
-import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
+import { GlLoadingIcon, GlEmptyState, GlButton } from '@gitlab/ui';
 import _ from 'underscore';
 import createFlash from '~/flash';
 import { s__, sprintf } from '~/locale';
-import DesignList from '../components/list/index.vue';
-import UploadForm from '../components/upload/form.vue';
 import UploadButton from '../components/upload/button.vue';
+import DeleteButton from '../components/delete_button.vue';
+import Design from '../components/list/item.vue';
+import DesignDestroyer from '../components/design_destroyer.vue';
+import DesignVersionDropdown from '../components/upload/design_version_dropdown.vue';
 import uploadDesignMutation from '../graphql/mutations/uploadDesign.mutation.graphql';
 import permissionsQuery from '../graphql/queries/permissions.query.graphql';
-import allDesignsMixin from '../mixins/all_designs';
 import projectQuery from '../graphql/queries/project.query.graphql';
+import allDesignsMixin from '../mixins/all_designs';
 
 const MAXIMUM_FILE_UPLOAD_LIMIT = 10;
 
 export default {
   components: {
     GlLoadingIcon,
-    DesignList,
-    UploadForm,
     UploadButton,
     GlEmptyState,
+    GlButton,
+    Design,
+    DesignDestroyer,
+    DesignVersionDropdown,
+    DeleteButton,
   },
   mixins: [allDesignsMixin],
   apollo: {
@@ -40,6 +45,7 @@ export default {
         createDesign: false,
       },
       isSaving: false,
+      selectedDesigns: [],
     };
   },
   computed: {
@@ -49,12 +55,29 @@ export default {
     canCreateDesign() {
       return this.permissions.createDesign;
     },
-    showUploadForm() {
-      return this.canCreateDesign && this.hasDesigns;
+    showToolbar() {
+      return this.canCreateDesign && this.allVersions.length > 0;
     },
     hasDesigns() {
       return this.designs.length > 0;
     },
+    hasSelectedDesigns() {
+      return this.selectedDesigns.length > 0;
+    },
+    canDeleteDesigns() {
+      return this.isLatestVersion && this.hasSelectedDesigns;
+    },
+    projectQueryBody() {
+      return {
+        query: projectQuery,
+        variables: { fullPath: this.projectPath, iid: this.issueIid, atVersion: null },
+      };
+    },
+    selectAllButtonText() {
+      return this.hasSelectedDesigns
+        ? s__('DesignManagement|Deselect all')
+        : s__('DesignManagement|Select all');
+    },
   },
   methods: {
     onUploadDesign(files) {
@@ -83,6 +106,8 @@ export default {
         image: '',
         filename: file.name,
         fullPath: '',
+        notesCount: 0,
+        event: 'NONE',
         diffRefs: {
           __typename: 'DiffRefs',
           baseSha: '',
@@ -116,10 +141,7 @@ export default {
             hasUpload: true,
           },
           update: (store, { data: { designManagementUpload } }) => {
-            const data = store.readQuery({
-              query: projectQuery,
-              variables: { fullPath: this.projectPath, iid: this.issueIid, atVersion: null },
-            });
+            const data = store.readQuery(this.projectQueryBody);
 
             const newDesigns = data.project.issue.designs.designs.edges.reduce((acc, design) => {
               if (!acc.find(d => d.filename === design.node.filename)) {
@@ -145,38 +167,26 @@ export default {
               ...data.project.issue.designs.versions.edges,
             ];
 
-            const newQueryData = {
-              project: {
-                // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
-                // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
-                __typename: 'Project',
-                id: '',
-                issue: {
-                  // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
-                  // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
-                  __typename: 'Issue',
-                  designs: {
-                    __typename: 'DesignCollection',
-                    designs: {
-                      __typename: 'DesignConnection',
-                      edges: newDesigns.map(design => ({
-                        __typename: 'DesignEdge',
-                        node: design,
-                      })),
-                    },
-                    versions: {
-                      __typename: 'DesignVersionConnection',
-                      edges: newVersions,
-                    },
-                  },
-                },
+            const updatedDesigns = {
+              __typename: 'DesignCollection',
+              designs: {
+                __typename: 'DesignConnection',
+                edges: newDesigns.map(design => ({
+                  __typename: 'DesignEdge',
+                  node: design,
+                })),
+              },
+              versions: {
+                __typename: 'DesignVersionConnection',
+                edges: newVersions,
               },
             };
 
+            data.project.issue.designs = updatedDesigns;
+
             store.writeQuery({
-              query: projectQuery,
-              variables: { fullPath: this.projectPath, iid: this.issueIid, atVersion: null },
-              data: newQueryData,
+              ...this.projectQueryBody,
+              data,
             });
           },
           optimisticResponse: {
@@ -200,25 +210,86 @@ export default {
           this.isSaving = false;
         });
     },
+    changeSelectedDesigns(filename) {
+      if (this.isDesignSelected(filename)) {
+        this.selectedDesigns = this.selectedDesigns.filter(design => design !== filename);
+      } else {
+        this.selectedDesigns.push(filename);
+      }
+    },
+    toggleDesignsSelection() {
+      if (this.hasSelectedDesigns) {
+        this.selectedDesigns = [];
+      } else {
+        this.selectedDesigns = this.designs.map(design => design.filename);
+      }
+    },
+    isDesignSelected(filename) {
+      return this.selectedDesigns.includes(filename);
+    },
+    onDesignDelete() {
+      this.selectedDesigns = [];
+      if (this.$route.query.version) this.$router.push({ name: 'designs' });
+    },
+  },
+  beforeRouteUpdate(to, from, next) {
+    this.selectedDesigns = [];
+    next();
   },
 };
 </script>
 
 <template>
   <div>
-    <upload-form
-      v-if="showUploadForm"
-      :can-upload-design="canCreateDesign"
-      :is-saving="isSaving"
-      :all-versions="allVersions"
-      @upload="onUploadDesign"
-    />
+    <header v-if="showToolbar" class="row-content-block border-top-0 p-2 d-flex">
+      <div class="d-flex justify-content-between align-items-center w-100">
+        <design-version-dropdown />
+        <div class="d-flex">
+          <gl-button
+            v-if="isLatestVersion"
+            variant="link"
+            class="mr-2 js-select-all"
+            @click="toggleDesignsSelection"
+            >{{ selectAllButtonText }}</gl-button
+          >
+          <design-destroyer
+            v-slot="{ mutate, loading, error }"
+            :filenames="selectedDesigns"
+            :project-path="projectPath"
+            :iid="issueIid"
+            @done="onDesignDelete"
+          >
+            <delete-button
+              :is-deleting="loading"
+              button-class="btn-danger btn-inverted mr-2"
+              :has-selected-designs="hasSelectedDesigns"
+              @deleteSelectedDesigns="mutate()"
+            >
+              {{ s__('DesignManagement|Delete selected') }}
+              <gl-loading-icon v-if="loading" inline class="ml-1" />
+            </delete-button>
+          </design-destroyer>
+          <upload-button v-if="canCreateDesign" :is-saving="isSaving" @upload="onUploadDesign" />
+        </div>
+      </div>
+    </header>
     <div class="mt-4">
       <gl-loading-icon v-if="isLoading" size="md" />
       <div v-else-if="error" class="alert alert-danger">
         {{ __('An error occurred while loading designs. Please try again.') }}
       </div>
-      <design-list v-else-if="hasDesigns" :designs="designs" />
+      <ol v-else-if="hasDesigns" class="list-unstyled row">
+        <li v-for="design in designs" :key="design.id" class="col-md-6 col-lg-4 mb-3">
+          <design v-bind="design" />
+          <input
+            v-if="isLatestVersion && canCreateDesign"
+            :checked="isDesignSelected(design.filename)"
+            type="checkbox"
+            class="design-checkbox"
+            @change="changeSelectedDesigns(design.filename)"
+          />
+        </li>
+      </ol>
       <gl-empty-state
         v-else
         :title="s__('DesignManagement|The one place for your designs')"
diff --git a/ee/app/assets/javascripts/design_management/utils/design_management_utils.js b/ee/app/assets/javascripts/design_management/utils/design_management_utils.js
index 60954419c5cdc8b2a0c2ed02a781aef559e80384..90d23c5c4ff5392428893fc1c0473a568e4842a1 100644
--- a/ee/app/assets/javascripts/design_management/utils/design_management_utils.js
+++ b/ee/app/assets/javascripts/design_management/utils/design_management_utils.js
@@ -1,3 +1,6 @@
+import { s__ } from '~/locale';
+import createFlash from '~/flash';
+
 /**
  * Returns formatted array that doesn't contain
  * `edges`->`node` nesting
@@ -29,3 +32,50 @@ export const extractDiscussions = discussions =>
 
 export const extractCurrentDiscussion = (discussions, id) =>
   discussions.edges.find(({ node }) => node.id === id);
+
+const deleteDesignsFromStore = (store, query, selectedDesigns) => {
+  const data = store.readQuery(query);
+
+  const changedDesigns = data.project.issue.designs.designs.edges.filter(
+    ({ node }) => !selectedDesigns.includes(node.filename),
+  );
+  data.project.issue.designs.designs.edges = [...changedDesigns];
+
+  store.writeQuery({
+    ...query,
+    data,
+  });
+};
+
+const addNewVersionToStore = (store, query, version) => {
+  if (!version) return;
+
+  const data = store.readQuery(query);
+  const newEdge = { node: version, __typename: 'DesignVersionEdge' };
+
+  data.project.issue.designs.versions.edges = [
+    newEdge,
+    ...data.project.issue.designs.versions.edges,
+  ];
+
+  store.writeQuery({
+    ...query,
+    data,
+  });
+};
+
+export const onDesignDeletionError = e => {
+  createFlash(s__('DesignManagement|We could not delete design(s). Please try again.'));
+  throw e;
+};
+
+export const updateStoreAfterDesignsDelete = (store, data, query, designs) => {
+  if (data.errors) {
+    onDesignDeletionError(new Error(data.errors));
+  } else {
+    deleteDesignsFromStore(store, query, designs);
+    addNewVersionToStore(store, query, data.version);
+  }
+};
+
+export const findVersionId = id => id.match('::Version/(.+$)')[1];
diff --git a/ee/app/assets/javascripts/epic/components/epic_body.vue b/ee/app/assets/javascripts/epic/components/epic_body.vue
index 1995c30b2122d1bcd88239a483c3e62f34e4f96e..55a81bc62394b97d6822bc98e0d3f9157dd3c803 100644
--- a/ee/app/assets/javascripts/epic/components/epic_body.vue
+++ b/ee/app/assets/javascripts/epic/components/epic_body.vue
@@ -1,7 +1,8 @@
 <script>
-import { mapState } from 'vuex';
+import { mapState, mapGetters } from 'vuex';
 
 import IssuableBody from '~/issue_show/components/app.vue';
+import IssuableSidebar from '~/issuable_sidebar/components/sidebar_app.vue';
 import RelatedItems from 'ee/related_issues/components/related_issues_root.vue';
 
 import EpicSidebar from './epic_sidebar.vue';
@@ -10,6 +11,7 @@ export default {
   epicsPathIdSeparator: '&',
   components: {
     IssuableBody,
+    IssuableSidebar,
     RelatedItems,
     EpicSidebar,
   },
@@ -30,10 +32,18 @@ export default {
       'initialDescriptionHtml',
       'initialDescriptionText',
       'lockVersion',
+      'sidebarCollapsed',
     ]),
+    ...mapGetters(['isUserSignedIn']),
     isEpicTreeEnabled() {
       return gon.features && gon.features.epicTrees;
     },
+    isVueIssuableEpicSidebarEnabled() {
+      return gon.features && gon.features.vueIssuableEpicSidebar;
+    },
+    sidebarStatusClass() {
+      return this.sidebarCollapsed ? 'right-sidebar-collapsed' : 'right-sidebar-expanded';
+    },
   },
 };
 </script>
@@ -84,6 +94,11 @@ export default {
       css-class="js-related-issues-block"
       path-id-separator="#"
     />
-    <epic-sidebar />
+    <issuable-sidebar
+      v-if="isVueIssuableEpicSidebarEnabled"
+      :signed-in="isUserSignedIn"
+      :sidebar-status-class="sidebarStatusClass"
+    />
+    <epic-sidebar v-else />
   </div>
 </template>
diff --git a/ee/app/assets/javascripts/packages/components/app.vue b/ee/app/assets/javascripts/packages/components/app.vue
index e04929e89a112736a19c6d63ddf07f2c05f01c22..9fa8338fe48ca0975f51f385aaed5bd013648bc0 100644
--- a/ee/app/assets/javascripts/packages/components/app.vue
+++ b/ee/app/assets/javascripts/packages/components/app.vue
@@ -184,11 +184,12 @@ export default {
         v-gl-modal="'delete-modal'"
         class="js-delete-button"
         variant="danger"
+        data-qa-selector="delete_button"
         >{{ __('Delete') }}</gl-button
       >
     </div>
 
-    <div class="row prepend-top-default">
+    <div class="row prepend-top-default" data-qa-selector="package_information_content">
       <package-information :type="packageEntity.package_type" :information="packageInformation" />
       <package-information
         v-if="packageMetadata"
@@ -223,9 +224,13 @@ export default {
       <div slot="modal-footer" class="w-100">
         <div class="float-right">
           <gl-button @click="cancelDelete()">{{ __('Cancel') }}</gl-button>
-          <gl-button data-method="delete" :to="destroyPath" variant="danger">{{
-            __('Delete')
-          }}</gl-button>
+          <gl-button
+            data-method="delete"
+            :to="destroyPath"
+            variant="danger"
+            data-qa-selector="delete_modal_button"
+            >{{ __('Delete') }}</gl-button
+          >
         </div>
       </div>
     </gl-modal>
diff --git a/ee/app/assets/javascripts/pages/projects/issues/show/index.js b/ee/app/assets/javascripts/pages/projects/issues/show/index.js
index 994123c7f539fc04b1030ecd1327165fd654b93f..1c3ffd8c8645b7d37671ea9cd76986e0ab9015ed 100644
--- a/ee/app/assets/javascripts/pages/projects/issues/show/index.js
+++ b/ee/app/assets/javascripts/pages/projects/issues/show/index.js
@@ -5,7 +5,9 @@ import UserCallout from '~/user_callout';
 
 document.addEventListener('DOMContentLoaded', () => {
   initShow();
-  initSidebarBundle();
+  if (gon.features && !gon.features.vueIssuableSidebar) {
+    initSidebarBundle();
+  }
   initRelatedIssues();
 
   if (document.getElementById('js-design-management')) {
diff --git a/ee/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/ee/app/assets/javascripts/pages/projects/merge_requests/show/index.js
index 4494b107f49086c47b874976ba1474cb19c5dbf7..4dcef73a5e5083828c33bf0833ce449386a95bbc 100644
--- a/ee/app/assets/javascripts/pages/projects/merge_requests/show/index.js
+++ b/ee/app/assets/javascripts/pages/projects/merge_requests/show/index.js
@@ -5,7 +5,9 @@ import { initReviewBar } from 'ee/batch_comments';
 
 document.addEventListener('DOMContentLoaded', () => {
   initShow();
-  initSidebarBundle();
+  if (gon.features && !gon.features.vueIssuableSidebar) {
+    initSidebarBundle();
+  }
   initMrNotes();
   initReviewBar();
 });
diff --git a/ee/app/assets/javascripts/pages/trials/apply/index.js b/ee/app/assets/javascripts/pages/trials/apply/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..003f412530e29f3b58484696bf1d8a06592aafe5
--- /dev/null
+++ b/ee/app/assets/javascripts/pages/trials/apply/index.js
@@ -0,0 +1 @@
+import 'ee/pages/trials/namespace_select';
diff --git a/ee/app/assets/javascripts/pages/trials/namespace_select.js b/ee/app/assets/javascripts/pages/trials/namespace_select.js
new file mode 100644
index 0000000000000000000000000000000000000000..02170fe09a56272b05b7fa98e52b6a2acaf866c9
--- /dev/null
+++ b/ee/app/assets/javascripts/pages/trials/namespace_select.js
@@ -0,0 +1,17 @@
+import $ from 'jquery';
+
+document.addEventListener('DOMContentLoaded', () => {
+  const namespaceId = $('#namespace_id');
+  const newGroupName = $('#group_name');
+
+  namespaceId.on('change', () => {
+    const enableNewGroupName = namespaceId.val() === '0';
+
+    newGroupName
+      .toggleClass('hidden', !enableNewGroupName)
+      .find('input')
+      .prop('required', enableNewGroupName);
+  });
+
+  namespaceId.trigger('change');
+});
diff --git a/ee/app/assets/javascripts/pages/trials/select/index.js b/ee/app/assets/javascripts/pages/trials/select/index.js
index 02170fe09a56272b05b7fa98e52b6a2acaf866c9..003f412530e29f3b58484696bf1d8a06592aafe5 100644
--- a/ee/app/assets/javascripts/pages/trials/select/index.js
+++ b/ee/app/assets/javascripts/pages/trials/select/index.js
@@ -1,17 +1 @@
-import $ from 'jquery';
-
-document.addEventListener('DOMContentLoaded', () => {
-  const namespaceId = $('#namespace_id');
-  const newGroupName = $('#group_name');
-
-  namespaceId.on('change', () => {
-    const enableNewGroupName = namespaceId.val() === '0';
-
-    newGroupName
-      .toggleClass('hidden', !enableNewGroupName)
-      .find('input')
-      .prop('required', enableNewGroupName);
-  });
-
-  namespaceId.trigger('change');
-});
+import 'ee/pages/trials/namespace_select';
diff --git a/ee/app/assets/javascripts/projects/custom_project_templates.js b/ee/app/assets/javascripts/projects/custom_project_templates.js
index f4930f73fc01421ecaab12914d7972adb959dd64..ddee917d4ff3a2afaba3bce000b205288215cc32 100644
--- a/ee/app/assets/javascripts/projects/custom_project_templates.js
+++ b/ee/app/assets/javascripts/projects/custom_project_templates.js
@@ -54,8 +54,8 @@ const bindEvents = () => {
   }
 
   function chooseTemplate() {
-    const value = $(this).val();
     const subgroupId = $(this).data('subgroup-id');
+    const templateName = $(this).data('template-name');
 
     if (subgroupId) {
       $subgroupWithTemplatesIdInput.val(subgroupId);
@@ -70,7 +70,7 @@ const bindEvents = () => {
     $projectFieldsForm.addClass('selected');
     $selectedIcon.empty();
 
-    $selectedTemplateText.text(value);
+    $selectedTemplateText.text(templateName);
 
     $(this)
       .parents('.template-option')
diff --git a/ee/app/assets/javascripts/related_items_tree/components/tree_item_body.vue b/ee/app/assets/javascripts/related_items_tree/components/tree_item_body.vue
index 6c0cdfb2b4fef693895072e02a790a2f0cd5206b..2731b276da3bc470e7105319d869c824a46d5528 100644
--- a/ee/app/assets/javascripts/related_items_tree/components/tree_item_body.vue
+++ b/ee/app/assets/javascripts/related_items_tree/components/tree_item_body.vue
@@ -42,7 +42,7 @@ export default {
     },
   },
   computed: {
-    ...mapState(['childrenFlags']),
+    ...mapState(['childrenFlags', 'userSignedIn']),
     itemReference() {
       return this.item.reference;
     },
@@ -88,6 +88,9 @@ export default {
         this.childrenFlags[this.itemReference].itemRemoveInProgress
       );
     },
+    showEmptySpacer() {
+      return !this.parentItem.userPermissions.adminEpic && this.userSignedIn;
+    },
   },
   methods: {
     ...mapActions(['setRemoveItemModalProps']),
@@ -105,7 +108,10 @@ export default {
 
 <template>
   <div class="card card-slim sortable-row flex-grow-1">
-    <div class="item-body card-body d-flex align-items-center p-2 p-xl-1 pl-xl-3">
+    <div
+      class="item-body card-body d-flex align-items-center p-2 pl-xl-3"
+      :class="{ 'p-xl-1': userSignedIn, 'item-logged-out pt-xl-2 pb-xl-2': !userSignedIn }"
+    >
       <div class="item-contents d-flex align-items-center flex-wrap flex-grow-1 flex-xl-nowrap">
         <div class="item-title d-flex align-items-center mb-1 mb-xl-0">
           <icon
@@ -158,30 +164,30 @@ export default {
             >{{ item.pathIdSeparator }}{{ itemId }}
           </div>
           <div
-            class="item-meta-child d-flex align-items-center order-0 flex-wrap mr-md-1 ml-md-auto ml-xl-2 flex-xl-nowrap"
+            class="item-meta-child d-flex align-items-center order-0 flex-wrap mr-md-1 ml-md-auto ml-xl-2 mt-2 mt-md-0 flex-xl-nowrap"
           >
             <item-milestone
               v-if="hasMilestone"
               :milestone="item.milestone"
-              class="d-flex align-items-center item-milestone"
+              class="d-flex align-items-center item-milestone mr-2 mr-md-0"
             />
             <item-due-date
               v-if="item.dueDate"
               :date="item.dueDate"
               tooltip-placement="top"
-              css-class="item-due-date d-flex align-items-center ml-2 mr-0"
+              css-class="item-due-date d-flex align-items-center ml-0 mr-2 ml-md-2 ml-sm-0 mr-sm-0"
             />
             <item-weight
               v-if="item.weight"
               :weight="item.weight"
-              class="item-weight d-flex align-items-center ml-2 mr-0"
+              class="item-weight d-flex align-items-center ml-2 mr-0 ml-md-2"
               tag-name="span"
             />
           </div>
           <item-assignees
             v-if="hasAssignees"
             :assignees="item.assignees"
-            class="item-assignees d-inline-flex align-items-center align-self-end ml-auto ml-md-0 mb-md-0 order-2 flex-xl-grow-0 mt-xl-0 mr-xl-1"
+            class="item-assignees d-inline-flex align-items-center align-self-end ml-0 ml-md-2 mt-2 mt-md-0 mt-xl-0 mr-xl-1 mb-md-0 order-2 flex-xl-grow-0"
           />
         </div>
         <gl-button
@@ -195,7 +201,7 @@ export default {
         >
           <icon :size="16" name="close" class="btn-item-remove-icon" />
         </gl-button>
-        <span v-if="!parentItem.userPermissions.adminEpic" class="p-3"></span>
+        <span v-if="showEmptySpacer" class="p-3"></span>
       </div>
     </div>
   </div>
diff --git a/ee/app/assets/javascripts/related_items_tree/components/tree_root.vue b/ee/app/assets/javascripts/related_items_tree/components/tree_root.vue
index 39ee58a284ca5a13c73a4ddc277826f23f923044..a54589cadd33d28efba1f2de884d2e34caff3e7c 100644
--- a/ee/app/assets/javascripts/related_items_tree/components/tree_root.vue
+++ b/ee/app/assets/javascripts/related_items_tree/components/tree_root.vue
@@ -1,6 +1,5 @@
 <script>
 import { mapState, mapActions } from 'vuex';
-import Draggable from 'vuedraggable';
 import { GlButton, GlLoadingIcon } from '@gitlab/ui';
 
 import { ChildType } from '../constants';
@@ -8,7 +7,6 @@ import TreeDragAndDropMixin from '../mixins/tree_dd_mixin';
 
 export default {
   components: {
-    Draggable,
     GlButton,
     GlLoadingIcon,
   },
@@ -29,7 +27,7 @@ export default {
     };
   },
   computed: {
-    ...mapState(['childrenFlags']),
+    ...mapState(['childrenFlags', 'userSignedIn']),
     currentItemIssuesBeginAtIndex() {
       return this.children.findIndex(item => item.type === ChildType.Issue);
     },
@@ -58,14 +56,10 @@ export default {
 </script>
 
 <template>
-  <draggable
-    tag="ul"
-    v-bind="dragOptions"
+  <component
+    :is="treeRootWrapper"
+    v-bind="treeRootOptions"
     class="list-unstyled related-items-list tree-root"
-    ghost-class="tree-item-drag-active"
-    :data-parent-reference="parentItem.reference"
-    :value="children"
-    :move="handleDragOnMove"
     @start="handleDragOnStart"
     @end="handleDragOnEnd"
   >
@@ -80,5 +74,5 @@ export default {
       >
       <gl-loading-icon v-else size="sm" class="mt-1 mb-1" />
     </li>
-  </draggable>
+  </component>
 </template>
diff --git a/ee/app/assets/javascripts/related_items_tree/mixins/tree_dd_mixin.js b/ee/app/assets/javascripts/related_items_tree/mixins/tree_dd_mixin.js
index dad0ea2e9cc3db05a216f0d23451076ab23c19f2..2b70beee3ffe83bb7e0d151100e4429320575ac2 100644
--- a/ee/app/assets/javascripts/related_items_tree/mixins/tree_dd_mixin.js
+++ b/ee/app/assets/javascripts/related_items_tree/mixins/tree_dd_mixin.js
@@ -1,14 +1,26 @@
+import Draggable from 'vuedraggable';
+
 import defaultSortableConfig from '~/sortable/sortable_config';
 import { ChildType, idProp, relativePositions } from '../constants';
 
 export default {
   computed: {
-    dragOptions() {
-      return {
+    treeRootWrapper() {
+      return this.userSignedIn ? Draggable : 'ul';
+    },
+    treeRootOptions() {
+      const options = {
         ...defaultSortableConfig,
         fallbackOnBody: false,
         group: this.parentItem.reference,
+        tag: 'ul',
+        'ghost-class': 'tree-item-drag-active',
+        'data-parent-reference': this.parentItem.reference,
+        value: this.children,
+        move: this.handleDragOnMove,
       };
+
+      return this.userSignedIn ? options : {};
     },
   },
   methods: {
diff --git a/ee/app/assets/javascripts/related_items_tree/related_items_tree_bundle.js b/ee/app/assets/javascripts/related_items_tree/related_items_tree_bundle.js
index 69a613ea21dd9532f673b6ad9b7eaa66091430d7..c9a9140fb8ff5a9fbe7d5b81633d5e090b14ef40 100644
--- a/ee/app/assets/javascripts/related_items_tree/related_items_tree_bundle.js
+++ b/ee/app/assets/javascripts/related_items_tree/related_items_tree_bundle.js
@@ -16,7 +16,7 @@ export default () => {
     return false;
   }
 
-  const { id, iid, fullPath, autoCompleteEpics, autoCompleteIssues } = el.dataset;
+  const { id, iid, fullPath, autoCompleteEpics, autoCompleteIssues, userSignedIn } = el.dataset;
   const initialData = JSON.parse(el.dataset.initial);
 
   Vue.component('tree-root', TreeRoot);
@@ -44,6 +44,7 @@ export default () => {
         issuesEndpoint: initialData.issueLinksEndpoint,
         autoCompleteEpics: parseBoolean(autoCompleteEpics),
         autoCompleteIssues: parseBoolean(autoCompleteIssues),
+        userSignedIn: parseBoolean(userSignedIn),
       });
     },
     methods: {
diff --git a/ee/app/assets/javascripts/related_items_tree/store/mutations.js b/ee/app/assets/javascripts/related_items_tree/store/mutations.js
index a9a32163927b9c4696cf26078824fc9ccb5923ff..c26f2c661895563c471f64d4b3f88fcee8259ba2 100644
--- a/ee/app/assets/javascripts/related_items_tree/store/mutations.js
+++ b/ee/app/assets/javascripts/related_items_tree/store/mutations.js
@@ -5,12 +5,13 @@ import * as types from './mutation_types';
 export default {
   [types.SET_INITIAL_CONFIG](
     state,
-    { epicsEndpoint, issuesEndpoint, autoCompleteEpics, autoCompleteIssues },
+    { epicsEndpoint, issuesEndpoint, autoCompleteEpics, autoCompleteIssues, userSignedIn },
   ) {
     state.epicsEndpoint = epicsEndpoint;
     state.issuesEndpoint = issuesEndpoint;
     state.autoCompleteEpics = autoCompleteEpics;
     state.autoCompleteIssues = autoCompleteIssues;
+    state.userSignedIn = userSignedIn;
   },
 
   [types.SET_INITIAL_PARENT_ITEM](state, data) {
diff --git a/ee/app/assets/javascripts/related_items_tree/store/state.js b/ee/app/assets/javascripts/related_items_tree/store/state.js
index 1c81bf541cc53599f0e56f2afc0fa42b4f30cda1..d7a1684eae7d539f4f33ba1a34261ccd50167d06 100644
--- a/ee/app/assets/javascripts/related_items_tree/store/state.js
+++ b/ee/app/assets/javascripts/related_items_tree/store/state.js
@@ -3,6 +3,7 @@ export default () => ({
   parentItem: {},
   epicsEndpoint: '',
   issuesEndpoint: '',
+  userSignedIn: false,
 
   children: {},
   childrenFlags: {},
diff --git a/ee/app/assets/javascripts/security_dashboard/components/filters.vue b/ee/app/assets/javascripts/security_dashboard/components/filters.vue
index 666f99d0132682e82bb4f46967a3d432405e4457..02b3f0fb07b9f333f4bd4775be46c8089ac6abbb 100644
--- a/ee/app/assets/javascripts/security_dashboard/components/filters.vue
+++ b/ee/app/assets/javascripts/security_dashboard/components/filters.vue
@@ -37,7 +37,7 @@ export default {
         <gl-toggle-vuex
           class="d-block mt-1 js-toggle"
           store-module="filters"
-          state-property="hide_dismissed"
+          state-property="hideDismissed"
           set-action="setToggleValue"
         />
       </div>
diff --git a/ee/app/assets/javascripts/security_dashboard/components/instance_security_dashboard.vue b/ee/app/assets/javascripts/security_dashboard/components/instance_security_dashboard.vue
index ec4326634da6f98ad5c77cae062d92dd57269c0a..b4c141fd8c9b09b48f20e37a8f0f26c28d85f0a8 100644
--- a/ee/app/assets/javascripts/security_dashboard/components/instance_security_dashboard.vue
+++ b/ee/app/assets/javascripts/security_dashboard/components/instance_security_dashboard.vue
@@ -2,6 +2,7 @@
 import { mapActions, mapState } from 'vuex';
 import { GlButton, GlEmptyState, GlLink, GlLoadingIcon } from '@gitlab/ui';
 import { s__ } from '~/locale';
+import ProjectManager from './project_manager.vue';
 import SecurityDashboard from './app.vue';
 
 export default {
@@ -11,6 +12,7 @@ export default {
     GlEmptyState,
     GlLink,
     GlLoadingIcon,
+    ProjectManager,
     SecurityDashboard,
   },
   props: {
@@ -26,7 +28,11 @@ export default {
       type: String,
       required: true,
     },
-    projectsEndpoint: {
+    projectAddEndpoint: {
+      type: String,
+      required: true,
+    },
+    projectListEndpoint: {
       type: String,
       required: true,
     },
@@ -54,7 +60,7 @@ export default {
     };
   },
   computed: {
-    ...mapState('projects', ['projects']),
+    ...mapState('projectSelector', ['projects']),
     toggleButtonProps() {
       return this.showProjectSelector
         ? {
@@ -71,7 +77,10 @@ export default {
     },
   },
   created() {
-    this.setProjectsEndpoint(this.projectsEndpoint);
+    this.setProjectEndpoints({
+      add: this.projectAddEndpoint,
+      list: this.projectListEndpoint,
+    });
     this.fetchProjects()
       // Failure to fetch projects will be handled in the store, so do nothing here.
       .catch(() => {})
@@ -80,7 +89,7 @@ export default {
       });
   },
   methods: {
-    ...mapActions('projects', ['setProjectsEndpoint', 'fetchProjects']),
+    ...mapActions('projectSelector', ['setProjectEndpoints', 'fetchProjects']),
     toggleProjectSelector() {
       this.showProjectSelector = !this.showProjectSelector;
     },
@@ -94,7 +103,6 @@ export default {
       <h2 class="page-title">{{ s__('SecurityDashboard|Security Dashboard') }}</h2>
       <gl-button
         v-if="isInitialized"
-        new-style
         class="page-title-controls js-project-selector-toggle"
         :variant="toggleButtonProps.variant"
         @click="toggleProjectSelector"
@@ -103,9 +111,7 @@ export default {
     </header>
 
     <template v-if="isInitialized">
-      <section v-if="showProjectSelector" class="js-dashboard-project-selector">
-        <h3>{{ s__('SecurityDashboard|Add or remove projects from your dashboard') }}</h3>
-      </section>
+      <project-manager v-if="showProjectSelector" />
 
       <template v-else>
         <gl-empty-state
@@ -125,7 +131,7 @@ export default {
             >.
           </template>
           <template #actions>
-            <gl-button new-style variant="success" @click="toggleProjectSelector">
+            <gl-button variant="success" @click="toggleProjectSelector">
               {{ s__('SecurityDashboard|Add projects') }}
             </gl-button>
           </template>
@@ -143,6 +149,6 @@ export default {
       </template>
     </template>
 
-    <gl-loading-icon v-else size="md" />
+    <gl-loading-icon v-else size="md" class="mt-4" />
   </article>
 </template>
diff --git a/ee/app/assets/javascripts/security_dashboard/components/project_list.vue b/ee/app/assets/javascripts/security_dashboard/components/project_list.vue
index d7499ba0a998d481e0d4070b8b4135831d4d995c..afbd5c193566448ff8d1fd024b3b44d2cd3fecb5 100644
--- a/ee/app/assets/javascripts/security_dashboard/components/project_list.vue
+++ b/ee/app/assets/javascripts/security_dashboard/components/project_list.vue
@@ -19,6 +19,10 @@ export default {
       type: Array,
       required: true,
     },
+    showLoadingIndicator: {
+      type: Boolean,
+      required: true,
+    },
   },
   methods: {
     projectRemoved(project) {
@@ -31,10 +35,11 @@ export default {
 <template>
   <section>
     <div>
-      <h3 class="h5 text-secondary border-bottom mb-3 pb-2">
+      <h4 class="h5 font-weight-bold text-secondary border-bottom mb-3 pb-2">
         {{ s__('SecurityDashboard|Projects added') }}
-        <gl-badge>{{ projects.length }}</gl-badge>
-      </h3>
+        <gl-badge pill class="font-weight-bold">{{ projects.length }}</gl-badge>
+        <gl-loading-icon v-if="showLoadingIndicator" size="sm" class="float-right" />
+      </h4>
       <ul v-if="projects.length" class="list-unstyled">
         <li
           v-for="project in projects"
diff --git a/ee/app/assets/javascripts/security_dashboard/components/project_manager.vue b/ee/app/assets/javascripts/security_dashboard/components/project_manager.vue
index 3df0557a3bcee323e6ae93c08efb8aac447c7aa7..7edd72969b06bfa4b6f468b7708952ed11068068 100644
--- a/ee/app/assets/javascripts/security_dashboard/components/project_manager.vue
+++ b/ee/app/assets/javascripts/security_dashboard/components/project_manager.vue
@@ -1,36 +1,27 @@
 <script>
-import { mapState, mapActions } from 'vuex';
-import { GlBadge, GlButton, GlLoadingIcon } from '@gitlab/ui';
-
-import Icon from '~/vue_shared/components/icon.vue';
+import { mapState, mapActions, mapGetters } from 'vuex';
+import { GlButton } from '@gitlab/ui';
 import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
-
 import ProjectList from './project_list.vue';
 
 export default {
   components: {
-    GlBadge,
     GlButton,
-    GlLoadingIcon,
-    Icon,
     ProjectList,
     ProjectSelector,
   },
   computed: {
     ...mapState('projectSelector', [
       'projects',
-      'isAddingProjects',
       'selectedProjects',
       'projectSearchResults',
-      'searchCount',
       'messages',
     ]),
-    isSearchingProjects() {
-      return this.searchCount > 0;
-    },
-    hasProjectsSelected() {
-      return this.selectedProjects.length > 0;
-    },
+    ...mapGetters('projectSelector', [
+      'canAddProjects',
+      'isSearchingProjects',
+      'isUpdatingProjects',
+    ]),
   },
   methods: {
     ...mapActions('projectSelector', [
@@ -41,10 +32,6 @@ export default {
       'setSearchQuery',
       'removeProject',
     ]),
-    addProjectsAndClearSearchResults() {
-      this.addProjects();
-      this.clearSearchResults();
-    },
     searched(query) {
       this.setSearchQuery(query);
       this.fetchSearchResults();
@@ -63,9 +50,9 @@ export default {
   <section class="container">
     <div class="row justify-content-center mt-md-4">
       <div class="col col-lg-7">
-        <h2 class="h5 border-bottom mb-4 pb-3">
+        <h3 class="text-3 font-weight-bold border-bottom mb-4 pb-3">
           {{ s__('SecurityDashboard|Add or remove projects from your dashboard') }}
-        </h2>
+        </h3>
         <div class="d-flex flex-column flex-md-row">
           <project-selector
             class="flex-grow mr-md-2"
@@ -79,12 +66,7 @@ export default {
             @projectClicked="projectClicked"
           />
           <div class="mb-3">
-            <gl-button
-              :disabled="!hasProjectsSelected"
-              new-style
-              variant="success"
-              @click="addProjectsAndClearSearchResults"
-            >
+            <gl-button :disabled="!canAddProjects" variant="success" @click="addProjects">
               {{ s__('SecurityDashboard|Add projects') }}
             </gl-button>
           </div>
@@ -92,8 +74,12 @@ export default {
       </div>
     </div>
     <div class="row justify-content-center mt-md-3">
-      <project-list :projects="projects" class="col col-lg-7" @projectRemoved="projectRemoved" />
-      <gl-loading-icon v-if="isAddingProjects" size="sm" />
+      <project-list
+        :projects="projects"
+        :show-loading-indicator="isUpdatingProjects"
+        class="col col-lg-7"
+        @projectRemoved="projectRemoved"
+      />
     </div>
   </section>
 </template>
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/actions.js b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/actions.js
index c02f679df912d1af75febd464509bc29cce662f8..a21ee78cba3996880cc32b722bd4d8c53928481e 100644
--- a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/actions.js
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/actions.js
@@ -1,18 +1,25 @@
 import Tracking from '~/tracking';
 import { getParameterValues } from '~/lib/utils/url_utility';
 import * as types from './mutation_types';
+import { ALL } from './constants';
+import { hasValidSelection } from './utils';
 
-export const setFilter = ({ commit }, payload) => {
-  commit(types.SET_FILTER, payload);
+export const setFilter = ({ commit }, { filterId, optionId, lazy = false }) => {
+  commit(types.SET_FILTER, { filterId, optionId, lazy });
 
   Tracking.event(document.body.dataset.page, 'set_filter', {
-    label: payload.filterId,
-    value: payload.optionId,
+    label: filterId,
+    value: optionId,
   });
 };
 
-export const setFilterOptions = ({ commit }, payload) => {
-  commit(types.SET_FILTER_OPTIONS, payload);
+export const setFilterOptions = ({ commit, state }, { filterId, options, lazy = false }) => {
+  commit(types.SET_FILTER_OPTIONS, { filterId, options });
+
+  const { selection } = state.filters.find(({ id }) => id === filterId);
+  if (!hasValidSelection({ selection, options })) {
+    commit(types.SET_FILTER, { filterId, optionId: ALL, lazy });
+  }
 };
 
 export const setAllFilters = ({ commit }, payload) => {
@@ -27,7 +34,7 @@ export const lockFilter = ({ commit }, payload) => {
 export const setHideDismissedToggleInitialState = ({ commit }) => {
   const [urlParam] = getParameterValues('scope');
   const showDismissed = urlParam === 'all';
-  commit(types.SET_TOGGLE_VALUE, { key: 'hide_dismissed', value: !showDismissed });
+  commit(types.SET_TOGGLE_VALUE, { key: 'hideDismissed', value: !showDismissed });
 };
 
 export const setToggleValue = ({ commit }, { key, value }) => {
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/getters.js b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/getters.js
index 7ff051f07e3495d28424b8da987d6afd508b8eef..7909bba6a5a86f70690de2068dcc76a797689bb5 100644
--- a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/getters.js
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/getters.js
@@ -32,10 +32,10 @@ export const activeFilters = state => {
     acc[filter.id] = [...Array.from(filter.selection)].filter(id => !isBaseFilterOption(id));
     return acc;
   }, {});
-  // hide_dismissed is hardcoded as it currently is an edge-case, more info in the MR:
+  // hideDismissed is hardcoded as it currently is an edge-case, more info in the MR:
   // https://gitlab.com/gitlab-org/gitlab/merge_requests/15333#note_208301144
   if (gon.features && gon.features.hideDismissedVulnerabilities) {
-    filters.scope = state.hide_dismissed ? 'dismissed' : 'all';
+    filters.scope = state.hideDismissed ? 'dismissed' : 'all';
   }
   return filters;
 };
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/mutations.js b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/mutations.js
index 13e75a20722d4bc69b11c0edce2e6b8b204623f2..f6bc61457561223fc607f55a2c3ce90af8c2cc87 100644
--- a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/mutations.js
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/mutations.js
@@ -19,7 +19,7 @@ export default {
 
       return { ...filter, selection };
     });
-    state.hide_dismissed = payload.scope !== 'all';
+    state.hideDismissed = payload.scope !== 'all';
   },
   [types.SET_FILTER](state, payload) {
     const { filterId, optionId } = payload;
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/state.js b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/state.js
index d8a368b662d44be1a837a0b972b24aa1d826662f..bb9130e6b719338a633140223def0e5fe19c2989 100644
--- a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/state.js
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/state.js
@@ -34,5 +34,5 @@ export default () => ({
       selection: new Set([BASE_FILTERS.project_id.id]),
     },
   ],
-  hide_dismissed: true,
+  hideDismissed: true,
 });
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/utils.js b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/utils.js
index d7269527218583808702991eaeb4311c4ed84f1b..3b549ad2262f0e9569a8fcc4555197cc988d6900 100644
--- a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/utils.js
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/utils.js
@@ -1,4 +1,13 @@
+import { isSubset } from '~/lib/utils/set';
 import { ALL } from './constants';
 
-// eslint-disable-next-line import/prefer-default-export
 export const isBaseFilterOption = id => id === ALL;
+
+/**
+ * Returns whether or not the given state filter has a valid selection,
+ * considering its available options.
+ * @param {Object} filter The filter from the state to check.
+ * @returns boolean
+ */
+export const hasValidSelection = ({ selection, options }) =>
+  isSubset(selection, new Set(options.map(({ id }) => id)));
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/actions.js b/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/actions.js
index f016f1a1a9ae1eea8b8d234e205e197c897a78ed..d8143e05da17a5f5501019b0bea76d047c26a7e3 100644
--- a/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/actions.js
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/actions.js
@@ -36,7 +36,8 @@ export const addProjects = ({ state, dispatch }) => {
       project_ids: state.selectedProjects.map(p => p.id),
     })
     .then(response => dispatch('receiveAddProjectsSuccess', response.data))
-    .catch(() => dispatch('receiveAddProjectsError'));
+    .catch(() => dispatch('receiveAddProjectsError'))
+    .finally(() => dispatch('clearSearchResults'));
 };
 
 export const requestAddProjects = ({ commit }) => {
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/getters.js b/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/getters.js
new file mode 100644
index 0000000000000000000000000000000000000000..c12bf3f19bc4c2992a5e270cb56d5377dff9036d
--- /dev/null
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/getters.js
@@ -0,0 +1,7 @@
+export const canAddProjects = ({ isAddingProjects, selectedProjects }) =>
+  !isAddingProjects && selectedProjects.length > 0;
+
+export const isSearchingProjects = ({ searchCount }) => searchCount > 0;
+
+export const isUpdatingProjects = ({ isAddingProjects, isLoadingProjects, isRemovingProject }) =>
+  isAddingProjects || isLoadingProjects || isRemovingProject;
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/index.js b/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/index.js
index 636092ce1a9aab25cda419c7e2c9bb1d6145029e..3e2701435b377175b61dcc3b582b86894c311a9d 100644
--- a/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/index.js
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/index.js
@@ -1,10 +1,12 @@
 import state from './state';
 import mutations from './mutations';
 import * as actions from './actions';
+import * as getters from './getters';
 
 export default () => ({
   namespaced: true,
   state,
   mutations,
   actions,
+  getters,
 });
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/actions.js b/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/actions.js
index 80e0386c3a512f3b9827264b45f1b3341fcc76ea..026038532b99eaff170a54b370f7e0b456299bae 100644
--- a/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/actions.js
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/actions.js
@@ -149,14 +149,40 @@ export const receiveCreateIssueError = ({ commit }, { flashError }) => {
 };
 
 export const dismissVulnerability = (
-  { dispatch, state },
+  { dispatch, state, rootState },
   { vulnerability, flashError, comment },
 ) => {
+  const page = state.pageInfo && state.pageInfo.page ? state.pageInfo.page : 1;
+  const dismissedVulnerabilitiesHidden = Boolean(
+    rootState.filters && rootState.filters.hideDismissed,
+  );
   dispatch('requestDismissVulnerability');
 
-  const toastMsg = sprintf(s__("Security Reports|Dismissed '%{vulnerabilityName}'"), {
-    vulnerabilityName: vulnerability.name,
-  });
+  const toastMsg = sprintf(
+    dismissedVulnerabilitiesHidden
+      ? s__(
+          "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view.",
+        )
+      : s__("Security Reports|Dismissed '%{vulnerabilityName}'"),
+    {
+      vulnerabilityName: vulnerability.name,
+    },
+  );
+  const toastOptions = dismissedVulnerabilitiesHidden
+    ? {
+        action: {
+          text: s__('Security Reports|Undo dismiss'),
+          onClick: (e, toastObject) => {
+            if (vulnerability.dismissal_feedback) {
+              dispatch('undoDismiss', { vulnerability })
+                .then(() => dispatch('fetchVulnerabilities', { page }))
+                .catch(() => {});
+              toastObject.goAway(0);
+            }
+          },
+        },
+      }
+    : {};
 
   axios
     .post(vulnerability.create_vulnerability_feedback_dismissal_path, {
@@ -175,7 +201,14 @@ export const dismissVulnerability = (
     .then(({ data }) => {
       dispatch('closeDismissalCommentBox');
       dispatch('receiveDismissVulnerabilitySuccess', { vulnerability, data });
-      toast(toastMsg);
+      if (dismissedVulnerabilitiesHidden) {
+        dispatch('fetchVulnerabilities', {
+          // If we just dismissed the last vulnerability on the active page,
+          // we load the previous page if any
+          page: state.vulnerabilities.length === 1 && page > 1 ? page - 1 : page,
+        });
+      }
+      toast(toastMsg, toastOptions);
     })
     .catch(() => {
       dispatch('receiveDismissVulnerabilityError', { flashError });
@@ -298,7 +331,7 @@ export const undoDismiss = ({ dispatch }, { vulnerability, flashError }) => {
 
   dispatch('requestUndoDismiss');
 
-  axios
+  return axios
     .delete(destroy_vulnerability_feedback_dismissal_path)
     .then(() => {
       dispatch('receiveUndoDismissSuccess', { vulnerability });
diff --git a/ee/app/assets/javascripts/security_dashboard/store/plugins/mediator.js b/ee/app/assets/javascripts/security_dashboard/store/plugins/mediator.js
index 460e994da3b3b3a94d9207a5a3ac05f57c38c468..e0fe496468642a421be1c1294bcb87692c56302f 100644
--- a/ee/app/assets/javascripts/security_dashboard/store/plugins/mediator.js
+++ b/ee/app/assets/javascripts/security_dashboard/store/plugins/mediator.js
@@ -7,7 +7,7 @@ export default store => {
     store.dispatch('vulnerabilities/fetchVulnerabilitiesHistory', payload);
   };
 
-  store.subscribe(({ type }) => {
+  store.subscribe(({ type, payload }) => {
     switch (type) {
       // SET_ALL_FILTERS mutations are triggered by navigation events, in such case we
       // want to preserve the page number that was set in the sync_with_router plugin
@@ -21,7 +21,9 @@ export default store => {
       // in that case we want to reset the page number
       case `filters/${filtersMutationTypes.SET_FILTER}`:
       case `filters/${filtersMutationTypes.SET_TOGGLE_VALUE}`: {
-        refreshVulnerabilities(store.getters['filters/activeFilters']);
+        if (!payload.lazy) {
+          refreshVulnerabilities(store.getters['filters/activeFilters']);
+        }
         break;
       }
       default:
diff --git a/ee/app/assets/javascripts/security_dashboard/store/plugins/project_selector.js b/ee/app/assets/javascripts/security_dashboard/store/plugins/project_selector.js
new file mode 100644
index 0000000000000000000000000000000000000000..3cc6bd6fb1ff92621e23ad3915a7cbe14cab6802
--- /dev/null
+++ b/ee/app/assets/javascripts/security_dashboard/store/plugins/project_selector.js
@@ -0,0 +1,23 @@
+import projectSelectorModule from '../modules/project_selector';
+import * as projectSelectorMutationTypes from '../modules/project_selector/mutation_types';
+import { BASE_FILTERS } from '../modules/filters/constants';
+
+export default store => {
+  store.registerModule('projectSelector', projectSelectorModule());
+
+  store.subscribe(({ type, payload }) => {
+    if (type === `projectSelector/${projectSelectorMutationTypes.RECEIVE_PROJECTS_SUCCESS}`) {
+      store.dispatch('filters/setFilterOptions', {
+        filterId: 'project_id',
+        options: [
+          BASE_FILTERS.project_id,
+          ...payload.map(({ name, id }) => ({
+            name,
+            id: id.toString(),
+          })),
+        ],
+        lazy: true,
+      });
+    }
+  });
+};
diff --git a/ee/app/assets/javascripts/vue_shared/license_management/report_mapper.js b/ee/app/assets/javascripts/vue_shared/license_management/report_mapper.js
new file mode 100644
index 0000000000000000000000000000000000000000..af8a18f14f74023f3b02f0f0a0d646bba02271d6
--- /dev/null
+++ b/ee/app/assets/javascripts/vue_shared/license_management/report_mapper.js
@@ -0,0 +1,30 @@
+import V2Report from './v2_report';
+
+const DEFAULT_VERSION = '1';
+
+export default class ReportMapper {
+  constructor() {
+    this.mappers = {
+      '1': report => report,
+      '2': report => new V2Report(report).toV1Schema(),
+    };
+  }
+
+  mapFrom(reportArtifact) {
+    const majorVersion = ReportMapper.majorVersionFor(reportArtifact);
+    return this.mapperFor(majorVersion)(reportArtifact);
+  }
+
+  mapperFor(majorVersion) {
+    return this.mappers[majorVersion];
+  }
+
+  static majorVersionFor(report) {
+    if (report && report.version) {
+      const [majorVersion] = report.version.split('.');
+      return majorVersion;
+    }
+
+    return DEFAULT_VERSION;
+  }
+}
diff --git a/ee/app/assets/javascripts/vue_shared/license_management/store/utils.js b/ee/app/assets/javascripts/vue_shared/license_management/store/utils.js
index fb314cc7cfb6a2b2c1be1a432e81007150bb4ba6..30786f868122341c5827661a944aa153a0c234e0 100644
--- a/ee/app/assets/javascripts/vue_shared/license_management/store/utils.js
+++ b/ee/app/assets/javascripts/vue_shared/license_management/store/utils.js
@@ -1,6 +1,7 @@
 import { n__, sprintf } from '~/locale';
 import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
 import { LICENSE_APPROVAL_STATUS } from 'ee/vue_shared/license_management/constants';
+import ReportMapper from 'ee/vue_shared/license_management/report_mapper';
 
 const toLowerCase = name => name.toLowerCase();
 /**
@@ -85,10 +86,13 @@ export const parseLicenseReportMetrics = (headMetrics, baseMetrics, managedLicen
   if (!headMetrics && !baseMetrics) {
     return [];
   }
+  const reportMapper = new ReportMapper();
+  const headReport = reportMapper.mapFrom(headMetrics);
+  const baseReport = reportMapper.mapFrom(baseMetrics);
 
-  const headLicenses = headMetrics.licenses || [];
-  const headDependencies = headMetrics.dependencies || [];
-  const baseLicenses = baseMetrics.licenses || [];
+  const headLicenses = headReport.licenses || [];
+  const headDependencies = headReport.dependencies || [];
+  const baseLicenses = baseReport.licenses || [];
   const managedLicenseList = managedLicenses || [];
 
   if (!headLicenses.length && !headDependencies.length) return [];
diff --git a/ee/app/assets/javascripts/vue_shared/license_management/v2_report.js b/ee/app/assets/javascripts/vue_shared/license_management/v2_report.js
new file mode 100644
index 0000000000000000000000000000000000000000..fda64aae3169d0c65b715bb653f09ea4cd886fe1
--- /dev/null
+++ b/ee/app/assets/javascripts/vue_shared/license_management/v2_report.js
@@ -0,0 +1,60 @@
+import { byLicenseNameComparator } from './store/utils';
+
+export default class V2Report {
+  constructor(report) {
+    this.report = report;
+    this.licenseMap = V2Report.createLicenseMap(report.licenses);
+    this.licenses = report.licenses.sort(byLicenseNameComparator).map(V2Report.mapFromLicense);
+  }
+
+  toV1Schema() {
+    return {
+      licenses: this.licenses,
+      dependencies: this.report.dependencies.map(v2Dependency =>
+        this.mapFromDependency(v2Dependency),
+      ),
+    };
+  }
+
+  combine(licenses, visitor) {
+    const reducer = (memo, licenseId) => {
+      const license = this.licenseMap[licenseId];
+      visitor(license);
+      if (memo) return { name: `${memo.name}, ${license.name}`, url: '' };
+      return { name: license.name, url: license.url };
+    };
+
+    return licenses.reduce(reducer, null);
+  }
+
+  incrementCountFor(licenseName) {
+    const matchingLicense = this.licenses.find(license => license.name === licenseName);
+    if (matchingLicense) matchingLicense.count += 1;
+  }
+
+  mapFromDependency({ name, description, url, licenses }) {
+    const combinedLicense = this.combine(licenses, license => {
+      this.incrementCountFor(license.name);
+    });
+
+    return {
+      license: combinedLicense,
+      dependency: { name, url, description },
+    };
+  }
+
+  static mapFromLicense({ name, url = '', count = 0 }) {
+    return { name, url, count };
+  }
+
+  static createLicenseMap(licenses) {
+    const identityMap = {};
+    licenses.forEach(item => {
+      identityMap[item.id] = {
+        name: item.name,
+        url: item.url,
+      };
+    });
+    return identityMap;
+  }
+}
diff --git a/ee/app/assets/stylesheets/components/design_management/design.scss b/ee/app/assets/stylesheets/components/design_management/design.scss
index cfbb88f5b8f554f6277866ab9cbb7b4351c18802..9710e609f30bfb53bfda01d70b21bfa8b13e8c78 100644
--- a/ee/app/assets/stylesheets/components/design_management/design.scss
+++ b/ee/app/assets/stylesheets/components/design_management/design.scss
@@ -11,6 +11,12 @@
   right: $gl-padding;
 }
 
+.design-checkbox {
+  position: absolute;
+  top: $gl-padding;
+  left: 30px;
+}
+
 .image-notes {
   overflow-y: scroll;
   padding: 0 $gl-padding;
diff --git a/ee/app/assets/stylesheets/components/design_list_item.scss b/ee/app/assets/stylesheets/components/design_management/design_list_item.scss
similarity index 100%
rename from ee/app/assets/stylesheets/components/design_list_item.scss
rename to ee/app/assets/stylesheets/components/design_management/design_list_item.scss
diff --git a/ee/app/assets/stylesheets/components/design_version_dropdown.scss b/ee/app/assets/stylesheets/components/design_management/design_version_dropdown.scss
similarity index 100%
rename from ee/app/assets/stylesheets/components/design_version_dropdown.scss
rename to ee/app/assets/stylesheets/components/design_management/design_version_dropdown.scss
diff --git a/ee/app/assets/stylesheets/components/related_items_tree.scss b/ee/app/assets/stylesheets/components/related_items_tree.scss
index 8138dec1985f4cd037ed1292c97089b88550ba32..8fdc61eed1fd009c9fd1f79c2110faf112d629c3 100644
--- a/ee/app/assets/stylesheets/components/related_items_tree.scss
+++ b/ee/app/assets/stylesheets/components/related_items_tree.scss
@@ -28,6 +28,11 @@
 
     .item-body {
       cursor: grab;
+
+      &.item-logged-out {
+        cursor: default;
+        min-height: $grid-size * 5;
+      }
     }
 
     .btn-tree-item-chevron {
diff --git a/ee/app/assets/stylesheets/pages/billings.scss b/ee/app/assets/stylesheets/pages/billings.scss
index 7ca8c4c0cd3461ebfd7d8ce4993f8635efa87ed0..e90a6b1ce8fa8e4497acf0da86c96808a6507ae4 100644
--- a/ee/app/assets/stylesheets/pages/billings.scss
+++ b/ee/app/assets/stylesheets/pages/billings.scss
@@ -20,133 +20,36 @@
 }
 
 .billing-plans {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  justify-content: center;
-  margin-top: 8px;
-
-  .card-header {
-    border-bottom: 0;
-  }
-
   .card {
-    display: flex;
-    flex-direction: column;
-    margin: 8px;
-    width: 100%;
-    border: 0;
-
-    &:first-of-type {
-      margin-left: 0;
-    }
-
-    &:last-of-type {
-      margin-right: 0;
+    &-header {
+      line-height: $gl-line-height-20;
     }
 
-    @include media-breakpoint-up(sm) {
-      width: 280px;
+    &-active {
+      background-color: $gray-light;
     }
 
-    .card-header,
     .card-body {
-      border-left: 1px solid $list-border;
-      border-right: 1px solid $list-border;
-    }
-
-    .card-header {
-      background-color: $blue-500;
-      color: $white-light;
-      border: 0;
-      border-radius: 4px 4px 0 0;
-      font-size: 20px;
-      text-align: center;
-    }
-
-    .card-body {
-      flex-grow: 1;
-      border-radius: 0 0 4px 4px;
-      border-bottom: 1px solid $list-border;
-      padding: 0;
-      display: flex;
-      flex-direction: column;
-
       .price-per-month {
         display: flex;
         flex-direction: row;
         color: $blue-500;
-        padding: 16px;
-        padding-bottom: 0;
-        justify-content: center;
-        font-size: 50px;
+        font-size: 48px;
         font-weight: $gl-font-weight-bold;
+        line-height: 1;
 
-        .billing-conditions {
+        .conditions {
           list-style: none;
-          font-size: 20px;
+          font-size: $gl-font-size-large;
           font-weight: $gl-font-weight-bold;
-          margin: auto 0;
-          line-height: 20px;
-          padding: 0;
+          line-height: $gl-line-height;
         }
       }
 
       .price-per-year {
         color: $blue-500;
-        text-align: center;
-        font-size: 12px;
+        font-size: $gl-font-size-small;
         font-weight: $gl-font-weight-bold;
-        padding-bottom: 16px;
-        height: 32px;
-      }
-
-      .feature-list {
-        display: flex;
-        flex-direction: column;
-        flex-grow: 1;
-        text-align: center;
-        margin: 0;
-
-        li {
-          background-color: $gray-light;
-
-          &:first-child {
-            border-top: 1px solid $list-border;
-          }
-
-          &:last-child {
-            border-bottom: 1px solid $list-border;
-            flex-grow: 1;
-            display: flex;
-            flex-direction: column;
-            justify-content: flex-end;
-          }
-
-          &:last-child:hover {
-            background-color: $gray-light;
-          }
-        }
-      }
-
-      .plan-action {
-        padding: 16px;
-
-        .btn {
-          width: 100%;
-        }
-      }
-    }
-
-    &.current {
-      .card-body {
-        border-color: $blue-600;
-        border-width: 2px;
-
-        .price-per-month,
-        .price-per-year {
-          color: $blue-600;
-        }
       }
     }
   }
diff --git a/ee/app/controllers/analytics/application_controller.rb b/ee/app/controllers/analytics/application_controller.rb
index 6c266ee5e9eebc45494650ae26367bb39059a77d..78db8312c5b07f8af6afc47f6c3fcfded1913a21 100644
--- a/ee/app/controllers/analytics/application_controller.rb
+++ b/ee/app/controllers/analytics/application_controller.rb
@@ -8,7 +8,7 @@ class Analytics::ApplicationController < ApplicationController
   private
 
   def self.check_feature_flag(flag, *args)
-    before_action(*args) { render_404 unless Feature.enabled?(flag) }
+    before_action(*args) { render_404 unless Feature.enabled?(flag, default_enabled: Gitlab::Analytics.feature_enabled_by_default?(flag)) }
   end
 
   def self.increment_usage_counter(counter_klass, counter, *args)
@@ -21,8 +21,7 @@ def authorize_view_by_action!(action)
 
   def check_feature_availability!(feature)
     return render_403 unless ::License.feature_available?(feature)
-    return unless @group
-    return render_403 unless @group.root_ancestor.feature_available?(feature)
+    return render_403 unless @group && @group.root_ancestor.feature_available?(feature)
   end
 
   def load_group
diff --git a/ee/app/controllers/analytics/cycle_analytics/stages_controller.rb b/ee/app/controllers/analytics/cycle_analytics/stages_controller.rb
index b5f2dafe7bd1f51ec8b3712de3f04488d6646fa4..706f841f964154a50e87924d4b1a8c0feb459385 100644
--- a/ee/app/controllers/analytics/cycle_analytics/stages_controller.rb
+++ b/ee/app/controllers/analytics/cycle_analytics/stages_controller.rb
@@ -6,10 +6,11 @@ class StagesController < Analytics::ApplicationController
       check_feature_flag Gitlab::Analytics::CYCLE_ANALYTICS_FEATURE_FLAG
 
       before_action :load_group
-      before_action :authorize_access!
 
       def index
-        result = stage_list_service.execute
+        return render_403 unless can?(current_user, :read_group_cycle_analytics, @group)
+
+        result = list_service.execute
 
         if result.success?
           render json: cycle_analytics_configuration(result.payload[:stages])
@@ -18,23 +19,55 @@ def index
         end
       end
 
-      private
+      def create
+        return render_403 unless can?(current_user, :create_group_stage, @group)
+
+        render_stage_service_result(create_service.execute)
+      end
+
+      def update
+        return render_403 unless can?(current_user, :update_group_stage, @group)
 
-      def authorize_access!
-        render_403 unless can?(current_user, :read_group_cycle_analytics, @group)
+        render_stage_service_result(update_service.execute)
       end
 
+      def destroy
+        return render_403 unless can?(current_user, :delete_group_stage, @group)
+
+        render_stage_service_result(delete_service.execute)
+      end
+
+      private
+
       def cycle_analytics_configuration(stages)
         stage_presenters = stages.map { |s| StagePresenter.new(s) }
 
         Analytics::CycleAnalytics::ConfigurationEntity.new(stages: stage_presenters)
       end
 
-      def stage_list_service
-        Analytics::CycleAnalytics::Stages::ListService.new(
-          parent: @group,
-          current_user: current_user
-        )
+      def list_service
+        Stages::ListService.new(parent: @group, current_user: current_user)
+      end
+
+      def create_service
+        Stages::CreateService.new(parent: @group, current_user: current_user, params: params.permit(:name, :start_event_identifier, :end_event_identifier))
+      end
+
+      def update_service
+        Stages::UpdateService.new(parent: @group, current_user: current_user, params: params.permit(:name, :start_event_identifier, :end_event_identifier, :id))
+      end
+
+      def delete_service
+        Stages::DeleteService.new(parent: @group, current_user: current_user, params: params.permit(:id))
+      end
+
+      def render_stage_service_result(result)
+        if result.success?
+          stage = StagePresenter.new(result.payload[:stage])
+          render json: Analytics::CycleAnalytics::StageEntity.new(stage), status: result.http_status
+        else
+          render json: { message: result.message, errors: result.payload[:errors] }, status: result.http_status
+        end
       end
     end
   end
diff --git a/ee/app/controllers/analytics/productivity_analytics_controller.rb b/ee/app/controllers/analytics/productivity_analytics_controller.rb
index 9f5b8df90ebeefa506f11e669994069e7b8dfa9d..1b09a7a5bfcd73f3116b5af683a439ceebe20c93 100644
--- a/ee/app/controllers/analytics/productivity_analytics_controller.rb
+++ b/ee/app/controllers/analytics/productivity_analytics_controller.rb
@@ -9,7 +9,8 @@ class Analytics::ProductivityAnalyticsController < Analytics::ApplicationControl
   before_action :load_project
   before_action -> {
     check_feature_availability!(:productivity_analytics)
-  }
+  }, if: -> { request.format.json? }
+
   before_action -> {
     authorize_view_by_action!(:view_productivity_analytics)
   }
diff --git a/ee/app/controllers/ee/admin/dashboard_controller.rb b/ee/app/controllers/ee/admin/dashboard_controller.rb
index 6f917e5ef12f9226946a2ffcf6705d4e6ddd914c..5ac223f80743c07d29f51e50569b693c94d2bcd9 100644
--- a/ee/app/controllers/ee/admin/dashboard_controller.rb
+++ b/ee/app/controllers/ee/admin/dashboard_controller.rb
@@ -7,6 +7,8 @@ module DashboardController
       extend ActiveSupport::Concern
       extend ::Gitlab::Utils::Override
 
+      LICENSE_BREAKDOWN_USER_LIMIT = 100_000
+
       override :index
       def index
         super
@@ -18,6 +20,16 @@ def stats
         @admin_count = ::User.admins.count
         @roles_count = ::ProjectAuthorization.roles_stats
       end
+
+      # The license section may time out if the number of users is
+      # high. To avoid 500 errors, just hide this section. This is a
+      # workaround for https://gitlab.com/gitlab-org/gitlab/issues/32287.
+      override :show_license_breakdown?
+      def show_license_breakdown?
+        return false unless @counts.is_a?(Hash)
+
+        @counts.fetch(::User, 0) < LICENSE_BREAKDOWN_USER_LIMIT
+      end
     end
   end
 end
diff --git a/ee/app/controllers/ee/projects/environments_controller.rb b/ee/app/controllers/ee/projects/environments_controller.rb
index d74bd444e8e292e860e815ad0c02e9caed364c4d..ca851e3d299eba08d18eda7e6eeec449c0ee2ab6 100644
--- a/ee/app/controllers/ee/projects/environments_controller.rb
+++ b/ee/app/controllers/ee/projects/environments_controller.rb
@@ -20,7 +20,7 @@ def logs
 
             result = PodLogsService.new(environment, params: params.permit!).execute
 
-            if result.nil?
+            if result[:status] == :processing
               head :accepted
             elsif result[:status] == :success
               render json: {
diff --git a/ee/app/controllers/ee/projects/pipelines_controller.rb b/ee/app/controllers/ee/projects/pipelines_controller.rb
index 926a24c381d463c952a235d1896717f454c507ce..e71074cb790fe4257d4882a4e26586979fdf5d2a 100644
--- a/ee/app/controllers/ee/projects/pipelines_controller.rb
+++ b/ee/app/controllers/ee/projects/pipelines_controller.rb
@@ -41,11 +41,6 @@ def licenses
           end
         end
       end
-
-      override :show_represent_params
-      def show_represent_params
-        super.merge(expanded: params[:expanded].to_a.map(&:to_i))
-      end
     end
   end
 end
diff --git a/ee/app/controllers/ee/projects/settings/operations_controller.rb b/ee/app/controllers/ee/projects/settings/operations_controller.rb
index 8b32017d7443eecde4624eb11810968402970d60..5813085e563fb5f93b7b3148b696384fb5f550a8 100644
--- a/ee/app/controllers/ee/projects/settings/operations_controller.rb
+++ b/ee/app/controllers/ee/projects/settings/operations_controller.rb
@@ -64,7 +64,7 @@ def permitted_project_params
           end
 
           if incident_management_available?
-            permitted_params[:incident_management_setting_attributes] = [:create_issue, :send_email, :issue_template_key]
+            permitted_params[:incident_management_setting_attributes] = ::Gitlab::Tracking::IncidentManagement.tracking_keys.keys
           end
 
           permitted_params
@@ -82,6 +82,17 @@ def render_update_response(result)
             end
           end
         end
+
+        override :track_events
+        def track_events(result)
+          super
+
+          if result[:status] == :success
+            ::Gitlab::Tracking::IncidentManagement.track_from_params(
+              update_params[:incident_management_setting_attributes]
+            )
+          end
+        end
       end
     end
   end
diff --git a/ee/app/controllers/ee/registrations_controller.rb b/ee/app/controllers/ee/registrations_controller.rb
index 9f723ba1bbd7300591e79c0cdf6b7be014b83115..1ffd97be4fe652a574deea32aad36251a2a1e830 100644
--- a/ee/app/controllers/ee/registrations_controller.rb
+++ b/ee/app/controllers/ee/registrations_controller.rb
@@ -15,7 +15,7 @@ def user_created_message(confirmed: false)
     end
 
     def sign_up_params
-      clean_params = params.require(:user).permit(:username, :email, :email_confirmation, :name, :password, :email_opted_in)
+      clean_params = super.merge(params.require(:user).permit(:email_opted_in))
 
       if clean_params[:email_opted_in] == '1'
         clean_params[:email_opted_in_ip] = request.remote_ip
diff --git a/ee/app/controllers/groups/epics_controller.rb b/ee/app/controllers/groups/epics_controller.rb
index a877816cb99616a2829a3a7a7f502d509564de04..94b645ae63579e73638032f6950acfa4519c4785 100644
--- a/ee/app/controllers/groups/epics_controller.rb
+++ b/ee/app/controllers/groups/epics_controller.rb
@@ -18,6 +18,7 @@ class Groups::EpicsController < Groups::ApplicationController
   before_action do
     push_frontend_feature_flag(:epic_trees, @group)
     push_frontend_feature_flag(:roadmap_graphql, @group)
+    push_frontend_feature_flag(:vue_issuable_epic_sidebar, @group)
   end
 
   def index
diff --git a/ee/app/controllers/operations_controller.rb b/ee/app/controllers/operations_controller.rb
index 03ce3a2158a5c1cf008b6a3fea9c4e38365c890e..96d819db1861aa5113567d1fe11c2e65d0d81a56 100644
--- a/ee/app/controllers/operations_controller.rb
+++ b/ee/app/controllers/operations_controller.rb
@@ -2,8 +2,7 @@
 
 class OperationsController < ApplicationController
   before_action :authorize_read_operations_dashboard!
-
-  before_action :dashboard_feature_flag, only: [:environments]
+  before_action :environments_dashboard_feature_flag, only: %i[environments environments_list]
 
   respond_to :json, only: [:list]
 
@@ -15,10 +14,6 @@ def index
   def environments
   end
 
-  def dashboard_feature_flag
-    push_frontend_feature_flag(:environments_dashboard)
-  end
-
   def list
     Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL)
     projects = load_projects(current_user)
@@ -61,6 +56,10 @@ def authorize_read_operations_dashboard!
     render_404 unless can?(current_user, :read_operations_dashboard)
   end
 
+  def environments_dashboard_feature_flag
+    render_404 unless Feature.enabled?(:environments_dashboard, current_user)
+  end
+
   def load_projects(current_user)
     Dashboard::Operations::ListService.new(current_user).execute
   end
diff --git a/ee/app/controllers/projects/alerting/notifications_controller.rb b/ee/app/controllers/projects/alerting/notifications_controller.rb
index 91da1bd4c6bcb55cd10764d2ce18b0022bdb6b9b..1fe318634699defe74ab564c82c29469ed8bc129 100644
--- a/ee/app/controllers/projects/alerting/notifications_controller.rb
+++ b/ee/app/controllers/projects/alerting/notifications_controller.rb
@@ -9,7 +9,6 @@ class NotificationsController < Projects::ApplicationController
       skip_before_action :project
 
       prepend_before_action :repository, :project_without_auth
-      before_action :check_generic_alert_endpoint_feature_flag!
 
       def create
         token = extract_alert_manager_token(request)
@@ -25,10 +24,6 @@ def project_without_auth
           .find_by_full_path("#{params[:namespace_id]}/#{params[:project_id]}")
       end
 
-      def check_generic_alert_endpoint_feature_flag!
-        render_404 unless Feature.enabled?(:generic_alert_endpoint, @project)
-      end
-
       def extract_alert_manager_token(request)
         Doorkeeper::OAuth::Token.from_bearer_authorization(request)
       end
diff --git a/ee/app/finders/boards/milestones_finder.rb b/ee/app/finders/boards/milestones_finder.rb
index 6825b2b1e2ebf89de9571772e5e77b35783c8f4e..7946c90325dd507ec9857ea7103167472fe9b394 100644
--- a/ee/app/finders/boards/milestones_finder.rb
+++ b/ee/app/finders/boards/milestones_finder.rb
@@ -15,7 +15,7 @@ def execute
 
     # rubocop: disable CodeReuse/Finder
     def finder_service
-      parent = @board.parent
+      parent = @board.resource_parent
 
       finder_params =
         if parent.is_a?(Group)
diff --git a/ee/app/finders/boards/users_finder.rb b/ee/app/finders/boards/users_finder.rb
index 3611197309cf1b09aa77bf21410085fa7793aa6f..d8d40d4cb8e792a4a3ca8867cf601cdbd2aed490 100644
--- a/ee/app/finders/boards/users_finder.rb
+++ b/ee/app/finders/boards/users_finder.rb
@@ -16,10 +16,10 @@ def execute
     # rubocop: disable CodeReuse/Finder
     def finder_service
       @finder_service ||=
-        if @board.parent.is_a?(Group)
-          GroupMembersFinder.new(@board.parent)
+        if @board.resource_parent.is_a?(Group)
+          GroupMembersFinder.new(@board.resource_parent)
         else
-          MembersFinder.new(@board.parent, @current_user)
+          MembersFinder.new(@board.resource_parent, @current_user)
         end
     end
     # rubocop: enable CodeReuse/Finder
diff --git a/ee/app/finders/ee/snippets_finder.rb b/ee/app/finders/ee/snippets_finder.rb
index ea1d89c1dbee9c0e47b7be68297fd8bb5fe81d20..eb1da28038b43bba573a1bb0eafa56e432933349 100644
--- a/ee/app/finders/ee/snippets_finder.rb
+++ b/ee/app/finders/ee/snippets_finder.rb
@@ -27,7 +27,7 @@ def init_collection
     #
     # When current_user is nil it returns only public personal snippets
     def snippets_of_authorized_projects_or_personal
-      queries = [restricted_global_snippets]
+      queries = [restricted_personal_snippets]
 
       if current_user && Ability.allowed?(current_user, :read_cross_project)
         queries << snippets_of_authorized_projects
@@ -36,14 +36,14 @@ def snippets_of_authorized_projects_or_personal
       find_union(queries, ::Snippet)
     end
 
-    def restricted_global_snippets
+    def restricted_personal_snippets
       if author
         snippets_for_author
       elsif current_user
         current_user.snippets
       else
         ::Snippet.public_to_user
-      end.only_global_snippets
+      end.only_personal_snippets
     end
   end
 end
diff --git a/ee/app/finders/feature_flags_finder.rb b/ee/app/finders/feature_flags_finder.rb
index 60a2ffac6ded465f8e5fd15bea87908014614af4..5c1945ae6ac58063c2e235ed718f0c49844345d9 100644
--- a/ee/app/finders/feature_flags_finder.rb
+++ b/ee/app/finders/feature_flags_finder.rb
@@ -10,7 +10,7 @@ def initialize(project, current_user, params = {})
     @params = params
   end
 
-  def execute
+  def execute(preload: true)
     unless Ability.allowed?(current_user, :read_feature_flag, project)
       return Operations::FeatureFlag.none
     end
@@ -19,6 +19,7 @@ def execute
     items = by_scope(items)
     items = for_list(items)
 
+    items = items.preload_relations if preload
     items.ordered
   end
 
diff --git a/ee/app/finders/security/pipeline_vulnerabilities_finder.rb b/ee/app/finders/security/pipeline_vulnerabilities_finder.rb
index 01680e11b1d73bec0dd1a78deffc4b80a2a03b9b..5f111f6681320a34679666b8f6b4fad4e4a91d8a 100644
--- a/ee/app/finders/security/pipeline_vulnerabilities_finder.rb
+++ b/ee/app/finders/security/pipeline_vulnerabilities_finder.rb
@@ -24,7 +24,11 @@ def initialize(pipeline:, params: {})
     end
 
     def execute
-      pipeline_reports&.each_with_object([]) do |(type, report), occurrences|
+      reports = pipeline_reports
+
+      return [] if reports.nil?
+
+      occurrences = reports.each_with_object([]) do |(type, report), occurrences|
         next unless requested_type?(type)
 
         raise ParseError, 'JSON parsing failed' if report.error.is_a?(Gitlab::Ci::Parsers::Security::Common::SecurityReportParserError)
@@ -34,6 +38,8 @@ def execute
 
         occurrences.concat(filtered_occurrences)
       end
+
+      occurrences.sort_by { |x| [x.severity, x.confidence] }
     end
 
     private
diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb
index 0db23a86b5d5c4b9cf642cd640f454839ee1b560..25e4be3cb0f5b33a1847d452d5614048ccbacfe4 100644
--- a/ee/app/graphql/ee/types/mutation_type.rb
+++ b/ee/app/graphql/ee/types/mutation_type.rb
@@ -9,6 +9,7 @@ module MutationType
         mount_mutation ::Mutations::DesignManagement::Upload, calls_gitaly: true
         mount_mutation ::Mutations::DesignManagement::Delete, calls_gitaly: true
         mount_mutation ::Mutations::EpicTree::Reorder
+        mount_mutation ::Mutations::Epics::Update
       end
     end
   end
diff --git a/ee/app/graphql/mutations/epic_tree/reorder.rb b/ee/app/graphql/mutations/epic_tree/reorder.rb
index 9252f989256529902820e056b2b8784231590a80..7867d714ce17877c1c072423651f790dbfbfdfa1 100644
--- a/ee/app/graphql/mutations/epic_tree/reorder.rb
+++ b/ee/app/graphql/mutations/epic_tree/reorder.rb
@@ -21,7 +21,7 @@ def resolve(args)
         params = args[:moved]
         moving_params = params.to_hash.slice(:adjacent_reference_id, :relative_position).merge(base_epic_id: args[:base_epic_id])
 
-        result = Epics::TreeReorderService.new(current_user, params[:id], moving_params).execute
+        result = ::Epics::TreeReorderService.new(current_user, params[:id], moving_params).execute
         errors = result[:status] == :error ? [result[:message]] : []
 
         { errors: errors }
diff --git a/ee/app/graphql/mutations/epics/update.rb b/ee/app/graphql/mutations/epics/update.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1c441fb79221f7106351158e4c1999df62bc6ecf
--- /dev/null
+++ b/ee/app/graphql/mutations/epics/update.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+module Mutations
+  module Epics
+    class Update < BaseMutation
+      include Mutations::ResolvesGroup
+
+      graphql_name 'UpdateEpic'
+
+      argument :group_path, GraphQL::ID_TYPE,
+               required: true,
+               description: "The group the epic to mutate is in"
+
+      argument :iid, GraphQL::STRING_TYPE,
+               required: true,
+               description: "The iid of the epic to mutate"
+
+      argument :title,
+                GraphQL::STRING_TYPE,
+                required: false,
+                description: 'The title of the epic'
+
+      argument :description,
+                GraphQL::STRING_TYPE,
+                required: false,
+                description: 'The description of the epic'
+
+      argument :start_date_fixed,
+                GraphQL::STRING_TYPE,
+                required: false,
+                description: 'The start date of the epic'
+
+      argument :due_date_fixed,
+                GraphQL::STRING_TYPE,
+                required: false,
+                description: 'The end date of the epic'
+
+      argument :start_date_is_fixed,
+                GraphQL::BOOLEAN_TYPE,
+                required: false,
+                description: 'Indicates start date should be sourced from start_date_fixed field not the issue milestones'
+
+      argument :due_date_is_fixed,
+                GraphQL::BOOLEAN_TYPE,
+                required: false,
+                description: 'Indicates end date should be sourced from due_date_fixed field not the issue milestones'
+
+      argument :state_event,
+                Types::EpicStateEventEnum,
+                required: false,
+                description: 'State event for the epic'
+
+      field :epic,
+            Types::EpicType,
+            null: true,
+            description: 'The epic after mutation'
+
+      authorize :admin_epic
+
+      def resolve(args)
+        group_path = args.delete(:group_path)
+        epic_iid = args.delete(:iid)
+
+        if args.empty?
+          raise Gitlab::Graphql::Errors::ArgumentError,
+            "The list of attributes to update is empty"
+        end
+
+        epic = authorized_find!(group_path: group_path, iid: epic_iid)
+        epic = ::Epics::UpdateService.new(epic.group, current_user, args).execute(epic)
+
+        {
+          epic: epic.reset,
+          errors: errors_on_object(epic)
+        }
+      end
+
+      private
+
+      def find_object(group_path:, iid:)
+        group = resolve_group(full_path: group_path)
+        resolver = Resolvers::EpicResolver
+          .single.new(object: group, context: context)
+
+        resolver.resolve(iid: iid)
+      end
+    end
+  end
+end
diff --git a/ee/app/graphql/types/epic_state_event_enum.rb b/ee/app/graphql/types/epic_state_event_enum.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a32bd105cd6a7acce49e165117df2bdbd0a81d28
--- /dev/null
+++ b/ee/app/graphql/types/epic_state_event_enum.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+  class EpicStateEventEnum < BaseEnum
+    graphql_name 'EpicStateEvent'
+    description 'State event of a GitLab Epic'
+
+    value 'REOPEN', value: 'reopen', description: 'Reopen the Epic'
+    value 'CLOSE', value: 'close', description: 'Close the Epic'
+  end
+end
diff --git a/ee/app/graphql/types/epic_type.rb b/ee/app/graphql/types/epic_type.rb
index 5635e3aa893e6bd7032782d7ec91ac8743bae558..d14a267da5c8d006ca0d7b26f03b005ceaaf2355 100644
--- a/ee/app/graphql/types/epic_type.rb
+++ b/ee/app/graphql/types/epic_type.rb
@@ -57,6 +57,13 @@ class EpicType < BaseObject
     field :reference, GraphQL::STRING_TYPE, null: false, method: :epic_reference do # rubocop:disable Graphql/Descriptions
       argument :full, GraphQL::BOOLEAN_TYPE, required: false, default_value: false # rubocop:disable Graphql/Descriptions
     end
+    field :participants, Types::UserType.connection_type, null: true, complexity: 5, description: 'List of participants for the epic'
+
+    field :subscribed, GraphQL::BOOLEAN_TYPE,
+      method: :subscribed?,
+      null: false,
+      complexity: 5,
+      description: 'Boolean flag for whether the currently logged in user is subscribed to this epic'
 
     field :issues, # rubocop:disable Graphql/Descriptions
           Types::EpicIssueType.connection_type,
diff --git a/ee/app/helpers/billing_plans_helper.rb b/ee/app/helpers/billing_plans_helper.rb
index e8fb42bf9a845970f5a74bfb26b7e10bced51a32..d917df3e91c50d342d7ac109e400f592d7036ad5 100644
--- a/ee/app/helpers/billing_plans_helper.rb
+++ b/ee/app/helpers/billing_plans_helper.rb
@@ -15,7 +15,7 @@ def current_plan?(plan)
 
   def plan_purchase_link(href, link_text)
     if href
-      link_to link_text, href, class: 'btn btn-primary btn-inverted'
+      link_to link_text, href, class: 'btn btn-success'
     else
       button_tag link_text, class: 'btn disabled'
     end
@@ -42,10 +42,32 @@ def plan_upgrade_url(group, plan)
     "#{EE::SUBSCRIPTIONS_URL}/gitlab/namespaces/#{group.id}/upgrade/#{plan.id}"
   end
 
+  def plan_purchase_url(group, plan)
+    "#{plan.purchase_link.href}&gl_namespace_id=#{group.id}"
+  end
+
+  def plan_feature_short_list(plan)
+    return [] unless plan.features
+
+    plan.features.sort_by! { |feature| feature.highlight ? 0 : 1 }[0...4]
+  end
+
+  def plan_purchase_or_upgrade_url(group, plan, current_plan)
+    if group.upgradable?
+      plan_upgrade_url(group, current_plan)
+    else
+      plan_purchase_url(group, plan)
+    end
+  end
+
   def show_trial_banner?(namespace)
     return false unless params[:trial]
 
     root = namespace.has_parent? ? namespace.root_ancestor : namespace
     root.trial_active?
   end
+
+  def namespace_for_user?(namespace)
+    namespace == current_user.namespace
+  end
 end
diff --git a/ee/app/helpers/ee/dashboard_helper.rb b/ee/app/helpers/ee/dashboard_helper.rb
index 3fa2970abe54bcb57b3896d988cb45a595c28059..0c5cc0218398d0b4143ea0ea2244c9f98679ca9d 100644
--- a/ee/app/helpers/ee/dashboard_helper.rb
+++ b/ee/app/helpers/ee/dashboard_helper.rb
@@ -39,7 +39,7 @@ def get_dashboard_nav_links
         links << :analytics if ::Gitlab::Analytics.any_features_enabled?
 
         if can?(current_user, :read_operations_dashboard)
-          links << :environments if ::Feature.enabled?(:environments_dashboard)
+          links << :environments if ::Feature.enabled?(:environments_dashboard, current_user)
           links << :operations
         end
 
diff --git a/ee/app/helpers/ee/groups_helper.rb b/ee/app/helpers/ee/groups_helper.rb
index 3da812855aeaf4f22c9126566eb783ac9933ad5e..49ac7bf7359a10fdcba950cde73fe57458db0681 100644
--- a/ee/app/helpers/ee/groups_helper.rb
+++ b/ee/app/helpers/ee/groups_helper.rb
@@ -33,16 +33,19 @@ def size_limit_message_for_group(group)
       "Repositories within this group #{show_lfs} will be restricted to this maximum size. Can be overridden inside each project. 0 for unlimited. Leave empty to inherit the global value."
     end
 
+    override :group_packages_nav_link_paths
     def group_packages_nav_link_paths
       %w[
         groups/packages#index
         groups/dependency_proxies#show
+        groups/container_registries#index
       ]
     end
 
     def group_packages_nav?
       group_packages_list_nav? ||
-        group_dependency_proxy_nav?
+        group_dependency_proxy_nav? ||
+        group_container_registry_nav?
     end
 
     def group_packages_list_nav?
diff --git a/ee/app/helpers/ee/milestones_helper.rb b/ee/app/helpers/ee/milestones_helper.rb
index 86be7700c211910f817520799d675dacbea4070e..335098a700ec12e9e61d136cfe192d97093966f1 100644
--- a/ee/app/helpers/ee/milestones_helper.rb
+++ b/ee/app/helpers/ee/milestones_helper.rb
@@ -20,7 +20,7 @@ def show_burndown_placeholder?(milestone, warning)
       return false if cookies['hide_burndown_message'].present?
       return false unless milestone.supports_burndown_charts?
 
-      warning.nil? && can?(current_user, :admin_milestone, milestone.parent)
+      warning.nil? && can?(current_user, :admin_milestone, milestone.resource_parent)
     end
 
     def data_warning_for(burndown)
diff --git a/ee/app/helpers/ee/system_note_helper.rb b/ee/app/helpers/ee/system_note_helper.rb
index 88f9c16f8c239108fd55d5b1df0bb0d70b46aa24..1525ef86441893035d320fabe8bed982982ef05b 100644
--- a/ee/app/helpers/ee/system_note_helper.rb
+++ b/ee/app/helpers/ee/system_note_helper.rb
@@ -19,9 +19,9 @@ module SystemNoteHelper
       'weight' => 'weight',
       'relate_epic' => 'epic',
       'unrelate_epic' => 'epic',
-      'design_added' => 'doc-image',
-      'design_modified' => 'doc-image',
-      'design_removed' => 'doc-image'
+      'designs_added' => 'doc-image',
+      'designs_modified' => 'doc-image',
+      'designs_removed' => 'doc-image'
     }.freeze
 
     override :system_note_icon_name
diff --git a/ee/app/helpers/ee/todos_helper.rb b/ee/app/helpers/ee/todos_helper.rb
index dbe38c7c05062b92ca4542c7c169041d6d1ce76a..0c994b377080d400391c37ba793ef91a08b8649e 100644
--- a/ee/app/helpers/ee/todos_helper.rb
+++ b/ee/app/helpers/ee/todos_helper.rb
@@ -32,7 +32,7 @@ def todos_design_path(todo)
       )
 
       designs_project_issue_path(
-        todo.parent,
+        todo.resource_parent,
         design.issue,
         path_options
       )
diff --git a/ee/app/mailers/ee/emails/notes.rb b/ee/app/mailers/ee/emails/notes.rb
index 8c5b3413d3fd6d5a816db37a61f1401d038c0ceb..ee53f35628c4ca8a10e4b89560b052152933ca92 100644
--- a/ee/app/mailers/ee/emails/notes.rb
+++ b/ee/app/mailers/ee/emails/notes.rb
@@ -16,7 +16,7 @@ def note_design_email(recipient_id, note_id, reason = nil)
 
         design = @note.noteable
         @target_url = ::Gitlab::Routing.url_helpers.designs_project_issue_url(
-          @note.parent,
+          @note.resource_parent,
           design.issue,
           note_target_url_query_params.merge(vueroute: design.filename)
         )
diff --git a/ee/app/models/approval_merge_request_rule.rb b/ee/app/models/approval_merge_request_rule.rb
index 4c15aed7badd7006f8ee1ac131e6a7d40e916c5b..08727c882fe58dcd48125b2c674db22b4c6a91a0 100644
--- a/ee/app/models/approval_merge_request_rule.rb
+++ b/ee/app/models/approval_merge_request_rule.rb
@@ -14,7 +14,7 @@ class ApprovalMergeRequestRule < ApplicationRecord
     )
   end
   scope :for_unmerged_merge_requests, -> (merge_requests = nil) do
-    query = joins(:merge_request).where.not(merge_requests: { state: 'merged' })
+    query = joins(:merge_request).where.not(merge_requests: { state_id: MergeRequest.available_states[:merged] })
 
     if merge_requests
       query.where(merge_request_id: merge_requests)
diff --git a/ee/app/models/concerns/alert_event_lifecycle.rb b/ee/app/models/concerns/alert_event_lifecycle.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4d2b717ead28ec8b1f7b1f5a74a218de6f0dc72d
--- /dev/null
+++ b/ee/app/models/concerns/alert_event_lifecycle.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module AlertEventLifecycle
+  extend ActiveSupport::Concern
+
+  included do
+    validates :started_at, presence: true
+    validates :status, presence: true
+
+    state_machine :status, initial: :none do
+      state :none, value: nil
+
+      state :firing, value: 0 do
+        validates :payload_key, presence: true
+        validates :ended_at, absence: true
+      end
+
+      state :resolved, value: 1 do
+        validates :ended_at, presence: true
+      end
+
+      event :fire do
+        transition none: :firing
+      end
+
+      event :resolve do
+        transition firing: :resolved
+      end
+
+      before_transition to: :firing do |alert_event, transition|
+        started_at = transition.args.first
+        alert_event.started_at = started_at
+      end
+
+      before_transition to: :resolved do |alert_event, transition|
+        ended_at = transition.args.first
+        alert_event.ended_at = ended_at || Time.current
+      end
+    end
+
+    scope :firing, -> { where(status: status_value_for(:firing)) }
+    scope :resolved, -> { where(status: status_value_for(:resolved)) }
+
+    scope :count_by_project_id, -> { group(:project_id).count }
+
+    def self.status_value_for(name)
+      state_machines[:status].states[name].value
+    end
+  end
+end
diff --git a/ee/app/models/concerns/ee/protected_ref.rb b/ee/app/models/concerns/ee/protected_ref.rb
index ef5a8af01a31ee04ef15b03e5a84979800a390a5..457039e7de3a709c98da633b3192e4d99eae7e25 100644
--- a/ee/app/models/concerns/ee/protected_ref.rb
+++ b/ee/app/models/concerns/ee/protected_ref.rb
@@ -22,7 +22,7 @@ def protected_ref_access_levels(*types)
         validates :"#{type}_access_levels", length: { is: 1 }, if: -> { false }
 
         # Returns access levels that grant the specified access type to the given user / group.
-        access_level_class = const_get("#{type}_access_level".classify)
+        access_level_class = const_get("#{type}_access_level".classify, false)
         protected_type = self.model_name.singular
         scope(
           :"#{type}_access_by_user",
diff --git a/ee/app/models/concerns/elastic/application_versioned_search.rb b/ee/app/models/concerns/elastic/application_versioned_search.rb
index 06e9095a8731ffd746765177f8fdd464ee205aac..fb7c84408491e70d24f36d85f67aba50b12769ac 100644
--- a/ee/app/models/concerns/elastic/application_versioned_search.rb
+++ b/ee/app/models/concerns/elastic/application_versioned_search.rb
@@ -4,7 +4,7 @@ module ApplicationVersionedSearch
     extend ActiveSupport::Concern
 
     FORWARDABLE_INSTANCE_METHODS = [:es_id, :es_parent].freeze
-    FORWARDABLE_CLASS_METHODS = [:elastic_search, :es_import, :nested?, :es_type, :index_name, :document_type, :mapping, :mappings, :settings, :import].freeze
+    FORWARDABLE_CLASS_METHODS = [:elastic_search, :es_import, :es_type, :index_name, :document_type, :mapping, :mappings, :settings, :import].freeze
 
     def __elasticsearch__(&block)
       @__elasticsearch__ ||= ::Elastic::MultiVersionInstanceProxy.new(self)
diff --git a/ee/app/models/ee/board.rb b/ee/app/models/ee/board.rb
index 5ab05696d435449bd5f3f37539d6b6cf87520456..bd11f8d4ab435b4b347414a130ed396f3a6bc94b 100644
--- a/ee/app/models/ee/board.rb
+++ b/ee/app/models/ee/board.rb
@@ -28,7 +28,7 @@ module Board
 
     override :scoped?
     def scoped?
-      return super unless parent.feature_available?(:scoped_issue_board)
+      return super unless resource_parent.feature_available?(:scoped_issue_board)
 
       EMPTY_SCOPE_STATE.exclude?(milestone_id) ||
         EMPTY_SCOPE_STATE.exclude?(weight) ||
@@ -37,7 +37,7 @@ def scoped?
     end
 
     def milestone
-      return unless parent&.feature_available?(:scoped_issue_board)
+      return unless resource_parent&.feature_available?(:scoped_issue_board)
 
       case milestone_id
       when ::Milestone::Upcoming.id
diff --git a/ee/app/models/ee/ci/build.rb b/ee/app/models/ee/ci/build.rb
index a94a1cf689b79756c1882773051a062807ab75b6..f6da6402249dbc9175605138e261c18a87cba012 100644
--- a/ee/app/models/ee/ci/build.rb
+++ b/ee/app/models/ee/ci/build.rb
@@ -21,10 +21,6 @@ module Build
 
         after_save :stick_build_if_status_changed
         delegate :service_specification, to: :runner_session, allow_nil: true
-
-        has_many :sourced_pipelines,
-          class_name: "::Ci::Sources::Pipeline",
-          foreign_key: :source_job_id
       end
 
       def shared_runners_minutes_limit_enabled?
diff --git a/ee/app/models/ee/ci/pipeline.rb b/ee/app/models/ee/ci/pipeline.rb
index 034447e911af356f0d08eba9160d70b879b9b62e..dd23c4f7b9e1a5a45e9560cf5e38ff1a6b7f276b 100644
--- a/ee/app/models/ee/ci/pipeline.rb
+++ b/ee/app/models/ee/ci/pipeline.rb
@@ -19,19 +19,13 @@ module Pipeline
         has_many :vulnerabilities_occurrence_pipelines, class_name: 'Vulnerabilities::OccurrencePipeline'
         has_many :vulnerability_findings, source: :occurrence, through: :vulnerabilities_occurrence_pipelines, class_name: 'Vulnerabilities::Occurrence'
 
-        has_one :source_pipeline, class_name: "::Ci::Sources::Pipeline", inverse_of: :pipeline
-        has_many :sourced_pipelines, class_name: "::Ci::Sources::Pipeline", foreign_key: :source_pipeline_id
-
-        has_one :triggered_by_pipeline, through: :source_pipeline, source: :source_pipeline
-        has_one :source_job, through: :source_pipeline, source: :source_job
-        has_one :source_bridge, through: :source_pipeline, source: :source_bridge
-        has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline
-
         has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id'
         has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
 
         has_many :downstream_bridges, class_name: '::Ci::Bridge', foreign_key: :upstream_pipeline_id
 
+        has_one :source_bridge, through: :source_pipeline, source: :source_bridge
+
         # Legacy way to fetch security reports based on job name. This has been replaced by the reports feature.
         scope :with_legacy_security_reports, -> do
           joins(:artifacts).where(ci_builds: { name: %w[sast dependency_scanning sast:container container_scanning dast] })
diff --git a/ee/app/models/ee/ci/pipeline_enums.rb b/ee/app/models/ee/ci/pipeline_enums.rb
index ceebcd941f5b036b66bccd51168efd4127ba5b15..7f6f33585ac7c0f3ceb3bddf44616ee1ce52e259 100644
--- a/ee/app/models/ee/ci/pipeline_enums.rb
+++ b/ee/app/models/ee/ci/pipeline_enums.rb
@@ -19,7 +19,7 @@ def failure_reasons
 
         override :sources
         def sources
-          super.merge(pipeline: 7, webide: 9)
+          super.merge(webide: 9)
         end
 
         override :config_sources
diff --git a/ee/app/models/ee/ci/sources/pipeline.rb b/ee/app/models/ee/ci/sources/pipeline.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a9b6108b262c6cb5577a7a1750d5352870f58608
--- /dev/null
+++ b/ee/app/models/ee/ci/sources/pipeline.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module EE
+  module Ci
+    module Sources
+      module Pipeline
+        extend ActiveSupport::Concern
+
+        prepended do
+          belongs_to :source_bridge,
+            class_name: "Ci::Bridge",
+            foreign_key: :source_job_id
+        end
+      end
+    end
+  end
+end
diff --git a/ee/app/models/ee/clusters/platforms/kubernetes.rb b/ee/app/models/ee/clusters/platforms/kubernetes.rb
index 440e889cafdda86f0eada0b6279ad7883b861b0a..835f3e8bd59cc4406ca32fb41280730e27d54547 100644
--- a/ee/app/models/ee/clusters/platforms/kubernetes.rb
+++ b/ee/app/models/ee/clusters/platforms/kubernetes.rb
@@ -27,17 +27,13 @@ def rollout_status(environment, data)
         end
 
         def read_pod_logs(pod_name, namespace, container: nil)
-          if ::Feature.enabled?(:pod_logs_reactive_cache)
-            with_reactive_cache(
-              'get_pod_log',
-              'pod_name' => pod_name,
-              'namespace' => namespace,
-              'container' => container
-            ) do |result|
-              result
-            end
-          else
-            pod_logs(pod_name, namespace, container: container)
+          with_reactive_cache(
+            'get_pod_log',
+            'pod_name' => pod_name,
+            'namespace' => namespace,
+            'container' => container
+          ) do |result|
+            result
           end
         end
 
@@ -59,13 +55,11 @@ def calculate_reactive_cache(request, opts)
         private
 
         def pod_logs(pod_name, namespace, container: nil)
-          handle_exceptions(_('Pod not found')) do
-            logs = kubeclient.get_pod_log(
-              pod_name, namespace, container: container, tail_lines: LOGS_LIMIT
-            ).body
+          logs = kubeclient.get_pod_log(
+            pod_name, namespace, container: container, tail_lines: LOGS_LIMIT
+          ).body
 
-            { logs: logs, status: :success }
-          end
+          { logs: logs, status: :success }
         end
 
         def handle_exceptions(resource_not_found_error_message, &block)
diff --git a/ee/app/models/ee/description_version.rb b/ee/app/models/ee/description_version.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3ff5f026107603df0242d3f1199020721c619093
--- /dev/null
+++ b/ee/app/models/ee/description_version.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module EE
+  module DescriptionVersion
+    extend ActiveSupport::Concern
+
+    prepended do
+      belongs_to :epic
+    end
+
+    class_methods do
+      def issuable_attrs
+        (super + %i(epic)).freeze
+      end
+    end
+  end
+end
diff --git a/ee/app/models/ee/environment.rb b/ee/app/models/ee/environment.rb
index 98da2c2cea021f37a04630d322b63ff64499f27b..f8b456036fdd9ed1d90f91cfd841f99f571ee223 100644
--- a/ee/app/models/ee/environment.rb
+++ b/ee/app/models/ee/environment.rb
@@ -25,7 +25,7 @@ module Environment
           .on(join_conditions)
 
         model
-          .joins(:deployments)
+          .joins(:successful_deployments)
           .joins(join.join_sources)
           .where(later_deployments[:id].eq(nil))
           .where(deployments[:cluster_id].eq(cluster.id))
diff --git a/ee/app/models/ee/epic.rb b/ee/app/models/ee/epic.rb
index 50cb0d24ed3439a0d1d33b99e05f8187dd55895e..f61c8b18779c0758e617f5157cf775adea7e2fdf 100644
--- a/ee/app/models/ee/epic.rb
+++ b/ee/app/models/ee/epic.rb
@@ -15,7 +15,11 @@ module Epic
       include RelativePositioning
       include UsageStatistics
 
-      enum state_id: { opened: 1, closed: 2 }
+      enum state_id: {
+        opened: ::Epic.available_states[:opened],
+        closed: ::Epic.available_states[:closed]
+      }
+
       alias_attribute :state, :state_id
 
       belongs_to :closed_by, class_name: 'User'
diff --git a/ee/app/models/ee/issue.rb b/ee/app/models/ee/issue.rb
index 594db486109f2c3b4248c8fb954c7292535efc02..63b6cf39bb96b4c190d3f285bb51e2ce6f965409 100644
--- a/ee/app/models/ee/issue.rb
+++ b/ee/app/models/ee/issue.rb
@@ -27,6 +27,7 @@ def most_recent
         end
       end
 
+      has_and_belongs_to_many :self_managed_prometheus_alert_events, join_table: :issues_self_managed_prometheus_alert_events
       has_and_belongs_to_many :prometheus_alert_events, join_table: :issues_prometheus_alert_events
       has_many :prometheus_alerts, through: :prometheus_alert_events
 
diff --git a/ee/app/models/ee/list.rb b/ee/app/models/ee/list.rb
index 96b8d55f61657b4e4e7ce9b7a9f2715a47d6eb2d..f07b2f8789a4b67ac64ce9d0c53eece038357b1b 100644
--- a/ee/app/models/ee/list.rb
+++ b/ee/app/models/ee/list.rb
@@ -23,10 +23,10 @@ class << base
       base.validates :max_issue_count, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
       base.validates :list_type,
         exclusion: { in: %w[assignee], message: _('Assignee lists not available with your current license') },
-        unless: -> { board&.parent&.feature_available?(:board_assignee_lists) }
+        unless: -> { board&.resource_parent&.feature_available?(:board_assignee_lists) }
       base.validates :list_type,
         exclusion: { in: %w[milestone], message: _('Milestone lists not available with your current license') },
-        unless: -> { board&.parent&.feature_available?(:board_milestone_lists) }
+        unless: -> { board&.resource_parent&.feature_available?(:board_milestone_lists) }
     end
 
     def assignee=(user)
diff --git a/ee/app/models/ee/milestone.rb b/ee/app/models/ee/milestone.rb
index e05d7e3eb7b5637769ff130174c0d9505a429368..3abdf5dc9529995fed895947fcfd887a6e2eefac 100644
--- a/ee/app/models/ee/milestone.rb
+++ b/ee/app/models/ee/milestone.rb
@@ -11,13 +11,13 @@ module Milestone
     end
 
     def supports_weight?
-      parent&.feature_available?(:issue_weights)
+      resource_parent&.feature_available?(:issue_weights)
     end
 
     def supports_burndown_charts?
       feature_name = group_milestone? ? :group_burndown_charts : :burndown_charts
 
-      parent&.feature_available?(feature_name) && supports_weight?
+      resource_parent&.feature_available?(feature_name) && supports_weight?
     end
   end
 end
diff --git a/ee/app/models/ee/note.rb b/ee/app/models/ee/note.rb
index 0cc8d50bf8a53c83935d166565d38e62dac412dd..cef7f727a5488be56dd65ca85d4fbc4d5c37f0c5 100644
--- a/ee/app/models/ee/note.rb
+++ b/ee/app/models/ee/note.rb
@@ -57,11 +57,10 @@ def for_design?
       noteable_type == DesignManagement::Design.name
     end
 
-    override :parent
-    def parent
+    override :resource_parent
+    def resource_parent
       for_epic? ? noteable.group : super
     end
-    alias_method :resource_parent, :parent
 
     def notify_after_create
       noteable&.after_note_created(self)
diff --git a/ee/app/models/ee/project.rb b/ee/app/models/ee/project.rb
index cb80d96935a912f753075ed26e425adff13e0ccf..6109ac622f379c26bcbc24ba6e6305ff07840b8e 100644
--- a/ee/app/models/ee/project.rb
+++ b/ee/app/models/ee/project.rb
@@ -77,14 +77,11 @@ module Project
       has_many :package_files, through: :packages, class_name: 'Packages::PackageFile'
       has_many :merge_trains, foreign_key: 'target_project_id', inverse_of: :target_project
 
-      has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_project_id
-
-      has_many :source_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :project_id
-
       has_many :webide_pipelines, -> { webide_source }, class_name: 'Ci::Pipeline', inverse_of: :project
 
       has_many :prometheus_alerts, inverse_of: :project
       has_many :prometheus_alert_events, inverse_of: :project
+      has_many :self_managed_prometheus_alert_events, inverse_of: :project
 
       has_many :operations_feature_flags, class_name: 'Operations::FeatureFlag'
       has_one :operations_feature_flags_client, class_name: 'Operations::FeatureFlagsClient'
@@ -643,8 +640,7 @@ def design_repository
     end
 
     def alerts_service_available?
-      ::Feature.enabled?(:generic_alert_endpoint, self) &&
-        feature_available?(:incident_management)
+      feature_available?(:incident_management)
     end
 
     def package_already_taken?(package_name)
diff --git a/ee/app/models/ee/todo.rb b/ee/app/models/ee/todo.rb
index 6af53b7d81d5aa203551855812129109d7454dd6..b2be39d6953c96ea2e6b154429881c48d2d839fe 100644
--- a/ee/app/models/ee/todo.rb
+++ b/ee/app/models/ee/todo.rb
@@ -9,11 +9,10 @@ module Todo
       include UsageStatistics
     end
 
-    override :parent
-    def parent
+    override :resource_parent
+    def resource_parent
       project || group
     end
-    alias_method :resource_parent, :parent
 
     def for_design?
       target_type == DesignManagement::Design.name
diff --git a/ee/app/models/ee/user.rb b/ee/app/models/ee/user.rb
index 388d52d6a44e7f4b5acf60e6d2ec936bb74e3f2f..a1452b21ae85925830e4355cc151ae17417d8d30 100644
--- a/ee/app/models/ee/user.rb
+++ b/ee/app/models/ee/user.rb
@@ -82,7 +82,8 @@ module User
 
       enum bot_type: {
         support_bot: 1,
-        alert_bot: 2
+        alert_bot: 2,
+        visual_review_bot: 3
       }
     end
 
@@ -107,6 +108,15 @@ def alert_bot
         end
       end
 
+      def visual_review_bot
+        email_pattern = "visual_review%s@#{Settings.gitlab.host}"
+
+        unique_internal(where(bot_type: :visual_review_bot), 'visual-review-bot', email_pattern) do |u|
+          u.bio = 'The Gitlab Visual Review feedback bot'
+          u.name = 'Gitlab Visual Review Bot'
+        end
+      end
+
       override :internal
       def internal
         super.or(where.not(bot_type: nil))
diff --git a/ee/app/models/prometheus_alert_event.rb b/ee/app/models/prometheus_alert_event.rb
index 29ec478edf1273c2e8eb48df9abc86a1e4092a16..c8afaf45625ba733a3f3c4050942eecded0da5f9 100644
--- a/ee/app/models/prometheus_alert_event.rb
+++ b/ee/app/models/prometheus_alert_event.rb
@@ -1,58 +1,20 @@
 # frozen_string_literal: true
 
 class PrometheusAlertEvent < ApplicationRecord
+  include AlertEventLifecycle
+
   belongs_to :project, required: true, validate: true, inverse_of: :prometheus_alert_events
   belongs_to :prometheus_alert, required: true, validate: true, inverse_of: :prometheus_alert_events
   has_and_belongs_to_many :related_issues, class_name: 'Issue', join_table: :issues_prometheus_alert_events
 
   validates :payload_key, uniqueness: { scope: :prometheus_alert_id }
 
-  validates :started_at, presence: true
-  validates :status, presence: true
-
   delegate :title, :prometheus_metric_id, to: :prometheus_alert
 
-  state_machine :status, initial: :none do
-    state :none, value: nil
-
-    state :firing, value: 0 do
-      validates :payload_key, presence: true
-      validates :ended_at, absence: true
-    end
-
-    state :resolved, value: 1 do
-      validates :payload_key, absence: true
-      validates :ended_at, presence: true
-    end
-
-    event :fire do
-      transition none: :firing
-    end
-
-    event :resolve do
-      transition firing: :resolved
-    end
-
-    before_transition to: :firing do |alert_event, transition|
-      started_at = transition.args.first
-      alert_event.started_at = started_at
-    end
-
-    before_transition to: :resolved do |alert_event, transition|
-      ended_at = transition.args.first
-      alert_event.payload_key = nil
-      alert_event.ended_at = ended_at
-    end
-  end
-
-  scope :firing, -> { where(status: status_value_for(:firing)) }
-  scope :resolved, -> { where(status: status_value_for(:resolved)) }
-
   scope :for_environment, -> (environment) do
     joins(:prometheus_alert).where(prometheus_alerts: { environment_id: environment })
   end
 
-  scope :count_by_project_id, -> { group(:project_id).count }
   scope :with_prometheus_alert, -> { includes(:prometheus_alert) }
 
   def self.last_by_project_id
diff --git a/ee/app/models/self_managed_prometheus_alert_event.rb b/ee/app/models/self_managed_prometheus_alert_event.rb
new file mode 100644
index 0000000000000000000000000000000000000000..49efd41efd9c1e445c1d4c08a4338e6a1a8511a0
--- /dev/null
+++ b/ee/app/models/self_managed_prometheus_alert_event.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class SelfManagedPrometheusAlertEvent < ApplicationRecord
+  include AlertEventLifecycle
+
+  belongs_to :project, validate: true, inverse_of: :self_managed_prometheus_alert_events
+  belongs_to :environment, validate: true
+  has_and_belongs_to_many :related_issues, class_name: 'Issue', join_table: :issues_self_managed_prometheus_alert_events
+
+  validates :payload_key, uniqueness: { scope: :project_id }
+
+  def self.find_or_initialize_by_payload_key(project, payload_key)
+    find_or_initialize_by(project: project, payload_key: payload_key) do |event|
+      yield event if block_given?
+    end
+  end
+
+  def self.payload_key_for(started_at, alert_name, query_expression)
+    plain = [started_at, alert_name, query_expression].join('/')
+
+    Digest::SHA1.hexdigest(plain)
+  end
+end
diff --git a/ee/app/models/vulnerabilities/occurrence.rb b/ee/app/models/vulnerabilities/occurrence.rb
index 3e3336681480d4ae1ab686526d166772cac00556..aef4a8a1b3844ae43d03179221a07431bde54c31 100644
--- a/ee/app/models/vulnerabilities/occurrence.rb
+++ b/ee/app/models/vulnerabilities/occurrence.rb
@@ -18,6 +18,7 @@ class Occurrence < ApplicationRecord
     belongs_to :project
     belongs_to :scanner, class_name: 'Vulnerabilities::Scanner'
     belongs_to :primary_identifier, class_name: 'Vulnerabilities::Identifier', inverse_of: :primary_occurrences
+    belongs_to :vulnerability, inverse_of: :findings
 
     has_many :occurrence_identifiers, class_name: 'Vulnerabilities::OccurrenceIdentifier'
     has_many :identifiers, through: :occurrence_identifiers, class_name: 'Vulnerabilities::Identifier'
@@ -76,7 +77,7 @@ class Occurrence < ApplicationRecord
     validates :raw_metadata, presence: true
 
     scope :report_type, -> (type) { where(report_type: report_types[type]) }
-    scope :ordered, -> { order("severity desc", :id) }
+    scope :ordered, -> { order(severity: :desc, confidence: :desc, id: :asc) }
 
     scope :by_report_types, -> (values) { where(report_type: values) }
     scope :by_projects, -> (values) { where(project_id: values) }
diff --git a/ee/app/models/vulnerability.rb b/ee/app/models/vulnerability.rb
index 088ecc04ebdc1bc5c482650a8331caaa4cc652ca..12957adc731d5ebe868a784633844edd3de5c6ad 100644
--- a/ee/app/models/vulnerability.rb
+++ b/ee/app/models/vulnerability.rb
@@ -10,6 +10,9 @@ class Vulnerability < ApplicationRecord
   belongs_to :last_edited_by, class_name: 'User'
   belongs_to :closed_by, class_name: 'User'
 
+  # TODO: temporary, remove when https://gitlab.com/gitlab-org/gitlab/merge_requests/18283 is merged and rebased onto
+  has_many :findings, class_name: 'Vulnerabilities::Occurrence', inverse_of: :vulnerability
+
   enum state: { opened: 1, closed: 2 }
   enum severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS, _prefix: :severity
   enum confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS, _prefix: :confidence
@@ -21,4 +24,6 @@ class Vulnerability < ApplicationRecord
   validates :title_html, length: { maximum: Issuable::TITLE_HTML_LENGTH_MAX }, allow_blank: true
   validates :description, length: { maximum: Issuable::DESCRIPTION_LENGTH_MAX }, allow_blank: true
   validates :description_html, length: { maximum: Issuable::DESCRIPTION_HTML_LENGTH_MAX }, allow_blank: true
+
+  scope :with_findings, -> { includes(:findings) }
 end
diff --git a/ee/app/policies/ee/base_policy.rb b/ee/app/policies/ee/base_policy.rb
index 8dc1dbb24d4829b24bbf04ba633aa9b98e92fae9..fa3a9667a215bab89b98e9cd9fb127c9c68e7c83 100644
--- a/ee/app/policies/ee/base_policy.rb
+++ b/ee/app/policies/ee/base_policy.rb
@@ -14,6 +14,9 @@ module BasePolicy
       with_scope :user
       condition(:alert_bot, score: 0) { @user&.alert_bot? }
 
+      with_scope :user
+      condition(:visual_review_bot, score: 0) { @user&.visual_review_bot? }
+
       with_scope :global
       condition(:license_block) { License.block_changes? }
     end
diff --git a/ee/app/policies/ee/environment_policy.rb b/ee/app/policies/ee/environment_policy.rb
index 407dcf3222c79191fed058c9ba2cf72b1dd85594..fd7efefdc7c1cf0f4022234739d19217bedcf828 100644
--- a/ee/app/policies/ee/environment_policy.rb
+++ b/ee/app/policies/ee/environment_policy.rb
@@ -9,6 +9,8 @@ module EnvironmentPolicy
       rule { ~deployable_by_user }.policy do
         prevent :stop_environment
         prevent :create_environment_terminal
+        prevent :create_deployment
+        prevent :update_deployment
       end
 
       private
diff --git a/ee/app/policies/ee/global_policy.rb b/ee/app/policies/ee/global_policy.rb
index deaaeaab42e4a0fda78f91ff3979564c685b25fe..6ed5b52c2503e8525139e859ca774dd681d7fda9 100644
--- a/ee/app/policies/ee/global_policy.rb
+++ b/ee/app/policies/ee/global_policy.rb
@@ -13,7 +13,7 @@ module GlobalPolicy
         License.feature_available?(:security_dashboard)
       end
 
-      rule { operations_dashboard_available }.enable :read_operations_dashboard
+      rule { ~anonymous & operations_dashboard_available }.enable :read_operations_dashboard
       rule { ~anonymous & security_dashboard_available }.enable :read_security_dashboard
 
       rule { admin }.policy do
diff --git a/ee/app/policies/ee/issue_policy.rb b/ee/app/policies/ee/issue_policy.rb
index 3a56a71f59fb844749ef0c59d513bbdb2024dd67..9e469e5719edbc15bf31b8ca13f7809177353025 100644
--- a/ee/app/policies/ee/issue_policy.rb
+++ b/ee/app/policies/ee/issue_policy.rb
@@ -4,11 +4,18 @@ module EE
   module IssuePolicy
     extend ActiveSupport::Concern
     prepended do
+      condition(:moved) { @subject.moved? }
+
       rule { ~can?(:read_issue) }.policy do
         prevent :read_design
         prevent :create_design
         prevent :destroy_design
       end
+
+      rule { locked | moved }.policy do
+        prevent :create_design
+        prevent :destroy_design
+      end
     end
   end
 end
diff --git a/ee/app/policies/ee/policy_actor.rb b/ee/app/policies/ee/policy_actor.rb
index a9534ac2f5f86282fce1c0561bcfe7a8d25300f9..7076a20ff23df9c0e6b784e3a3a5060bc49cc6ce 100644
--- a/ee/app/policies/ee/policy_actor.rb
+++ b/ee/app/policies/ee/policy_actor.rb
@@ -13,5 +13,9 @@ def support_bot?
     def alert_bot?
       false
     end
+
+    def visual_review_bot?
+      false
+    end
   end
 end
diff --git a/ee/app/policies/ee/project_policy.rb b/ee/app/policies/ee/project_policy.rb
index 25bf2699010fb0d88638b4601501677ca4e6f794..968d3abeaa45cdacdc452c55bd8f0f6c39d50cfe 100644
--- a/ee/app/policies/ee/project_policy.rb
+++ b/ee/app/policies/ee/project_policy.rb
@@ -102,6 +102,11 @@ module ProjectPolicy
         prevent :read_project
       end
 
+      rule { visual_review_bot }.policy do
+        prevent :read_note
+        enable :create_note
+      end
+
       rule { license_block }.policy do
         prevent :create_issue
         prevent :create_merge_request_in
@@ -146,10 +151,12 @@ module ProjectPolicy
 
       rule { can?(:developer_access) }.policy do
         enable :read_project_security_dashboard
+        enable :dismiss_vulnerability
       end
 
       rule { security_dashboard_feature_disabled }.policy do
         prevent :read_project_security_dashboard
+        prevent :dismiss_vulnerability
       end
 
       rule { can?(:read_project) & (can?(:read_merge_request) | can?(:read_build)) }.enable :read_vulnerability_feedback
@@ -194,6 +201,7 @@ module ProjectPolicy
         enable :read_deployment
         enable :read_pages
         enable :read_project_security_dashboard
+        enable :dismiss_vulnerability
       end
 
       rule { auditor & ~guest }.policy do
@@ -284,6 +292,7 @@ def lookup_access_level!
       return ::Gitlab::Access::NO_ACCESS if needs_new_sso_session?
       return ::Gitlab::Access::REPORTER if alert_bot?
       return ::Gitlab::Access::GUEST if support_bot? && service_desk_enabled?
+      return ::Gitlab::Access::NO_ACCESS if visual_review_bot?
 
       super
     end
diff --git a/ee/app/presenters/ee/commit_status_presenter.rb b/ee/app/presenters/ee/commit_status_presenter.rb
index 6e7ed1a36171beaf38a404628172f6150d054631..5232e2450ca81024da52deaec34c0255fc4c92da 100644
--- a/ee/app/presenters/ee/commit_status_presenter.rb
+++ b/ee/app/presenters/ee/commit_status_presenter.rb
@@ -4,7 +4,7 @@ module CommitStatusPresenter
     extend ActiveSupport::Concern
 
     prepended do
-      EE_CALLOUT_FAILURE_MESSAGES = const_get(:CALLOUT_FAILURE_MESSAGES).merge(
+      EE_CALLOUT_FAILURE_MESSAGES = const_get(:CALLOUT_FAILURE_MESSAGES, false).merge(
         protected_environment_failure: 'The environment this job is deploying to is protected. Only users with permission may successfully run this job.',
         insufficient_bridge_permissions: 'This job could not be executed because of insufficient permissions to create a downstream pipeline.',
         insufficient_upstream_permissions: 'This job could not be executed because of insufficient permissions to track the upstream project.',
diff --git a/ee/app/presenters/epic_presenter.rb b/ee/app/presenters/epic_presenter.rb
index 85df845eb4bd28cb024b87733f9a01dfca52a16a..47f2a0409e12b58a884ab96b0201cc95d6da9d61 100644
--- a/ee/app/presenters/epic_presenter.rb
+++ b/ee/app/presenters/epic_presenter.rb
@@ -35,13 +35,17 @@ def epic_reference(full: false)
     end
   end
 
+  def subscribed?
+    epic.subscribed?(current_user)
+  end
+
   private
 
   def initial_data
     {
       labels: epic.labels,
       participants: participants,
-      subscribed: epic.subscribed?(current_user)
+      subscribed: subscribed?
     }
   end
 
diff --git a/ee/app/serializers/analytics/cycle_analytics/stage_entity.rb b/ee/app/serializers/analytics/cycle_analytics/stage_entity.rb
index 1594ed88210d019c040027da7f5a5e8de8ccdd2b..58f8b9fe13f1cec9e4f06186c6cad862e8d4a630 100644
--- a/ee/app/serializers/analytics/cycle_analytics/stage_entity.rb
+++ b/ee/app/serializers/analytics/cycle_analytics/stage_entity.rb
@@ -8,8 +8,8 @@ class StageEntity < Grape::Entity
       expose :description
       expose :id
       expose :custom
-      expose :start_event_identifier, if: :custom?
-      expose :end_event_identifier, if: :custom?
+      expose :start_event_identifier, if: -> (s) { s.custom? }
+      expose :end_event_identifier, if: -> (s) { s.custom? }
 
       def id
         object.id || object.name
diff --git a/ee/app/serializers/ee/merge_request_widget_entity.rb b/ee/app/serializers/ee/merge_request_widget_entity.rb
index 5042629284ac1dbaadd72185e380c2bec6d20504..342b54789b9858f06a58363ee8a390e9cf30426c 100644
--- a/ee/app/serializers/ee/merge_request_widget_entity.rb
+++ b/ee/app/serializers/ee/merge_request_widget_entity.rb
@@ -159,7 +159,7 @@ def blocking_merge_requests
         visible_mrs = object.visible_blocking_merge_requests(current_user)
         visible_mrs_by_state = visible_mrs
           .map { |mr| represent_blocking_mr(mr) }
-          .group_by { |blocking_mr| blocking_mr.object.state_name }
+          .group_by { |blocking_mr| blocking_mr.object.state_id_name }
 
         {
           total_count: visible_mrs.count + hidden_blocking_count,
diff --git a/ee/app/serializers/ee/pipeline_details_entity.rb b/ee/app/serializers/ee/pipeline_details_entity.rb
deleted file mode 100644
index 5a55bd7defb1089b5fb3ad905dc95e8a33d2df7f..0000000000000000000000000000000000000000
--- a/ee/app/serializers/ee/pipeline_details_entity.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-module EE
-  module PipelineDetailsEntity
-    extend ActiveSupport::Concern
-
-    prepended do
-      expose :triggered_by_pipeline, as: :triggered_by, with: TriggeredPipelineEntity
-      expose :triggered_pipelines, as: :triggered, using: TriggeredPipelineEntity
-    end
-  end
-end
diff --git a/ee/app/serializers/ee/pipeline_serializer.rb b/ee/app/serializers/ee/pipeline_serializer.rb
deleted file mode 100644
index bd09d4ca9f232b70bc7ccdc57f01b6c04df2b4d3..0000000000000000000000000000000000000000
--- a/ee/app/serializers/ee/pipeline_serializer.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module EE
-  module PipelineSerializer
-    extend ActiveSupport::Concern
-    extend ::Gitlab::Utils::Override
-
-    private
-
-    override :preloaded_relations
-    def preloaded_relations
-      super.concat([
-        { triggered_by_pipeline: [:project, :user] },
-        { triggered_pipelines: [:project, :user] }
-      ])
-    end
-  end
-end
diff --git a/ee/app/serializers/feature_flag_serializer.rb b/ee/app/serializers/feature_flag_serializer.rb
index 3eebafa0bdda10368e7b2f1df1ba39e0fbd5f205..e0ff33cc61af91313bfb3e11c6caad8e49cd8786 100644
--- a/ee/app/serializers/feature_flag_serializer.rb
+++ b/ee/app/serializers/feature_flag_serializer.rb
@@ -5,10 +5,6 @@ class FeatureFlagSerializer < BaseSerializer
   entity FeatureFlagEntity
 
   def represent(resource, opts = {})
-    if resource.is_a?(ActiveRecord::Relation)
-      resource = resource.preload_relations
-    end
-
     super(resource, opts)
   end
 end
diff --git a/ee/app/serializers/license_entity.rb b/ee/app/serializers/license_entity.rb
index b077b119299137d43548eb4b6579dd11ccec8f8f..a108d525929fd57b9a072df09ce25dd42d012133 100644
--- a/ee/app/serializers/license_entity.rb
+++ b/ee/app/serializers/license_entity.rb
@@ -3,6 +3,7 @@
 class LicenseEntity < Grape::Entity
   class ComponentEntity < Grape::Entity
     expose :name
+    expose :path, as: :blob_path
   end
 
   expose :name
diff --git a/ee/app/services/analytics/cycle_analytics/stages/base_service.rb b/ee/app/services/analytics/cycle_analytics/stages/base_service.rb
index 625d45dbaaec3f21960e80c2ff12db532cb5340c..aa8676c0f893a86477c58c1bc8ac486316fcf725 100644
--- a/ee/app/services/analytics/cycle_analytics/stages/base_service.rb
+++ b/ee/app/services/analytics/cycle_analytics/stages/base_service.rb
@@ -2,52 +2,54 @@
 
 module Analytics
   module CycleAnalytics
-    class BaseService
-      include Gitlab::Allowable
+    module Stages
+      class BaseService
+        include Gitlab::Allowable
 
-      def initialize(parent:, current_user:, params: {})
-        @parent = parent
-        @current_user = current_user
-      end
+        def initialize(parent:, current_user:, params: {})
+          @parent = parent
+          @current_user = current_user
+        end
 
-      def execute
-        raise NotImplementedError
-      end
+        def execute
+          raise NotImplementedError
+        end
 
-      private
+        private
 
-      attr_reader :parent, :current_user, :params
+        attr_reader :parent, :current_user, :params
 
-      def success(stage, http_status = :created)
-        ServiceResponse.success(payload: { stage: stage }, http_status: http_status)
-      end
+        def success(stage, http_status = :created)
+          ServiceResponse.success(payload: { stage: stage }, http_status: http_status)
+        end
 
-      def error(stage)
-        ServiceResponse.error(message: 'Invalid parameters', payload: { errors: stage.errors }, http_status: :unprocessable_entity)
-      end
+        def error(stage)
+          ServiceResponse.error(message: 'Invalid parameters', payload: { errors: stage.errors }, http_status: :unprocessable_entity)
+        end
 
-      def not_found
-        ServiceResponse.error(message: 'Stage not found', payload: {}, http_status: :not_found)
-      end
+        def not_found
+          ServiceResponse.error(message: 'Stage not found', payload: {}, http_status: :not_found)
+        end
 
-      def forbidden
-        ServiceResponse.error(message: 'Forbidden', payload: {}, http_status: :forbidden)
-      end
+        def forbidden
+          ServiceResponse.error(message: 'Forbidden', payload: {}, http_status: :forbidden)
+        end
 
-      def persist_default_stages!
-        persisted_default_stages = parent.cycle_analytics_stages.default_stages
+        def persist_default_stages!
+          persisted_default_stages = parent.cycle_analytics_stages.default_stages
 
-        # make sure that we persist default stages only once
-        stages_to_persist = build_default_stages.select do |new_default_stage|
-          !persisted_default_stages.find { |s| s.name.eql?(new_default_stage.name) }
-        end
+          # make sure that we persist default stages only once
+          stages_to_persist = build_default_stages.select do |new_default_stage|
+            !persisted_default_stages.find { |s| s.name.eql?(new_default_stage.name) }
+          end
 
-        stages_to_persist.each(&:save!)
-      end
+          stages_to_persist.each(&:save!)
+        end
 
-      def build_default_stages
-        Gitlab::Analytics::CycleAnalytics::DefaultStages.all.map do |params|
-          parent.cycle_analytics_stages.build(params)
+        def build_default_stages
+          Gitlab::Analytics::CycleAnalytics::DefaultStages.all.map do |params|
+            parent.cycle_analytics_stages.build(params)
+          end
         end
       end
     end
diff --git a/ee/app/services/analytics/cycle_analytics/stages/delete_service.rb b/ee/app/services/analytics/cycle_analytics/stages/delete_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c2c62ef07f8e7a14d5655e55026073b61a628e4e
--- /dev/null
+++ b/ee/app/services/analytics/cycle_analytics/stages/delete_service.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Analytics
+  module CycleAnalytics
+    module Stages
+      class DeleteService < BaseService
+        def initialize(parent:, current_user:, params:)
+          super
+
+          @stage = Analytics::CycleAnalytics::StageFinder.new(parent: parent, stage_id: params[:id]).execute
+        end
+
+        def execute
+          return forbidden if !can?(current_user, :delete_group_stage, parent) || @stage.default_stage?
+
+          @stage.destroy!
+
+          success(@stage, :ok)
+        end
+      end
+    end
+  end
+end
diff --git a/ee/app/services/ee/boards/lists/list_service.rb b/ee/app/services/ee/boards/lists/list_service.rb
index f33ef77c973a8a5cf305a196030d6ab4df7ffca2..325b3ee581e35c5dde7a751462cfe274d1e9f41d 100644
--- a/ee/app/services/ee/boards/lists/list_service.rb
+++ b/ee/app/services/ee/boards/lists/list_service.rb
@@ -25,7 +25,7 @@ def execute(board)
         private
 
         def list_type_features_availability(board)
-          parent = board.parent
+          parent = board.resource_parent
 
           LICENSED_LIST_TYPES.each_with_object({}) do |list_type, hash|
             list_type_key = ::List.list_types[list_type]
diff --git a/ee/app/services/ee/boards/lists/update_service.rb b/ee/app/services/ee/boards/lists/update_service.rb
index eb719f50b6c6e6afd710a71b00100a510fe1d367..3bb72eedd205699c899b63dda61ff124bf143e0b 100644
--- a/ee/app/services/ee/boards/lists/update_service.rb
+++ b/ee/app/services/ee/boards/lists/update_service.rb
@@ -16,7 +16,7 @@ def execute_by_params(list)
         end
 
         def max_issue_count?(list)
-          params.has_key?(:max_issue_count) && list.board.parent.feature_available?(:wip_limits)
+          params.has_key?(:max_issue_count) && list.board.resource_parent.feature_available?(:wip_limits)
         end
 
         def update_max_issue_count(list)
diff --git a/ee/app/services/ee/ci/pipeline_trigger_service.rb b/ee/app/services/ee/ci/pipeline_trigger_service.rb
deleted file mode 100644
index bc09ea2fd37fc0a1479d95bce9f6ec20350007c9..0000000000000000000000000000000000000000
--- a/ee/app/services/ee/ci/pipeline_trigger_service.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-
-module EE
-  module Ci
-    module PipelineTriggerService
-      extend ::Gitlab::Utils::Override
-      include ::Gitlab::Utils::StrongMemoize
-
-      private
-
-      override :create_pipeline_from_job
-      def create_pipeline_from_job(job)
-        # this check is to not leak the presence of the project if user cannot read it
-        return unless can?(job.user, :read_project, project)
-
-        return error("400 Job has to be running", 400) unless job.running?
-
-        pipeline = ::Ci::CreatePipelineService.new(project, job.user, ref: params[:ref])
-          .execute(:pipeline, ignore_skip_ci: true) do |pipeline|
-            source = job.sourced_pipelines.build(
-              source_pipeline: job.pipeline,
-              source_project: job.project,
-              pipeline: pipeline,
-              project: project)
-
-            pipeline.source_pipeline = source
-            pipeline.variables.build(variables)
-          end
-
-        if pipeline.persisted?
-          success(pipeline: pipeline)
-        else
-          error(pipeline.errors.messages, 400)
-        end
-      end
-
-      override :job_from_token
-      def job_from_token
-        strong_memoize(:job) do
-          ::Ci::Build.find_by_token(params[:token].to_s)
-        end
-      end
-    end
-  end
-end
diff --git a/ee/app/services/ee/deployments/after_create_service.rb b/ee/app/services/ee/deployments/after_create_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4a0108d6abce94835d612719f58f647c751739fa
--- /dev/null
+++ b/ee/app/services/ee/deployments/after_create_service.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module EE
+  module Deployments
+    module AfterCreateService
+      extend ::Gitlab::Utils::Override
+
+      override :execute
+      def execute
+        super.tap do |deployment|
+          deployment.project.repository.log_geo_updated_event
+        end
+      end
+    end
+  end
+end
diff --git a/ee/app/services/ee/issues/create_service.rb b/ee/app/services/ee/issues/create_service.rb
index 549d2fdae3f2a524e87cd9745dda0c8b67424e3b..07c4a43974f705094e4bf4590793bc3743c3e5ab 100644
--- a/ee/app/services/ee/issues/create_service.rb
+++ b/ee/app/services/ee/issues/create_service.rb
@@ -5,6 +5,18 @@ module Issues
     module CreateService
       extend ::Gitlab::Utils::Override
 
+      override :filter_params
+      def filter_params(issue)
+        epic_iid = params.delete(:epic_iid)
+        group = issue.project.group
+        if epic_iid.present? && group && can?(current_user, :admin_epic, group)
+          finder = EpicsFinder.new(current_user, group_id: group.id)
+          params[:epic] = finder.find_by!(iid: epic_iid) # rubocop: disable CodeReuse/ActiveRecord
+        end
+
+        super
+      end
+
       override :before_create
       def before_create(issue)
         handle_issue_epic_link(issue)
diff --git a/ee/app/services/ee/issues/update_service.rb b/ee/app/services/ee/issues/update_service.rb
index c7d6079f587bda16c112328e212df759bab9390b..10ab74dc08b4ed845889b0351855164ceb7eb039 100644
--- a/ee/app/services/ee/issues/update_service.rb
+++ b/ee/app/services/ee/issues/update_service.rb
@@ -17,6 +17,18 @@ def execute(issue)
         result
       end
 
+      override :filter_params
+      def filter_params(issue)
+        epic_iid = params.delete(:epic_iid)
+        group = issue.project.group
+        if epic_iid.present? && group && can?(current_user, :admin_epic, group)
+          finder = EpicsFinder.new(current_user, group_id: group.id)
+          params[:epic] = finder.find_by!(iid: epic_iid) # rubocop: disable CodeReuse/ActiveRecord
+        end
+
+        super
+      end
+
       private
 
       def handle_epic(issue)
diff --git a/ee/app/services/ee/notes/quick_actions_service.rb b/ee/app/services/ee/notes/quick_actions_service.rb
index a9494a8b4d91239cd5a593dc75e75ca858f54fae..9303209c92b2e0f78b66e90bf73c2577a9e4357b 100644
--- a/ee/app/services/ee/notes/quick_actions_service.rb
+++ b/ee/app/services/ee/notes/quick_actions_service.rb
@@ -7,7 +7,7 @@ module QuickActionsService
       include ::Gitlab::Utils::StrongMemoize
 
       prepended do
-        EE_UPDATE_SERVICES = const_get(:UPDATE_SERVICES).merge(
+        EE_UPDATE_SERVICES = const_get(:UPDATE_SERVICES, false).merge(
           'Epic' => Epics::UpdateService
         ).freeze
         EE::Notes::QuickActionsService.private_constant :EE_UPDATE_SERVICES
diff --git a/ee/app/services/ee/search/snippet_service.rb b/ee/app/services/ee/search/snippet_service.rb
index 635b92dcc789c9eda5fa586b2fe84b8b922fbde5..03add48a9bb7e6cdeeebb4bed63ffaea4d9932cd 100644
--- a/ee/app/services/ee/search/snippet_service.rb
+++ b/ee/app/services/ee/search/snippet_service.rb
@@ -9,7 +9,7 @@ module SnippetService
       def execute
         return super unless use_elasticsearch?
 
-        ::Gitlab::Elastic::SnippetSearchResults.new(current_user, params[:search])
+        ::Gitlab::Elastic::SnippetSearchResults.new(current_user, params[:search], elastic_projects, nil, true)
       end
 
       # This method is used in the top-level SearchService, so cannot be in-lined into #execute
diff --git a/ee/app/services/ee/update_deployment_service.rb b/ee/app/services/ee/update_deployment_service.rb
deleted file mode 100644
index 562e4ada2e8b78ab3f145a8177c68fb4176adcb4..0000000000000000000000000000000000000000
--- a/ee/app/services/ee/update_deployment_service.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-module EE
-  module UpdateDeploymentService
-    extend ::Gitlab::Utils::Override
-
-    override :execute
-    def execute
-      super.tap do |deployment|
-        deployment.project.repository.log_geo_updated_event
-      end
-    end
-  end
-end
diff --git a/ee/app/services/elastic/index_record_service.rb b/ee/app/services/elastic/index_record_service.rb
index 6195e8c7b3553cb90e413711a4728de66d771829..4b5c0b4a82916d94660b0f28bc5735874ba1cee9 100644
--- a/ee/app/services/elastic/index_record_service.rb
+++ b/ee/app/services/elastic/index_record_service.rb
@@ -15,7 +15,7 @@ def execute(record, indexing, options = {})
 
       record.__elasticsearch__.client = client
 
-      import(record, record.class.nested?, indexing)
+      import(record, indexing)
 
       initial_index_project(record) if record.class == Project && indexing
 
@@ -65,12 +65,12 @@ def import_association(association, options = {})
       raise ImportError.new(errors.inspect)
     end
 
-    def import(record, nested, indexing)
+    def import(record, indexing)
       operation = indexing ? 'index_document' : 'update_document'
       response = nil
 
       IMPORT_RETRY_COUNT.times do
-        response = if nested
+        response = if record.es_parent
                      record.__elasticsearch__.__send__ operation, routing: record.es_parent # rubocop:disable GitlabSecurity/PublicSend
                    else
                      record.__elasticsearch__.__send__ operation # rubocop:disable GitlabSecurity/PublicSend
diff --git a/ee/app/services/epics/create_service.rb b/ee/app/services/epics/create_service.rb
index 5e8e9258731763ea3312b810d6ff62fb72db0992..d5f474fd4d35e91afb4db1c099988c737c7df6e4 100644
--- a/ee/app/services/epics/create_service.rb
+++ b/ee/app/services/epics/create_service.rb
@@ -20,7 +20,17 @@ def before_create(epic)
     end
 
     def whitelisted_epic_params
-      params.slice(:title, :description, :start_date, :end_date, :milestone, :label_ids, :parent_id)
+      result = params.slice(:title, :description, :label_ids, :parent_id)
+
+      if params[:start_date_fixed] && params[:start_date_is_fixed]
+        result[:start_date] = params[:start_date_fixed]
+      end
+
+      if params[:due_date_fixed] && params[:due_date_is_fixed]
+        result[:end_date] = params[:due_date_fixed]
+      end
+
+      result
     end
   end
 end
diff --git a/ee/app/services/feature_flags/create_service.rb b/ee/app/services/feature_flags/create_service.rb
index bcc0c0972768016984cdeb234d2c614f05bcf3ee..8699746478befc262ad08a8ff7907791b905388f 100644
--- a/ee/app/services/feature_flags/create_service.rb
+++ b/ee/app/services/feature_flags/create_service.rb
@@ -3,6 +3,8 @@
 module FeatureFlags
   class CreateService < FeatureFlags::BaseService
     def execute
+      return error('Access Denied', 403) unless can_create?
+
       ActiveRecord::Base.transaction do
         feature_flag = project.operations_feature_flags.new(params)
 
@@ -11,7 +13,7 @@ def execute
 
           success(feature_flag: feature_flag)
         else
-          error(feature_flag.errors.full_messages)
+          error(feature_flag.errors.full_messages, 400)
         end
       end
     end
@@ -28,5 +30,9 @@ def audit_message(feature_flag)
 
       message_parts.join(" ")
     end
+
+    def can_create?
+      Ability.allowed?(current_user, :create_feature_flag, project)
+    end
   end
 end
diff --git a/ee/app/services/feature_flags/destroy_service.rb b/ee/app/services/feature_flags/destroy_service.rb
index c80d869787cdeb25cd0c033b3a5032ee44450755..c77e3e03ec31fa7f0044b3d256bbafe8704c7843 100644
--- a/ee/app/services/feature_flags/destroy_service.rb
+++ b/ee/app/services/feature_flags/destroy_service.rb
@@ -3,6 +3,14 @@
 module FeatureFlags
   class DestroyService < FeatureFlags::BaseService
     def execute(feature_flag)
+      destroy_feature_flag(feature_flag)
+    end
+
+    private
+
+    def destroy_feature_flag(feature_flag)
+      return error('Access Denied', 403) unless can_destroy?(feature_flag)
+
       ActiveRecord::Base.transaction do
         if feature_flag.destroy
           save_audit_event(audit_event(feature_flag))
@@ -14,10 +22,12 @@ def execute(feature_flag)
       end
     end
 
-    private
-
     def audit_message(feature_flag)
       "Deleted feature flag <strong>#{feature_flag.name}</strong>."
     end
+
+    def can_destroy?(feature_flag)
+      Ability.allowed?(current_user, :destroy_feature_flag, feature_flag)
+    end
   end
 end
diff --git a/ee/app/services/feature_flags/update_service.rb b/ee/app/services/feature_flags/update_service.rb
index 250f78a6160a6159d9cb5aba910cc415df4ffe14..5ad0dc004260dea8de21c54fff4318006d7dcbc8 100644
--- a/ee/app/services/feature_flags/update_service.rb
+++ b/ee/app/services/feature_flags/update_service.rb
@@ -9,6 +9,8 @@ class UpdateService < FeatureFlags::BaseService
     }.freeze
 
     def execute(feature_flag)
+      return error('Access Denied', 403) unless can_update?(feature_flag)
+
       ActiveRecord::Base.transaction do
         feature_flag.assign_attributes(params)
 
@@ -71,5 +73,9 @@ def updated_scope_message(scope)
 
       message + '.'
     end
+
+    def can_update?(feature_flag)
+      Ability.allowed?(current_user, :update_feature_flag, feature_flag)
+    end
   end
 end
diff --git a/ee/app/services/pod_logs_service.rb b/ee/app/services/pod_logs_service.rb
index 33fca6cbcecaf3c0e0a7caeecd6f7c7b7cb0c19b..dc0350ca776868a1efa7549ca9a3994be0d45cb5 100644
--- a/ee/app/services/pod_logs_service.rb
+++ b/ee/app/services/pod_logs_service.rb
@@ -1,18 +1,35 @@
 # frozen_string_literal: true
 
 class PodLogsService < ::BaseService
+  include Stepable
+
   attr_reader :environment
 
   K8S_NAME_MAX_LENGTH = 253
 
   PARAMS = %w(pod_name container_name).freeze
 
+  SUCCESS_RETURN_KEYS = [:status, :logs].freeze
+
+  steps :check_param_lengths,
+    :check_deployment_platform,
+    :check_pod_name,
+    :pod_logs,
+    :split_logs,
+    :filter_return_keys
+
   def initialize(environment, params: {})
     @environment = environment
     @params = filter_params(params.dup).to_hash
   end
 
   def execute
+    execute_steps
+  end
+
+  private
+
+  def check_param_lengths(_result)
     pod_name = params['pod_name'].presence
     container_name = params['container_name'].presence
 
@@ -24,36 +41,56 @@ def execute
         ' %{max_length} chars' % { max_length: K8S_NAME_MAX_LENGTH }))
     end
 
+    success(pod_name: pod_name, container_name: container_name)
+  end
+
+  def check_deployment_platform(result)
     unless environment.deployment_platform
-      return error('No deployment platform')
+      return error(_('No deployment platform available'))
     end
 
+    success(result)
+  end
+
+  def check_pod_name(result)
     # If pod_name is not received as parameter, get the pod logs of the first
     # pod of this environment.
-    pod_name ||= environment.pod_names&.first
+    result[:pod_name] ||= environment.pod_names&.first
 
-    pod_logs(pod_name, container_name)
-  end
+    unless result[:pod_name]
+      return error(_('No pods available'))
+    end
 
-  private
+    success(result)
+  end
 
-  def pod_logs(pod_name, container_name)
-    result = environment.deployment_platform.read_pod_logs(
-      pod_name,
+  def pod_logs(result)
+    response = environment.deployment_platform.read_pod_logs(
+      result[:pod_name],
       namespace,
-      container: container_name
+      container: result[:container_name]
     )
 
-    return unless result
+    return { status: :processing } unless response
 
-    if result[:status] == :error
-      error(result[:error])
+    result[:logs] = response[:logs]
+
+    if response[:status] == :error
+      error(response[:error])
     else
-      logs = split_by_newline(result[:logs])
-      success(logs: logs)
+      success(result)
     end
   end
 
+  def split_logs(result)
+    logs = split_by_newline(result[:logs])
+    success(logs: logs)
+  end
+
+  def filter_return_keys(result)
+    result.slice(*SUCCESS_RETURN_KEYS)
+  end
+
   def filter_params(params)
     params.slice(*PARAMS)
   end
diff --git a/ee/app/services/projects/alerting/notify_service.rb b/ee/app/services/projects/alerting/notify_service.rb
index 0308261eb82f97c14d621b314711d174c04de652..b69d05ea15b2b3cd4e4efb77c508d60574a61108 100644
--- a/ee/app/services/projects/alerting/notify_service.rb
+++ b/ee/app/services/projects/alerting/notify_service.rb
@@ -20,17 +20,12 @@ def execute(token)
 
       delegate :alerts_service, to: :project
 
-      def generic_alert_endpoint_enabled?
-        Feature.enabled?(:generic_alert_endpoint, project)
-      end
-
       def incident_management_available?
         project.feature_available?(:incident_management)
       end
 
       def alerts_service_activated?
         incident_management_available? &&
-          generic_alert_endpoint_enabled? &&
           alerts_service.try(:active?)
       end
 
diff --git a/ee/app/services/projects/prometheus/alerts/create_events_service.rb b/ee/app/services/projects/prometheus/alerts/create_events_service.rb
index f4b234b655acd304b68fda25fe29f8d0171d93ae..92b567773fc0263cb4ba7469d85bfc1d67c94029 100644
--- a/ee/app/services/projects/prometheus/alerts/create_events_service.rb
+++ b/ee/app/services/projects/prometheus/alerts/create_events_service.rb
@@ -16,32 +16,24 @@ def create_events_from(alerts)
         end
 
         def create_event(payload)
-          return unless payload.respond_to?(:dig)
+          parsed_alert = Gitlab::Alerting::Alert.new(project: project, payload: payload)
 
-          status = payload.dig('status')
-          return unless status
+          return unless parsed_alert.valid?
 
-          started_at = validate_date(payload['startsAt'])
-          return unless started_at
+          event = if parsed_alert.gitlab_managed?
+                    build_managed_prometheus_alert_event(parsed_alert)
+                  else
+                    build_self_managed_prometheus_alert_event(parsed_alert)
+                  end
 
-          ended_at = validate_date(payload['endsAt'])
-          return unless ended_at
-
-          gitlab_alert_id = payload.dig('labels', 'gitlab_alert_id')
-          return unless gitlab_alert_id
-
-          alert = find_alert(gitlab_alert_id)
-          return unless alert
-
-          payload_key = PrometheusAlertEvent.payload_key_for(gitlab_alert_id, started_at)
-          event = PrometheusAlertEvent.find_or_initialize_by_payload_key(project, alert, payload_key)
-
-          result = case status
-                   when 'firing'
-                     event.fire(started_at)
-                   when 'resolved'
-                     event.resolve(ended_at)
-                   end
+          if event
+            result = case parsed_alert.status
+                     when 'firing'
+                       event.fire(parsed_alert.starts_at)
+                     when 'resolved'
+                       event.resolve(parsed_alert.ends_at)
+                     end
+          end
 
           event if result
         end
@@ -57,12 +49,24 @@ def find_alert(metric)
             .first
         end
 
-        def validate_date(date)
-          return unless date
+        def build_managed_prometheus_alert_event(parsed_alert)
+          alert = find_alert(parsed_alert.metric_id)
+
+          return if alert.blank?
+
+          payload_key = PrometheusAlertEvent.payload_key_for(parsed_alert.metric_id, parsed_alert.starts_at_raw)
+
+          PrometheusAlertEvent.find_or_initialize_by_payload_key(parsed_alert.project, alert, payload_key)
+        end
+
+        def build_self_managed_prometheus_alert_event(parsed_alert)
+          payload_key = SelfManagedPrometheusAlertEvent.payload_key_for(parsed_alert.starts_at_raw, parsed_alert.title, parsed_alert.full_query)
 
-          Time.rfc3339(date)
-          date
-        rescue ArgumentError
+          SelfManagedPrometheusAlertEvent.find_or_initialize_by_payload_key(parsed_alert.project, payload_key) do |event|
+            event.environment      = parsed_alert.environment
+            event.title            = parsed_alert.title
+            event.query_expression = parsed_alert.full_query
+          end
         end
       end
     end
diff --git a/ee/app/services/security/licenses_list_service.rb b/ee/app/services/security/licenses_list_service.rb
index 8877298aca6c76e7ab9904b1441dbd01d1f972e5..50c5c68efc423e187f848ff6abe993fc99265de5 100644
--- a/ee/app/services/security/licenses_list_service.rb
+++ b/ee/app/services/security/licenses_list_service.rb
@@ -8,11 +8,20 @@ def initialize(pipeline:)
     end
 
     def execute
-      pipeline.license_scanning_report.licenses
+      report.merge_dependencies_info!(dependencies) if dependencies.any?
+      report.licenses
     end
 
     private
 
     attr_reader :pipeline
+
+    def dependencies
+      @dependencies ||= pipeline.dependency_list_report.dependencies_with_licenses
+    end
+
+    def report
+      @report ||= pipeline.license_scanning_report
+    end
   end
 end
diff --git a/ee/app/services/security/merge_reports_service.rb b/ee/app/services/security/merge_reports_service.rb
index 9e0788de86739bc70db07f85b3629d3350df5459..44b8ae62aa58b554d0942d80e71abf766765374c 100644
--- a/ee/app/services/security/merge_reports_service.rb
+++ b/ee/app/services/security/merge_reports_service.rb
@@ -77,9 +77,10 @@ def deduplicate_occurrences!
         occurrence.identifiers.each do |identifier|
           # TODO: remove .downcase here after the DAST parser is harmonized to the common library identifiers' keys format
           # See https://gitlab.com/gitlab-org/gitlab/issues/11976#note_191257912
-          next if identifier.external_type.casecmp("cwe").zero? # ignored because it describes a class of vulnerabilities
+          next if %w[cwe wasc].include?(identifier.external_type.downcase) # ignored because these describe a class of vulnerabilities
 
           seen = check_or_mark_seen_identifier!(identifier, occurrence.location.fingerprint, seen_identifiers)
+
           break if seen
         end
 
diff --git a/ee/app/services/security/sync_reports_to_approval_rules_service.rb b/ee/app/services/security/sync_reports_to_approval_rules_service.rb
index c9f93193f6669780dc03d152abdc186bb5bca0c3..084698af2cc8c41d8d8c99118672ebf10491adb4 100644
--- a/ee/app/services/security/sync_reports_to_approval_rules_service.rb
+++ b/ee/app/services/security/sync_reports_to_approval_rules_service.rb
@@ -37,12 +37,13 @@ def sync_license_management_rules
     end
 
     def sync_vulnerability_rules
-      reports = pipeline.security_reports.reports
-      safe = reports.any? && reports.none? do |_report_type, report|
-        report.unsafe_severity?
-      end
+      reports = pipeline.security_reports
+      # If we have some reports, then we want to sync them early;
+      # If we don't have reports, then we should wait until pipeline stops.
+      return if reports.empty? && !pipeline.complete?
+      return if reports.violates_default_policy?
 
-      remove_required_approvals_for(ApprovalMergeRequestRule.security_report) if safe
+      remove_required_approvals_for(ApprovalMergeRequestRule.security_report)
     end
 
     def remove_required_approvals_for(rules)
diff --git a/ee/app/services/vulnerabilities/dismiss_service.rb b/ee/app/services/vulnerabilities/dismiss_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ba1d41307be0717f8182b3debbd200751da2d83f
--- /dev/null
+++ b/ee/app/services/vulnerabilities/dismiss_service.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Vulnerabilities
+  class DismissService
+    include Gitlab::Allowable
+
+    FindingsDismissResult = Struct.new(:ok?, :finding, :message)
+
+    def initialize(current_user, vulnerability)
+      @current_user = current_user
+      @vulnerability = vulnerability
+      @project = vulnerability.project
+    end
+
+    def execute
+      raise Gitlab::Access::AccessDeniedError unless can?(@current_user, :dismiss_vulnerability, @project)
+
+      @vulnerability.transaction do
+        result = dismiss_findings
+
+        unless result.ok?
+          handle_finding_dismissal_error(result.finding, result.message)
+          raise ActiveRecord::Rollback
+        end
+
+        @vulnerability.update(state: 'closed')
+      end
+
+      @vulnerability
+    end
+
+    private
+
+    def feedback_service_for(finding)
+      VulnerabilityFeedback::CreateService.new(@project, @current_user, feedback_params_for(finding))
+    end
+
+    def feedback_params_for(finding)
+      {
+        category: finding.report_type,
+        feedback_type: 'dismissal',
+        project_fingerprint: finding.project_fingerprint
+      }
+    end
+
+    def dismiss_findings
+      @vulnerability.findings.each do |finding|
+        result = feedback_service_for(finding).execute
+
+        return FindingsDismissResult.new(false, finding, result[:message]) if result[:status] == :error
+      end
+
+      FindingsDismissResult.new(true)
+    end
+
+    def handle_finding_dismissal_error(finding, message)
+      @vulnerability.errors.add(
+        :base,
+        :finding_dismissal_error,
+        message: _("failed to dismiss associated finding(id=%{finding_id}): %{message}") %
+          {
+            finding_id: finding.id,
+            message: message
+          })
+    end
+  end
+end
diff --git a/ee/app/views/admin/geo/nodes/_form.html.haml b/ee/app/views/admin/geo/nodes/_form.html.haml
index 18311298fc04bb4bd57fe2278d3eed6bd0a32c83..f19507a4151d1c07257ff6daeab934b5b30ef2d1 100644
--- a/ee/app/views/admin/geo/nodes/_form.html.haml
+++ b/ee/app/views/admin/geo/nodes/_form.html.haml
@@ -70,13 +70,11 @@
     = form.number_field :minimum_reverification_interval, class: 'form-control col-sm-2', min: 1
     .form-text.text-muted= s_('Geo|Control the minimum interval in days that a repository should be reverified for this primary node')
 
-- if ::Feature.enabled?(:geo_object_storage_replication)
-  .form-group.row.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
-    .col-sm-10
-      = form.label :sync_object_storage, _('Object Storage replication'), class: 'label-bold'
-      .form-check
-        = form.check_box :sync_object_storage, class: 'form-check-input'
-        = form.label :sync_object_storage, class: 'form-check-label' do
-          %span= s_('Geo|Allow this secondary node to replicate content on Object Storage')
-        .form-text.text-muted= s_('Geo|If enabled, and if object storage is enabled, GitLab will handle Object Storage replication using Geo')
-
+.form-group.row.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
+  .col-sm-10
+    = form.label :sync_object_storage, _('Object Storage replication'), class: 'label-bold'
+    .form-check
+      = form.check_box :sync_object_storage, class: 'form-check-input'
+      = form.label :sync_object_storage, class: 'form-check-label' do
+        %span= s_('Geo|Allow this secondary node to replicate content on Object Storage')
+      .form-text.text-muted= s_('Geo|If enabled, and if object storage is enabled, GitLab will handle Object Storage replication using Geo')
diff --git a/ee/app/views/dashboard/projects/_blank_state_ee_trial.html.haml b/ee/app/views/dashboard/projects/_blank_state_ee_trial.html.haml
index 128fb6131e629d7e03fd2f5d091ae4d849f29d66..0afa2f5520ec586193cbc9266d301743c2ee7286 100644
--- a/ee/app/views/dashboard/projects/_blank_state_ee_trial.html.haml
+++ b/ee/app/views/dashboard/projects/_blank_state_ee_trial.html.haml
@@ -1,6 +1,6 @@
 .blank-state
   .blank-state-icon
-    = custom_icon("ee_trial", size: 50)
+    = image_tag("illustrations/lock_promotion")
   .blank-state-body
     %h3.blank-state-title
       Unlock more features with GitLab Ultimate
diff --git a/ee/app/views/groups/_templates_setting.html.haml b/ee/app/views/groups/_templates_setting.html.haml
index c465853b8382a405c6291e9bd66c4b7bc0ce4ed8..cbfd979b52e5adaff07eff3ac6dadfcbf68ac85e 100644
--- a/ee/app/views/groups/_templates_setting.html.haml
+++ b/ee/app/views/groups/_templates_setting.html.haml
@@ -1,6 +1,6 @@
 - return unless @group.feature_available?(:custom_file_templates_for_namespace)
 
-%section.settings.no-animate#js-templates{ class: ('expanded' if expanded) }
+%section.settings.no-animate#js-templates{ class: ('expanded' if expanded), data: { qa_selector: 'file_template_repositories' } }
   .settings-header
     %h4
       = _('Templates')
@@ -18,6 +18,6 @@
             .form-text.text-muted
               = _('Select a template repository')
               = link_to icon('question-circle'), help_page_path('user/group/index.md', anchor: 'group-file-templates-premium'), target: '_blank'
-          = project_select_tag('group[file_template_project_id]', class: 'project-item-select hidden-filter-value', toggle_class: 'js-project-search js-project-filter js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
+          = project_select_tag('group[file_template_project_id]', class: 'project-item-select hidden-filter-value qa-file-template-repository-dropdown', toggle_class: 'js-project-search js-project-filter js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
             placeholder: _('Search projects'), idAttribute: 'id', data: { order_by: 'last_activity_at', idattribute: 'id', simple_filter: true, allow_clear: true }, value: @group.checked_file_template_project_id)
-      = f.submit _('Save changes'), class: "btn btn-success"
+      = f.submit _('Save changes'), class: "btn btn-success", data: { qa_selector: 'save_changes_button' }
diff --git a/ee/app/views/groups/billings/index.html.haml b/ee/app/views/groups/billings/index.html.haml
index de5d9940c180bdc153a19a5b9dada8ecc70e17d3..33907fcd52a6369262ba01716268a89117fc9e09 100644
--- a/ee/app/views/groups/billings/index.html.haml
+++ b/ee/app/views/groups/billings/index.html.haml
@@ -1,16 +1,11 @@
 - page_title "Billing"
 - current_plan = subscription_plan_info(@plans_data, @group.actual_plan_name)
-- support_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: EE::CUSTOMER_SUPPORT_URL }
-- support_link_end   = '</a>'.html_safe
 
 - if @top_most_group
   - top_most_group_plan = subscription_plan_info(@plans_data, @top_most_group.actual_plan_name)
   = render 'shared/billings/billing_plan_header', namespace: @group, plan: top_most_group_plan, parent_group: @top_most_group
 - else
-  = render 'shared/billings/billing_plan_header', namespace: @group, plan: current_plan
+  = render 'shared/billings/billing_plans', plans_data: @plans_data, namespace: @group
 
-  #js-billing-plans{ data: subscription_plan_data_attributes(@group, current_plan) }
 
-- if @group.actual_plan
-  .center
-    = s_('BillingPlans|If you would like to downgrade your plan please contact %{support_link_start}Customer Support%{support_link_end}.').html_safe % { support_link_start: support_link_start, support_link_end: support_link_end }
+  #js-billing-plans{ data: subscription_plan_data_attributes(@group, current_plan) }
diff --git a/ee/app/views/groups/epics/show.html.haml b/ee/app/views/groups/epics/show.html.haml
index bf90dc4394fe236430b02f7d88db8d991dfbfe61..b690d143c084ae75a35fc4498ac9259dfe149b97 100644
--- a/ee/app/views/groups/epics/show.html.haml
+++ b/ee/app/views/groups/epics/show.html.haml
@@ -33,6 +33,7 @@
             full_path: @group.full_path,
             auto_complete_epics: 'true',
             auto_complete_issues: 'false',
+            user_signed_in: current_user.present? ? 'true' : 'false',
             initial: issuable_initial_data(@epic).to_json } }
     #roadmap.tab-pane
       .row
diff --git a/ee/app/views/groups/sidebar/_packages.html.haml b/ee/app/views/groups/sidebar/_packages.html.haml
index d3395012a64ee67d6340c3549eaeeb4f9f536525..23d2e0dd975593e7001e46e34450bd5c7af58d94 100644
--- a/ee/app/views/groups/sidebar/_packages.html.haml
+++ b/ee/app/views/groups/sidebar/_packages.html.haml
@@ -1,13 +1,15 @@
+- packages_link = group_packages_list_nav? ? group_packages_path(@group) : group_container_registries_path(@group)
+
 - if group_packages_nav?
   = nav_link(path: group_packages_nav_link_paths) do
-    = link_to group_packages_path(@group) do
+    = link_to packages_link, title: _('Packages') do
       .nav-icon-container
         = sprite_icon('package')
       %span.nav-item-name
         = _('Packages')
     %ul.sidebar-sub-level-items
       = nav_link(controller: [:packages, :repositories], html_options: { class: "fly-out-top-item" } ) do
-        = link_to group_packages_path(@group) do
+        = link_to packages_link, title: _('Packages') do
           %strong.fly-out-top-item-name
             = _('Packages')
       %li.divider.fly-out-top-item
@@ -15,6 +17,10 @@
         = nav_link(controller: 'groups/packages') do
           = link_to group_packages_path(@group), title: _('Packages') do
             %span= _('List')
+      - if group_container_registry_nav?
+        = nav_link(controller: 'groups/container_registries') do
+          = link_to group_container_registries_path(@group), title: _('Container Registry') do
+            %span= _('Container Registry')
       - if group_dependency_proxy_nav?
         = nav_link(controller: 'groups/dependency_proxies') do
           = link_to group_dependency_proxy_path(@group), title: _('Dependency Proxy') do
diff --git a/ee/app/views/layouts/nav/sidebar/_project_packages_link.html.haml b/ee/app/views/layouts/nav/sidebar/_project_packages_link.html.haml
index 95e15069b5ac396d5ed0ac2e0e88f21dbe214af3..3dac7facb9449f639aac86d2b0814fb5d753e82d 100644
--- a/ee/app/views/layouts/nav/sidebar/_project_packages_link.html.haml
+++ b/ee/app/views/layouts/nav/sidebar/_project_packages_link.html.haml
@@ -2,7 +2,7 @@
 
 - if (project_nav_tab?(:packages) || project_nav_tab?(:container_registry))
   = nav_link controller: [:packages, :repositories] do
-    = link_to packages_link do
+    = link_to packages_link, data: { qa_selector: 'packages_link' } do
       .nav-icon-container
         = sprite_icon('package')
       %span.nav-item-name
diff --git a/ee/app/views/layouts/nav/sidebar/_tracing_link.html.haml b/ee/app/views/layouts/nav/sidebar/_tracing_link.html.haml
index 25ba9a6c62d699c7bac5f478346c3f8936e783e7..f9ab183e20cf17602a23b3689a58f236f2916b37 100644
--- a/ee/app/views/layouts/nav/sidebar/_tracing_link.html.haml
+++ b/ee/app/views/layouts/nav/sidebar/_tracing_link.html.haml
@@ -2,12 +2,6 @@
 
 - if project_nav_tab? :settings
   = nav_link(controller: :tracings, action: [:show]) do
-    - if @project.tracing_external_url.present?
-      = link_to sanitize(@project.tracing_external_url, scrubber: Rails::Html::TextOnlyScrubber.new), target: "_blank", rel: 'noopener noreferrer' do
-        %span
-          = _('Tracing')
-          %i.strong.ml-1.fa.fa-external-link
-    - else
-      = link_to project_tracing_path(@project), title: _('Tracing') do
-        %span
-          = _('Tracing')
+    = link_to project_tracing_path(@project), title: _('Tracing') do
+      %span
+        = _('Tracing')
diff --git a/ee/app/views/projects/_merge_request_approvals_settings_form.html.haml b/ee/app/views/projects/_merge_request_approvals_settings_form.html.haml
index 46d3218401a1cf9390cab8327a36e7d7285c47b1..a187c7e541be362e8258fe3fe26cd569f7ab9afc 100644
--- a/ee/app/views/projects/_merge_request_approvals_settings_form.html.haml
+++ b/ee/app/views/projects/_merge_request_approvals_settings_form.html.haml
@@ -12,6 +12,17 @@
     .text-center.prepend-top-default
       = sprite_icon('spinner', size: 24, css_class: 'gl-spinner')
 
+- if project.code_owner_approval_required_available?
+  .alert.alert-dismissible.alert-primary.fade.in.show{ role: "alert" }
+    %button.close{ type: "button", 'data-dismiss': "alert", 'aria-label': "Close" }
+      %span
+        = sprite_icon('close', size: 16)
+    %span
+      - banner_url = project_settings_repository_path(project, anchor: 'js-protected-branches-settings')
+      - banner_link_start = '<a href="%{url}"><strong>'.html_safe % { url: banner_url }
+      = _('The "Require approval from CODEOWNERS" setting was moved to %{banner_link_start}Protected Branches%{banner_link_end}').html_safe % { banner_link_start: banner_link_start, banner_link_end: '</strong></a>'.html_safe}
+      = link_to icon('question-circle'), help_page_path('user/project/protected_branches', anchor: 'protected-branches-approval-by-code-owners-premium'), target: '_blank'
+
 .form-group
   .form-check
     = form.check_box(:disable_overriding_approvers_per_merge_request, { checked: can_override_approvers, class: 'form-check-input' }, false, true)
diff --git a/ee/app/views/projects/issues/export_csv/_button.html.haml b/ee/app/views/projects/issues/export_csv/_button.html.haml
index 2583a7bcc7023d860390d829174bb74e909757c7..9f8d5383e7a2c50e1cc6215a033e865b15af46e6 100644
--- a/ee/app/views/projects/issues/export_csv/_button.html.haml
+++ b/ee/app/views/projects/issues/export_csv/_button.html.haml
@@ -1,4 +1,4 @@
 - if (current_user && @project.feature_available?(:export_issues)) || show_promotions?
   %button.csv_download_link.btn.has-tooltip{ title: _('Export as CSV'),
     data: { toggle: 'modal', target: '.issues-export-modal' } }
-    = sprite_icon('download')
+    = sprite_icon('export')
diff --git a/ee/app/views/projects/packages/packages/index.html.haml b/ee/app/views/projects/packages/packages/index.html.haml
index 912c6e0a4de7af2d8740fbcaed0dcf5b6e8169df..a6d5cc4b1edb9f572e91baed3977d36d1f931073 100644
--- a/ee/app/views/projects/packages/packages/index.html.haml
+++ b/ee/app/views/projects/packages/packages/index.html.haml
@@ -31,11 +31,11 @@
         = _('Created')
       .table-section.section-10{ role: 'rowheader' }
     - @packages.each do |package|
-      .gl-responsive-table-row.package-row.px-2
+      .gl-responsive-table-row.package-row.px-2{ data: { qa_selector: "package_row" } }
         .table-section.section-30
           .table-mobile-header{ role: "rowheader" }= _("Name")
           .table-mobile-content.flex-truncate-parent
-            = link_to package.name, project_package_path(@project, package), class: 'flex-truncate-child'
+            = link_to package.name, project_package_path(@project, package), class: 'flex-truncate-child', data: { qa_selector: "package_link" }
         .table-section.section-20
           .table-mobile-header{ role: "rowheader" }= _("Version")
           .table-mobile-content
diff --git a/ee/app/views/projects/tracings/show.html.haml b/ee/app/views/projects/tracings/show.html.haml
index 4f80a85746830ad0a6a765c257d9fc69147efa50..b3ec279f017fdfc6b4e7466b20f0d064c28e323f 100644
--- a/ee/app/views/projects/tracings/show.html.haml
+++ b/ee/app/views/projects/tracings/show.html.haml
@@ -1,19 +1,33 @@
 - @content_class = "limit-container-width" unless fluid_layout
 - page_title _("Tracing")
 
-.row.empty-state
-  .col-12
-    .svg-content
-      = image_tag 'illustrations/monitoring/tracing.svg', style: 'max-height: 254px'
+- if @project.tracing_external_url.present?
+  %h3.page-title= _('Tracing')
+  - jaeger_link = link_to('Jaeger tracing', 'https://www.jaegertracing.io/', target: "_blank", rel: "noreferrer")
+  %p.light= _("GitLab uses %{jaeger_link} to monitor distributed systems.").html_safe % { jaeger_link: jaeger_link }
 
-  .col-12
-    .text-content
-      %h4.text-left= _('Troubleshoot and monitor your application with tracing')
-      %p
-        - jaeger_help_url = "https://www.jaegertracing.io/docs/1.7/getting-started/"
-        - link_start_tag = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: jaeger_help_url }
-        - link_end_tag = "#{sprite_icon('external-link', size: 16, css_class: 'ml-1 vertical-align-middle')}</a>".html_safe
-        = _('To get started, link this page to your Jaeger server, or find out how to %{link_start_tag}install Jaeger%{link_end_tag}').html_safe % { link_start_tag: link_start_tag, link_end_tag: link_end_tag }
+  = content_for :flash_message do
+    .alert.alert-warning.flex-alert
+      .alert-message
+        = _("Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse.")
 
-      .text-center
-        = render 'tracing_button'
+  .card
+    - iframe_permissions = "allow-forms allow-scripts allow-same-origin allow-popups"
+    %iframe.border-0{ src: sanitize(@project.tracing_external_url, scrubber: Rails::Html::TextOnlyScrubber.new), width: '100%', height: 970, sandbox: iframe_permissions }
+- else
+  .row.empty-state
+    .col-12
+      .svg-content
+        = image_tag 'illustrations/monitoring/tracing.svg'
+
+    .col-12
+      .text-content
+        %h4.text-left= _('Troubleshoot and monitor your application with tracing')
+        %p
+          - jaeger_help_url = "https://www.jaegertracing.io/docs/1.7/getting-started/"
+          - link_start_tag = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: jaeger_help_url }
+          - link_end_tag = "#{sprite_icon('external-link', size: 16, css_class: 'ml-1 vertical-align-middle')}</a>".html_safe
+          = _('To get started, link this page to your Jaeger server, or find out how to %{link_start_tag}install Jaeger%{link_end_tag}').html_safe % { link_start_tag: link_start_tag, link_end_tag: link_end_tag }
+
+          .text-center
+            = render 'tracing_button'
diff --git a/ee/app/views/shared/billings/_billing_plan.html.haml b/ee/app/views/shared/billings/_billing_plan.html.haml
index e90daebf5e217b1d9109b7a3c0210f07c30c9c9c..173e24a92247ff8f8e614c0fc7e29df86a29a3b1 100644
--- a/ee/app/views/shared/billings/_billing_plan.html.haml
+++ b/ee/app/views/shared/billings/_billing_plan.html.haml
@@ -1,41 +1,38 @@
-.card{ class: ('current' if current_plan?(plan)) }
-  .card-header.bg-info.text-white
-    = plan.name
+- purchase_link = plan.purchase_link
+- is_current_plan = purchase_link.action == 'current_plan'
 
-  .card-body
-    .price-per-month
-      .append-right-5
-        = number_to_plan_currency(plan.price_per_month)
+.col-md-6.col-lg-3
+  .card.mb-5{ class: ("card-active" if is_current_plan) }
+    .card-header.font-weight-bold.p-3
+      = plan.name
+      - if is_current_plan
+        .pull-right.text-muted
+          = _("Current Plan")
 
-      %ul.billing-conditions
-        %li= s_("BillingPlans|per user")
-        %li= s_("BillingPlans|monthly")
-    .price-per-year
-      - if plan.price_per_year > 0
+    .card-body
+      .price-per-month
+        .append-right-5
+          = number_to_plan_currency(plan.price_per_month)
+
+        %ul.conditions.p-0.my-auto
+          %li= s_("BillingPlans|per user")
+          %li= s_("BillingPlans|monthly")
+      .price-per-year.text-left{ class: ("invisible" unless plan.price_per_year.positive?) }
         - price_per_year = number_to_plan_currency(plan.price_per_year)
-        = s_("BillingPlans|paid annually at %{price_per_year}") % { price_per_year: price_per_year }
+        = s_("BillingPlans|billed annually at %{price_per_year}") % { price_per_year: price_per_year }
 
-    %ul.feature-list.bordered-list
-      - plan.features.each do |feature|
-        %li
-          - if feature.highlight
-            %strong= feature.title
-          - else
-            = feature.title
-      %li
-        - if plan.about_page_href
-          = link_to s_("BillingPlans|See all %{plan_name} features") % { plan_name: plan.name }, plan.about_page_href
+      %hr.mt-3.mb-3
 
-    - purchase_link = plan.purchase_link
+      %ul.unstyled-list
+        - plan_feature_short_list(plan).each do |feature|
+          %li.p-0{ class: ("font-weight-bold" if feature.highlight) }
+            = feature.title
+        %li.p-0.pt-3
+          - if plan.about_page_href
+            = link_to s_("BillingPlans|See all %{plan_name} features") % { plan_name: plan.name }, EE::SUBSCRIPTIONS_COMPARISON_URL
 
     - if purchase_link
-      .plan-action
-        - href = purchase_link.href&.concat("&gl_namespace_id=#{namespace.id}")
-
-        - case purchase_link.action
-        - when 'downgrade'
-          = plan_purchase_link(href, s_("BillingPlans|Downgrade"))
-        - when 'current_plan'
-          = plan_purchase_link(href, s_("BillingPlans|Current plan"))
-        - when 'upgrade'
-          = plan_purchase_link(href, s_("BillingPlans|Upgrade"))
+      .card-footer.p-3
+        .pull-right{ class: ("invisible" unless purchase_link.action == 'upgrade' || is_current_plan) }
+          - upgrade_button_class = "disabled" if is_current_plan
+          = link_to s_('BillingPlan|Upgrade'), plan_purchase_or_upgrade_url(namespace, plan, current_plan), class: "btn btn-success #{upgrade_button_class}"
diff --git a/ee/app/views/shared/billings/_billing_plan_header.html.haml b/ee/app/views/shared/billings/_billing_plan_header.html.haml
index e4066ddbbaeaf6cf0b378e2f25fec58a28cd4a12..782e98be3fe34a08ae514bd5e82e85da1d7807db 100644
--- a/ee/app/views/shared/billings/_billing_plan_header.html.haml
+++ b/ee/app/views/shared/billings/_billing_plan_header.html.haml
@@ -2,20 +2,12 @@
 
 .mb-2= render_billings_gold_trial(current_user, parent_group || namespace)
 
-- if show_trial_banner?(parent_group || namespace)
-  .user-callout
-    .alert.bordered-box.landing.justify-content-start.pl-md-8
-      %button{ type:"button", class:"btn btn-default close", "data-dismiss":"alert", "aria-label": _("Close") }
-        = sprite_icon('close', size: 16)
-      .svg-container
-        = custom_icon('trial_activated_banner')
-      .user-callout-copy
-        %h4
-          = s_("BillingPlans|Congratulations, your new trial is activated")
+- if namespace_for_user?(namespace)
+  = render_if_exists 'trials/banner', namespace: namespace
 
 .billing-plan-header.content-block.center
   .billing-plan-logo
-    - if namespace == current_user.namespace
+    - if namespace_for_user?(namespace)
       .avatar-container.s96.home-panel-avatar.append-right-default.float-none.mx-auto.mb-4.mt-1
         = user_avatar_without_link(user: current_user, class: 'mb-3', size: 96)
     - elsif @group.avatar_url.present?
@@ -25,11 +17,10 @@
         = group_icon(@group, class: 'avatar avatar-tile s96', width: 96, height: 96, alt: @group.name)
 
   %h4
-    - plan_link = plan.about_page_href ? link_to(plan.code.titleize, plan.about_page_href) : plan.name
-    - if namespace == current_user.namespace
-      = s_("BillingPlans|@%{user_name} you are currently using the %{plan_link} plan.").html_safe % { user_name: current_user.username, plan_link: plan_link }
+    - if namespace_for_user?(namespace)
+      = s_("BillingPlans|@%{user_name} you are currently using the %{plan_name} plan.").html_safe % { user_name: current_user.username, plan_name: plan.code.titleize }
     - else
-      = s_("BillingPlans|%{group_name} is currently using the %{plan_link} plan.").html_safe % { group_name: namespace.full_name, plan_link: plan_link }
+      = s_("BillingPlans|%{group_name} is currently using the %{plan_name} plan.").html_safe % { group_name: namespace.full_name, plan_name: plan.code.titleize }
 
   - if parent_group
     %p= s_("BillingPlans|This group uses the plan associated with its parent group.")
diff --git a/ee/app/views/shared/billings/_billing_plans.html.haml b/ee/app/views/shared/billings/_billing_plans.html.haml
index 4581154cf1d55c991adcb4ef4c86568f3792fbb9..4113cf9d70dd1dc60b90dcbc82652fcb38f32d1a 100644
--- a/ee/app/views/shared/billings/_billing_plans.html.haml
+++ b/ee/app/views/shared/billings/_billing_plans.html.haml
@@ -6,14 +6,9 @@
   = render 'shared/billings/billing_plan_header', namespace: namespace, plan: current_plan
 
 - unless namespace.gold_plan?
-  - if namespace.upgradable?
-    .gl-p-4.center
-      = link_to s_('BillingPlan|Upgrade plan'), plan_upgrade_url(namespace, current_plan), class: 'btn btn-success'
-
-  - else
-    .billing-plans
-      - plans_data.each do |plan|
-        = render 'shared/billings/billing_plan', namespace: namespace, plan: plan
+  .billing-plans.mt-5.row
+    - plans_data.each do |plan|
+      = render 'shared/billings/billing_plan', namespace: namespace, plan: plan, current_plan: current_plan
 
 - if namespace.actual_plan
   .center
diff --git a/ee/app/views/shared/epic/_search_bar.html.haml b/ee/app/views/shared/epic/_search_bar.html.haml
index 90074f360f7b300c59e5ed3487ebfdf111c28a02..6b4bce0e4299b2dc3e0407da06557cb84308c0f7 100644
--- a/ee/app/views/shared/epic/_search_bar.html.haml
+++ b/ee/app/views/shared/epic/_search_bar.html.haml
@@ -30,12 +30,11 @@
           = check_box_tag "check-all-issues", nil, false, class: "check-all-issues left"
       .epics-other-filters.filtered-search-wrapper.d-flex.flex-column.flex-md-row
         .filtered-search-box
-          = dropdown_tag(custom_icon('icon_history'),
+          = dropdown_tag(_('Recent searches'),
             options: { wrapper_class: "filtered-search-history-dropdown-wrapper",
             toggle_class: "filtered-search-history-dropdown-toggle-button",
             dropdown_class: "filtered-search-history-dropdown",
-            content_class: "filtered-search-history-dropdown-content",
-            title: "Recent searches" }) do
+            content_class: "filtered-search-history-dropdown-content" }) do
             .js-filtered-search-history-dropdown{ data: { full_path: search_history_storage_prefix } }
           .filtered-search-box-input-container.droplab-dropdown
             .scroll-container
diff --git a/ee/app/views/shared/issuable/_board_create_list_dropdown.html.haml b/ee/app/views/shared/issuable/_board_create_list_dropdown.html.haml
index 29d8600a652ed4a62088daeb309563d0e4ee75d3..6a39c16ff3f68b77946cfccf23bb654dd4a9fd03 100644
--- a/ee/app/views/shared/issuable/_board_create_list_dropdown.html.haml
+++ b/ee/app/views/shared/issuable/_board_create_list_dropdown.html.haml
@@ -1,5 +1,5 @@
-- assignee_lists_available = board.parent.feature_available?(:board_assignee_lists)
-- milestone_lists_available = board.parent.feature_available?(:board_milestone_lists)
+- assignee_lists_available = board.resource_parent.feature_available?(:board_assignee_lists)
+- milestone_lists_available = board.resource_parent.feature_available?(:board_milestone_lists)
 
 - if assignee_lists_available || milestone_lists_available
   .dropdown.boards-add-list.prepend-left-10#js-add-list
@@ -24,7 +24,7 @@
       .tab-content
         #tab-labels.tab-pane.tab-pane-labels.active.js-tab-container-labels{ role: 'tabpanel' }
           = render partial: "shared/issuable/label_page_default", locals: { show_title: false, show_footer: true, show_create: true, show_boards_content: true, content_title: _('Label lists show all issues with the selected label.') }
-          - if can?(current_user, :admin_label, board.parent)
+          - if can?(current_user, :admin_label, board.resource_parent)
             = render partial: "shared/issuable/label_page_create", locals: { show_close: false }
 
         - if assignee_lists_available
diff --git a/ee/app/views/trials/_banner.html.haml b/ee/app/views/trials/_banner.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..5509fbddaf1831b88023ba9725c087f17374385e
--- /dev/null
+++ b/ee/app/views/trials/_banner.html.haml
@@ -0,0 +1,11 @@
+- return unless show_trial_banner?(namespace)
+.user-callout
+  .alert.bordered-box.landing.justify-content-start.pl-md-8
+    %button{ type:"button", class:"btn btn-default close", "data-dismiss":"alert", "aria-label": _("Close") }
+      = sprite_icon('close', size: 16)
+    .svg-container
+      = custom_icon('trial_activated_banner')
+    .user-callout-copy
+      %h4
+        = s_("BillingPlans|Congratulations, your new trial is activated")
+
diff --git a/ee/app/views/users/_custom_project_templates.html.haml b/ee/app/views/users/_custom_project_templates.html.haml
index 3c98d10a38822a6710262549ba5133f7d2329573..22e0c393cf0fcd2ab04b060893cd85b996396c67 100644
--- a/ee/app/views/users/_custom_project_templates.html.haml
+++ b/ee/app/views/users/_custom_project_templates.html.haml
@@ -14,7 +14,7 @@
           %a.btn.btn-default.append-right-10{ href: project_path(template), rel: 'noopener noreferrer', target: '_blank' }
             = _('Preview')
           %label.btn.btn-success.custom-template-button.choose-template.append-bottom-0{ for: template.name }
-            %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name }
+            %input{ type: "radio", autocomplete: "off", name: "project[template_project_id]", id: template.name, value: template.id, data: { template_name: template.name } }
             %span.qa-use-template-button
               = _('Use template')
 
diff --git a/ee/app/views/users/_custom_project_templates_from_groups.html.haml b/ee/app/views/users/_custom_project_templates_from_groups.html.haml
index c2610f1e7dae653a09cc767f5ea4e19ecca5adcc..b8e4c1e1f13b943637b141e606614d290178eef4 100644
--- a/ee/app/views/users/_custom_project_templates_from_groups.html.haml
+++ b/ee/app/views/users/_custom_project_templates_from_groups.html.haml
@@ -33,7 +33,7 @@
               %a.btn.btn-default.append-right-10{ href: project_path(project), rel: 'noopener noreferrer', target: '_blank' }
                 = _('Preview')
               %label.btn.btn-success.custom-template-button.choose-template.append-bottom-0{ for: project.name }
-                %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: project.name, value: project.name, data: { subgroup_id: project.namespace_id } }
+                %input{ type: "radio", autocomplete: "off", name: "project[template_project_id]", id: project.name, value: project.id, data: { subgroup_id: project.namespace_id, template_name: project.name } }
                 %span.qa-use-template-button
                   = _('Use template')
 
diff --git a/ee/app/workers/admin_emails_worker.rb b/ee/app/workers/admin_emails_worker.rb
index 2793b5afa4f1b3fadc02ab1e3072746b7a638468..1062c238b0f62b5e2d80aa38cb8d21dec8479bf1 100644
--- a/ee/app/workers/admin_emails_worker.rb
+++ b/ee/app/workers/admin_emails_worker.rb
@@ -3,6 +3,8 @@
 class AdminEmailsWorker
   include ApplicationWorker
 
+  feature_category_not_owned!
+
   # rubocop: disable CodeReuse/ActiveRecord
   def perform(recipient_id, subject, body)
     recipient_list(recipient_id).pluck(:id).uniq.each do |user_id|
diff --git a/ee/app/workers/clear_shared_runners_minutes_worker.rb b/ee/app/workers/clear_shared_runners_minutes_worker.rb
index ac50772435baa79081f361b7fc2701c5ac4c7ba7..651bc7fad9d3f75d443313aead216dc3ca968dc0 100644
--- a/ee/app/workers/clear_shared_runners_minutes_worker.rb
+++ b/ee/app/workers/clear_shared_runners_minutes_worker.rb
@@ -6,6 +6,8 @@ class ClearSharedRunnersMinutesWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :continuous_integration
+
   def perform
     return unless try_obtain_lease
 
diff --git a/ee/app/workers/concerns/geo_queue.rb b/ee/app/workers/concerns/geo_queue.rb
index 46eda1d04d64e968d920bcc61fa05400ca0a83ff..eaa53e9601c1825d83ac6df3a73e0ad9e59e4f4b 100644
--- a/ee/app/workers/concerns/geo_queue.rb
+++ b/ee/app/workers/concerns/geo_queue.rb
@@ -6,5 +6,6 @@ module GeoQueue
 
   included do
     queue_namespace :geo
+    feature_category :geo_replication
   end
 end
diff --git a/ee/app/workers/create_github_webhook_worker.rb b/ee/app/workers/create_github_webhook_worker.rb
index 80ac00997daeeb8d13948f3ecd7087cc08e6707a..707a5604599f976a408019b461b6d0213c98b815 100644
--- a/ee/app/workers/create_github_webhook_worker.rb
+++ b/ee/app/workers/create_github_webhook_worker.rb
@@ -4,6 +4,8 @@ class CreateGithubWebhookWorker
   include ApplicationWorker
   include GrapePathHelpers::NamedRouteMatcher
 
+  feature_category :integrations
+
   attr_reader :project
 
   def perform(project_id)
diff --git a/ee/app/workers/design_management/new_version_worker.rb b/ee/app/workers/design_management/new_version_worker.rb
index 2798d0f646cce19f7200e2341ed0a561902f770b..a3a34e80621a551a3dd72b301158d15af9cc74f2 100644
--- a/ee/app/workers/design_management/new_version_worker.rb
+++ b/ee/app/workers/design_management/new_version_worker.rb
@@ -4,6 +4,8 @@ module DesignManagement
   class NewVersionWorker
     include ApplicationWorker
 
+    feature_category :design_management
+
     def perform(version_id)
       version = DesignManagement::Version.find(version_id)
 
diff --git a/ee/app/workers/elastic_batch_project_indexer_worker.rb b/ee/app/workers/elastic_batch_project_indexer_worker.rb
index 2ec12c17b8051f48d1bee93eaaec6784bea67a24..c20edc0381af54d28c8bb39162d7c5f5e0494c8e 100644
--- a/ee/app/workers/elastic_batch_project_indexer_worker.rb
+++ b/ee/app/workers/elastic_batch_project_indexer_worker.rb
@@ -3,6 +3,8 @@
 class ElasticBatchProjectIndexerWorker
   include ApplicationWorker
 
+  feature_category :search
+
   # Batch indexing is a generally a onetime option, so give finer control over
   # queuing and concurrency
 
diff --git a/ee/app/workers/elastic_commit_indexer_worker.rb b/ee/app/workers/elastic_commit_indexer_worker.rb
index 9330cd06b74ba158fb70dddfd6f047a3b22aef89..cabc73f27bd5dbd59d51ad243ec27640b1aba73b 100644
--- a/ee/app/workers/elastic_commit_indexer_worker.rb
+++ b/ee/app/workers/elastic_commit_indexer_worker.rb
@@ -3,6 +3,7 @@
 class ElasticCommitIndexerWorker
   include ApplicationWorker
 
+  feature_category :search
   sidekiq_options retry: 2
 
   def perform(project_id, oldrev = nil, newrev = nil, wiki = false)
diff --git a/ee/app/workers/elastic_full_index_worker.rb b/ee/app/workers/elastic_full_index_worker.rb
index e0a61a922df5bae9a8f3a8924ce351b4433cfc38..5c844316c0e104f1770022cd9b4cb3349d0f122b 100644
--- a/ee/app/workers/elastic_full_index_worker.rb
+++ b/ee/app/workers/elastic_full_index_worker.rb
@@ -8,6 +8,7 @@ class ElasticFullIndexWorker
   include ApplicationWorker
 
   sidekiq_options retry: 2
+  feature_category :search
 
   def perform(start_id, end_id)
     return true unless Gitlab::CurrentSettings.elasticsearch_indexing?
diff --git a/ee/app/workers/elastic_indexer_worker.rb b/ee/app/workers/elastic_indexer_worker.rb
index 627422abece2e3cd5a717ef0f75579d5e7467412..820afde34eb504b714be15f9f02e7a64dcb1ae01 100644
--- a/ee/app/workers/elastic_indexer_worker.rb
+++ b/ee/app/workers/elastic_indexer_worker.rb
@@ -4,6 +4,7 @@ class ElasticIndexerWorker
   include Elasticsearch::Model::Client::ClassMethods
 
   sidekiq_options retry: 2
+  feature_category :search
 
   def perform(operation, class_name, record_id, es_id, options = {})
     return true unless Gitlab::CurrentSettings.elasticsearch_indexing?
@@ -18,12 +19,12 @@ def perform(operation, class_name, record_id, es_id, options = {})
         options
       )
     when /delete/
-      if klass.nested?
+      if options['es_parent']
         client.delete(
           index: klass.index_name,
           type: klass.document_type,
           id: es_id,
-          routing: options["es_parent"]
+          routing: options['es_parent']
         )
       else
         clear_project_data(record_id, es_id) if klass == Project
diff --git a/ee/app/workers/elastic_namespace_indexer_worker.rb b/ee/app/workers/elastic_namespace_indexer_worker.rb
index a493c53927556fefdcfef8793f48104b75ae00f9..d51ac44c20256ff97c77f83d0595ad5d749ab6ca 100644
--- a/ee/app/workers/elastic_namespace_indexer_worker.rb
+++ b/ee/app/workers/elastic_namespace_indexer_worker.rb
@@ -3,6 +3,7 @@
 class ElasticNamespaceIndexerWorker
   include ApplicationWorker
 
+  feature_category :search
   sidekiq_options retry: 2
 
   def perform(namespace_id, operation)
diff --git a/ee/app/workers/export_csv_worker.rb b/ee/app/workers/export_csv_worker.rb
index 84c66fd2a9c7661ed1dc8a409f4440729ea1f1d1..22a0c0619c3dbcfe91f06a7c833f1a710d3ac0db 100644
--- a/ee/app/workers/export_csv_worker.rb
+++ b/ee/app/workers/export_csv_worker.rb
@@ -3,6 +3,8 @@
 class ExportCsvWorker
   include ApplicationWorker
 
+  feature_category :issue_tracking
+
   def perform(current_user_id, project_id, params)
     @current_user = User.find(current_user_id)
     @project = Project.find(project_id)
diff --git a/ee/app/workers/geo/metrics_update_worker.rb b/ee/app/workers/geo/metrics_update_worker.rb
index 7c9f1ba76eef9465621d48ac269242702b15acc7..aefa07089dad81d2c8bd409f10a428641c92830b 100644
--- a/ee/app/workers/geo/metrics_update_worker.rb
+++ b/ee/app/workers/geo/metrics_update_worker.rb
@@ -6,6 +6,8 @@ class MetricsUpdateWorker
     include ExclusiveLeaseGuard
     include CronjobQueue
 
+    feature_category :geo_replication
+
     LEASE_TIMEOUT = 5.minutes
 
     def perform
diff --git a/ee/app/workers/geo/prune_event_log_worker.rb b/ee/app/workers/geo/prune_event_log_worker.rb
index 0b3b138427a43c637a2f1a703a1d137c0ae80f26..df8ea66d27703e7d3ffb18e741a4b20fe979c167 100644
--- a/ee/app/workers/geo/prune_event_log_worker.rb
+++ b/ee/app/workers/geo/prune_event_log_worker.rb
@@ -7,6 +7,8 @@ class PruneEventLogWorker
     include ::Gitlab::Utils::StrongMemoize
     include ::Gitlab::Geo::LogHelpers
 
+    feature_category :geo_replication
+
     LEASE_TIMEOUT = 5.minutes
 
     def perform
diff --git a/ee/app/workers/geo/scheduler/per_shard_scheduler_worker.rb b/ee/app/workers/geo/scheduler/per_shard_scheduler_worker.rb
index a8c06bdcc5b9b48fef673642e31cef929acc9a0f..899e4f83dbf43e38041a34b8734b403a5d259c2a 100644
--- a/ee/app/workers/geo/scheduler/per_shard_scheduler_worker.rb
+++ b/ee/app/workers/geo/scheduler/per_shard_scheduler_worker.rb
@@ -8,6 +8,8 @@ class PerShardSchedulerWorker
       include ::Gitlab::Geo::LogHelpers
       include ::EachShardWorker
 
+      feature_category :geo_replication
+
       def perform
         each_eligible_shard { |shard_name| schedule_job(shard_name) }
       end
diff --git a/ee/app/workers/geo/sidekiq_cron_config_worker.rb b/ee/app/workers/geo/sidekiq_cron_config_worker.rb
index 1567fc12debda0065720a415e233d4e82a3c3d77..b691dd6288f95bf247ac61c4f01a45b1dfc0a6c1 100644
--- a/ee/app/workers/geo/sidekiq_cron_config_worker.rb
+++ b/ee/app/workers/geo/sidekiq_cron_config_worker.rb
@@ -5,6 +5,8 @@ class SidekiqCronConfigWorker
     include ApplicationWorker
     include CronjobQueue
 
+    feature_category :geo_replication
+
     def perform
       Gitlab::Geo::CronManager.new.execute
     end
diff --git a/ee/app/workers/historical_data_worker.rb b/ee/app/workers/historical_data_worker.rb
index 178b29b298ed6371c9b66da421ffece366cbacd6..143704d9e399b9a8998f52cc25f918f0f9e7d7fb 100644
--- a/ee/app/workers/historical_data_worker.rb
+++ b/ee/app/workers/historical_data_worker.rb
@@ -4,6 +4,8 @@ class HistoricalDataWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :license_compliance
+
   def perform
     return if License.current.nil? || License.current&.trial?
 
diff --git a/ee/app/workers/import_software_licenses_worker.rb b/ee/app/workers/import_software_licenses_worker.rb
index aaa9df9797de1ad60c79c37d952fcf59184e6397..4a26608344202f74f7bb2221729b4b07b7fdc28d 100644
--- a/ee/app/workers/import_software_licenses_worker.rb
+++ b/ee/app/workers/import_software_licenses_worker.rb
@@ -4,6 +4,7 @@ class ImportSoftwareLicensesWorker
   include ApplicationWorker
 
   queue_namespace :cronjob
+  feature_category :license_compliance
 
   def perform
     catalogue.each do |spdx_license|
diff --git a/ee/app/workers/incident_management/process_alert_worker.rb b/ee/app/workers/incident_management/process_alert_worker.rb
index 4f4497747f43694b41ab96f330f2a25a0866c561..f3d5bc5c66b9f6e6d9e3cf28d247f589c0b9cc17 100644
--- a/ee/app/workers/incident_management/process_alert_worker.rb
+++ b/ee/app/workers/incident_management/process_alert_worker.rb
@@ -5,6 +5,7 @@ class ProcessAlertWorker
     include ApplicationWorker
 
     queue_namespace :incident_management
+    feature_category :incident_management
 
     def perform(project_id, alert)
       project = find_project(project_id)
diff --git a/ee/app/workers/incident_management/process_prometheus_alert_worker.rb b/ee/app/workers/incident_management/process_prometheus_alert_worker.rb
index 428e4f923d074954b0a9526a29da5bdc952c52ca..48003533acecaf7bebaf1d8dc7fd17831eae785d 100644
--- a/ee/app/workers/incident_management/process_prometheus_alert_worker.rb
+++ b/ee/app/workers/incident_management/process_prometheus_alert_worker.rb
@@ -5,12 +5,14 @@ class ProcessPrometheusAlertWorker
     include ApplicationWorker
 
     queue_namespace :incident_management
+    feature_category :incident_management
 
     def perform(project_id, alert_hash)
       project = find_project(project_id)
       return unless project
 
-      event = find_prometheus_alert_event(alert_hash)
+      parsed_alert = Gitlab::Alerting::Alert.new(project: project, payload: alert_hash)
+      event = find_prometheus_alert_event(parsed_alert)
       issue = create_issue(project, alert_hash)
 
       relate_issue_to_event(event, issue)
@@ -22,14 +24,26 @@ def find_project(project_id)
       Project.find_by_id(project_id)
     end
 
-    def find_prometheus_alert_event(alert_hash)
-      started_at = alert_hash.dig('startsAt')
-      gitlab_alert_id = alert_hash.dig('labels', 'gitlab_alert_id')
-      payload_key = PrometheusAlertEvent.payload_key_for(gitlab_alert_id, started_at)
+    def find_prometheus_alert_event(alert)
+      if alert.gitlab_managed?
+        find_gitlab_managed_event(alert)
+      else
+        find_self_managed_event(alert)
+      end
+    end
+
+    def find_gitlab_managed_event(alert)
+      payload_key = PrometheusAlertEvent.payload_key_for(alert.metric_id, alert.starts_at_raw)
 
       PrometheusAlertEvent.find_by_payload_key(payload_key)
     end
 
+    def find_self_managed_event(alert)
+      payload_key = SelfManagedPrometheusAlertEvent.payload_key_for(alert.starts_at_raw, alert.title, alert.full_query)
+
+      SelfManagedPrometheusAlertEvent.find_by_payload_key(payload_key)
+    end
+
     def create_issue(project, alert)
       IncidentManagement::CreateIssueService
         .new(project, alert)
diff --git a/ee/app/workers/jira_connect/sync_branch_worker.rb b/ee/app/workers/jira_connect/sync_branch_worker.rb
index 1603b0579393c98c7ceeb060bbbd3cab19bcc69a..3f7161a27cda2f6b4a5023cbe0188f34747833e4 100644
--- a/ee/app/workers/jira_connect/sync_branch_worker.rb
+++ b/ee/app/workers/jira_connect/sync_branch_worker.rb
@@ -5,6 +5,7 @@ class SyncBranchWorker
     include ApplicationWorker
 
     queue_namespace :jira_connect
+    feature_category :integrations
 
     def perform(project_id, branch_name, commit_shas)
       project = Project.find_by_id(project_id)
diff --git a/ee/app/workers/jira_connect/sync_merge_request_worker.rb b/ee/app/workers/jira_connect/sync_merge_request_worker.rb
index 097ca71dae68fbe05194ef89d6cbd75427309974..6eb69bb2c16a899fe232eed1ee5a64044ede4ea8 100644
--- a/ee/app/workers/jira_connect/sync_merge_request_worker.rb
+++ b/ee/app/workers/jira_connect/sync_merge_request_worker.rb
@@ -5,6 +5,7 @@ class SyncMergeRequestWorker
     include ApplicationWorker
 
     queue_namespace :jira_connect
+    feature_category :integrations
 
     def perform(merge_request_id)
       merge_request = MergeRequest.find_by_id(merge_request_id)
diff --git a/ee/app/workers/ldap_all_groups_sync_worker.rb b/ee/app/workers/ldap_all_groups_sync_worker.rb
index a98dea96d2e168bdd8b812e9fe5b2b2d3fbaf75f..651fad0cc402313c3d27643f0f0323e256d56c9f 100644
--- a/ee/app/workers/ldap_all_groups_sync_worker.rb
+++ b/ee/app/workers/ldap_all_groups_sync_worker.rb
@@ -4,6 +4,8 @@ class LdapAllGroupsSyncWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :authentication_and_authorization
+
   def perform
     return unless Gitlab::Auth::LDAP::Config.group_sync_enabled?
 
diff --git a/ee/app/workers/ldap_group_sync_worker.rb b/ee/app/workers/ldap_group_sync_worker.rb
index 3f4c7b9ed1537d9747a42985f4fb66ddb5c9d10a..7e9c5d82b1549a57e5ce054f9895d1a4a40fa2fa 100644
--- a/ee/app/workers/ldap_group_sync_worker.rb
+++ b/ee/app/workers/ldap_group_sync_worker.rb
@@ -3,6 +3,8 @@
 class LdapGroupSyncWorker
   include ApplicationWorker
 
+  feature_category :authentication_and_authorization
+
   # rubocop: disable CodeReuse/ActiveRecord
   def perform(group_ids, provider = nil)
     return unless Gitlab::Auth::LDAP::Config.group_sync_enabled?
diff --git a/ee/app/workers/ldap_sync_worker.rb b/ee/app/workers/ldap_sync_worker.rb
index 7b66a77a0e9b5a3c65ee9a0fccd453ad1f80c723..02735c0edf8f1f2e7e55e53cbeb0ebb3b61e53ef 100644
--- a/ee/app/workers/ldap_sync_worker.rb
+++ b/ee/app/workers/ldap_sync_worker.rb
@@ -4,6 +4,8 @@ class LdapSyncWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :authentication_and_authorization
+
   # rubocop: disable CodeReuse/ActiveRecord
   # rubocop: disable Gitlab/RailsLogger
   def perform
diff --git a/ee/app/workers/new_epic_worker.rb b/ee/app/workers/new_epic_worker.rb
index 8cf9915aa380396d99888750311062a9d7628d99..527254ae322c514e218e82800b373dfdc7a9fb17 100644
--- a/ee/app/workers/new_epic_worker.rb
+++ b/ee/app/workers/new_epic_worker.rb
@@ -4,6 +4,8 @@ class NewEpicWorker
   include ApplicationWorker
   include NewIssuable
 
+  feature_category :agile_portfolio_management
+
   def perform(epic_id, user_id)
     return unless objects_found?(epic_id, user_id)
 
diff --git a/ee/app/workers/project_import_schedule_worker.rb b/ee/app/workers/project_import_schedule_worker.rb
index 8daecf227a81b11ea1903342e0d88506a4bbc164..7c795fd4e64627e9b38ffaaa4145f77174a29a13 100644
--- a/ee/app/workers/project_import_schedule_worker.rb
+++ b/ee/app/workers/project_import_schedule_worker.rb
@@ -6,6 +6,7 @@ class ProjectImportScheduleWorker
   include ApplicationWorker
   prepend WaitableWorker
 
+  feature_category :importers
   sidekiq_options retry: false
 
   # rubocop: disable CodeReuse/ActiveRecord
diff --git a/ee/app/workers/project_update_repository_storage_worker.rb b/ee/app/workers/project_update_repository_storage_worker.rb
index 92389774b71cb52f42ecbeb4f3dc3fb30f085eb1..77b9b11f7d368f2c316b8e662f59a2cd8d1a7bfe 100644
--- a/ee/app/workers/project_update_repository_storage_worker.rb
+++ b/ee/app/workers/project_update_repository_storage_worker.rb
@@ -3,6 +3,8 @@
 class ProjectUpdateRepositoryStorageWorker
   include ApplicationWorker
 
+  feature_category :source_code_management
+
   def perform(project_id, new_repository_storage_key)
     project = Project.find(project_id)
 
diff --git a/ee/app/workers/pseudonymizer_worker.rb b/ee/app/workers/pseudonymizer_worker.rb
index 751266c33a55ac48f8770fdb8fcdd42870c47ffd..0feec6dbb5e71796a7c4a7291d0ddb07a9866946 100644
--- a/ee/app/workers/pseudonymizer_worker.rb
+++ b/ee/app/workers/pseudonymizer_worker.rb
@@ -4,6 +4,8 @@ class PseudonymizerWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :integrations
+
   def perform
     return unless Gitlab::CurrentSettings.pseudonymizer_enabled?
 
diff --git a/ee/app/workers/refresh_license_compliance_checks_worker.rb b/ee/app/workers/refresh_license_compliance_checks_worker.rb
index af78526561dde2bedde52c1d8e3df3986821f14c..3ffc049607eefea5e651667b46f34fa9bf143754 100644
--- a/ee/app/workers/refresh_license_compliance_checks_worker.rb
+++ b/ee/app/workers/refresh_license_compliance_checks_worker.rb
@@ -3,6 +3,8 @@
 class RefreshLicenseComplianceChecksWorker
   include ApplicationWorker
 
+  feature_category :license_compliance
+
   def perform(project_id)
     project = Project.find(project_id)
     project_approval_rule = project
diff --git a/ee/app/workers/repository_push_audit_event_worker.rb b/ee/app/workers/repository_push_audit_event_worker.rb
index 6ceb82a7d1573e82fe4fb70d17cf83bd0fddd22d..03e75c54d59c783867a67b3e8f34f45dfc0a6bac 100644
--- a/ee/app/workers/repository_push_audit_event_worker.rb
+++ b/ee/app/workers/repository_push_audit_event_worker.rb
@@ -3,6 +3,8 @@
 class RepositoryPushAuditEventWorker
   include ApplicationWorker
 
+  feature_category :authentication_and_authorization
+
   def perform(changes, project_id, user_id)
     project = Project.find(project_id)
     user = User.find(user_id)
diff --git a/ee/app/workers/repository_update_mirror_worker.rb b/ee/app/workers/repository_update_mirror_worker.rb
index a625ef572e0194682b7c666ef6224730e9e70020..040c0a39c6359d00d002d964eae5f933fd4fc0e5 100644
--- a/ee/app/workers/repository_update_mirror_worker.rb
+++ b/ee/app/workers/repository_update_mirror_worker.rb
@@ -7,6 +7,8 @@ class RepositoryUpdateMirrorWorker
   include Gitlab::ShellAdapter
   include ProjectStartImport
 
+  feature_category :importers
+
   # Retry not necessary. It will try again at the next update interval.
   sidekiq_options retry: false, status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
 
diff --git a/ee/app/workers/sync_security_reports_to_report_approval_rules_worker.rb b/ee/app/workers/sync_security_reports_to_report_approval_rules_worker.rb
index 2b0c45022734ca07eaff7974a5352754bd35b343..568ce1aa12661b2a714749839e77a095acdf12fd 100644
--- a/ee/app/workers/sync_security_reports_to_report_approval_rules_worker.rb
+++ b/ee/app/workers/sync_security_reports_to_report_approval_rules_worker.rb
@@ -6,6 +6,8 @@ class SyncSecurityReportsToReportApprovalRulesWorker
   include ApplicationWorker
   include PipelineQueue
 
+  feature_category :static_application_security_testing
+
   def perform(pipeline_id)
     pipeline = Ci::Pipeline.find_by_id(pipeline_id)
     return unless pipeline
diff --git a/ee/app/workers/update_all_mirrors_worker.rb b/ee/app/workers/update_all_mirrors_worker.rb
index a186f6401855bb9a8ece670b28d7ed58514ad755..dfb3e6b2da076b88313e09edcdc599cd9c60de14 100644
--- a/ee/app/workers/update_all_mirrors_worker.rb
+++ b/ee/app/workers/update_all_mirrors_worker.rb
@@ -4,6 +4,8 @@ class UpdateAllMirrorsWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :source_code_management
+
   LEASE_TIMEOUT = 5.minutes
   SCHEDULE_WAIT_TIMEOUT = 4.minutes
   LEASE_KEY = 'update_all_mirrors'.freeze
diff --git a/ee/app/workers/update_max_seats_used_for_gitlab_com_subscriptions_worker.rb b/ee/app/workers/update_max_seats_used_for_gitlab_com_subscriptions_worker.rb
index 82eefae1c9762b4043b98dde0c464eb042f9a7cc..77207b01041d660b8582d18ff6117b73b0e2bf53 100644
--- a/ee/app/workers/update_max_seats_used_for_gitlab_com_subscriptions_worker.rb
+++ b/ee/app/workers/update_max_seats_used_for_gitlab_com_subscriptions_worker.rb
@@ -4,6 +4,8 @@ class UpdateMaxSeatsUsedForGitlabComSubscriptionsWorker
   include ApplicationWorker
   include CronjobQueue
 
+  feature_category :license_compliance
+
   # rubocop: disable CodeReuse/ActiveRecord
   def perform
     return if ::Gitlab::Database.read_only?
diff --git a/ee/changelogs/unreleased/11089-allow-users-to-destroy-hide-designs.yml b/ee/changelogs/unreleased/11089-allow-users-to-destroy-hide-designs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..57c0917e9ddef582c544e8713a8d8babb495d5b3
--- /dev/null
+++ b/ee/changelogs/unreleased/11089-allow-users-to-destroy-hide-designs.yml
@@ -0,0 +1,5 @@
+---
+title: Front-End UI for design deletion
+merge_request: 15034
+author:
+type: added
diff --git a/ee/changelogs/unreleased/13034-update-dast-report-fixture.yml b/ee/changelogs/unreleased/13034-update-dast-report-fixture.yml
new file mode 100644
index 0000000000000000000000000000000000000000..08e935d2733b97d6f65e0ee08d0644b5b8bb3146
--- /dev/null
+++ b/ee/changelogs/unreleased/13034-update-dast-report-fixture.yml
@@ -0,0 +1,5 @@
+---
+title: Fix deduplication of WASC vulnerabilities in the Security dashboard
+merge_request: 17778
+author:
+type: fixed
diff --git a/ee/changelogs/unreleased/13083-js-diffing-v2-reports.yml b/ee/changelogs/unreleased/13083-js-diffing-v2-reports.yml
new file mode 100644
index 0000000000000000000000000000000000000000..626938265f306b4e3e9d2082ed77ce40fbb71c65
--- /dev/null
+++ b/ee/changelogs/unreleased/13083-js-diffing-v2-reports.yml
@@ -0,0 +1,5 @@
+---
+title: Update the frontend diffing code to support v2 license scan reports
+merge_request: 18105
+author:
+type: changed
diff --git a/ee/changelogs/unreleased/32765-vuln-check-approvals-required.yml b/ee/changelogs/unreleased/32765-vuln-check-approvals-required.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6f5c90aede8d0eb893a47d68131ec13d7e843dc0
--- /dev/null
+++ b/ee/changelogs/unreleased/32765-vuln-check-approvals-required.yml
@@ -0,0 +1,5 @@
+---
+title: Add default empty values to prevent parser errors from approving the Vulnerability-Check rule
+merge_request: 18423
+author:
+type: fixed
diff --git a/ee/changelogs/unreleased/33184-associate-self-managed-prometheus-alerts-and-issues.yml b/ee/changelogs/unreleased/33184-associate-self-managed-prometheus-alerts-and-issues.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8f55354225a86d3a7fae314ac1698b1460b4b4bd
--- /dev/null
+++ b/ee/changelogs/unreleased/33184-associate-self-managed-prometheus-alerts-and-issues.yml
@@ -0,0 +1,5 @@
+---
+title: Associate self-managed Prometheus Alerts and Issues
+merge_request: 18046
+author:
+type: added
diff --git a/ee/changelogs/unreleased/34291-code-owner-ribbon.yml b/ee/changelogs/unreleased/34291-code-owner-ribbon.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6dd5109c0e2c4b1a84c1852dfa6b9e0138de7d17
--- /dev/null
+++ b/ee/changelogs/unreleased/34291-code-owner-ribbon.yml
@@ -0,0 +1,5 @@
+---
+title: Add alert message for feature 'require approval from code owners' being moved
+merge_request: 18715
+author:
+type: changed
diff --git a/ee/changelogs/unreleased/5503-dast-for-the-default-branch.yml b/ee/changelogs/unreleased/5503-dast-for-the-default-branch.yml
new file mode 100644
index 0000000000000000000000000000000000000000..51c1edf9e54d8f13f2964df50443cd96721c4809
--- /dev/null
+++ b/ee/changelogs/unreleased/5503-dast-for-the-default-branch.yml
@@ -0,0 +1,5 @@
+---
+title: Implement DAST for default branches
+merge_request: 17789
+author:
+type: added
diff --git a/ee/changelogs/unreleased/9102-hide-dismissed-vulnerabilities-dismiss-action.yml b/ee/changelogs/unreleased/9102-hide-dismissed-vulnerabilities-dismiss-action.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b66bea61969e67ed695f9c871244dcc72389b30e
--- /dev/null
+++ b/ee/changelogs/unreleased/9102-hide-dismissed-vulnerabilities-dismiss-action.yml
@@ -0,0 +1,5 @@
+---
+title: Implement dismissal behaviour when dismissed vulnerabilities are hidden
+merge_request: 16207
+author:
+type: changed
diff --git a/ee/changelogs/unreleased/add-purchase-point-to-group-billing-page.yml b/ee/changelogs/unreleased/add-purchase-point-to-group-billing-page.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6c38173a995620374313dfe0f5d8852793b864a4
--- /dev/null
+++ b/ee/changelogs/unreleased/add-purchase-point-to-group-billing-page.yml
@@ -0,0 +1,5 @@
+---
+title: Show Billing Plan as Cards in profile and groups
+merge_request: 15437
+author:
+type: added
diff --git a/ee/changelogs/unreleased/enable-pa-ff-by-default.yml b/ee/changelogs/unreleased/enable-pa-ff-by-default.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e8a49af33fc3203859d14524ababf709d9f499e6
--- /dev/null
+++ b/ee/changelogs/unreleased/enable-pa-ff-by-default.yml
@@ -0,0 +1,5 @@
+---
+title: Enable Productivity Analytics feature by default
+merge_request: 18754
+author:
+type: changed
diff --git a/ee/changelogs/unreleased/issue_31913.yml b/ee/changelogs/unreleased/issue_31913.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b021984929e98e0e71f9a2263cc00c88ab46d316
--- /dev/null
+++ b/ee/changelogs/unreleased/issue_31913.yml
@@ -0,0 +1,5 @@
+---
+title: Expose subscribed attribute for Epics in GraphQL
+merge_request: 18607
+author:
+type: added
diff --git a/ee/changelogs/unreleased/issue_31920.yml b/ee/changelogs/unreleased/issue_31920.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0205bd1bacad02a17db043a25e3820326bc3925f
--- /dev/null
+++ b/ee/changelogs/unreleased/issue_31920.yml
@@ -0,0 +1,5 @@
+---
+title: Expose epic participants on GraphQL
+merge_request: 18691
+author:
+type: added
diff --git a/ee/changelogs/unreleased/remove-generic_alert_endpoint-feature-flag.yml b/ee/changelogs/unreleased/remove-generic_alert_endpoint-feature-flag.yml
new file mode 100644
index 0000000000000000000000000000000000000000..59b03ec27df64ef2478b6bdaf9f39274f6e16b31
--- /dev/null
+++ b/ee/changelogs/unreleased/remove-generic_alert_endpoint-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Adds a generic alert integration which can accept alerts from any source via a generic webhook receiver.
+merge_request:
+author:
+type: added
diff --git a/ee/changelogs/unreleased/rjain-remove-old-label.yml b/ee/changelogs/unreleased/rjain-remove-old-label.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fc1faa5d4debb61398c805b4fd08da4b3b3b9d83
--- /dev/null
+++ b/ee/changelogs/unreleased/rjain-remove-old-label.yml
@@ -0,0 +1,5 @@
+---
+title: Scoped labels do not remove old label in board sidebar
+merge_request: 18313
+author:
+type: fixed
diff --git a/ee/changelogs/unreleased/update-design-management-empty-state-button-style.yml b/ee/changelogs/unreleased/update-design-management-empty-state-button-style.yml
new file mode 100644
index 0000000000000000000000000000000000000000..101b8486198cb7babea4802e142380da20055844
--- /dev/null
+++ b/ee/changelogs/unreleased/update-design-management-empty-state-button-style.yml
@@ -0,0 +1,5 @@
+---
+title: Change design management empty state button style
+merge_request: 18060
+author: George Tsiolis
+type: fixed
diff --git a/ee/changelogs/unreleased/winh-issues-api-epic-ee.yml b/ee/changelogs/unreleased/winh-issues-api-epic-ee.yml
new file mode 100644
index 0000000000000000000000000000000000000000..552c34015aa073980650d4ba905167c521be3194
--- /dev/null
+++ b/ee/changelogs/unreleased/winh-issues-api-epic-ee.yml
@@ -0,0 +1,5 @@
+---
+title: Add epic_iid parameter to issues API
+merge_request: 15640
+author:
+type: changed
diff --git a/ee/config/routes/analytics.rb b/ee/config/routes/analytics.rb
index 645c324e7038f51193ce2cf0185831c9599ddacc..af5c65a8997e16ac3d056ee18886a8d857438e08 100644
--- a/ee/config/routes/analytics.rb
+++ b/ee/config/routes/analytics.rb
@@ -10,7 +10,7 @@
   constraints(::Constraints::FeatureConstrainer.new(Gitlab::Analytics::CYCLE_ANALYTICS_FEATURE_FLAG)) do
     resource :cycle_analytics, only: :show
     namespace :cycle_analytics do
-      resources :stages, only: [:index]
+      resources :stages, only: [:index, :create, :update, :destroy]
     end
   end
 
diff --git a/ee/db/fixtures/development/27_plans.rb b/ee/db/fixtures/development/27_plans.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b0aa2e88e80276a2bac9099bc36b18cbf13f8759
--- /dev/null
+++ b/ee/db/fixtures/development/27_plans.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+Gitlab::Seeder.quiet do
+  Plan::ALL_HOSTED_PLANS.each do |plan|
+    Plan.create!(name: plan, title: plan.titleize)
+
+    print '.'
+  end
+end
diff --git a/ee/lib/api/epics.rb b/ee/lib/api/epics.rb
index ce77ff053723f49a1459c9ee44a201278fd8919c..4af281cde430df51835f255b410607641da2a0d7 100644
--- a/ee/lib/api/epics.rb
+++ b/ee/lib/api/epics.rb
@@ -54,7 +54,9 @@ class Epics < Grape::API
       get ':id/(-/)epics/:epic_iid' do
         authorize_can_read!
 
-        present epic, with: EE::API::Entities::Epic, user: current_user
+        present epic, options, user: current_user,
+                               with: EE::API::Entities::Epic,
+                               include_subscribed: true
       end
 
       desc 'Create a new epic' do
diff --git a/ee/lib/api/feature_flags.rb b/ee/lib/api/feature_flags.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f0c577bcb2c3667a6f31e5bf391b3e1305f3cd90
--- /dev/null
+++ b/ee/lib/api/feature_flags.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+module API
+  class FeatureFlags < Grape::API
+    include PaginationParams
+
+    FEATURE_FLAG_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS
+        .merge(name: API::NO_SLASH_URL_PART_REGEX)
+
+    before do
+      not_found! unless Feature.enabled?(:feature_flag_api, user_project)
+      authorize_read_feature_flags!
+    end
+
+    params do
+      requires :id, type: String, desc: 'The ID of a project'
+    end
+    resource 'projects/:id', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+      resource :feature_flags do
+        desc 'Get all feature flags of a project' do
+          success EE::API::Entities::FeatureFlag
+        end
+        params do
+          optional :scope, type: String, desc: 'The scope of feature flags',
+                                         values: %w[enabled disabled]
+          use :pagination
+        end
+        get do
+          feature_flags = ::FeatureFlagsFinder
+            .new(user_project, current_user, declared_params(include_missing: false))
+            .execute
+
+          present paginate(feature_flags), with: EE::API::Entities::FeatureFlag
+        end
+
+        desc 'Create a new feature flag' do
+          success EE::API::Entities::FeatureFlag
+        end
+        params do
+          requires :name, type: String, desc: 'The name of feature flag'
+          optional :description, type: String, desc: 'The description of the feature flag'
+          optional :scopes, type: Array do
+            requires :environment_scope, type: String, desc: 'The environment scope of the scope'
+            requires :active, type: Boolean, desc: 'Active/inactive of the scope'
+            requires :strategies, type: JSON, desc: 'The strategies of the scope'
+          end
+        end
+        post do
+          authorize_create_feature_flag!
+
+          param = declared_params(include_missing: false)
+          param[:scopes_attributes] = param.delete(:scopes) if param.key?(:scopes)
+
+          result = ::FeatureFlags::CreateService
+            .new(user_project, current_user, param)
+            .execute
+
+          if result[:status] == :success
+            present result[:feature_flag], with: EE::API::Entities::FeatureFlag
+          else
+            render_api_error!(result[:message], result[:http_status])
+          end
+        end
+      end
+
+      params do
+        requires :name, type: String, desc: 'The name of the feature flag'
+      end
+      resource 'feature_flags/:name', requirements: FEATURE_FLAG_ENDPOINT_REQUIREMENTS do
+        desc 'Get a feature flag of a project' do
+          success EE::API::Entities::FeatureFlag
+        end
+        get do
+          authorize_read_feature_flag!
+
+          present feature_flag, with: EE::API::Entities::FeatureFlag
+        end
+
+        desc 'Delete a feature flag' do
+          success EE::API::Entities::FeatureFlag
+        end
+        delete do
+          authorize_destroy_feature_flag!
+
+          result = ::FeatureFlags::DestroyService
+            .new(user_project, current_user, declared_params(include_missing: false))
+            .execute(feature_flag)
+
+          if result[:status] == :success
+            present result[:feature_flag], with: EE::API::Entities::FeatureFlag
+          else
+            render_api_error!(result[:message], result[:http_status])
+          end
+        end
+      end
+    end
+
+    helpers do
+      def authorize_read_feature_flags!
+        authorize! :read_feature_flag, user_project
+      end
+
+      def authorize_read_feature_flag!
+        authorize! :read_feature_flag, feature_flag
+      end
+
+      def authorize_create_feature_flag!
+        authorize! :create_feature_flag, user_project
+      end
+
+      def authorize_destroy_feature_flag!
+        authorize! :destroy_feature_flag, feature_flag
+      end
+
+      def feature_flag
+        @feature_flag ||=
+          user_project.operations_feature_flags.find_by_name!(params[:name])
+      end
+    end
+  end
+end
diff --git a/ee/lib/api/vulnerabilities.rb b/ee/lib/api/vulnerabilities.rb
index 63e58a366ba8266ce0193f8b55e279c275f7e4d4..93a19ad1bfe4507cf819e578baeba240afd13675 100644
--- a/ee/lib/api/vulnerabilities.rb
+++ b/ee/lib/api/vulnerabilities.rb
@@ -10,6 +10,24 @@ class Vulnerabilities < Grape::API
       def vulnerabilities_by(project)
         Security::VulnerabilitiesFinder.new(project).execute
       end
+
+      def find_vulnerability!
+        Vulnerability.with_findings.find(params[:id])
+      end
+
+      def find_and_authorize_vulnerability!(action)
+        find_vulnerability!.tap do |vulnerability|
+          authorize! action, vulnerability.project
+        end
+      end
+
+      def render_vulnerability(vulnerability)
+        if vulnerability.valid?
+          present vulnerability, with: VulnerabilityEntity
+        else
+          render_validation_error!(vulnerability)
+        end
+      end
     end
 
     before do
@@ -17,9 +35,28 @@ def vulnerabilities_by(project)
     end
 
     params do
-      requires :id, type: String, desc: 'The ID of a project'
+      requires :id, type: String, desc: 'The ID of a vulnerability'
+    end
+    resource :vulnerabilities do
+      desc 'Dismiss a vulnerability' do
+        success VulnerabilityEntity
+      end
+      post ':id/dismiss' do
+        if Feature.enabled?(:first_class_vulnerabilities)
+          vulnerability = find_and_authorize_vulnerability!(:dismiss_vulnerability)
+          break not_modified! if vulnerability.closed?
+
+          vulnerability = ::Vulnerabilities::DismissService.new(current_user, vulnerability).execute
+          render_vulnerability(vulnerability)
+        else
+          not_found!
+        end
+      end
     end
 
+    params do
+      requires :id, type: String, desc: 'The ID of a project'
+    end
     resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
       params do
         # These params have no effect for Vulnerabilities API but are required to support falling back to
diff --git a/ee/lib/ee.rb b/ee/lib/ee.rb
index 83ba929b054729733d377cec79cce46ab6922070..c120b626f4ac0912e5e0aa2fad63a05be4c5000b 100644
--- a/ee/lib/ee.rb
+++ b/ee/lib/ee.rb
@@ -2,7 +2,8 @@
 
 module EE
   SUBSCRIPTIONS_URL = ENV.fetch('CUSTOMER_PORTAL_URL', 'https://customers.gitlab.com').freeze
-  SUBSCRIPTIONS_PLANS_URL = "#{SUBSCRIPTIONS_URL}/plans".freeze
+  SUBSCRIPTIONS_COMPARISON_URL = "https://about.gitlab.com/pricing/gitlab-com/feature-comparison".freeze
   SUBSCRIPTIONS_MORE_MINUTES_URL = "#{SUBSCRIPTIONS_URL}/buy_pipeline_minutes".freeze
+  SUBSCRIPTIONS_PLANS_URL = "#{SUBSCRIPTIONS_URL}/plans".freeze
   CUSTOMER_SUPPORT_URL = 'https://support.gitlab.com'.freeze
 end
diff --git a/ee/lib/ee/api/api.rb b/ee/lib/ee/api/api.rb
index 375dd0a67f13bfd56883e7fb8f332327eb3fe363..3adfa1d2250bc4464160aa0eacda260a26dfb6d8 100644
--- a/ee/lib/ee/api/api.rb
+++ b/ee/lib/ee/api/api.rb
@@ -18,6 +18,7 @@ module API
         mount ::API::EpicIssues
         mount ::API::EpicLinks
         mount ::API::Epics
+        mount ::API::FeatureFlags
         mount ::API::ContainerRegistryEvent
         mount ::API::Geo
         mount ::API::GeoNodes
diff --git a/ee/lib/ee/api/entities.rb b/ee/lib/ee/api/entities.rb
index 6a5e8622ef45132d076b05c9eae64bcb2f65b467..54dbb3070f9d27953ffde6f33d544cc34563bc08 100644
--- a/ee/lib/ee/api/entities.rb
+++ b/ee/lib/ee/api/entities.rb
@@ -147,7 +147,7 @@ module Board
           expose :name
           expose :group, using: ::API::Entities::BasicGroupDetails
 
-          with_options if: ->(board, _) { board.parent.feature_available?(:scoped_issue_board) } do
+          with_options if: ->(board, _) { board.resource_parent.feature_available?(:scoped_issue_board) } do
             expose :milestone do |board|
               if board.milestone.is_a?(Milestone)
                 ::API::Entities::Milestone.represent(board.milestone)
@@ -168,7 +168,7 @@ module List
         prepended do
           expose :milestone, using: ::API::Entities::Milestone, if: -> (entity, _) { entity.milestone? }
           expose :user, as: :assignee, using: ::API::Entities::UserSafe, if: -> (entity, _) { entity.assignee? }
-          expose :max_issue_count, if: -> (list, _) { list.board.parent.feature_available?(:wip_limits) }
+          expose :max_issue_count, if: -> (list, _) { list.board.resource_parent.feature_available?(:wip_limits) }
         end
       end
 
@@ -323,6 +323,15 @@ class Epic < Grape::Entity
           end
         end
 
+        # Calculating the value of subscribed field triggers Markdown
+        # processing. We can't do that for multiple epics
+        # requests in a single API request.
+        expose :subscribed, if: -> (_, options) { options.fetch(:include_subscribed, false) } do |epic, options|
+          user = options[:user]
+
+          user.present? ? epic.subscribed?(user) : false
+        end
+
         def web_url
           ::Gitlab::Routing.url_helpers.group_epic_url(object.group, object)
         end
@@ -562,7 +571,7 @@ class GeoNode < Grape::Entity
         expose :repos_max_capacity
         expose :verification_max_capacity
         expose :container_repositories_max_capacity
-        expose :sync_object_storage, if: ->(geo_node, _) { ::Feature.enabled?(:geo_object_storage_replication) && geo_node.secondary? }
+        expose :sync_object_storage, if: ->(geo_node, _) { geo_node.secondary? }
 
         # Retained for backwards compatibility. Remove in API v5
         expose :clone_protocol do |_record, _options|
@@ -834,6 +843,23 @@ def can_read_vulnerabilities?(user, project)
           Ability.allowed?(user, :read_project_security_dashboard, project)
         end
       end
+
+      class FeatureFlag < Grape::Entity
+        class Scope < Grape::Entity
+          expose :id
+          expose :active
+          expose :environment_scope
+          expose :strategies
+          expose :created_at
+          expose :updated_at
+        end
+
+        expose :name
+        expose :description
+        expose :created_at
+        expose :updated_at
+        expose :scopes, using: Scope
+      end
     end
   end
 end
diff --git a/ee/lib/ee/api/helpers/issues_helpers.rb b/ee/lib/ee/api/helpers/issues_helpers.rb
index 2533b7a043e815ed837888898fa6c5277f4ebf04..561ccec10c7e128b4518dbaf66fc6d128ce302e0 100644
--- a/ee/lib/ee/api/helpers/issues_helpers.rb
+++ b/ee/lib/ee/api/helpers/issues_helpers.rb
@@ -9,6 +9,7 @@ module IssuesHelpers
         prepended do
           params :optional_issue_params_ee do
             optional :weight, type: Integer, desc: 'The weight of the issue'
+            optional :epic_iid, type: Integer, desc: 'The IID of an epic to associate the issue with'
           end
 
           params :optional_issues_params_ee do
@@ -21,7 +22,7 @@ module IssuesHelpers
 
           override :update_params_at_least_one_of
           def update_params_at_least_one_of
-            [*super, :weight]
+            [*super, :weight, :epic_iid]
           end
 
           override :sort_options
diff --git a/ee/lib/ee/gitlab/ci/config/entry/jobs.rb b/ee/lib/ee/gitlab/ci/config/entry/jobs.rb
index 01f2f4df1714bd7196bed2b13e0d929166cdecf5..48b7e8685bfcaacad8fd6cde614778986d6f802b 100644
--- a/ee/lib/ee/gitlab/ci/config/entry/jobs.rb
+++ b/ee/lib/ee/gitlab/ci/config/entry/jobs.rb
@@ -9,7 +9,7 @@ module Jobs
             extend ActiveSupport::Concern
 
             prepended do
-              EE_TYPES = const_get(:TYPES) + [::EE::Gitlab::Ci::Config::Entry::Bridge]
+              EE_TYPES = const_get(:TYPES, false) + [::EE::Gitlab::Ci::Config::Entry::Bridge]
             end
 
             class_methods do
diff --git a/ee/lib/ee/gitlab/ci/status/build/failed.rb b/ee/lib/ee/gitlab/ci/status/build/failed.rb
index 62cf8f8685b1227fd12df09cf8336b807ac3d435..3f3fdc0cf4005b19749c5ebd6b0c509ff6552016 100644
--- a/ee/lib/ee/gitlab/ci/status/build/failed.rb
+++ b/ee/lib/ee/gitlab/ci/status/build/failed.rb
@@ -9,7 +9,7 @@ module Failed
             extend ActiveSupport::Concern
 
             prepended do
-              EE_REASONS = const_get(:REASONS).merge(
+              EE_REASONS = const_get(:REASONS, false).merge(
                 protected_environment_failure: 'protected environment failure',
                 invalid_bridge_trigger: 'downstream pipeline trigger definition is invalid',
                 downstream_bridge_project_not_found: 'downstream project could not be found',
diff --git a/ee/lib/elastic/class_proxy_util.rb b/ee/lib/elastic/class_proxy_util.rb
index dd6fe7b21e6f3bbffebd0919f79e2d66041e5498..897c95f733c91e2de485917e5c0d93fb950111d5 100644
--- a/ee/lib/elastic/class_proxy_util.rb
+++ b/ee/lib/elastic/class_proxy_util.rb
@@ -9,7 +9,7 @@ module ClassProxyUtil
     def initialize(target)
       super(target)
 
-      config = version_namespace.const_get('Config')
+      config = version_namespace.const_get('Config', false)
 
       @index_name = config.index_name
       @document_type = config.document_type
diff --git a/ee/lib/elastic/instance_proxy_util.rb b/ee/lib/elastic/instance_proxy_util.rb
index 8af7738363c29b597c69fb0d7acfdcec668fe822..b285b0d7a7ca7039edc80856db39e099170f4a68 100644
--- a/ee/lib/elastic/instance_proxy_util.rb
+++ b/ee/lib/elastic/instance_proxy_util.rb
@@ -9,7 +9,7 @@ module InstanceProxyUtil
     def initialize(target)
       super(target)
 
-      config = version_namespace.const_get('Config')
+      config = version_namespace.const_get('Config', false)
 
       @index_name = config.index_name
       @document_type = config.document_type
diff --git a/ee/lib/elastic/latest/application_class_proxy.rb b/ee/lib/elastic/latest/application_class_proxy.rb
index 2ffbe12747d156f7c870a6bf53a5bfe46e6c1aa0..85b4263f4506a48c1bc4d2e6e44d5a37c53f7f3e 100644
--- a/ee/lib/elastic/latest/application_class_proxy.rb
+++ b/ee/lib/elastic/latest/application_class_proxy.rb
@@ -5,11 +5,6 @@ module Latest
     class ApplicationClassProxy < Elasticsearch::Model::Proxy::ClassMethodsProxy
       include ClassProxyUtil
 
-      # Should be overridden for all nested models
-      def nested?
-        false
-      end
-
       def es_type
         target.name.underscore
       end
diff --git a/ee/lib/elastic/latest/application_instance_proxy.rb b/ee/lib/elastic/latest/application_instance_proxy.rb
index c3d32bf83516baa6f3002b5d51b1bb89c975f432..81c94624a1743e418bf63f7966ef51f2a4931077 100644
--- a/ee/lib/elastic/latest/application_instance_proxy.rb
+++ b/ee/lib/elastic/latest/application_instance_proxy.rb
@@ -20,13 +20,16 @@ def es_id
       private
 
       def generic_attributes
-        {
-          'join_field' => {
+        attributes = { 'type' => es_type }
+
+        if es_parent
+          attributes['join_field'] = {
             'name' => es_type,
             'parent' => es_parent
-          },
-          'type' => es_type
-        }
+          }
+        end
+
+        attributes
       end
     end
   end
diff --git a/ee/lib/elastic/latest/config.rb b/ee/lib/elastic/latest/config.rb
index fc60496cb570699dec46038646c5df6d621febc3..5c8778da49b6e5563e427cb7d37515a92b642df4 100644
--- a/ee/lib/elastic/latest/config.rb
+++ b/ee/lib/elastic/latest/config.rb
@@ -66,6 +66,7 @@ module Config
               blob
               wiki_blob
               commit
+              snippet
             )
           }
         # ES6 requires a single type per index, so we implement our own "type"
diff --git a/ee/lib/elastic/latest/issue_class_proxy.rb b/ee/lib/elastic/latest/issue_class_proxy.rb
index 356d8ae4905e4f23339dee3f009dcdfde8b78cc4..78b373ba3885d543dd74dd327fd4ae12c9a20e28 100644
--- a/ee/lib/elastic/latest/issue_class_proxy.rb
+++ b/ee/lib/elastic/latest/issue_class_proxy.rb
@@ -3,10 +3,6 @@
 module Elastic
   module Latest
     class IssueClassProxy < ApplicationClassProxy
-      def nested?
-        true
-      end
-
       def elastic_search(query, options: {})
         query_hash =
           if query =~ /#(\d+)\z/
diff --git a/ee/lib/elastic/latest/merge_request_class_proxy.rb b/ee/lib/elastic/latest/merge_request_class_proxy.rb
index a2465d72d68585680d6fa06d0cf00910db88d86e..3db3b887c4893998b41924de134640fe79bb4e40 100644
--- a/ee/lib/elastic/latest/merge_request_class_proxy.rb
+++ b/ee/lib/elastic/latest/merge_request_class_proxy.rb
@@ -3,10 +3,6 @@
 module Elastic
   module Latest
     class MergeRequestClassProxy < ApplicationClassProxy
-      def nested?
-        true
-      end
-
       def elastic_search(query, options: {})
         query_hash =
           if query =~ /\!(\d+)\z/
diff --git a/ee/lib/elastic/latest/milestone_class_proxy.rb b/ee/lib/elastic/latest/milestone_class_proxy.rb
index 195fa4967dd26c318c089d6f66a0f36533cf509c..c0b4d786bb7f3d70421397ac6145d465154fa9f1 100644
--- a/ee/lib/elastic/latest/milestone_class_proxy.rb
+++ b/ee/lib/elastic/latest/milestone_class_proxy.rb
@@ -3,10 +3,6 @@
 module Elastic
   module Latest
     class MilestoneClassProxy < ApplicationClassProxy
-      def nested?
-        true
-      end
-
       def elastic_search(query, options: {})
         options[:in] = %w(title^2 description)
 
diff --git a/ee/lib/elastic/latest/note_class_proxy.rb b/ee/lib/elastic/latest/note_class_proxy.rb
index 673473631d2a130d50ba17b9e63f979cfe883846..a2132331d13f3999f8e6121c7ff3fe41b5cb1a74 100644
--- a/ee/lib/elastic/latest/note_class_proxy.rb
+++ b/ee/lib/elastic/latest/note_class_proxy.rb
@@ -7,10 +7,6 @@ def es_type
         'note'
       end
 
-      def nested?
-        true
-      end
-
       def elastic_search(query, options: {})
         options[:in] = ['note']
 
diff --git a/ee/lib/elastic/latest/note_instance_proxy.rb b/ee/lib/elastic/latest/note_instance_proxy.rb
index 2575b08c89b88ab3527883bede72c88c0de2b8f7..9fa201056eae775c625c97df19411d351dddf2e9 100644
--- a/ee/lib/elastic/latest/note_instance_proxy.rb
+++ b/ee/lib/elastic/latest/note_instance_proxy.rb
@@ -16,9 +16,9 @@ def as_indexed_json(options = {})
 
         if noteable.is_a?(Issue)
           data['issue'] = {
-            assignee_id: noteable.assignee_ids,
-            author_id: noteable.author_id,
-            confidential: noteable.confidential
+            'assignee_id' => noteable.assignee_ids,
+            'author_id' => noteable.author_id,
+            'confidential' => noteable.confidential
           }
         end
 
diff --git a/ee/lib/elastic/latest/snippet_class_proxy.rb b/ee/lib/elastic/latest/snippet_class_proxy.rb
index 72c5915f4e40937ae2464a235db07e36e044b61b..b7256b0941c1e7f469c6503729c8d5ee6c4b8c00 100644
--- a/ee/lib/elastic/latest/snippet_class_proxy.rb
+++ b/ee/lib/elastic/latest/snippet_class_proxy.rb
@@ -5,14 +5,14 @@ module Latest
     class SnippetClassProxy < ApplicationClassProxy
       def elastic_search(query, options: {})
         query_hash = basic_query_hash(%w(title file_name), query)
-        query_hash = filter(query_hash, options[:user])
+        query_hash = filter(query_hash, options)
 
         search(query_hash)
       end
 
       def elastic_search_code(query, options: {})
         query_hash = basic_query_hash(%w(content), query)
-        query_hash = filter(query_hash, options[:user])
+        query_hash = filter(query_hash, options)
 
         search(query_hash)
       end
@@ -23,50 +23,91 @@ def es_type
 
       private
 
-      def filter(query_hash, user)
-        return query_hash if user && user.full_private_access?
-
-        filter =
-          if user
-            {
-              bool: {
-                should: [
-                  { term: { author_id: user.id } },
-                  { terms: { project_id: authorized_project_ids_for_user(user) } },
-                  {
-                    bool: {
-                      filter: [
-                        { terms: { visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL] } },
-                        { term: { type: self.es_type } }
-                      ],
-                      must_not: { exists: { field: 'project_id' } }
-                    }
-                  }
-                ]
-              }
-            }
-          else
-            {
-              bool: {
-                filter: [
-                  { term: { visibility_level: Snippet::PUBLIC } },
-                  { term: { type: self.es_type } }
-                ],
-                must_not: { exists: { field: 'project_id' } }
-              }
-            }
-          end
+      def filter(query_hash, options)
+        user = options[:current_user]
+        return query_hash if user&.full_private_access?
+
+        filter_conditions =
+          filter_personal_snippets(user, options) +
+          filter_project_snippets(user, options)
+
+        # Match any of the filter conditions, in addition to the existing conditions
+        query_hash[:query][:bool][:filter] << {
+          bool: {
+            should: filter_conditions
+          }
+        }
 
-        query_hash[:query][:bool][:filter] = filter
         query_hash
       end
 
-      def authorized_project_ids_for_user(user)
-        if Ability.allowed?(user, :read_cross_project)
-          user.authorized_projects.pluck_primary_key
-        else
-          []
+      def filter_personal_snippets(user, options)
+        filter_conditions = []
+
+        # Include accessible personal snippets
+        filter_conditions << {
+          bool: {
+            filter: [
+              { terms: { visibility_level: Gitlab::VisibilityLevel.levels_for_user(user) } }
+            ],
+            must_not: { exists: { field: 'project_id' } }
+          }
+        }
+
+        # Include authored personal snippets
+        if user
+          filter_conditions << {
+            bool: {
+              filter: [
+                { term: { author_id: user.id } }
+              ],
+              must_not: { exists: { field: 'project_id' } }
+            }
+          }
         end
+
+        filter_conditions
+      end
+
+      def filter_project_snippets(user, options)
+        return [] unless Ability.allowed?(user, :read_cross_project)
+
+        filter_conditions = []
+
+        # Include public/internal project snippets for accessible projects
+        filter_conditions << {
+          bool: {
+            filter: [
+              { terms: { visibility_level: Gitlab::VisibilityLevel.levels_for_user(user) } },
+              {
+                has_parent: {
+                  parent_type: 'project',
+                  query: {
+                    bool: project_ids_query(
+                      user,
+                      options[:project_ids],
+                      options[:public_and_internal_projects],
+                      'snippets'
+                    )
+                  }
+                }
+              }
+            ]
+          }
+        }
+
+        # Include all project snippets for authorized projects
+        if user
+          filter_conditions << {
+            bool: {
+              must: [
+                { terms: { project_id: user.authorized_projects(Gitlab::Access::GUEST).pluck_primary_key } }
+              ]
+            }
+          }
+        end
+
+        filter_conditions
       end
     end
   end
diff --git a/ee/lib/elastic/latest/snippet_instance_proxy.rb b/ee/lib/elastic/latest/snippet_instance_proxy.rb
index 00ccd45d1f62232f5e10e0f203dde37b6a1eb1ba..430fa79eecad56adf5cffa9d1e9c039f788d98a4 100644
--- a/ee/lib/elastic/latest/snippet_instance_proxy.rb
+++ b/ee/lib/elastic/latest/snippet_instance_proxy.rb
@@ -28,10 +28,7 @@ def as_indexed_json(options = {})
           data['content'] = data['content'].mb_chars.limit(MAX_INDEX_SIZE).to_s # rubocop: disable CodeReuse/ActiveRecord
         end
 
-        # ES6 is now single-type per index, so we implement our own typing
-        data['type'] = es_type
-
-        data
+        data.merge(generic_attributes)
       end
     end
   end
diff --git a/ee/lib/elastic/multi_version_util.rb b/ee/lib/elastic/multi_version_util.rb
index f4fc109deca12a45802a55aa7f6c9227526b8099..dc087e7edefec1185ce509a368d3b9ff015cdec7 100644
--- a/ee/lib/elastic/multi_version_util.rb
+++ b/ee/lib/elastic/multi_version_util.rb
@@ -12,8 +12,8 @@ module MultiVersionUtil
 
     # @params version [String, Module] can be a string "V12p1" or module (Elastic::V12p1)
     def version(version)
-      version = Elastic.const_get(version) if version.is_a?(String)
-      version.const_get(proxy_class_name).new(data_target)
+      version = Elastic.const_get(version, false) if version.is_a?(String)
+      version.const_get(proxy_class_name, false).new(data_target)
     end
 
     private
diff --git a/ee/lib/gitlab/alerting/alert.rb b/ee/lib/gitlab/alerting/alert.rb
index 1a83bf1e233be767be700dfc5b25bab44b039dac..fbf040297f7537c21a389913739ac811b51a6022 100644
--- a/ee/lib/gitlab/alerting/alert.rb
+++ b/ee/lib/gitlab/alerting/alert.rb
@@ -15,6 +15,12 @@ def gitlab_alert
         end
       end
 
+      def metric_id
+        strong_memoize(:metric_id) do
+          payload&.dig('labels', 'gitlab_alert_id')
+        end
+      end
+
       def title
         strong_memoize(:title) do
           gitlab_alert&.title || parse_title_from_payload
@@ -28,7 +34,9 @@ def description
       end
 
       def environment
-        gitlab_alert&.environment
+        strong_memoize(:environment) do
+          gitlab_alert&.environment || parse_environment_from_payload
+        end
       end
 
       def annotations
@@ -43,6 +51,18 @@ def starts_at
         end
       end
 
+      def starts_at_raw
+        strong_memoize(:starts_at_raw) do
+          payload&.dig('startsAt')
+        end
+      end
+
+      def ends_at
+        strong_memoize(:ends_at) do
+          parse_datetime_from_payload('endsAt')
+        end
+      end
+
       def full_query
         strong_memoize(:full_query) do
           gitlab_alert&.full_query || parse_expr_from_payload
@@ -55,8 +75,18 @@ def alert_markdown
         end
       end
 
+      def status
+        strong_memoize(:status) do
+          payload&.dig('status')
+        end
+      end
+
+      def gitlab_managed?
+        metric_id.present?
+      end
+
       def valid?
-        project && title && starts_at
+        payload.respond_to?(:dig) && project && title && starts_at
       end
 
       def present
@@ -65,8 +95,17 @@ def present
 
       private
 
+      def parse_environment_from_payload
+        environment_name = payload&.dig('labels', 'gitlab_environment_name')
+
+        return unless environment_name
+
+        EnvironmentsFinder.new(project, nil, { name: environment_name })
+          .find
+          &.first
+      end
+
       def parse_gitlab_alert_from_payload
-        metric_id = payload&.dig('labels', 'gitlab_alert_id')
         return unless metric_id
 
         Projects::Prometheus::AlertsFinder
diff --git a/ee/lib/gitlab/analytics.rb b/ee/lib/gitlab/analytics.rb
index 95fdb4aecf5296ceeadc46a5e27cb49f3514c4f6..be2044e1870d984f3b14d62077887d93ee637fd9 100644
--- a/ee/lib/gitlab/analytics.rb
+++ b/ee/lib/gitlab/analytics.rb
@@ -15,8 +15,12 @@ module Analytics
       TASKS_BY_TYPE_CHART_FEATURE_FLAG
     ].freeze
 
+    FEATURE_FLAG_DEFAULTS = {
+      PRODUCTIVITY_ANALYTICS_FEATURE_FLAG => true
+    }.freeze
+
     def self.any_features_enabled?
-      FEATURE_FLAGS.any? { |flag| Feature.enabled?(flag) }
+      FEATURE_FLAGS.any? { |flag| Feature.enabled?(flag, default_enabled: feature_enabled_by_default?(flag)) }
     end
 
     def self.code_analytics_enabled?
@@ -28,7 +32,11 @@ def self.cycle_analytics_enabled?
     end
 
     def self.productivity_analytics_enabled?
-      Feature.enabled?(PRODUCTIVITY_ANALYTICS_FEATURE_FLAG)
+      Feature.enabled?(PRODUCTIVITY_ANALYTICS_FEATURE_FLAG, default_enabled: feature_enabled_by_default?(PRODUCTIVITY_ANALYTICS_FEATURE_FLAG))
+    end
+
+    def self.feature_enabled_by_default?(flag)
+      !!FEATURE_FLAG_DEFAULTS[flag]
     end
   end
 end
diff --git a/ee/lib/gitlab/background_migration/migrate_approver_to_approval_rules.rb b/ee/lib/gitlab/background_migration/migrate_approver_to_approval_rules.rb
index 16ec5519dfa721569b26c2fff03eed0bf4e94c1a..8ea6a6ff6bb0b9a8c43a6c144bb62bdd9badcdd3 100644
--- a/ee/lib/gitlab/background_migration/migrate_approver_to_approval_rules.rb
+++ b/ee/lib/gitlab/background_migration/migrate_approver_to_approval_rules.rb
@@ -99,8 +99,16 @@ def approver_group_ids
           @approver_group_ids ||= ApproverGroup.where(target_type: 'MergeRequest', target_id: id).joins(:group).pluck(distinct(:group_id))
         end
 
+        def merged_state_id
+          3
+        end
+
+        def closed_state_id
+          2
+        end
+
         def sync_code_owners_with_approvers
-          return if state == 'merged' || state == 'closed'
+          return if state_id == merged_state_id || state == closed_state_id
 
           Gitlab::GitalyClient.allow_n_plus_1_calls do
             gl_merge_request = ::MergeRequest.find(id)
diff --git a/ee/lib/gitlab/ci/parsers/security/common.rb b/ee/lib/gitlab/ci/parsers/security/common.rb
index 9c97d468e0d839e761462ce2feefa45f8c448e76..57cf66498d360887d18494b5892172cd06e170aa 100644
--- a/ee/lib/gitlab/ci/parsers/security/common.rb
+++ b/ee/lib/gitlab/ci/parsers/security/common.rb
@@ -29,7 +29,7 @@ def parse_report(json_data)
 
           # map remediations to relevant vulnerabilities
           def collate_remediations(report_data)
-            return report_data["vulnerabilities"] unless report_data["remediations"]
+            return report_data["vulnerabilities"] || [] unless report_data["remediations"]
 
             report_data["vulnerabilities"].map do |vulnerability|
               # Grab the first available remediation.
@@ -49,8 +49,8 @@ def create_vulnerability(report, data, version)
                 uuid: SecureRandom.uuid,
                 report_type: report.type,
                 name: data['message'],
-                compare_key: data['cve'],
-                location: create_location(data['location']),
+                compare_key: data['cve'] || '',
+                location: create_location(data['location'] || {}),
                 severity: parse_level(data['severity']),
                 confidence: parse_level(data['confidence']),
                 scanner: scanner,
diff --git a/ee/lib/gitlab/ci/reports/dependency_list/report.rb b/ee/lib/gitlab/ci/reports/dependency_list/report.rb
index 2969c138dfc991cf281a33e58a5376013acc6d45..eac3a04ea98399047511337471f0f6488041b09a 100644
--- a/ee/lib/gitlab/ci/reports/dependency_list/report.rb
+++ b/ee/lib/gitlab/ci/reports/dependency_list/report.rb
@@ -23,6 +23,10 @@ def apply_license(license)
               dependency[:licenses].push(name: license.name, url: license.url)
             end
           end
+
+          def dependencies_with_licenses
+            dependencies.select { |dependency| dependency[:licenses].any? }
+          end
         end
       end
     end
diff --git a/ee/lib/gitlab/ci/reports/license_scanning/dependency.rb b/ee/lib/gitlab/ci/reports/license_scanning/dependency.rb
index a93c4603d453a5fd31b43e20c166fb0c484df292..9453f434f4399d56bd2ce94333e0daccf6b48ab6 100644
--- a/ee/lib/gitlab/ci/reports/license_scanning/dependency.rb
+++ b/ee/lib/gitlab/ci/reports/license_scanning/dependency.rb
@@ -5,6 +5,7 @@ module Ci
     module Reports
       module LicenseScanning
         class Dependency
+          attr_accessor :path
           attr_reader :name
 
           def initialize(name)
diff --git a/ee/lib/gitlab/ci/reports/license_scanning/report.rb b/ee/lib/gitlab/ci/reports/license_scanning/report.rb
index 5c67e25e8fc79fd2dd290f43f57ac233fe40bef0..2d1f282fca08a581a97d256f7133a2f23fe6588a 100644
--- a/ee/lib/gitlab/ci/reports/license_scanning/report.rb
+++ b/ee/lib/gitlab/ci/reports/license_scanning/report.rb
@@ -33,6 +33,28 @@ def add(license)
             found_licenses[license.canonical_id] ||= license
           end
 
+          def by_license_name(name)
+            licenses.find { |license| license.name == name }
+          end
+
+          def merge_dependencies_info!(dependencies_with_licenses)
+            return if found_licenses.empty?
+
+            found_licenses.values.each do |license|
+              matched_dependencies = dependencies_with_licenses.select do |dependency|
+                dependency[:licenses].map { |l| l[:name] }.include?(license.name)
+              end
+
+              matched_dependencies.each do |dependency|
+                license_dependency = license.dependencies.find { |l_dependency| l_dependency.name == dependency[:name] }
+
+                next unless license_dependency
+
+                license_dependency.path = dependency.dig(:location, :blob_path)
+              end
+            end
+          end
+
           def violates?(software_license_policies)
             policies_with_matching_license_name = software_license_policies.blacklisted.with_license_by_name(license_names)
             policies_with_matching_spdx_id = software_license_policies.blacklisted.by_spdx(licenses.map(&:id).compact)
diff --git a/ee/lib/gitlab/ci/reports/security/report.rb b/ee/lib/gitlab/ci/reports/security/report.rb
index 9cf018ae48191052148e657a3c7c3b4720f6a122..086e0f14a43c00a274dc7b2ae533fd0770844505 100644
--- a/ee/lib/gitlab/ci/reports/security/report.rb
+++ b/ee/lib/gitlab/ci/reports/security/report.rb
@@ -54,7 +54,12 @@ def merge!(other)
           end
 
           def unsafe_severity?
-            occurrences.any? { |occurrence| UNSAFE_SEVERITIES.include?(occurrence.severity) }
+            !safe?
+          end
+
+          def safe?
+            severities = occurrences.map(&:severity).compact.map(&:downcase)
+            (severities & UNSAFE_SEVERITIES).empty?
           end
         end
       end
diff --git a/ee/lib/gitlab/ci/reports/security/reports.rb b/ee/lib/gitlab/ci/reports/security/reports.rb
index 0c6122b48dfa4dc94576eae0f6180bc7cb42a887..40d763700a2ad1705fa771a93ed0520592055216 100644
--- a/ee/lib/gitlab/ci/reports/security/reports.rb
+++ b/ee/lib/gitlab/ci/reports/security/reports.rb
@@ -7,6 +7,8 @@ module Security
         class Reports
           attr_reader :reports, :commit_sha
 
+          delegate :empty?, to: :reports
+
           def initialize(commit_sha)
             @reports = {}
             @commit_sha = commit_sha
@@ -15,6 +17,10 @@ def initialize(commit_sha)
           def get_report(report_type)
             reports[report_type] ||= Report.new(report_type, commit_sha)
           end
+
+          def violates_default_policy?
+            reports.values.any? { |report| report.unsafe_severity? }
+          end
         end
       end
     end
diff --git a/ee/lib/gitlab/elastic/snippet_search_results.rb b/ee/lib/gitlab/elastic/snippet_search_results.rb
index d02a6d859210d2442ae622752a71bbf0343abdd7..d146930dfed80a54860f2c9d77fd44169a87cc5a 100644
--- a/ee/lib/gitlab/elastic/snippet_search_results.rb
+++ b/ee/lib/gitlab/elastic/snippet_search_results.rb
@@ -2,7 +2,18 @@
 
 module Gitlab
   module Elastic
-    class SnippetSearchResults < ::Gitlab::SnippetSearchResults
+    class SnippetSearchResults < Gitlab::Elastic::SearchResults
+      def objects(scope, page = 1)
+        page = (page || 1).to_i
+
+        case scope
+        when 'snippet_titles'
+          eager_load(snippet_titles, page, eager: { project: [:route, :namespace] })
+        when 'snippet_blobs'
+          eager_load(snippet_blobs, page, eager: { project: [:route, :namespace] })
+        end
+      end
+
       def formatted_count(scope)
         case scope
         when 'snippet_titles'
@@ -25,11 +36,11 @@ def snippet_blobs_count
       private
 
       def snippet_titles
-        Snippet.elastic_search(query, options: search_params)
+        Snippet.elastic_search(query, options: base_options)
       end
 
       def snippet_blobs
-        Snippet.elastic_search_code(query, options: search_params)
+        Snippet.elastic_search_code(query, options: base_options)
       end
 
       def limited_snippet_titles_count
@@ -43,10 +54,6 @@ def limited_snippet_blobs_count
       def paginated_objects(relation, page)
         super.records
       end
-
-      def search_params
-        { user: current_user }
-      end
     end
   end
 end
diff --git a/ee/spec/controllers/admin/dashboard_controller_spec.rb b/ee/spec/controllers/admin/dashboard_controller_spec.rb
index 562424f795287dbccc13df1f0c1d78a0e1333417..1bb86a58beed1d5d6f46c77c3a8aa271f5e1790a 100644
--- a/ee/spec/controllers/admin/dashboard_controller_spec.rb
+++ b/ee/spec/controllers/admin/dashboard_controller_spec.rb
@@ -4,6 +4,8 @@
 
 describe Admin::DashboardController do
   describe '#index' do
+    render_views
+
     it "allows an admin user to access the page" do
       sign_in(create(:user, :admin))
 
@@ -27,5 +29,31 @@
 
       expect(response).to have_gitlab_http_status(404)
     end
+
+    it 'shows the license breakdown' do
+      sign_in(create(:user, :admin))
+
+      get :index
+
+      expect(response.body).to include('Users in License')
+    end
+
+    context 'when the user count is high' do
+      let(:counts) do
+        described_class::COUNTED_ITEMS.each_with_object({}) { |model, hash| hash[model] = described_class::LICENSE_BREAKDOWN_USER_LIMIT + 1 }
+      end
+
+      before do
+        expect(Gitlab::Database::Count).to receive(:approximate_counts).and_return(counts)
+
+        sign_in(create(:admin))
+      end
+
+      it 'hides the license breakdown' do
+        get :index
+
+        expect(response.body).not_to include('Users in License')
+      end
+    end
   end
 end
diff --git a/ee/spec/controllers/analytics/cycle_analytics/stages_controller_spec.rb b/ee/spec/controllers/analytics/cycle_analytics/stages_controller_spec.rb
index 9084596fd9da791048749e6c82650a3c0a326e68..6147e0a8e171e17325b553bfc54e025cee4a5aac 100644
--- a/ee/spec/controllers/analytics/cycle_analytics/stages_controller_spec.rb
+++ b/ee/spec/controllers/analytics/cycle_analytics/stages_controller_spec.rb
@@ -3,12 +3,10 @@
 require 'spec_helper'
 
 describe Analytics::CycleAnalytics::StagesController do
-  let(:user) { create(:user) }
-  let(:group) { create(:group) }
+  let_it_be(:user) { create(:user) }
+  let_it_be(:group, refind: true) { create(:group) }
   let(:params) { { group_id: group.full_path } }
 
-  subject { get :index, params: params }
-
   before do
     stub_feature_flags(Gitlab::Analytics::CYCLE_ANALYTICS_FEATURE_FLAG => true)
     stub_licensed_features(cycle_analytics_for_groups: true)
@@ -17,86 +15,148 @@
     sign_in(user)
   end
 
-  it 'succeeds' do
-    subject
+  describe 'GET `index`' do
+    subject { get :index, params: params }
 
-    expect(response).to be_successful
-    expect(response).to match_response_schema('analytics/cycle_analytics/stages', dir: 'ee')
-  end
+    it 'succeeds' do
+      subject
 
-  it 'returns correct start events' do
-    subject
+      expect(response).to be_successful
+      expect(response).to match_response_schema('analytics/cycle_analytics/stages', dir: 'ee')
+    end
 
-    response_start_events = json_response['stages'].map { |s| s['start_event_identifier'] }
-    start_events = Gitlab::Analytics::CycleAnalytics::DefaultStages.all.map { |s| s['start_event_identifier'] }
+    it 'returns correct start events' do
+      subject
 
-    expect(response_start_events).to eq(start_events)
-  end
+      response_start_events = json_response['stages'].map { |s| s['start_event_identifier'] }
+      start_events = Gitlab::Analytics::CycleAnalytics::DefaultStages.all.map { |s| s['start_event_identifier'] }
 
-  it 'returns correct event names' do
-    subject
+      expect(response_start_events).to eq(start_events)
+    end
 
-    response_event_names = json_response['events'].map { |s| s['name'] }
-    event_names = Gitlab::Analytics::CycleAnalytics::StageEvents.events.map(&:name)
+    it 'returns correct event names' do
+      subject
 
-    expect(response_event_names).to eq(event_names)
-  end
+      response_event_names = json_response['events'].map { |s| s['name'] }
+      event_names = Gitlab::Analytics::CycleAnalytics::StageEvents.events.map(&:name)
 
-  it 'succeeds for subgroups' do
-    subgroup = create(:group, parent: group)
-    params[:group_id] = subgroup.full_path
+      expect(response_event_names).to eq(event_names)
+    end
 
-    subject
+    it 'succeeds for subgroups' do
+      subgroup = create(:group, parent: group)
+      params[:group_id] = subgroup.full_path
 
-    expect(response).to be_successful
-  end
+      subject
 
-  it 'renders 404 when group_id is not provided' do
-    params[:group_id] = nil
+      expect(response).to be_successful
+    end
 
-    subject
+    it 'renders `forbidden` based on the response of the service object' do
+      expect_any_instance_of(Analytics::CycleAnalytics::Stages::ListService).to receive(:can?).and_return(false)
 
-    expect(response).to have_gitlab_http_status(:not_found)
+      subject
+
+      expect(response).to have_gitlab_http_status(:forbidden)
+    end
+
+    include_examples 'group permission check on the controller level'
   end
 
-  it 'renders 404 when group is missing' do
-    params[:group_id] = 'missing_group'
+  describe 'POST `create`' do
+    subject { post :create, params: params }
 
-    subject
+    include_examples 'group permission check on the controller level'
 
-    expect(response).to have_gitlab_http_status(:not_found)
-  end
+    context 'when valid parameters are given' do
+      before do
+        params.merge!({
+          name: 'my new stage',
+          start_event_identifier: :merge_request_created,
+          end_event_identifier: :merge_request_merged
+        })
+      end
 
-  it 'renders 404 when feature flag is disabled' do
-    stub_feature_flags(Gitlab::Analytics::CYCLE_ANALYTICS_FEATURE_FLAG => false)
+      it 'creates the stage' do
+        subject
 
-    subject
+        expect(response).to be_successful
+        expect(response).to match_response_schema('analytics/cycle_analytics/stage', dir: 'ee')
+      end
+    end
 
-    expect(response).to have_gitlab_http_status(:not_found)
+    include_context 'when invalid stage parameters are given'
   end
 
-  it 'renders 403 when user has no reporter access' do
-    GroupMember.where(user: user).delete_all
-    group.add_guest(user)
+  describe 'PUT `update`' do
+    let(:stage) { create(:cycle_analytics_group_stage, parent: group) }
+    subject { put :update, params: params.merge(id: stage.id) }
 
-    subject
+    include_examples 'group permission check on the controller level'
 
-    expect(response).to have_gitlab_http_status(:forbidden)
-  end
+    context 'when valid parameters are given' do
+      before do
+        params.merge!({
+          name: 'my updated stage',
+          start_event_identifier: :merge_request_created,
+          end_event_identifier: :merge_request_merged
+        })
+      end
+
+      it 'succeeds' do
+        subject
 
-  it 'renders 403 when feature is not available for the group' do
-    stub_licensed_features(cycle_analytics_for_groups: false)
+        expect(response).to be_successful
+        expect(response).to match_response_schema('analytics/cycle_analytics/stage', dir: 'ee')
+      end
 
-    subject
+      it 'updates the name attribute' do
+        subject
 
-    expect(response).to have_gitlab_http_status(:forbidden)
+        stage.reload
+
+        expect(stage.name).to eq(params[:name])
+      end
+    end
+
+    include_context 'when invalid stage parameters are given'
   end
 
-  it 'renders 403 based on the response of the service object' do
-    expect_any_instance_of(Analytics::CycleAnalytics::Stages::ListService).to receive(:can?).and_return(false)
+  describe 'DELETE `destroy`' do
+    let(:stage) { create(:cycle_analytics_group_stage, parent: group) }
+
+    subject { delete :destroy, params: params }
+
+    before do
+      params[:id] = stage.id
+    end
+
+    include_examples 'group permission check on the controller level'
+
+    context 'when persisted stage id is passed' do
+      it 'succeeds' do
+        subject
+
+        expect(response).to be_successful
+      end
+
+      it 'deletes the record' do
+        subject
+
+        expect(group.reload.cycle_analytics_stages.find_by(id: stage.id)).to be_nil
+      end
+    end
+
+    context 'when default stage id is passed' do
+      before do
+        params[:id] = Gitlab::Analytics::CycleAnalytics::DefaultStages.names.first
+      end
 
-    subject
+      it 'fails with `forbidden` response' do
+        subject
 
-    expect(response).to have_gitlab_http_status(:forbidden)
+        expect(response).to have_gitlab_http_status(:forbidden)
+      end
+    end
   end
 end
diff --git a/ee/spec/controllers/analytics/productivity_analytics_controller_spec.rb b/ee/spec/controllers/analytics/productivity_analytics_controller_spec.rb
index a6718a0cae16b93af94a5f7121aa8c9e335e79fb..c59ea020a1aa767b350d959d5a2ea30c81adc980 100644
--- a/ee/spec/controllers/analytics/productivity_analytics_controller_spec.rb
+++ b/ee/spec/controllers/analytics/productivity_analytics_controller_spec.rb
@@ -4,6 +4,7 @@
 
 describe Analytics::ProductivityAnalyticsController do
   let(:current_user) { create(:user) }
+  let(:group) { create :group }
 
   before do
     sign_in(current_user) if current_user
@@ -12,8 +13,6 @@
   end
 
   describe 'usage counter' do
-    let(:group) { create :group }
-
     before do
       group.add_owner(current_user)
     end
@@ -29,7 +28,7 @@
     it "doesn't increment the usage counter when JSON request is sent" do
       expect(Gitlab::UsageDataCounters::ProductivityAnalyticsCounter).not_to receive(:count).with(:views)
 
-      get :show, format: :json
+      get :show, format: :json, params: { group_id: group }
 
       expect(response).to be_successful
     end
@@ -38,14 +37,6 @@
   describe 'GET show' do
     subject { get :show }
 
-    it 'checks for premium license' do
-      stub_licensed_features(productivity_analytics: false)
-
-      subject
-
-      expect(response).to have_gitlab_http_status(403)
-    end
-
     it 'authorizes for ability to view analytics' do
       expect(Ability).to receive(:allowed?).with(current_user, :view_productivity_analytics, :global).and_return(false)
 
@@ -54,7 +45,9 @@
       expect(response).to have_gitlab_http_status(403)
     end
 
-    it 'renders show template' do
+    it 'renders show template regardless of license' do
+      stub_licensed_features(productivity_analytics: false)
+
       subject
 
       expect(response).to be_successful
@@ -73,6 +66,7 @@
 
   describe 'GET show.json' do
     subject { get :show, format: :json, params: params }
+
     let(:params) { {} }
 
     let(:analytics_mock) { instance_double('ProductivityAnalytics') }
@@ -86,75 +80,102 @@
               .and_return(analytics_mock)
     end
 
+    it 'checks for premium license' do
+      stub_licensed_features(productivity_analytics: false)
+
+      subject
+
+      expect(response).to have_gitlab_http_status(403)
+    end
+
+    context 'without group_id specified' do
+      it 'returns 403' do
+        subject
+
+        expect(response).to have_gitlab_http_status(403)
+      end
+    end
+
     context 'with non-existing group_id' do
       let(:params) { { group_id: 'SOMETHING_THAT_DOES_NOT_EXIST' } }
 
       it 'renders 404' do
         subject
+
         expect(response).to have_gitlab_http_status(404)
       end
     end
 
     context 'with non-existing project_id' do
-      let(:group) { create :group }
-      let(:params) { { group_id: group.full_path, project_id: 'SOMETHING_THAT_DOES_NOT_EXIST' } }
+      let(:params) { { group_id: group, project_id: 'SOMETHING_THAT_DOES_NOT_EXIST' } }
 
       it 'renders 404' do
         subject
+
         expect(response).to have_gitlab_http_status(404)
       end
     end
 
-    context 'for list of MRs' do
-      let!(:merge_request ) { create :merge_request, :merged}
-
-      let(:serializer_mock) { instance_double('BaseSerializer') }
+    context 'with group specified' do
+      let(:params) { { group_id: group } }
 
       before do
-        allow(BaseSerializer).to receive(:new).with(current_user: current_user).and_return(serializer_mock)
-        allow(analytics_mock).to receive(:merge_requests_extended).and_return(MergeRequest.all)
-        allow(serializer_mock).to receive(:represent)
-                                    .with(merge_request, {}, ProductivityAnalyticsMergeRequestEntity)
-                                    .and_return('mr_representation')
+        group.add_owner(current_user)
       end
 
-      it 'serializes whatever analytics returns with ProductivityAnalyticsMergeRequestEntity' do
-        subject
+      context 'for list of MRs' do
+        let!(:merge_request ) { create :merge_request, :merged}
 
-        expect(response.body).to eq '["mr_representation"]'
-      end
+        let(:serializer_mock) { instance_double('BaseSerializer') }
 
-      it 'sets pagination headers' do
-        subject
+        before do
+          allow(BaseSerializer).to receive(:new).with(current_user: current_user).and_return(serializer_mock)
+          allow(analytics_mock).to receive(:merge_requests_extended).and_return(MergeRequest.all)
+          allow(serializer_mock).to receive(:represent)
+                                      .with(merge_request, {}, ProductivityAnalyticsMergeRequestEntity)
+                                      .and_return('mr_representation')
+        end
+
+        it 'serializes whatever analytics returns with ProductivityAnalyticsMergeRequestEntity' do
+          subject
+
+          expect(response.body).to eq '["mr_representation"]'
+        end
 
-        expect(response.headers['X-Per-Page']).to eq '20'
-        expect(response.headers['X-Page']).to eq '1'
-        expect(response.headers['X-Next-Page']).to eq ''
-        expect(response.headers['X-Prev-Page']).to eq ''
-        expect(response.headers['X-Total']).to eq '1'
-        expect(response.headers['X-Total-Pages']).to eq '1'
+        it 'sets pagination headers' do
+          subject
+
+          expect(response.headers['X-Per-Page']).to eq '20'
+          expect(response.headers['X-Page']).to eq '1'
+          expect(response.headers['X-Next-Page']).to eq ''
+          expect(response.headers['X-Prev-Page']).to eq ''
+          expect(response.headers['X-Total']).to eq '1'
+          expect(response.headers['X-Total-Pages']).to eq '1'
+        end
       end
-    end
 
-    context 'for scatterplot charts' do
-      let(:params) { { chart_type: 'scatterplot', metric_type: 'commits_count' } }
-      it 'renders whatever analytics returns for scatterplot' do
-        allow(analytics_mock).to receive(:scatterplot_data).with(type: 'commits_count').and_return('scatterplot_data')
+      context 'for scatterplot charts' do
+        let(:params) { super().merge({ chart_type: 'scatterplot', metric_type: 'commits_count' }) }
 
-        subject
+        it 'renders whatever analytics returns for scatterplot' do
+          allow(analytics_mock).to receive(:scatterplot_data).with(type: 'commits_count').and_return('scatterplot_data')
+
+          subject
 
-        expect(response.body).to eq 'scatterplot_data'
+          expect(response.body).to eq 'scatterplot_data'
+        end
       end
-    end
 
-    context 'for histogram charts' do
-      let(:params) { { chart_type: 'histogram', metric_type: 'commits_count' } }
-      it 'renders whatever analytics returns for histogram' do
-        allow(analytics_mock).to receive(:histogram_data).with(type: 'commits_count').and_return('histogram_data')
+      context 'for histogram charts' do
+        let(:params) { super().merge({ chart_type: 'histogram', metric_type: 'commits_count' }) }
 
-        subject
+        it 'renders whatever analytics returns for histogram' do
+          allow(analytics_mock).to receive(:histogram_data).with(type: 'commits_count').and_return('histogram_data')
+
+          subject
 
-        expect(response.body).to eq 'histogram_data'
+          expect(response.body).to eq 'histogram_data'
+        end
       end
     end
   end
diff --git a/ee/spec/controllers/groups/group_members_controller_spec.rb b/ee/spec/controllers/groups/group_members_controller_spec.rb
index a614b2182b0cf44b34ba4cdab360745041741720..026404fe0652045a6c36e09a0a80232bca0038c7 100644
--- a/ee/spec/controllers/groups/group_members_controller_spec.rb
+++ b/ee/spec/controllers/groups/group_members_controller_spec.rb
@@ -6,7 +6,7 @@
   include ExternalAuthorizationServiceHelpers
 
   let(:user)  { create(:user) }
-  let(:group) { create(:group, :public, :access_requestable) }
+  let(:group) { create(:group, :public) }
   let(:membership) { create(:group_member, group: group) }
 
   before do
diff --git a/ee/spec/controllers/operations_controller_spec.rb b/ee/spec/controllers/operations_controller_spec.rb
index 15cfc0b1e4e4ab841ece21a4723988555b08b4a1..4844b416edb906d163e7a91652c36958a75afb19 100644
--- a/ee/spec/controllers/operations_controller_spec.rb
+++ b/ee/spec/controllers/operations_controller_spec.rb
@@ -34,6 +34,15 @@
       expect(response).to render_template(:index)
     end
 
+    it 'renders regardless of the environments_dashboard feature flag' do
+      stub_feature_flags(environments_dashboard: { enabled: false, thing: user })
+
+      get :index
+
+      expect(response).to have_gitlab_http_status(200)
+      expect(response).to render_template(:index)
+    end
+
     context 'with an anonymous user' do
       before do
         sign_out(user)
@@ -57,6 +66,24 @@
       expect(response).to render_template(:environments)
     end
 
+    it 'returns a 404 when the feature is disabled' do
+      stub_feature_flags(environments_dashboard: { enabled: false, thing: user })
+
+      get :environments
+
+      expect(response).to have_gitlab_http_status(:not_found)
+    end
+
+    it 'renders the view when the feature is disabled for a different user' do
+      other_user = create(:user)
+      stub_feature_flags(environments_dashboard: { enabled: false, thing: other_user })
+
+      get :environments
+
+      expect(response).to have_gitlab_http_status(:ok)
+      expect(response).to render_template(:environments)
+    end
+
     context 'with an anonymous user' do
       before do
         sign_out(user)
@@ -138,6 +165,17 @@
         expect(expected_project['last_alert']['id']).to eq(last_firing_alert.id)
       end
 
+      it 'returns a list of added projects regardless of the environments_dashboard feature flag' do
+        stub_feature_flags(environments_dashboard: { enabled: false, thing: user })
+
+        get :list
+
+        expect(response).to have_gitlab_http_status(200)
+        expect(response).to match_response_schema('dashboard/operations/list', dir: 'ee')
+        expect(json_response['projects'].size).to eq(1)
+        expect(expected_project['id']).to eq(project.id)
+      end
+
       context 'without sufficient access level' do
         before do
           project.add_reporter(user)
@@ -226,6 +264,30 @@
           user.update!(ops_dashboard_projects: [project])
         end
 
+        it 'returns a 404 when the feature is disabled' do
+          stub_feature_flags(environments_dashboard: { enabled: false, thing: user })
+          environment = create(:environment, project: project)
+          ci_build = create(:ci_build, project: project)
+          create(:deployment, :success, project: project, environment: environment, deployable: ci_build)
+
+          get :environments_list
+
+          expect(response).to have_gitlab_http_status(:not_found)
+        end
+
+        it 'returns a project when the feature is disabled for another user' do
+          other_user = create(:user)
+          stub_feature_flags(environments_dashboard: { enabled: false, thing: other_user })
+          environment = create(:environment, project: project)
+          ci_build = create(:ci_build, project: project)
+          create(:deployment, :success, project: project, environment: environment, deployable: ci_build)
+
+          get :environments_list
+
+          expect(response).to have_gitlab_http_status(:ok)
+          expect(response).to match_response_schema('dashboard/operations/environments_list', dir: 'ee')
+        end
+
         it 'returns a project without an environment' do
           get :environments_list
 
@@ -452,6 +514,16 @@
         expect(user.ops_dashboard_projects).to contain_exactly(project_a, project_b)
       end
 
+      it 'adds projects to the dashboard regardless of the environments_dashboard feature flag' do
+        stub_feature_flags(environments_dashboard: { enabled: false, thing: user })
+
+        post :create, params: { project_ids: [project_a.id, project_b.id.to_s] }
+
+        expect(response).to have_gitlab_http_status(200)
+        expect(json_response).to match_schema('dashboard/operations/add', dir: 'ee')
+        expect(json_response['added']).to contain_exactly(project_a.id, project_b.id)
+      end
+
       it 'cannot add a project twice' do
         post :create, params: { project_ids: [project_a.id, project_a.id] }
 
@@ -530,7 +602,17 @@
         expect(response).to have_gitlab_http_status(200)
 
         user.reload
-        expect(user.ops_dashboard_projects).not_to eq([project])
+        expect(user.ops_dashboard_projects).to eq([])
+      end
+
+      it 'removes a project regardless of the environments_dashboard feature flag' do
+        stub_feature_flags(environments_dashboard: { enabled: false, thing: user })
+
+        delete :destroy, params: { project_id: project.id }
+
+        expect(response).to have_gitlab_http_status(200)
+        user.reload
+        expect(user.ops_dashboard_projects).to eq([])
       end
     end
 
diff --git a/ee/spec/controllers/projects/alerting/notifications_controller_spec.rb b/ee/spec/controllers/projects/alerting/notifications_controller_spec.rb
index 016f5618d691134f0f677cd06089617c5634078c..1a02f2f1fa18745ae6ef0b3f3717abe371980e89 100644
--- a/ee/spec/controllers/projects/alerting/notifications_controller_spec.rb
+++ b/ee/spec/controllers/projects/alerting/notifications_controller_spec.rb
@@ -22,84 +22,66 @@ def make_request(body = {})
       post :create, params: project_params, body: body.to_json, as: :json
     end
 
-    context 'when feature flag is on' do
-      before do
-        stub_feature_flags(generic_alert_endpoint: true)
+    context 'when notification service succeeds' do
+      let(:payload) do
+        {
+          title: 'Alert title',
+          hosts: 'https://gitlab.com'
+        }
       end
 
-      context 'when notification service succeeds' do
-        let(:payload) do
-          {
-            title: 'Alert title',
-            hosts: 'https://gitlab.com'
-          }
-        end
-
-        let(:permitted_params) { ActionController::Parameters.new(payload).permit! }
-
-        it 'responds with ok' do
-          make_request
-
-          expect(response).to have_gitlab_http_status(:ok)
-        end
+      let(:permitted_params) { ActionController::Parameters.new(payload).permit! }
 
-        it 'does not pass excluded parameters to the notify service' do
-          make_request(payload)
+      it 'responds with ok' do
+        make_request
 
-          expect(Projects::Alerting::NotifyService)
-            .to have_received(:new)
-            .with(project, nil, permitted_params)
-        end
+        expect(response).to have_gitlab_http_status(:ok)
       end
 
-      context 'when notification service fails' do
-        let(:service_response) { ServiceResponse.error(message: 'Unauthorized', http_status: 401) }
-
-        it 'responds with the service response' do
-          make_request
+      it 'does not pass excluded parameters to the notify service' do
+        make_request(payload)
 
-          expect(response).to have_gitlab_http_status(:unauthorized)
-        end
+        expect(Projects::Alerting::NotifyService)
+          .to have_received(:new)
+          .with(project, nil, permitted_params)
       end
+    end
 
-      context 'bearer token' do
-        context 'when set' do
-          it 'extracts bearer token' do
-            request.headers['HTTP_AUTHORIZATION'] = 'Bearer some token'
+    context 'when notification service fails' do
+      let(:service_response) { ServiceResponse.error(message: 'Unauthorized', http_status: 401) }
 
-            expect(notify_service).to receive(:execute).with('some token')
+      it 'responds with the service response' do
+        make_request
 
-            make_request
-          end
+        expect(response).to have_gitlab_http_status(:unauthorized)
+      end
+    end
 
-          it 'pass nil if cannot extract a non-bearer token' do
-            request.headers['HTTP_AUTHORIZATION'] = 'some token'
+    context 'bearer token' do
+      context 'when set' do
+        it 'extracts bearer token' do
+          request.headers['HTTP_AUTHORIZATION'] = 'Bearer some token'
 
-            expect(notify_service).to receive(:execute).with(nil)
+          expect(notify_service).to receive(:execute).with('some token')
 
-            make_request
-          end
+          make_request
         end
 
-        context 'when missing' do
-          it 'passes nil' do
-            expect(notify_service).to receive(:execute).with(nil)
+        it 'pass nil if cannot extract a non-bearer token' do
+          request.headers['HTTP_AUTHORIZATION'] = 'some token'
 
-            make_request
-          end
-        end
-      end
-    end
+          expect(notify_service).to receive(:execute).with(nil)
 
-    context 'when feature flag is off' do
-      before do
-        stub_feature_flags(generic_alert_endpoint: false)
+          make_request
+        end
       end
 
-      it 'responds with not_found' do
-        make_request
+      context 'when missing' do
+        it 'passes nil' do
+          expect(notify_service).to receive(:execute).with(nil)
 
-        expect(response).to have_gitlab_http_status(:not_found)
+          make_request
+        end
       end
     end
   end
diff --git a/ee/spec/controllers/projects/environments_controller_spec.rb b/ee/spec/controllers/projects/environments_controller_spec.rb
index e851fe507b06c2cd6ac84009f464244e4ea9f9a6..da9321f0584cfb2488c283f90842c2292852a019 100644
--- a/ee/spec/controllers/projects/environments_controller_spec.rb
+++ b/ee/spec/controllers/projects/environments_controller_spec.rb
@@ -170,8 +170,8 @@
         it_behaves_like 'resource not found', 'Pod not found'
       end
 
-      context 'when service returns nil' do
-        let(:service_result) { nil }
+      context 'when service returns status processing' do
+        let(:service_result) { { status: :processing } }
 
         it 'renders accepted' do
           get :logs, params: environment_params(pod_name: pod_name, format: :json)
diff --git a/ee/spec/controllers/projects/pipelines_controller_spec.rb b/ee/spec/controllers/projects/pipelines_controller_spec.rb
index c6d01bab471281a2c487d368198d76912026e458..eeef46b44b5065cfbc8ba6f4af2139245eb7b87d 100644
--- a/ee/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/ee/spec/controllers/projects/pipelines_controller_spec.rb
@@ -3,9 +3,9 @@
 require 'spec_helper'
 
 describe Projects::PipelinesController do
-  set(:user) { create(:user) }
-  set(:project) { create(:project, :repository) }
-  set(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
+  let_it_be(:user) { create(:user) }
+  let_it_be(:project) { create(:project, :repository) }
+  let_it_be(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
 
   before do
     project.add_developer(user)
@@ -13,191 +13,6 @@
     sign_in(user)
   end
 
-  describe 'GET show.json' do
-    set(:source_project) { create(:project) }
-    set(:target_project) { create(:project) }
-    set(:root_pipeline) { create_pipeline(project) }
-    set(:source_pipeline) { create_pipeline(source_project) }
-    set(:source_of_source_pipeline) { create_pipeline(source_project) }
-    set(:target_pipeline) { create_pipeline(target_project) }
-    set(:target_of_target_pipeline) { create_pipeline(target_project) }
-    before do
-      create_link(source_of_source_pipeline, source_pipeline)
-      create_link(source_pipeline, root_pipeline)
-      create_link(root_pipeline, target_pipeline)
-      create_link(target_pipeline, target_of_target_pipeline)
-    end
-
-    shared_examples 'not expanded' do
-      let(:expected_stages) { be_nil }
-
-      it 'does return base details' do
-        get_pipeline_json(root_pipeline)
-
-        expect(json_response['triggered_by']).to include('id' => source_pipeline.id)
-        expect(json_response['triggered']).to contain_exactly(
-          include('id' => target_pipeline.id))
-      end
-
-      it 'does not expand triggered_by pipeline' do
-        get_pipeline_json(root_pipeline)
-
-        triggered_by = json_response['triggered_by']
-        expect(triggered_by['triggered_by']).to be_nil
-        expect(triggered_by['triggered']).to be_nil
-        expect(triggered_by['details']['stages']).to expected_stages
-      end
-
-      it 'does not expand triggered pipelines' do
-        get_pipeline_json(root_pipeline)
-
-        first_triggered = json_response['triggered'].first
-        expect(first_triggered['triggered_by']).to be_nil
-        expect(first_triggered['triggered']).to be_nil
-        expect(first_triggered['details']['stages']).to expected_stages
-      end
-    end
-
-    shared_examples 'expanded' do
-      it 'does return base details' do
-        get_pipeline_json(root_pipeline)
-
-        expect(json_response['triggered_by']).to include('id' => source_pipeline.id)
-        expect(json_response['triggered']).to contain_exactly(
-          include('id' => target_pipeline.id))
-      end
-
-      it 'does expand triggered_by pipeline' do
-        get_pipeline_json(root_pipeline)
-
-        triggered_by = json_response['triggered_by']
-        expect(triggered_by['triggered_by']).to include(
-          'id' => source_of_source_pipeline.id)
-        expect(triggered_by['details']['stages']).not_to be_nil
-      end
-
-      it 'does not recursively expand triggered_by' do
-        get_pipeline_json(root_pipeline)
-
-        triggered_by = json_response['triggered_by']
-        expect(triggered_by['triggered']).to be_nil
-      end
-
-      it 'does expand triggered pipelines' do
-        get_pipeline_json(root_pipeline)
-
-        first_triggered = json_response['triggered'].first
-        expect(first_triggered['triggered']).to contain_exactly(
-          include('id' => target_of_target_pipeline.id))
-        expect(first_triggered['details']['stages']).not_to be_nil
-      end
-
-      it 'does not recursively expand triggered' do
-        get_pipeline_json(root_pipeline)
-
-        first_triggered = json_response['triggered'].first
-        expect(first_triggered['triggered_by']).to be_nil
-      end
-    end
-
-    context 'when it does have permission to read other projects' do
-      before do
-        source_project.add_developer(user)
-        target_project.add_developer(user)
-      end
-
-      context 'when not-expanding any pipelines' do
-        let(:expanded) { nil }
-
-        it_behaves_like 'not expanded'
-      end
-
-      context 'when expanding non-existing pipeline' do
-        let(:expanded) { [-1] }
-
-        it_behaves_like 'not expanded'
-      end
-
-      context 'when expanding pipeline that is not directly expandable' do
-        let(:expanded) { [source_of_source_pipeline.id, target_of_target_pipeline.id] }
-
-        it_behaves_like 'not expanded'
-      end
-
-      context 'when expanding self' do
-        let(:expanded) { [root_pipeline.id] }
-
-        context 'it does not recursively expand pipelines' do
-          it_behaves_like 'not expanded'
-        end
-      end
-
-      context 'when expanding source and target pipeline' do
-        let(:expanded) { [source_pipeline.id, target_pipeline.id] }
-
-        it_behaves_like 'expanded'
-
-        context 'when expand depth is limited to 1' do
-          before do
-            stub_const('TriggeredPipelineEntity::MAX_EXPAND_DEPTH', 1)
-          end
-
-          it_behaves_like 'not expanded' do
-            # We expect that triggered/triggered_by is not expanded,
-            # but we still return details.stages for that pipeline
-            let(:expected_stages) { be_a(Array) }
-          end
-        end
-      end
-
-      context 'when expanding all' do
-        let(:expanded) do
-          [
-            source_of_source_pipeline.id,
-            source_pipeline.id,
-            root_pipeline.id,
-            target_pipeline.id,
-            target_of_target_pipeline.id
-          ]
-        end
-
-        it_behaves_like 'expanded'
-      end
-    end
-
-    context 'when does not have permission to read other projects' do
-      let(:expanded) { [source_pipeline.id, target_pipeline.id] }
-
-      it_behaves_like 'not expanded'
-    end
-
-    def create_pipeline(project)
-      create(:ci_empty_pipeline, project: project).tap do |pipeline|
-        create(:ci_build, pipeline: pipeline, stage: 'test', name: 'rspec')
-      end
-    end
-
-    def create_link(source_pipeline, pipeline)
-      source_pipeline.sourced_pipelines.create!(
-        source_job: source_pipeline.builds.all.sample,
-        source_project: source_pipeline.project,
-        project: pipeline.project,
-        pipeline: pipeline
-      )
-    end
-
-    def get_pipeline_json(pipeline)
-      params = {
-        namespace_id: pipeline.project.namespace,
-        project_id: pipeline.project,
-        id: pipeline,
-        expanded: expanded
-      }
-
-      get :show, params: params.compact, format: :json
-    end
-  end
-
   describe 'GET security' do
     context 'with a sast artifact' do
       before do
diff --git a/ee/spec/controllers/projects/project_members_controller_spec.rb b/ee/spec/controllers/projects/project_members_controller_spec.rb
index 5ea77934dde9243b76d85070ce97cb82312e2f20..b022da2c7d1dab9b06d293a5e7eb7d8c2ab8669a 100644
--- a/ee/spec/controllers/projects/project_members_controller_spec.rb
+++ b/ee/spec/controllers/projects/project_members_controller_spec.rb
@@ -4,7 +4,7 @@
 
 describe Projects::ProjectMembersController do
   let(:user) { create(:user) }
-  let(:project) { create(:project, :public, :access_requestable, namespace: namespace) }
+  let(:project) { create(:project, :public, namespace: namespace) }
   let(:namespace) { create :group }
 
   describe 'POST apply_import' do
@@ -15,6 +15,7 @@
         source_project_id: another_project.id
       })
     end
+
     let(:another_project) { create(:project, :private) }
     let(:member) { create(:user) }
 
@@ -53,6 +54,7 @@
         project_id: project
       }
     end
+
     let(:access_level) { nil }
 
     before do
diff --git a/ee/spec/controllers/projects/settings/operations_controller_spec.rb b/ee/spec/controllers/projects/settings/operations_controller_spec.rb
index 7ab85d937cb81f4e2dd94ace26bf9593d85d6ae1..21d652daf25b8f1543986ed58f9e51fcf91d847e 100644
--- a/ee/spec/controllers/projects/settings/operations_controller_spec.rb
+++ b/ee/spec/controllers/projects/settings/operations_controller_spec.rb
@@ -380,6 +380,34 @@
           )
         end
       end
+
+      context 'updating each incident management setting' do
+        let(:project) { create(:project) }
+        let(:new_incident_management_settings) { {} }
+
+        before do
+          project.add_maintainer(user)
+        end
+
+        shared_examples 'a gitlab tracking event' do |params, event_key|
+          it "creates a gitlab tracking event #{event_key}" do
+            new_incident_management_settings = params
+
+            expect(Gitlab::Tracking).to receive(:event)
+              .with('IncidentManagement::Settings', event_key, kind_of(Hash))
+
+            update_project(project,
+              incident_management_params: new_incident_management_settings)
+          end
+        end
+
+        it_behaves_like 'a gitlab tracking event', { create_issue: '1' }, 'enabled_issue_auto_creation_on_alerts'
+        it_behaves_like 'a gitlab tracking event', { create_issue: '0' }, 'disabled_issue_auto_creation_on_alerts'
+        it_behaves_like 'a gitlab tracking event', { issue_template_key: 'template' }, 'enabled_issue_template_on_alerts'
+        it_behaves_like 'a gitlab tracking event', { issue_template_key: nil }, 'disabled_issue_template_on_alerts'
+        it_behaves_like 'a gitlab tracking event', { send_email: '1' }, 'enabled_sending_emails'
+        it_behaves_like 'a gitlab tracking event', { send_email: '0' }, 'disabled_sending_emails'
+      end
     end
 
     context 'without a license' do
diff --git a/ee/spec/controllers/registrations_controller_spec.rb b/ee/spec/controllers/registrations_controller_spec.rb
index 163bed900c515426760f439825ddadb374004b36..2adfd146c8af22009daa1566c2bbb283107b3c21 100644
--- a/ee/spec/controllers/registrations_controller_spec.rb
+++ b/ee/spec/controllers/registrations_controller_spec.rb
@@ -3,10 +3,6 @@
 require 'spec_helper'
 
 describe RegistrationsController do
-  before do
-    stub_feature_flags(invisible_captcha: false)
-  end
-
   describe '#create' do
     context 'when the user opted-in' do
       let(:user_params) { { user: attributes_for(:user, email_opted_in: '1') } }
diff --git a/ee/spec/controllers/trial_registrations_controller_spec.rb b/ee/spec/controllers/trial_registrations_controller_spec.rb
index 72dd06bf4524d0060057bf97a5757fd582c06f85..4ce0aef123f011353bdf992689586619009c3639 100644
--- a/ee/spec/controllers/trial_registrations_controller_spec.rb
+++ b/ee/spec/controllers/trial_registrations_controller_spec.rb
@@ -45,7 +45,6 @@
 
   describe '#create' do
     before do
-      stub_feature_flags(invisible_captcha: false)
       stub_application_setting(send_user_confirmation_email: true)
     end
 
diff --git a/ee/spec/elastic_integration/global_search_spec.rb b/ee/spec/elastic_integration/global_search_spec.rb
index 101d3d69c7bcaa51563b13b38ee86063a650e8dc..9f194ee8f60e475dcde92caa18d53450a8c26f13 100644
--- a/ee/spec/elastic_integration/global_search_spec.rb
+++ b/ee/spec/elastic_integration/global_search_spec.rb
@@ -159,7 +159,7 @@ def create_items(project, feature_settings = nil)
 
   # access_level can be :disabled, :enabled or :private
   def feature_settings(access_level)
-    Hash[features.collect { |k| ["#{k}_access_level", ProjectFeature.const_get(access_level.to_s.upcase)] }]
+    Hash[features.collect { |k| ["#{k}_access_level", ProjectFeature.const_get(access_level.to_s.upcase, false)] }]
   end
 
   def expect_no_items_to_be_found(user)
diff --git a/ee/spec/factories/ci/builds.rb b/ee/spec/factories/ci/builds.rb
index ace6a7893eeb470e1cd9275c1d37df011d8cc73b..237855549a4b597be1c824a1bd411708a7de73f2 100644
--- a/ee/spec/factories/ci/builds.rb
+++ b/ee/spec/factories/ci/builds.rb
@@ -95,5 +95,11 @@
         build.job_artifacts << create(:ee_ci_job_artifact, :corrupted_license_management_report, job: build)
       end
     end
+
+    trait :low_severity_dast_report do
+      after(:build) do |build|
+        build.job_artifacts << create(:ee_ci_job_artifact, :low_severity_dast_report, job: build)
+      end
+    end
   end
 end
diff --git a/ee/spec/factories/ci/job_artifacts.rb b/ee/spec/factories/ci/job_artifacts.rb
index 452826693f47008666ea43b2b3e2b3b215bfc0b3..32c69804aa2b3535e281b7dcc112d1da4a347294 100644
--- a/ee/spec/factories/ci/job_artifacts.rb
+++ b/ee/spec/factories/ci/job_artifacts.rb
@@ -182,6 +182,16 @@
       end
     end
 
+    trait :low_severity_dast_report do
+      file_format { :raw }
+      file_type { :dast }
+
+      after(:build) do |artifact, _|
+        artifact.file = fixture_file_upload(
+          Rails.root.join('ee/spec/fixtures/security_reports/master/gl-dast-report-low-severity.json'), 'text/plain')
+      end
+    end
+
     trait :metrics do
       file_format { :gzip }
       file_type { :metrics }
diff --git a/ee/spec/factories/dependencies.rb b/ee/spec/factories/dependencies.rb
index 8d253165f0899071b8ac842487b41869b7abdc47..39498876071c85ba256769d2c455b2e8077fc173 100644
--- a/ee/spec/factories/dependencies.rb
+++ b/ee/spec/factories/dependencies.rb
@@ -2,17 +2,21 @@
 
 FactoryBot.define do
   factory :dependency, class: Hash do
-    name { 'nokogiri' }
+    sequence(:name) { |n| "library#{n}" }
     packager { 'Ruby (Bundler)' }
     version { '1.8.0' }
     licenses { [] }
-    location do
+    sequence(:location) do |n|
       {
-        blob_path: '/some_project/path/Gemfile.lock',
-        path:      'Gemfile.lock'
+        blob_path: "/some_project/path/File_#{n}.lock",
+        path:      "File_#{n}.lock"
       }
     end
 
+    trait :nokogiri do
+      name { 'nokogiri' }
+    end
+
     trait :with_vulnerabilities do
       vulnerabilities do
         [{
diff --git a/ee/spec/factories/self_managed_prometheus_alert_event.rb b/ee/spec/factories/self_managed_prometheus_alert_event.rb
new file mode 100644
index 0000000000000000000000000000000000000000..238942e2c46c77e062ab9b4bc47d6c40705d2ed3
--- /dev/null
+++ b/ee/spec/factories/self_managed_prometheus_alert_event.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+  factory :self_managed_prometheus_alert_event do
+    project
+    sequence(:payload_key) { |n| "hash payload key #{n}" }
+    status { SelfManagedPrometheusAlertEvent.status_value_for(:firing) }
+    title { 'alert' }
+    query_expression { 'vector(2)' }
+    started_at { Time.now }
+
+    trait :resolved do
+      status { SelfManagedPrometheusAlertEvent.status_value_for(:resolved) }
+      ended_at { Time.now }
+      payload_key { nil }
+    end
+
+    trait :none do
+      status { nil }
+      started_at { nil }
+    end
+  end
+end
diff --git a/ee/spec/factories/smartcard_identities.rb b/ee/spec/factories/smartcard_identities.rb
index 5c7b8953991c03ca09498fcdf210818d4e21bbcb..e03622424c780986c665512f0089537795daaa30 100644
--- a/ee/spec/factories/smartcard_identities.rb
+++ b/ee/spec/factories/smartcard_identities.rb
@@ -3,6 +3,7 @@
 FactoryBot.define do
   factory :smartcard_identity do
     subject { 'CN=gitlab-user/emailAddress=gitlab-user@random-corp.org' }
+
     issuer { 'O=Random Corp Ltd, CN=Random Corp' }
 
     association :user
diff --git a/ee/spec/factories/vulnerabilities.rb b/ee/spec/factories/vulnerabilities.rb
index 7c1bb96825ac61863fdbd96d94315fcb9bf8872e..407d3b0efb9c0f390cb835bbc94deed4fa17d2e1 100644
--- a/ee/spec/factories/vulnerabilities.rb
+++ b/ee/spec/factories/vulnerabilities.rb
@@ -17,5 +17,15 @@
       state { :closed }
       closed_at { Time.now }
     end
+
+    trait :with_findings do
+      after(:build) do |vulnerability|
+        vulnerability.findings = build_list(
+          :vulnerabilities_occurrence,
+          2,
+          vulnerability: vulnerability,
+          project: vulnerability.project)
+      end
+    end
   end
 end
diff --git a/ee/spec/features/billings/billing_plans_spec.rb b/ee/spec/features/billings/billing_plans_spec.rb
index 94f7a5bffd2585b30761c00ece2771b2218627de..4decde9a27773f443a3f0ffed156b2a5dc609698 100644
--- a/ee/spec/features/billings/billing_plans_spec.rb
+++ b/ee/spec/features/billings/billing_plans_spec.rb
@@ -60,7 +60,7 @@ def external_upgrade_url(namespace, plan)
 
       it 'displays correct plan actions' do
         expected_actions = plans_data.map { |data| data.fetch(:purchase_link).fetch(:action) }
-        plan_actions = page.all('.billing-plans .card .plan-action')
+        plan_actions = page.all('.billing-plans .card .card-footer')
         expect(plan_actions.length).to eq(expected_actions.length)
 
         expected_actions.each_with_index do |expected_action, index|
@@ -68,13 +68,13 @@ def external_upgrade_url(namespace, plan)
 
           case expected_action
           when 'downgrade'
-            expect(action).to have_content('Downgrade')
-            expect(action).to have_css('.disabled')
+            expect(action).not_to have_link('Upgrade')
+            expect(action).not_to have_css('.disabled')
           when 'current_plan'
-            expect(action).to have_content('Current plan')
+            expect(action).to have_link('Upgrade')
             expect(action).to have_css('.disabled')
           when 'upgrade'
-            expect(action).to have_content('Upgrade')
+            expect(action).to have_link('Upgrade')
             expect(action).not_to have_css('.disabled')
           end
         end
@@ -100,7 +100,7 @@ def external_upgrade_url(namespace, plan)
         end
 
         page.within('.content') do
-          expect(page).to have_link('Upgrade plan', href: external_upgrade_url(namespace, bronze_plan))
+          expect(page).to have_link('Upgrade', href: external_upgrade_url(namespace, bronze_plan))
           expect(page).to have_content('downgrade your plan')
           expect(page).to have_link('Customer Support', href: EE::CUSTOMER_SUPPORT_URL)
         end
@@ -126,7 +126,6 @@ def external_upgrade_url(namespace, plan)
         end
 
         page.within('.content') do
-          expect(page).not_to have_link('Upgrade plan')
           expect(page).to have_content('downgrade your plan')
           expect(page).to have_link('Customer Support', href: EE::CUSTOMER_SUPPORT_URL)
         end
@@ -162,8 +161,8 @@ def external_upgrade_url(namespace, plan)
           end
         end
 
-        it 'does not display the billing plans table' do
-          expect(page).not_to have_css('.billing-plans')
+        it 'does display the billing plans table' do
+          expect(page).to have_css('.billing-plans')
         end
 
         it 'displays subscription table', :js do
diff --git a/ee/spec/features/boards/group_boards/multiple_boards_spec.rb b/ee/spec/features/boards/group_boards/multiple_boards_spec.rb
index f1f970d2691bc7b99524ba27a83aea53e56fa345..2dc2ed84de4b270e7ac7507d38f03e5c262e4029 100644
--- a/ee/spec/features/boards/group_boards/multiple_boards_spec.rb
+++ b/ee/spec/features/boards/group_boards/multiple_boards_spec.rb
@@ -41,7 +41,7 @@
     end
 
     it 'shows a license warning when group has more than one board' do
-      create(:board, parent: parent)
+      create(:board, resource_parent: parent)
 
       visit boards_path
       wait_for_requests
diff --git a/ee/spec/features/boards/sidebar_spec.rb b/ee/spec/features/boards/sidebar_spec.rb
index b47cb0f385726559f18cc35c3e373fd13f53fd0d..ced9f7aac9f0eb75106f1be973946c44a7570109 100644
--- a/ee/spec/features/boards/sidebar_spec.rb
+++ b/ee/spec/features/boards/sidebar_spec.rb
@@ -250,4 +250,43 @@
       end
     end
   end
+
+  context 'scoped labels' do
+    let!(:scoped_label_1) { create(:label, project: project, name: 'Scoped::Label1') }
+    let!(:scoped_label_2) { create(:label, project: project, name: 'Scoped::Label2') }
+
+    before do
+      stub_licensed_features(scoped_labels: true)
+
+      visit project_board_path(project, board)
+      wait_for_requests
+    end
+
+    it 'removes existing scoped label' do
+      click_card(card1)
+
+      page.within('.labels') do
+        click_link 'Edit'
+
+        wait_for_requests
+
+        click_link scoped_label_1.title
+        click_link scoped_label_2.title
+
+        wait_for_requests
+
+        find('.dropdown-menu-close-icon').click
+
+        page.within('.value') do
+          expect(page).to have_selector('.scoped-label-wrapper', count: 1)
+          expect(page).not_to have_content(scoped_label_1.title)
+          expect(page).to have_content(scoped_label_2.title)
+        end
+      end
+
+      expect(card1).to have_selector('.scoped-label-wrapper', count: 1)
+      expect(card1).not_to have_content(scoped_label_1.title)
+      expect(card1).to have_content(scoped_label_2.title)
+    end
+  end
 end
diff --git a/ee/spec/features/groups/billing_spec.rb b/ee/spec/features/groups/billing_spec.rb
index 0b1f05c30b5d5cde22e5fc541660b34db9bcc29e..4482c78d4825b17420f903a692bbb436e64ad099 100644
--- a/ee/spec/features/groups/billing_spec.rb
+++ b/ee/spec/features/groups/billing_spec.rb
@@ -13,6 +13,10 @@ def formatted_date(date)
     date.strftime("%B %-d, %Y")
   end
 
+  def subscription_table
+    '.subscription-table'
+  end
+
   before do
     stub_full_request("https://customers.gitlab.com/gitlab_plans?plan=#{plan}")
       .to_return(status: 200, body: File.new(Rails.root.join('ee/spec/fixtures/gitlab_com_plans.json')))
@@ -34,9 +38,11 @@ def formatted_date(date)
       visit group_billings_path(group)
 
       expect(page).to have_content("#{group.name} is currently using the Free plan")
-      expect(page).to have_content("start date #{formatted_date(subscription.start_date)}")
-      expect(page).to have_link("Upgrade", href: "#{EE::SUBSCRIPTIONS_URL}/subscriptions")
-      expect(page).not_to have_link("Manage")
+      within subscription_table do
+        expect(page).to have_content("start date #{formatted_date(subscription.start_date)}")
+        expect(page).to have_link("Upgrade", href: "#{EE::SUBSCRIPTIONS_URL}/subscriptions")
+        expect(page).not_to have_link("Manage")
+      end
     end
   end
 
@@ -54,25 +60,29 @@ def formatted_date(date)
         "#{EE::SUBSCRIPTIONS_URL}/gitlab/namespaces/#{group.id}/upgrade/bronze-external-id"
 
       expect(page).to have_content("#{group.name} is currently using the Bronze plan")
-      expect(page).to have_content("start date #{formatted_date(subscription.start_date)}")
-      expect(page).to have_link("Upgrade", href: upgrade_url)
-      expect(page).to have_link("Manage", href: "#{EE::SUBSCRIPTIONS_URL}/subscriptions")
+      within subscription_table do
+        expect(page).to have_content("start date #{formatted_date(subscription.start_date)}")
+        expect(page).to have_link("Upgrade", href: upgrade_url)
+        expect(page).to have_link("Manage", href: "#{EE::SUBSCRIPTIONS_URL}/subscriptions")
+      end
     end
   end
 
   context 'with a legacy paid plan' do
     let(:plan) { 'bronze' }
 
-    before do
-      group.update_attribute(:plan, bronze_plan)
+    let!(:subscription) do
+      create(:gitlab_subscription, end_date: 1.week.ago, namespace: group, hosted_plan: bronze_plan, seats: 15)
     end
 
     it 'shows the proper title and subscription data' do
       visit group_billings_path(group)
 
       expect(page).to have_content("#{group.name} is currently using the Bronze plan")
-      expect(page).not_to have_link("Upgrade")
-      expect(page).to have_link("Manage", href: "#{EE::SUBSCRIPTIONS_URL}/subscriptions")
+      within subscription_table do
+        expect(page).not_to have_link("Upgrade")
+        expect(page).to have_link("Manage", href: "#{EE::SUBSCRIPTIONS_URL}/subscriptions")
+      end
     end
   end
 end
diff --git a/ee/spec/features/projects/environments_pod_logs_spec.rb b/ee/spec/features/projects/environments_pod_logs_spec.rb
index c15c588c43f90862bd50730f38a84013f31660cb..3be64d377dcd3340da9a367c361c7c174832dcaa 100644
--- a/ee/spec/features/projects/environments_pod_logs_spec.rb
+++ b/ee/spec/features/projects/environments_pod_logs_spec.rb
@@ -16,21 +16,18 @@
   before do
     stub_licensed_features(pod_logs: true)
 
-    # We're setting this feature flag to false since the FE does not support it
-    # as yet.
-    stub_feature_flags(pod_logs_reactive_cache: false)
-
     create(:cluster, :provided_by_gcp, environment_scope: '*', projects: [project])
     create(:deployment, :success, environment: environment)
 
-    stub_kubeclient_logs(pod_name, environment.deployment_namespace, container: nil)
+    stub_kubeclient_pod_details(pod_name, environment.deployment_namespace)
+    stub_kubeclient_logs(pod_name, environment.deployment_namespace, container: 'container-0')
 
     allow_any_instance_of(EE::Environment).to receive(:pod_names).and_return(pod_names)
 
     sign_in(project.owner)
   end
 
-  context 'with logs' do
+  context 'with logs', :use_clean_rails_memory_store_caching do
     it "shows pod logs" do
       visit logs_project_environment_path(environment.project, environment, pod_name: pod_name)
 
diff --git a/ee/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb b/ee/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
index ec6db7cc358cf24bc0860e5d426f270fd3b61187..f39265ec1bb06c2f9a83a29c48c093115f517f2a 100644
--- a/ee/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
+++ b/ee/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
@@ -24,7 +24,7 @@
       wait_for_requests
     end
 
-    it 'uploads design' do
+    it 'uploads designs' do
       attach_file(:design_file, logo_fixture, make_visible: true)
 
       expect(page).to have_selector('.js-design-list-item', count: 1)
@@ -32,6 +32,10 @@
       within first('#designs-tab .card') do
         expect(page).to have_content('dk.png')
       end
+
+      attach_file(:design_file, gif_fixture, make_visible: true)
+
+      expect(page).to have_selector('.js-design-list-item', count: 2)
     end
   end
 
@@ -48,4 +52,8 @@
   def logo_fixture
     Rails.root.join('spec', 'fixtures', 'dk.png')
   end
+
+  def gif_fixture
+    Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
+  end
 end
diff --git a/ee/spec/features/projects/new_project_spec.rb b/ee/spec/features/projects/new_project_spec.rb
index c9fe7cfc0de45e7485c50c6d027363ed6761df8a..d3235ce2fc2baeaa699392833b05cea7dc9f2ae3 100644
--- a/ee/spec/features/projects/new_project_spec.rb
+++ b/ee/spec/features/projects/new_project_spec.rb
@@ -224,7 +224,7 @@ def visit_create_from_group_template_tab
           visit_create_from_group_template_tab
 
           page.within('.custom-project-templates') do
-            page.find(".template-option input[value='#{subgroup1_project1.name}']").first(:xpath, './/..').click
+            page.find(".template-option input[value='#{subgroup1_project1.id}']").first(:xpath, './/..').click
             wait_for_all_requests
           end
         end
@@ -253,7 +253,7 @@ def visit_create_from_group_template_tab
             page.within('#create-from-template-pane') do
               click_button 'Change template'
 
-              page.find(:xpath, "//input[@type='radio' and @value='#{subgroup1_project1.name}']/..").click
+              page.find(:xpath, "//input[@type='radio' and @value='#{subgroup1_project1.id}']/..").click
 
               wait_for_all_requests
             end
diff --git a/ee/spec/features/projects/services/user_activates_alerts_spec.rb b/ee/spec/features/projects/services/user_activates_alerts_spec.rb
index 2d1cf0660de33abb0887a824e2bef1f2cb387888..d0a7672cf0384e365428a68856c444ec00fb3897 100644
--- a/ee/spec/features/projects/services/user_activates_alerts_spec.rb
+++ b/ee/spec/features/projects/services/user_activates_alerts_spec.rb
@@ -25,7 +25,6 @@
   context 'when feature available', :js do
     before do
       stub_licensed_features(incident_management: true)
-      stub_feature_flags(generic_alert_endpoint: true)
     end
 
     context 'when service is deactivated' do
@@ -60,14 +59,6 @@
         expect(reset_key.value).to be_present
       end
     end
-
-    context 'when feature flag `generic_alert_endpoint` disabled' do
-      before do
-        stub_feature_flags(generic_alert_endpoint: false)
-      end
-
-      it_behaves_like 'no service'
-    end
   end
 
   context 'when feature unavailable' do
diff --git a/ee/spec/features/search/elastic/snippet_search_spec.rb b/ee/spec/features/search/elastic/snippet_search_spec.rb
index a250cae9f4cad54a7f8b4036883dce1999493132..ffb2641cf4fa58cb5258d22269093cfde378275a 100644
--- a/ee/spec/features/search/elastic/snippet_search_spec.rb
+++ b/ee/spec/features/search/elastic/snippet_search_spec.rb
@@ -3,33 +3,111 @@
 require 'spec_helper'
 
 describe 'Snippet elastic search', :js, :elastic do
-  let(:user) { create(:user) }
-  let(:project) { create(:project, namespace: user.namespace) }
+  let(:public_project) { create(:project, :public) }
+  let(:authorized_user) { create(:user) }
+  let(:authorized_project) { create(:project, namespace: authorized_user.namespace) }
 
   before do
     stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
 
-    project.add_maintainer(user)
-    sign_in(user)
+    authorized_project.add_maintainer(authorized_user)
+
+    create(:personal_snippet, :public, content: 'public personal snippet')
+    create(:project_snippet, :public, content: 'public project snippet', project: public_project)
+
+    create(:personal_snippet, :internal, content: 'internal personal snippet')
+    create(:project_snippet, :internal, content: 'internal project snippet', project: public_project)
+
+    create(:personal_snippet, :private, content: 'private personal snippet')
+    create(:project_snippet, :private, content: 'private project snippet', project: public_project)
+
+    create(:personal_snippet, :private, content: 'authorized personal snippet', author: authorized_user)
+    create(:project_snippet, :private, content: 'authorized project snippet', project: authorized_project)
+
+    Gitlab::Elastic::Helper.refresh_index
+
+    sign_in(current_user) if current_user
+    visit explore_snippets_path
+    submit_search('snippet')
   end
 
-  describe 'searching' do
-    it 'finds a personal snippet' do
-      create(:personal_snippet, author: user, content: 'Test searching for personal snippets')
+  context 'as anonymous user' do
+    let(:current_user) { nil }
+
+    it 'finds only public snippets' do
+      within('.results') do
+        expect(page).to have_content('public personal snippet')
+        expect(page).to have_content('public project snippet')
 
-      visit explore_snippets_path
-      submit_search('Test')
+        expect(page).not_to have_content('internal personal snippet')
+        expect(page).not_to have_content('internal project snippet')
 
-      expect(page).to have_selector('.results', text: 'Test searching for personal snippets')
+        expect(page).not_to have_content('authorized personal snippet')
+        expect(page).not_to have_content('authorized project snippet')
+
+        expect(page).not_to have_content('private personal snippet')
+        expect(page).not_to have_content('private project snippet')
+      end
     end
+  end
+
+  context 'as logged in user' do
+    let(:current_user) { create(:user) }
+
+    it 'finds only public and internal snippets' do
+      within('.results') do
+        expect(page).to have_content('public personal snippet')
+        expect(page).to have_content('public project snippet')
+
+        expect(page).to have_content('internal personal snippet')
+        expect(page).to have_content('internal project snippet')
+
+        expect(page).not_to have_content('private personal snippet')
+        expect(page).not_to have_content('private project snippet')
+
+        expect(page).not_to have_content('authorized personal snippet')
+        expect(page).not_to have_content('authorized project snippet')
+      end
+    end
+  end
+
+  context 'as authorized user' do
+    let(:current_user) { authorized_user }
+
+    it 'finds only public, internal, and authorized private snippets' do
+      within('.results') do
+        expect(page).to have_content('public personal snippet')
+        expect(page).to have_content('public project snippet')
+
+        expect(page).to have_content('internal personal snippet')
+        expect(page).to have_content('internal project snippet')
+
+        expect(page).not_to have_content('private personal snippet')
+        expect(page).not_to have_content('private project snippet')
+
+        expect(page).to have_content('authorized personal snippet')
+        expect(page).to have_content('authorized project snippet')
+      end
+    end
+  end
+
+  context 'as administrator' do
+    let(:current_user) { create(:admin) }
+
+    it 'finds all snippets' do
+      within('.results') do
+        expect(page).to have_content('public personal snippet')
+        expect(page).to have_content('public project snippet')
 
-    it 'finds a project snippet' do
-      create(:project_snippet, project: project, content: 'Test searching for personal snippets')
+        expect(page).to have_content('internal personal snippet')
+        expect(page).to have_content('internal project snippet')
 
-      visit explore_snippets_path
-      submit_search('Test')
+        expect(page).to have_content('private personal snippet')
+        expect(page).to have_content('private project snippet')
 
-      expect(page).to have_selector('.results', text: 'Test searching for personal snippets')
+        expect(page).to have_content('authorized personal snippet')
+        expect(page).to have_content('authorized project snippet')
+      end
     end
   end
 end
diff --git a/ee/spec/features/signup_spec.rb b/ee/spec/features/signup_spec.rb
index 70a45b6f98284bf23b40764df678865428de1487..d0258175bd7d73b55ac02c86295a021e46d9bc98 100644
--- a/ee/spec/features/signup_spec.rb
+++ b/ee/spec/features/signup_spec.rb
@@ -5,10 +5,6 @@
 describe 'Signup on EE' do
   let(:user_attrs) { attributes_for(:user) }
 
-  before do
-    stub_feature_flags(invisible_captcha: false)
-  end
-
   context 'for Gitlab.com' do
     before do
       expect(Gitlab).to receive(:com?).and_return(true).at_least(:once)
diff --git a/ee/spec/features/trial_registrations/signup_spec.rb b/ee/spec/features/trial_registrations/signup_spec.rb
index d28ffb42dd387d8f265fee0fef81a7f38bffa159..825230fad61fd60c10aa6c089f22024de6ebcf50 100644
--- a/ee/spec/features/trial_registrations/signup_spec.rb
+++ b/ee/spec/features/trial_registrations/signup_spec.rb
@@ -7,7 +7,6 @@
 
   describe 'on GitLab.com' do
     before do
-      stub_feature_flags(invisible_captcha: false)
       stub_feature_flags(improved_trial_signup: true)
       allow(Gitlab).to receive(:com?).and_return(true).at_least(:once)
     end
diff --git a/ee/spec/features/trials/capture_lead_spec.rb b/ee/spec/features/trials/capture_lead_spec.rb
index 7d0e93d514db1d0abee1f3d42a42baa8a8ed01cf..f3c5aa5aa9aa10a9e6e0344e5a89caf5de50fdba 100644
--- a/ee/spec/features/trials/capture_lead_spec.rb
+++ b/ee/spec/features/trials/capture_lead_spec.rb
@@ -7,7 +7,6 @@
   let(:user) { create(:user) }
 
   before do
-    stub_feature_flags(invisible_captcha: false)
     stub_feature_flags(improved_trial_signup: true)
     allow(Gitlab).to receive(:com?).and_return(true).at_least(:once)
     sign_in(user)
diff --git a/ee/spec/features/trials/select_namespace_spec.rb b/ee/spec/features/trials/select_namespace_spec.rb
index 139c0191c6add35bb27aaf9153c29e421ffe87e2..64244199b5f9951a9b583ff3c9351cfe0a8d5546 100644
--- a/ee/spec/features/trials/select_namespace_spec.rb
+++ b/ee/spec/features/trials/select_namespace_spec.rb
@@ -9,7 +9,6 @@
   let(:user) { create(:user) }
 
   before do
-    stub_feature_flags(invisible_captcha: false)
     stub_feature_flags(improved_trial_signup: true)
     allow(Gitlab).to receive(:com?).and_return(true).at_least(:once)
     sign_in(user)
@@ -84,13 +83,14 @@
           click_button 'Start your free trial'
 
           message = page.find('#new_group_name').native.attribute('validationMessage')
+
           expect(message).to eq('Please fill out this field.')
           expect(current_path).to eq(select_trials_path)
         end
       end
     end
 
-    context 'selects an existing user' do
+    context 'selects an existing group' do
       before do
         visit select_trials_path
         wait_for_all_requests
@@ -98,20 +98,42 @@
         select2 user.namespace.id, from: '#namespace_id'
       end
 
-      it 'does not show the new group name input' do
-        expect(page).not_to have_field('New Group Name')
-      end
+      context 'without trial plan' do
+        it 'does not show the new group name input' do
+          expect(page).not_to have_field('New Group Name')
+        end
+
+        it 'applies trial and redirects to dashboard' do
+          expect_any_instance_of(GitlabSubscriptions::ApplyTrialService).to receive(:execute) do
+            { success: true }
+          end
+
+          click_button 'Start your free trial'
+
+          wait_for_requests
 
-      it 'applies trial and redirects to dashboard' do
-        expect_any_instance_of(GitlabSubscriptions::ApplyTrialService).to receive(:execute) do
-          { success: true }
+          expect(current_path).to eq("/#{user.namespace.path}")
         end
+      end
 
-        click_button 'Start your free trial'
+      context 'with trial plan' do
+        let!(:error_message) { 'Validation failed: Gl namespace can have only one trial' }
 
-        wait_for_requests
+        it 'shows validation error' do
+          expect_any_instance_of(GitlabSubscriptions::ApplyTrialService).to receive(:execute) do
+            { success: false, errors: error_message }
+          end
+
+          click_button 'Start your free trial'
 
-        expect(current_path).to eq("/#{user.namespace.path}")
+          expect(find('.flash-text')).to have_text(error_message)
+          expect(current_path).to eq(apply_trials_path)
+
+          # new group name should be functional
+          select2 '0', from: '#namespace_id'
+
+          expect(page).to have_field('New Group Name')
+        end
       end
     end
   end
diff --git a/ee/spec/features/trials/show_trial_banner_spec.rb b/ee/spec/features/trials/show_trial_banner_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8c9145bd718fd8c8a6bede7ee6d5de50553f3e15
--- /dev/null
+++ b/ee/spec/features/trials/show_trial_banner_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Show trial banner', :js do
+  include StubRequests
+
+  let!(:user) { create(:user) }
+  let!(:group) { create(:group) }
+  let!(:bronze_plan) { create(:bronze_plan) }
+  let(:plans_data) do
+    JSON.parse(File.read(Rails.root.join('ee/spec/fixtures/gitlab_com_plans.json'))).map do |data|
+      data.deep_symbolize_keys
+    end
+  end
+
+  before do
+    stub_application_setting(check_namespace_plan: true)
+    allow(Gitlab).to receive(:com?).and_return(true).at_least(:once)
+    stub_full_request("#{EE::SUBSCRIPTIONS_URL}/gitlab_plans?plan=bronze")
+      .to_return(status: 200, body: plans_data.to_json)
+
+    group.add_owner(user)
+    create(:gitlab_subscription, namespace: user.namespace, hosted_plan: bronze_plan, trial: true, trial_ends_on: Date.current + 1.month)
+    create(:gitlab_subscription, namespace: group, hosted_plan: bronze_plan, trial: true, trial_ends_on: Date.current + 1.month)
+
+    gitlab_sign_in(user)
+  end
+
+  context "when user's trial is active" do
+    it 'renders congratulations banner for user in profile billing page' do
+      visit profile_billings_path + '?trial=true'
+
+      expect(page).to have_content('Congratulations, your new trial is activated')
+    end
+  end
+
+  context "when group's trial is active" do
+    it 'renders congratulations banner for group in group details page' do
+      visit group_path(group, trial: true)
+
+      expect(find('.user-callout').text).to have_content('Congratulations, your new trial is activated')
+    end
+
+    it 'does not render congratulations banner for group in group billing page' do
+      visit group_billings_path(group, trial: true)
+
+      expect(page).not_to have_content('Congratulations, your new trial is activated')
+    end
+  end
+end
diff --git a/ee/spec/finders/ee/group_members_finder_spec.rb b/ee/spec/finders/ee/group_members_finder_spec.rb
index 58c525915f9463a86f8887f7891dfa77dbdf638b..b0947f5fd4421ad04dbe538ee8f90c261e579f41 100644
--- a/ee/spec/finders/ee/group_members_finder_spec.rb
+++ b/ee/spec/finders/ee/group_members_finder_spec.rb
@@ -4,6 +4,7 @@
 
 describe GroupMembersFinder do
   subject(:finder) { described_class.new(group) }
+
   let(:group) { create :group }
 
   let(:non_owner_access_level) { Gitlab::Access.options.values.sample }
diff --git a/ee/spec/finders/feature_flags_finder_spec.rb b/ee/spec/finders/feature_flags_finder_spec.rb
index af3df86a9f20bf78de7dc779f5086d7bebf4d390..90d54c6fa1d82320066af36ec1d0ba45c956bd04 100644
--- a/ee/spec/finders/feature_flags_finder_spec.rb
+++ b/ee/spec/finders/feature_flags_finder_spec.rb
@@ -20,15 +20,22 @@
   end
 
   describe '#execute' do
-    subject { finder.execute }
+    subject { finder.execute(args) }
 
     let!(:feature_flag_1) { create(:operations_feature_flag, name: 'flag-a', project: project) }
     let!(:feature_flag_2) { create(:operations_feature_flag, name: 'flag-b', project: project) }
+    let(:args) { {} }
 
     it 'returns feature flags ordered by name' do
       is_expected.to eq([feature_flag_1, feature_flag_2])
     end
 
+    it 'preloads relations by default' do
+      expect(Operations::FeatureFlag).to receive(:preload_relations).and_call_original
+
+      subject
+    end
+
     context 'when user is a reporter' do
       let(:user) { reporter }
 
@@ -58,6 +65,16 @@
       end
     end
 
+    context 'when preload option is false' do
+      let(:args) { { preload: false } }
+
+      it 'does not preload relations' do
+        expect(Operations::FeatureFlag).not_to receive(:preload_relations)
+
+        subject
+      end
+    end
+
     context 'when it is presented for list' do
       let!(:feature_flag_1) { create(:operations_feature_flag, project: project, active: false) }
       let!(:feature_flag_2) { create(:operations_feature_flag, project: project, active: false) }
diff --git a/ee/spec/finders/productivity_analytics_finder_spec.rb b/ee/spec/finders/productivity_analytics_finder_spec.rb
index 7ad64f1c22a51925af59f1b5cbf8fde5c00fa195..c4e75a3b2583cf7723775cf2fc32de90a5cc572e 100644
--- a/ee/spec/finders/productivity_analytics_finder_spec.rb
+++ b/ee/spec/finders/productivity_analytics_finder_spec.rb
@@ -4,6 +4,7 @@
 
 describe ProductivityAnalyticsFinder do
   subject { described_class.new(current_user, search_params.merge(state: :merged)) }
+
   let(:current_user) { create(:admin) }
   let(:search_params) { {} }
 
diff --git a/ee/spec/finders/security/pipeline_vulnerabilities_finder_spec.rb b/ee/spec/finders/security/pipeline_vulnerabilities_finder_spec.rb
index 7e975f38d89d0c48eabd8c6621df6e2d67c2a618..52a146c2125593cc08554d502ae8ca3242d1f77f 100644
--- a/ee/spec/finders/security/pipeline_vulnerabilities_finder_spec.rb
+++ b/ee/spec/finders/security/pipeline_vulnerabilities_finder_spec.rb
@@ -3,6 +3,22 @@
 require 'spec_helper'
 
 describe Security::PipelineVulnerabilitiesFinder do
+  class NoDeduplicationMergeReportsService
+    def initialize(*source_reports)
+      @source_reports = source_reports
+    end
+
+    def execute
+      @source_reports.last
+    end
+  end
+
+  def disable_deduplication
+    allow(::Security::MergeReportsService).to receive(:new) do |*args|
+      NoDeduplicationMergeReportsService.new(*args)
+    end
+  end
+
   describe '#execute' do
     set(:project) { create(:project, :repository) }
     set(:pipeline) { create(:ci_pipeline, :success, project: project) }
@@ -18,16 +34,38 @@
     set(:artifact_sast) { create(:ee_ci_job_artifact, :sast, job: build_sast, project: project) }
 
     let(:cs_count) { read_fixture(artifact_cs)['unapproved'].count }
-    let(:dast_count) { read_fixture(artifact_dast)['site'].first['alerts'].first['instances'].count }
     let(:ds_count) { read_fixture(artifact_ds)['vulnerabilities'].count }
     let(:sast_count) { read_fixture(artifact_sast)['vulnerabilities'].count }
+    let(:dast_count) do
+      read_fixture(artifact_dast)['site'].sum do |site|
+        site['alerts'].sum do |alert|
+          alert['instances'].size
+        end
+      end
+    end
 
     before do
       stub_licensed_features(sast: true, dependency_scanning: true, container_scanning: true, dast: true)
+      # Stub out deduplication, if not done the expectations will vary based on the fixtures (which may/may not have duplicates)
+      disable_deduplication
     end
 
     subject { described_class.new(pipeline: pipeline, params: params).execute }
 
+    context 'by order' do
+      let(:params) { { report_type: %w[sast] } }
+      let!(:occurrence1) { build(:vulnerabilities_occurrence, confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:high],   severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS[:high]) }
+      let!(:occurrence2) { build(:vulnerabilities_occurrence, confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:medium], severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS[:critical]) }
+      let!(:occurrence3) { build(:vulnerabilities_occurrence, confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:high],   severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS[:critical]) }
+      let!(:res) { [occurrence3, occurrence2, occurrence1] }
+
+      it 'orders by severity and confidence' do
+        allow_any_instance_of(described_class).to receive(:filter).and_return(res)
+
+        expect(subject).to eq([occurrence3, occurrence2, occurrence1])
+      end
+    end
+
     context 'by report type' do
       context 'when sast' do
         let(:params) { { report_type: %w[sast] } }
@@ -121,7 +159,7 @@
         subject { described_class.new(pipeline: pipeline).execute }
 
         it 'returns all vulnerability severity levels' do
-          expect(subject.map(&:severity).uniq).to match_array %w[undefined unknown low medium high critical]
+          expect(subject.map(&:severity).uniq).to match_array %w[undefined unknown low medium high critical info]
         end
       end
 
@@ -159,7 +197,7 @@
         it 'filters by all params' do
           expect(subject.count).to eq cs_count + dast_count + ds_count + sast_count
           expect(subject.map(&:confidence).uniq).to match_array %w[undefined unknown low medium high]
-          expect(subject.map(&:severity).uniq).to match_array %w[undefined unknown low medium high critical]
+          expect(subject.map(&:severity).uniq).to match_array %w[undefined unknown low medium high critical info]
         end
       end
 
diff --git a/ee/spec/fixtures/api/schemas/analytics/cycle_analytics/stage.json b/ee/spec/fixtures/api/schemas/analytics/cycle_analytics/stage.json
index a702d60e641a97c147dd60fb0bf78eb0a12893c0..725d6188cd2035a9107ba9bd3abc0234c0a01c3b 100644
--- a/ee/spec/fixtures/api/schemas/analytics/cycle_analytics/stage.json
+++ b/ee/spec/fixtures/api/schemas/analytics/cycle_analytics/stage.json
@@ -21,5 +21,5 @@
       "type": "boolean"
     }
   },
-  "additionalProperties": false
+  "additionalProperties": true
 }
diff --git a/ee/spec/fixtures/api/schemas/analytics/cycle_analytics/validation_error.json b/ee/spec/fixtures/api/schemas/analytics/cycle_analytics/validation_error.json
new file mode 100644
index 0000000000000000000000000000000000000000..34944b99b91da4805f49c25e49647c2162ba064f
--- /dev/null
+++ b/ee/spec/fixtures/api/schemas/analytics/cycle_analytics/validation_error.json
@@ -0,0 +1,19 @@
+{
+  "type": "object",
+  "properties": {
+    "message": { 
+      "type": "string" 
+    },
+    "errors": { 
+      "type": "object",
+      "additionalProperties" : {
+        "type" : "array",
+        "items": {
+          "type": "string"
+        }
+      }
+    }
+  },
+  "required": ["message", "errors"],
+  "additionalProperties": false
+}
diff --git a/ee/spec/fixtures/api/schemas/licenses_list.json b/ee/spec/fixtures/api/schemas/licenses_list.json
index c62a4a857acb2971ba913a86f8a94f2e2d2fe9bf..15a97882c398101f310b4b5038ad162b3056f227 100644
--- a/ee/spec/fixtures/api/schemas/licenses_list.json
+++ b/ee/spec/fixtures/api/schemas/licenses_list.json
@@ -26,6 +26,9 @@
             "properties": {
               "name": {
                 "type": "string"
+              },
+              "blob_path": {
+                "type": "string"
               }
             }
           }
diff --git a/ee/spec/fixtures/api/schemas/public_api/v4/epic.json b/ee/spec/fixtures/api/schemas/public_api/v4/epic.json
index 534d8e6cbc53b6bcc893a779bfb63fdf450d7f9c..051e45321e236add22bb33f4b45406a69ae33714 100644
--- a/ee/spec/fixtures/api/schemas/public_api/v4/epic.json
+++ b/ee/spec/fixtures/api/schemas/public_api/v4/epic.json
@@ -42,7 +42,8 @@
     "closed_at": { "type": ["string", "null"] },
     "web_edit_url": { "type":  "string" },
     "web_url": { "type":  "string" },
-    "reference": { "type":  "string" }
+    "reference": { "type":  "string" },
+    "subscribed": { "type": ["boolean", "null"] }
   },
   "required": [
     "id", "iid", "group_id", "title"
diff --git a/ee/spec/fixtures/api/schemas/public_api/v4/feature_flag.json b/ee/spec/fixtures/api/schemas/public_api/v4/feature_flag.json
new file mode 100644
index 0000000000000000000000000000000000000000..43818cdaca85d3eca6beed404e73b628a9068bff
--- /dev/null
+++ b/ee/spec/fixtures/api/schemas/public_api/v4/feature_flag.json
@@ -0,0 +1,12 @@
+{
+  "type": "object",
+  "required": ["name"],
+  "properties": {
+    "name": { "type": "string" },
+    "description": { "type": ["string", "null"] },
+    "created_at": { "type": "date" },
+    "updated_at": { "type": "date" },
+    "scopes": { "type": "array", "items": { "$ref": "feature_flag_scope.json" } }
+  },
+  "additionalProperties": false
+}
diff --git a/ee/spec/fixtures/api/schemas/public_api/v4/feature_flag_scope.json b/ee/spec/fixtures/api/schemas/public_api/v4/feature_flag_scope.json
new file mode 100644
index 0000000000000000000000000000000000000000..18402af482e22ca4958f4b7e6bd7a90fe2d9000a
--- /dev/null
+++ b/ee/spec/fixtures/api/schemas/public_api/v4/feature_flag_scope.json
@@ -0,0 +1,17 @@
+{
+  "type": "object",
+  "required": [
+    "id",
+    "environment_scope",
+    "active"
+  ],
+  "properties": {
+    "id": { "type": "integer" },
+    "environment_scope": { "type": "string" },
+    "active": { "type": "boolean" },
+    "created_at": { "type": "date" },
+    "updated_at": { "type": "date" },
+    "strategies": { "type": "array", "items": { "$ref": "feature_flag_strategy.json" } }
+  },
+  "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/evidences/author.json b/ee/spec/fixtures/api/schemas/public_api/v4/feature_flag_strategy.json
similarity index 57%
rename from spec/fixtures/api/schemas/evidences/author.json
rename to ee/spec/fixtures/api/schemas/public_api/v4/feature_flag_strategy.json
index 1b49446900a1e3e8ca0c77827282e6457c10e707..5a2777dc8ea75c72e7019b86a196c85e74cfe08a 100644
--- a/spec/fixtures/api/schemas/evidences/author.json
+++ b/ee/spec/fixtures/api/schemas/public_api/v4/feature_flag_strategy.json
@@ -1,14 +1,13 @@
 {
   "type": "object",
   "required": [
-    "id",
-    "name",
-    "email"
+    "name"
   ],
   "properties": {
-    "id": { "type": "integer" },
     "name": { "type": "string" },
-    "email": { "type": "string" }
+    "parameters": {
+      "type": "object"
+    }
   },
   "additionalProperties": false
 }
diff --git a/ee/spec/fixtures/api/schemas/public_api/v4/feature_flags.json b/ee/spec/fixtures/api/schemas/public_api/v4/feature_flags.json
new file mode 100644
index 0000000000000000000000000000000000000000..c19df0443d983ad72f481ec51d226769958ccbb2
--- /dev/null
+++ b/ee/spec/fixtures/api/schemas/public_api/v4/feature_flags.json
@@ -0,0 +1,9 @@
+{
+  "type": "array",
+  "items": {
+    "type": "object",
+    "properties": {
+      "$ref": "./feature_flag.json"
+    }
+  }
+}
diff --git a/ee/spec/fixtures/security_reports/feature-branch/gl-dast-report.json b/ee/spec/fixtures/security_reports/feature-branch/gl-dast-report.json
deleted file mode 100644
index 3a308bf047efd80ebbd27d5c19c8c38eea2f6bef..0000000000000000000000000000000000000000
--- a/ee/spec/fixtures/security_reports/feature-branch/gl-dast-report.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
-  "site": {
-    "alerts": [
-      {
-        "sourceid": "3",
-        "wascid": "15",
-        "cweid": "16",
-        "reference": "<p>http://msdn.microsoft.com/en-us/library/ie/gg622941%28v=vs.85%29.aspx</p><p>https://www.owasp.org/index.php/List_of_useful_HTTP_headers</p>",
-        "otherinfo": "<p>This issue still applies to error type pages (401, 403, 500, etc) as those pages are often still affected by injection issues, in which case there is still concern for browsers sniffing pages away from their actual content type.</p><p>At \"High\" threshold this scanner will not alert on client or server error responses.</p>",
-        "solution": "<p>Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to 'nosniff' for all web pages.</p><p>If possible, ensure that the end user uses a standards-compliant and modern web browser that does not perform MIME-sniffing at all, or that can be directed by the web application/web server to not perform MIME-sniffing.</p>",
-        "count": "2",
-        "pluginid": "10021",
-        "alert": "X-Content-Type-Options Header Missing",
-        "name": "X-Content-Type-Options Header Missing",
-        "riskcode": "1",
-        "confidence": "2",
-        "riskdesc": "Low (Medium)",
-        "desc": "<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to 'nosniff'. This allows older versions of Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing.</p>",
-        "instances": [
-          {
-            "param": "X-Content-Type-Options",
-            "method": "GET",
-            "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io"
-          },
-          {
-            "param": "X-Content-Type-Options",
-            "method": "GET",
-            "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io/"
-          }
-        ]
-      }
-    ],
-    "@ssl": "false",
-    "@port": "80",
-    "@host": "bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io",
-    "@name": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io"
-  },
-  "@generated": "Fri, 13 Apr 2018 09:22:01",
-  "@version": "2.7.0"
-}
diff --git a/ee/spec/fixtures/security_reports/master/gl-dast-report-low-severity.json b/ee/spec/fixtures/security_reports/master/gl-dast-report-low-severity.json
new file mode 100644
index 0000000000000000000000000000000000000000..bb9931c73225d839f05538942d04e5f5ce4449f2
--- /dev/null
+++ b/ee/spec/fixtures/security_reports/master/gl-dast-report-low-severity.json
@@ -0,0 +1,468 @@
+{
+  "site": [
+    {
+      "@port": "8080",
+      "@host": "goat",
+      "@name": "http://goat:8080",
+      "alerts": [
+        {
+          "count": "4",
+          "riskdesc": "Low (Medium)",
+          "name": "Absence of Anti-CSRF Tokens",
+          "reference": "<p>http://projects.webappsec.org/Cross-Site-Request-Forgery</p><p>http://cwe.mitre.org/data/definitions/352.html</p>",
+          "otherinfo": "<p>No known Anti-CSRF token [anticsrf, CSRFToken, __RequestVerificationToken, csrfmiddlewaretoken, authenticity_token, OWASP_CSRFTOKEN, anoncsrf, csrf_token, _csrf, _csrfSecret] was found in the following HTML form: [Form 1: \"exampleInputEmail1\" \"exampleInputPassword1\" ].</p>",
+          "sourceid": "3",
+          "confidence": "2",
+          "alert": "Absence of Anti-CSRF Tokens",
+          "instances": [
+            {
+              "evidence": "<form method=\"POST\" style=\"width: 200px;\" action=\"/WebGoat/login\">",
+              "uri": "http://goat:8080/WebGoat/login",
+              "method": "GET"
+            },
+            {
+              "evidence": "<form method=\"POST\" style=\"width: 200px;\" action=\"/WebGoat/login\">",
+              "uri": "http://goat:8080/WebGoat/login?error",
+              "method": "GET"
+            },
+            {
+              "evidence": "<form class=\"form-horizontal\" action=\"/WebGoat/register.mvc\" method=\"POST\">",
+              "uri": "http://goat:8080/WebGoat/registration",
+              "method": "GET"
+            },
+            {
+              "evidence": "<form class=\"form-horizontal\" action=\"/WebGoat/register.mvc\" method=\"POST\">",
+              "uri": "http://goat:8080/WebGoat/register.mvc",
+              "method": "POST"
+            }
+          ],
+          "pluginid": "10202",
+          "riskcode": "1",
+          "wascid": "9",
+          "solution": "<p>Phase: Architecture and Design</p><p>Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid.</p><p>For example, use anti-CSRF packages such as the OWASP CSRFGuard.</p><p></p><p>Phase: Implementation</p><p>Ensure that your application is free of cross-site scripting issues, because most CSRF defenses can be bypassed using attacker-controlled script.</p><p></p><p>Phase: Architecture and Design</p><p>Generate a unique nonce for each form, place the nonce into the form, and verify the nonce upon receipt of the form. Be sure that the nonce is not predictable (CWE-330).</p><p>Note that this can be bypassed using XSS.</p><p></p><p>Identify especially dangerous operations. When the user performs a dangerous operation, send a separate confirmation request to ensure that the user intended to perform that operation.</p><p>Note that this can be bypassed using XSS.</p><p></p><p>Use the ESAPI Session Management control.</p><p>This control includes a component for CSRF.</p><p></p><p>Do not use the GET method for any request that triggers a state change.</p><p></p><p>Phase: Implementation</p><p>Check the HTTP Referer header to see if the request originated from an expected page. This could break legitimate functionality, because users or proxies may have disabled sending the Referer for privacy reasons.</p>",
+          "cweid": "352",
+          "desc": "<p>No Anti-CSRF tokens were found in a HTML submission form.</p><p>A cross-site request forgery is an attack that involves forcing a victim to send an HTTP request to a target destination without their knowledge or intent in order to perform an action as the victim. The underlying cause is application functionality using predictable URL/form actions in a repeatable way. The nature of the attack is that CSRF exploits the trust that a web site has for a user. By contrast, cross-site scripting (XSS) exploits the trust that a user has for a web site. Like XSS, CSRF attacks are not necessarily cross-site, but they can be. Cross-site request forgery is also known as CSRF, XSRF, one-click attack, session riding, confused deputy, and sea surf.</p><p></p><p>CSRF attacks are effective in a number of situations, including:</p><p>    * The victim has an active session on the target site.</p><p>    * The victim is authenticated via HTTP auth on the target site.</p><p>    * The victim is on the same local network as the target site.</p><p></p><p>CSRF has primarily been used to perform an action against a target site using the victim's privileges, but recent techniques have been discovered to disclose information by gaining access to the response. The risk of information disclosure is dramatically increased when the target site is vulnerable to XSS, because XSS can be used as a platform for CSRF, allowing the attack to operate within the bounds of the same-origin policy.</p>"
+        },
+        {
+          "count": "2",
+          "riskdesc": "Low (Medium)",
+          "name": "Cookie Without SameSite Attribute",
+          "reference": "<p>https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site</p>",
+          "sourceid": "3",
+          "confidence": "2",
+          "alert": "Cookie Without SameSite Attribute",
+          "instances": [
+            {
+              "evidence": "Set-Cookie: JSESSIONID",
+              "uri": "http://goat:8080/WebGoat/logout",
+              "param": "JSESSIONID",
+              "method": "GET"
+            },
+            {
+              "evidence": "Set-Cookie: JSESSIONID",
+              "uri": "http://goat:8080/WebGoat/login?logout",
+              "param": "JSESSIONID",
+              "method": "GET"
+            }
+          ],
+          "pluginid": "10054",
+          "riskcode": "1",
+          "wascid": "13",
+          "solution": "<p>Ensure that the SameSite attribute is set to either 'lax' or ideally 'strict' for all cookies.</p>",
+          "cweid": "16",
+          "desc": "<p>A cookie has been set without the SameSite attribute, which means that the cookie can be sent as a result of a 'cross-site' request. The SameSite attribute is an effective counter measure to cross-site request forgery, cross-site script inclusion, and timing attacks.</p>"
+        },
+        {
+          "count": "2",
+          "riskdesc": "Informational (Low)",
+          "name": "Loosely Scoped Cookie",
+          "reference": "<p>https://tools.ietf.org/html/rfc6265#section-4.1</p><p>https://www.owasp.org/index.php/Testing_for_cookies_attributes_(OTG-SESS-002)</p><p>http://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_cookies</p>",
+          "otherinfo": "<p>The origin domain used for comparison was: </p><p>goat</p><p>JSESSIONID=78EC2C9D7CE583610DCC7826EE416D7F</p><p></p>",
+          "sourceid": "3",
+          "confidence": "1",
+          "alert": "Loosely Scoped Cookie",
+          "instances": [
+            {
+              "uri": "http://goat:8080/WebGoat/login?logout",
+              "method": "GET"
+            },
+            {
+              "uri": "http://goat:8080/WebGoat/logout",
+              "method": "GET"
+            }
+          ],
+          "pluginid": "90033",
+          "riskcode": "0",
+          "wascid": "15",
+          "solution": "<p>Always scope cookies to a FQDN (Fully Qualified Domain Name).</p>",
+          "cweid": "565",
+          "desc": "<p>Cookies can be scoped by domain or path. This check is only concerned with domain scope.The domain scope applied to a cookie determines which domains can access it. For example, a cookie can be scoped strictly to a subdomain e.g. www.nottrusted.com, or loosely scoped to a parent domain e.g. nottrusted.com. In the latter case, any subdomain of nottrusted.com can access the cookie. Loosely scoped cookies are common in mega-applications like google.com and live.com. Cookies set from a subdomain like app.foo.bar are transmitted only to that domain by the browser. However, cookies scoped to a parent-level domain may be transmitted to the parent, or any subdomain of the parent.</p>"
+        },
+        {
+          "count": "4",
+          "riskdesc": "Informational (Medium)",
+          "name": "Information Disclosure - Suspicious Comments",
+          "reference": "<p></p>",
+          "otherinfo": "<p><!--<button type=\"button\" id=\"admin-button\" class=\"btn btn-default right_nav_button\" title=\"Administrator\">--></p><p><!--<button type=\"button\" id=\"user-management\" class=\"btn btn-default right_nav_button\"--></p><p><!--title=\"User management\">--></p><p></p>",
+          "sourceid": "3",
+          "confidence": "2",
+          "alert": "Information Disclosure - Suspicious Comments",
+          "instances": [
+            {
+              "uri": "http://goat:8080/WebGoat/start.mvc",
+              "method": "GET"
+            },
+            {
+              "uri": "http://goat:8080/WebGoat/js/html5shiv.js",
+              "method": "GET"
+            },
+            {
+              "uri": "http://goat:8080/WebGoat/js/modernizr-2.6.2.min.js",
+              "method": "GET"
+            },
+            {
+              "uri": "http://goat:8080/WebGoat/js/respond.min.js",
+              "method": "GET"
+            }
+          ],
+          "pluginid": "10027",
+          "riskcode": "0",
+          "wascid": "13",
+          "solution": "<p>Remove all comments that return information that may help an attacker and fix any underlying problems they refer to.</p>",
+          "cweid": "200",
+          "desc": "<p>The response appears to contain suspicious comments which may help an attacker.</p>"
+        },
+        {
+          "count": "5",
+          "riskdesc": "Informational (Low)",
+          "name": "Timestamp Disclosure - Unix",
+          "reference": "<p>https://www.owasp.org/index.php/Top_10_2013-A6-Sensitive_Data_Exposure</p><p>http://projects.webappsec.org/w/page/13246936/Information%20Leakage</p>",
+          "otherinfo": "<p>00000000, which evaluates to: 1970-01-01 00:00:00</p>",
+          "sourceid": "3",
+          "confidence": "1",
+          "alert": "Timestamp Disclosure - Unix",
+          "instances": [
+            {
+              "evidence": "00000000",
+              "uri": "http://goat:8080/WebGoat/plugins/bootstrap/css/bootstrap.min.css",
+              "method": "GET"
+            },
+            {
+              "evidence": "33333333",
+              "uri": "http://goat:8080/WebGoat/plugins/bootstrap/css/bootstrap.min.css",
+              "method": "GET"
+            },
+            {
+              "evidence": "42857143",
+              "uri": "http://goat:8080/WebGoat/plugins/bootstrap/css/bootstrap.min.css",
+              "method": "GET"
+            },
+            {
+              "evidence": "80000000",
+              "uri": "http://goat:8080/WebGoat/plugins/bootstrap/css/bootstrap.min.css",
+              "method": "GET"
+            },
+            {
+              "evidence": "66666667",
+              "uri": "http://goat:8080/WebGoat/plugins/bootstrap/css/bootstrap.min.css",
+              "method": "GET"
+            }
+          ],
+          "pluginid": "10096",
+          "riskcode": "0",
+          "wascid": "13",
+          "solution": "<p>Manually confirm that the timestamp data is not sensitive, and that the data cannot be aggregated to disclose exploitable patterns.</p>",
+          "cweid": "200",
+          "desc": "<p>A timestamp was disclosed by the application/web server - Unix</p>"
+        },
+        {
+          "count": "2",
+          "riskdesc": "Low (Medium)",
+          "name": "Cookie No HttpOnly Flag",
+          "reference": "<p>http://www.owasp.org/index.php/HttpOnly</p>",
+          "sourceid": "3",
+          "confidence": "2",
+          "alert": "Cookie No HttpOnly Flag",
+          "instances": [
+            {
+              "evidence": "Set-Cookie: JSESSIONID",
+              "uri": "http://goat:8080/WebGoat/login?logout",
+              "param": "JSESSIONID",
+              "method": "GET"
+            },
+            {
+              "evidence": "Set-Cookie: JSESSIONID",
+              "uri": "http://goat:8080/WebGoat/logout",
+              "param": "JSESSIONID",
+              "method": "GET"
+            }
+          ],
+          "pluginid": "10010",
+          "riskcode": "1",
+          "wascid": "13",
+          "solution": "<p>Ensure that the HttpOnly flag is set for all cookies.</p>",
+          "cweid": "16",
+          "desc": "<p>A cookie has been set without the HttpOnly flag, which means that the cookie can be accessed by JavaScript. If a malicious script can be run on this page then the cookie will be accessible and can be transmitted to another site. If this is a session cookie then session hijacking may be possible.</p>"
+        },
+        {
+          "count": "1",
+          "riskdesc": "Informational (Low)",
+          "name": "Charset Mismatch (Header Versus Meta Content-Type Charset)",
+          "reference": "<p>http://code.google.com/p/browsersec/wiki/Part2#Character_set_handling_and_detection</p>",
+          "otherinfo": "<p>There was a charset mismatch between the HTTP Header and the META content-type encoding declarations: [UTF-8] and [ISO-8859-1] do not match.</p>",
+          "sourceid": "3",
+          "confidence": "1",
+          "alert": "Charset Mismatch (Header Versus Meta Content-Type Charset)",
+          "instances": [
+            {
+              "uri": "http://goat:8080/WebGoat/start.mvc",
+              "method": "GET"
+            }
+          ],
+          "pluginid": "90011",
+          "riskcode": "0",
+          "wascid": "15",
+          "solution": "<p>Force UTF-8 for all text content in both the HTTP header and meta tags in HTML or encoding declarations in XML.</p>",
+          "cweid": "16",
+          "desc": "<p>This check identifies responses where the HTTP Content-Type header declares a charset different from the charset defined by the body of the HTML or XML. When there's a charset mismatch between the HTTP header and content body Web browsers can be forced into an undesirable content-sniffing mode to determine the content's correct character set.</p><p></p><p>An attacker could manipulate content on the page to be interpreted in an encoding of their choice. For example, if an attacker can control content at the beginning of the page, they could inject script using UTF-7 encoded text and manipulate some browsers into interpreting that text.</p>"
+        }
+      ],
+      "@ssl": "false"
+    }
+  ],
+  "spider": {
+    "progress": "100",
+    "state": "FINISHED",
+    "result": {
+      "urlsIoError": [],
+      "urlsOutOfScope": [
+        "http://getbootstrap.com/",
+        "http://daneden.me/animate",
+        "http://fontawesome.io/",
+        "https://github.com/twbs/bootstrap/blob/master/LICENSE",
+        "https://github.com/nickpettit/glide",
+        "http://fontawesome.io/license"
+      ],
+      "urlsInScope": [
+        {
+          "url": "http://goat:8080",
+          "statusReason": "",
+          "reasonNotProcessed": "Not Text",
+          "processed": "false",
+          "method": "GET",
+          "statusCode": "404"
+        },
+        {
+          "url": "http://goat:8080/robots.txt",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "404"
+        },
+        {
+          "url": "http://goat:8080/sitemap.xml",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "404"
+        },
+        {
+          "url": "http://goat:8080/WebGoat",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "302"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/attack",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "302"
+        },
+        {
+          "url": "http://goat:8080/",
+          "statusReason": "",
+          "reasonNotProcessed": "Not Text",
+          "processed": "false",
+          "method": "GET",
+          "statusCode": "404"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "302"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/start.mvc",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/welcome.mvc",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "302"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/logout",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "302"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/css/main.css",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/images/favicon.ico",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "404"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/plugins/bootstrap/css/bootstrap.min.css",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/css/font-awesome.min.css",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/css/coderay.css",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/css/animate.css",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/js/modernizr-2.6.2.min.js",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/css/lessons.css",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/js/html5shiv.js",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/js/respond.min.js",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/js/libs/require.min.js",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/login?logout",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "302"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/login",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/login",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "POST",
+          "statusCode": "302"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/login?error",
+          "statusReason": "",
+          "reasonNotProcessed": "Max Depth",
+          "processed": "false",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/registration",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/register.mvc",
+          "statusReason": "",
+          "reasonNotProcessed": "Max Depth",
+          "processed": "false",
+          "method": "POST",
+          "statusCode": "200"
+        }
+      ]
+    }
+  },
+  "@generated": "Fri, 13 Apr 2018 09:22:01",
+  "@version": "2.7.0"
+}
diff --git a/ee/spec/fixtures/security_reports/master/gl-dast-report.json b/ee/spec/fixtures/security_reports/master/gl-dast-report.json
index df459d9419da4470c5ab61b895978b3cca92ad4e..b6f9aaa2a37faae473fd44c3e81a580bbe6885dd 100644
--- a/ee/spec/fixtures/security_reports/master/gl-dast-report.json
+++ b/ee/spec/fixtures/security_reports/master/gl-dast-report.json
@@ -1,42 +1,505 @@
 {
   "site": [
     {
+      "@port": "8080",
+      "@host": "goat",
+      "@name": "http://goat:8080",
       "alerts": [
         {
+          "count": "4",
+          "riskdesc": "High (Medium)",
+          "name": "Anti CSRF Tokens Scanner",
+          "reference": "<p>http://projects.webappsec.org/Cross-Site-Request-Forgery</p><p>http://cwe.mitre.org/data/definitions/352.html</p>",
+          "sourceid": "1",
+          "confidence": "2",
+          "alert": "Anti CSRF Tokens Scanner",
+          "instances": [
+            {
+              "evidence": "<form method=\"POST\" style=\"width: 200px;\" action=\"/WebGoat/login\">",
+              "uri": "http://goat:8080/WebGoat/login?error",
+              "method": "GET"
+            },
+            {
+              "evidence": "<form method=\"POST\" style=\"width: 200px;\" action=\"/WebGoat/login\">",
+              "uri": "http://goat:8080/WebGoat/login",
+              "method": "GET"
+            },
+            {
+              "evidence": "<form class=\"form-horizontal\" action=\"/WebGoat/register.mvc\" method=\"POST\">",
+              "uri": "http://goat:8080/WebGoat/register.mvc",
+              "method": "POST"
+            },
+            {
+              "evidence": "<form class=\"form-horizontal\" action=\"/WebGoat/register.mvc\" method=\"POST\">",
+              "uri": "http://goat:8080/WebGoat/registration",
+              "method": "GET"
+            }
+          ],
+          "pluginid": "20012",
+          "riskcode": "3",
+          "wascid": "9",
+          "solution": "<p>Phase: Architecture and Design</p><p>Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid.</p><p>For example, use anti-CSRF packages such as the OWASP CSRFGuard.</p><p></p><p>Phase: Implementation</p><p>Ensure that your application is free of cross-site scripting issues, because most CSRF defenses can be bypassed using attacker-controlled script.</p><p></p><p>Phase: Architecture and Design</p><p>Generate a unique nonce for each form, place the nonce into the form, and verify the nonce upon receipt of the form. Be sure that the nonce is not predictable (CWE-330).</p><p>Note that this can be bypassed using XSS.</p><p></p><p>Identify especially dangerous operations. When the user performs a dangerous operation, send a separate confirmation request to ensure that the user intended to perform that operation.</p><p>Note that this can be bypassed using XSS.</p><p></p><p>Use the ESAPI Session Management control.</p><p>This control includes a component for CSRF.</p><p></p><p>Do not use the GET method for any request that triggers a state change.</p><p></p><p>Phase: Implementation</p><p>Check the HTTP Referer header to see if the request originated from an expected page. This could break legitimate functionality, because users or proxies may have disabled sending the Referer for privacy reasons.</p>",
+          "cweid": "352",
+          "desc": "<p>A cross-site request forgery is an attack that involves forcing a victim to send an HTTP request to a target destination without their knowledge or intent in order to perform an action as the victim. The underlying cause is application functionality using predictable URL/form actions in a repeatable way. The nature of the attack is that CSRF exploits the trust that a web site has for a user. By contrast, cross-site scripting (XSS) exploits the trust that a user has for a web site. Like XSS, CSRF attacks are not necessarily cross-site, but they can be. Cross-site request forgery is also known as CSRF, XSRF, one-click attack, session riding, confused deputy, and sea surf.</p><p></p><p>CSRF attacks are effective in a number of situations, including:</p><p>    * The victim has an active session on the target site.</p><p>    * The victim is authenticated via HTTP auth on the target site.</p><p>    * The victim is on the same local network as the target site.</p><p></p><p>CSRF has primarily been used to perform an action against a target site using the victim's privileges, but recent techniques have been discovered to disclose information by gaining access to the response. The risk of information disclosure is dramatically increased when the target site is vulnerable to XSS, because XSS can be used as a platform for CSRF, allowing the attack to operate within the bounds of the same-origin policy.</p>"
+        },
+        {
+          "count": "4",
+          "riskdesc": "Low (Medium)",
+          "name": "Absence of Anti-CSRF Tokens",
+          "reference": "<p>http://projects.webappsec.org/Cross-Site-Request-Forgery</p><p>http://cwe.mitre.org/data/definitions/352.html</p>",
+          "otherinfo": "<p>No known Anti-CSRF token [anticsrf, CSRFToken, __RequestVerificationToken, csrfmiddlewaretoken, authenticity_token, OWASP_CSRFTOKEN, anoncsrf, csrf_token, _csrf, _csrfSecret] was found in the following HTML form: [Form 1: \"exampleInputEmail1\" \"exampleInputPassword1\" ].</p>",
           "sourceid": "3",
-          "wascid": "15",
-          "cweid": "16",
-          "reference": "<p>http://msdn.microsoft.com/en-us/library/ie/gg622941%28v=vs.85%29.aspx</p><p>https://www.owasp.org/index.php/List_of_useful_HTTP_headers</p>",
-          "otherinfo": "<p>This issue still applies to error type pages (401, 403, 500, etc) as those pages are often still affected by injection issues, in which case there is still concern for browsers sniffing pages away from their actual content type.</p><p>At \"High\" threshold this scanner will not alert on client or server error responses.</p>",
-          "solution": "<p>Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to 'nosniff' for all web pages.</p><p>If possible, ensure that the end user uses a standards-compliant and modern web browser that does not perform MIME-sniffing at all, or that can be directed by the web application/web server to not perform MIME-sniffing.</p>",
+          "confidence": "2",
+          "alert": "Absence of Anti-CSRF Tokens",
+          "instances": [
+            {
+              "evidence": "<form method=\"POST\" style=\"width: 200px;\" action=\"/WebGoat/login\">",
+              "uri": "http://goat:8080/WebGoat/login",
+              "method": "GET"
+            },
+            {
+              "evidence": "<form method=\"POST\" style=\"width: 200px;\" action=\"/WebGoat/login\">",
+              "uri": "http://goat:8080/WebGoat/login?error",
+              "method": "GET"
+            },
+            {
+              "evidence": "<form class=\"form-horizontal\" action=\"/WebGoat/register.mvc\" method=\"POST\">",
+              "uri": "http://goat:8080/WebGoat/registration",
+              "method": "GET"
+            },
+            {
+              "evidence": "<form class=\"form-horizontal\" action=\"/WebGoat/register.mvc\" method=\"POST\">",
+              "uri": "http://goat:8080/WebGoat/register.mvc",
+              "method": "POST"
+            }
+          ],
+          "pluginid": "10202",
+          "riskcode": "1",
+          "wascid": "9",
+          "solution": "<p>Phase: Architecture and Design</p><p>Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid.</p><p>For example, use anti-CSRF packages such as the OWASP CSRFGuard.</p><p></p><p>Phase: Implementation</p><p>Ensure that your application is free of cross-site scripting issues, because most CSRF defenses can be bypassed using attacker-controlled script.</p><p></p><p>Phase: Architecture and Design</p><p>Generate a unique nonce for each form, place the nonce into the form, and verify the nonce upon receipt of the form. Be sure that the nonce is not predictable (CWE-330).</p><p>Note that this can be bypassed using XSS.</p><p></p><p>Identify especially dangerous operations. When the user performs a dangerous operation, send a separate confirmation request to ensure that the user intended to perform that operation.</p><p>Note that this can be bypassed using XSS.</p><p></p><p>Use the ESAPI Session Management control.</p><p>This control includes a component for CSRF.</p><p></p><p>Do not use the GET method for any request that triggers a state change.</p><p></p><p>Phase: Implementation</p><p>Check the HTTP Referer header to see if the request originated from an expected page. This could break legitimate functionality, because users or proxies may have disabled sending the Referer for privacy reasons.</p>",
+          "cweid": "352",
+          "desc": "<p>No Anti-CSRF tokens were found in a HTML submission form.</p><p>A cross-site request forgery is an attack that involves forcing a victim to send an HTTP request to a target destination without their knowledge or intent in order to perform an action as the victim. The underlying cause is application functionality using predictable URL/form actions in a repeatable way. The nature of the attack is that CSRF exploits the trust that a web site has for a user. By contrast, cross-site scripting (XSS) exploits the trust that a user has for a web site. Like XSS, CSRF attacks are not necessarily cross-site, but they can be. Cross-site request forgery is also known as CSRF, XSRF, one-click attack, session riding, confused deputy, and sea surf.</p><p></p><p>CSRF attacks are effective in a number of situations, including:</p><p>    * The victim has an active session on the target site.</p><p>    * The victim is authenticated via HTTP auth on the target site.</p><p>    * The victim is on the same local network as the target site.</p><p></p><p>CSRF has primarily been used to perform an action against a target site using the victim's privileges, but recent techniques have been discovered to disclose information by gaining access to the response. The risk of information disclosure is dramatically increased when the target site is vulnerable to XSS, because XSS can be used as a platform for CSRF, allowing the attack to operate within the bounds of the same-origin policy.</p>"
+        },
+        {
           "count": "2",
-          "pluginid": "10021",
-          "alert": "X-Content-Type-Options Header Missing",
-          "name": "X-Content-Type-Options Header Missing",
+          "riskdesc": "Low (Medium)",
+          "name": "Cookie Without SameSite Attribute",
+          "reference": "<p>https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site</p>",
+          "sourceid": "3",
+          "confidence": "2",
+          "alert": "Cookie Without SameSite Attribute",
+          "instances": [
+            {
+              "evidence": "Set-Cookie: JSESSIONID",
+              "uri": "http://goat:8080/WebGoat/logout",
+              "param": "JSESSIONID",
+              "method": "GET"
+            },
+            {
+              "evidence": "Set-Cookie: JSESSIONID",
+              "uri": "http://goat:8080/WebGoat/login?logout",
+              "param": "JSESSIONID",
+              "method": "GET"
+            }
+          ],
+          "pluginid": "10054",
           "riskcode": "1",
+          "wascid": "13",
+          "solution": "<p>Ensure that the SameSite attribute is set to either 'lax' or ideally 'strict' for all cookies.</p>",
+          "cweid": "16",
+          "desc": "<p>A cookie has been set without the SameSite attribute, which means that the cookie can be sent as a result of a 'cross-site' request. The SameSite attribute is an effective counter measure to cross-site request forgery, cross-site script inclusion, and timing attacks.</p>"
+        },
+        {
+          "count": "2",
+          "riskdesc": "Informational (Low)",
+          "name": "Loosely Scoped Cookie",
+          "reference": "<p>https://tools.ietf.org/html/rfc6265#section-4.1</p><p>https://www.owasp.org/index.php/Testing_for_cookies_attributes_(OTG-SESS-002)</p><p>http://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_cookies</p>",
+          "otherinfo": "<p>The origin domain used for comparison was: </p><p>goat</p><p>JSESSIONID=78EC2C9D7CE583610DCC7826EE416D7F</p><p></p>",
+          "sourceid": "3",
+          "confidence": "1",
+          "alert": "Loosely Scoped Cookie",
+          "instances": [
+            {
+              "uri": "http://goat:8080/WebGoat/login?logout",
+              "method": "GET"
+            },
+            {
+              "uri": "http://goat:8080/WebGoat/logout",
+              "method": "GET"
+            }
+          ],
+          "pluginid": "90033",
+          "riskcode": "0",
+          "wascid": "15",
+          "solution": "<p>Always scope cookies to a FQDN (Fully Qualified Domain Name).</p>",
+          "cweid": "565",
+          "desc": "<p>Cookies can be scoped by domain or path. This check is only concerned with domain scope.The domain scope applied to a cookie determines which domains can access it. For example, a cookie can be scoped strictly to a subdomain e.g. www.nottrusted.com, or loosely scoped to a parent domain e.g. nottrusted.com. In the latter case, any subdomain of nottrusted.com can access the cookie. Loosely scoped cookies are common in mega-applications like google.com and live.com. Cookies set from a subdomain like app.foo.bar are transmitted only to that domain by the browser. However, cookies scoped to a parent-level domain may be transmitted to the parent, or any subdomain of the parent.</p>"
+        },
+        {
+          "count": "4",
+          "riskdesc": "Informational (Medium)",
+          "name": "Information Disclosure - Suspicious Comments",
+          "reference": "<p></p>",
+          "otherinfo": "<p><!--<button type=\"button\" id=\"admin-button\" class=\"btn btn-default right_nav_button\" title=\"Administrator\">--></p><p><!--<button type=\"button\" id=\"user-management\" class=\"btn btn-default right_nav_button\"--></p><p><!--title=\"User management\">--></p><p></p>",
+          "sourceid": "3",
           "confidence": "2",
+          "alert": "Information Disclosure - Suspicious Comments",
+          "instances": [
+            {
+              "uri": "http://goat:8080/WebGoat/start.mvc",
+              "method": "GET"
+            },
+            {
+              "uri": "http://goat:8080/WebGoat/js/html5shiv.js",
+              "method": "GET"
+            },
+            {
+              "uri": "http://goat:8080/WebGoat/js/modernizr-2.6.2.min.js",
+              "method": "GET"
+            },
+            {
+              "uri": "http://goat:8080/WebGoat/js/respond.min.js",
+              "method": "GET"
+            }
+          ],
+          "pluginid": "10027",
+          "riskcode": "0",
+          "wascid": "13",
+          "solution": "<p>Remove all comments that return information that may help an attacker and fix any underlying problems they refer to.</p>",
+          "cweid": "200",
+          "desc": "<p>The response appears to contain suspicious comments which may help an attacker.</p>"
+        },
+        {
+          "count": "5",
+          "riskdesc": "Informational (Low)",
+          "name": "Timestamp Disclosure - Unix",
+          "reference": "<p>https://www.owasp.org/index.php/Top_10_2013-A6-Sensitive_Data_Exposure</p><p>http://projects.webappsec.org/w/page/13246936/Information%20Leakage</p>",
+          "otherinfo": "<p>00000000, which evaluates to: 1970-01-01 00:00:00</p>",
+          "sourceid": "3",
+          "confidence": "1",
+          "alert": "Timestamp Disclosure - Unix",
+          "instances": [
+            {
+              "evidence": "00000000",
+              "uri": "http://goat:8080/WebGoat/plugins/bootstrap/css/bootstrap.min.css",
+              "method": "GET"
+            },
+            {
+              "evidence": "33333333",
+              "uri": "http://goat:8080/WebGoat/plugins/bootstrap/css/bootstrap.min.css",
+              "method": "GET"
+            },
+            {
+              "evidence": "42857143",
+              "uri": "http://goat:8080/WebGoat/plugins/bootstrap/css/bootstrap.min.css",
+              "method": "GET"
+            },
+            {
+              "evidence": "80000000",
+              "uri": "http://goat:8080/WebGoat/plugins/bootstrap/css/bootstrap.min.css",
+              "method": "GET"
+            },
+            {
+              "evidence": "66666667",
+              "uri": "http://goat:8080/WebGoat/plugins/bootstrap/css/bootstrap.min.css",
+              "method": "GET"
+            }
+          ],
+          "pluginid": "10096",
+          "riskcode": "0",
+          "wascid": "13",
+          "solution": "<p>Manually confirm that the timestamp data is not sensitive, and that the data cannot be aggregated to disclose exploitable patterns.</p>",
+          "cweid": "200",
+          "desc": "<p>A timestamp was disclosed by the application/web server - Unix</p>"
+        },
+        {
+          "count": "2",
           "riskdesc": "Low (Medium)",
-          "desc": "<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to 'nosniff'. This allows older versions of Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing.</p>",
+          "name": "Cookie No HttpOnly Flag",
+          "reference": "<p>http://www.owasp.org/index.php/HttpOnly</p>",
+          "sourceid": "3",
+          "confidence": "2",
+          "alert": "Cookie No HttpOnly Flag",
           "instances": [
             {
-              "param": "X-Content-Type-Options",
-              "method": "GET",
-              "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io"
+              "evidence": "Set-Cookie: JSESSIONID",
+              "uri": "http://goat:8080/WebGoat/login?logout",
+              "param": "JSESSIONID",
+              "method": "GET"
             },
             {
-              "param": "X-Content-Type-Options",
-              "method": "GET",
-              "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io/"
+              "evidence": "Set-Cookie: JSESSIONID",
+              "uri": "http://goat:8080/WebGoat/logout",
+              "param": "JSESSIONID",
+              "method": "GET"
+            }
+          ],
+          "pluginid": "10010",
+          "riskcode": "1",
+          "wascid": "13",
+          "solution": "<p>Ensure that the HttpOnly flag is set for all cookies.</p>",
+          "cweid": "16",
+          "desc": "<p>A cookie has been set without the HttpOnly flag, which means that the cookie can be accessed by JavaScript. If a malicious script can be run on this page then the cookie will be accessible and can be transmitted to another site. If this is a session cookie then session hijacking may be possible.</p>"
+        },
+        {
+          "count": "1",
+          "riskdesc": "Informational (Low)",
+          "name": "Charset Mismatch (Header Versus Meta Content-Type Charset)",
+          "reference": "<p>http://code.google.com/p/browsersec/wiki/Part2#Character_set_handling_and_detection</p>",
+          "otherinfo": "<p>There was a charset mismatch between the HTTP Header and the META content-type encoding declarations: [UTF-8] and [ISO-8859-1] do not match.</p>",
+          "sourceid": "3",
+          "confidence": "1",
+          "alert": "Charset Mismatch (Header Versus Meta Content-Type Charset)",
+          "instances": [
+            {
+              "uri": "http://goat:8080/WebGoat/start.mvc",
+              "method": "GET"
             }
-          ]
+          ],
+          "pluginid": "90011",
+          "riskcode": "0",
+          "wascid": "15",
+          "solution": "<p>Force UTF-8 for all text content in both the HTTP header and meta tags in HTML or encoding declarations in XML.</p>",
+          "cweid": "16",
+          "desc": "<p>This check identifies responses where the HTTP Content-Type header declares a charset different from the charset defined by the body of the HTML or XML. When there's a charset mismatch between the HTTP header and content body Web browsers can be forced into an undesirable content-sniffing mode to determine the content's correct character set.</p><p></p><p>An attacker could manipulate content on the page to be interpreted in an encoding of their choice. For example, if an attacker can control content at the beginning of the page, they could inject script using UTF-7 encoded text and manipulate some browsers into interpreting that text.</p>"
         }
       ],
-      "@ssl": "false",
-      "@port": "80",
-      "@host": "bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io",
-      "@name": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io"
+      "@ssl": "false"
     }
   ],
+  "spider": {
+    "progress": "100",
+    "state": "FINISHED",
+    "result": {
+      "urlsIoError": [],
+      "urlsOutOfScope": [
+        "http://getbootstrap.com/",
+        "http://daneden.me/animate",
+        "http://fontawesome.io/",
+        "https://github.com/twbs/bootstrap/blob/master/LICENSE",
+        "https://github.com/nickpettit/glide",
+        "http://fontawesome.io/license"
+      ],
+      "urlsInScope": [
+        {
+          "url": "http://goat:8080",
+          "statusReason": "",
+          "reasonNotProcessed": "Not Text",
+          "processed": "false",
+          "method": "GET",
+          "statusCode": "404"
+        },
+        {
+          "url": "http://goat:8080/robots.txt",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "404"
+        },
+        {
+          "url": "http://goat:8080/sitemap.xml",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "404"
+        },
+        {
+          "url": "http://goat:8080/WebGoat",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "302"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/attack",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "302"
+        },
+        {
+          "url": "http://goat:8080/",
+          "statusReason": "",
+          "reasonNotProcessed": "Not Text",
+          "processed": "false",
+          "method": "GET",
+          "statusCode": "404"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "302"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/start.mvc",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/welcome.mvc",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "302"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/logout",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "302"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/css/main.css",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/images/favicon.ico",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "404"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/plugins/bootstrap/css/bootstrap.min.css",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/css/font-awesome.min.css",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/css/coderay.css",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/css/animate.css",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/js/modernizr-2.6.2.min.js",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/css/lessons.css",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/js/html5shiv.js",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/js/respond.min.js",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/js/libs/require.min.js",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/login?logout",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "302"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/login",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/login",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "POST",
+          "statusCode": "302"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/login?error",
+          "statusReason": "",
+          "reasonNotProcessed": "Max Depth",
+          "processed": "false",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/registration",
+          "statusReason": "",
+          "reasonNotProcessed": "",
+          "processed": "true",
+          "method": "GET",
+          "statusCode": "200"
+        },
+        {
+          "url": "http://goat:8080/WebGoat/register.mvc",
+          "statusReason": "",
+          "reasonNotProcessed": "Max Depth",
+          "processed": "false",
+          "method": "POST",
+          "statusCode": "200"
+        }
+      ]
+    }
+  },
   "@generated": "Fri, 13 Apr 2018 09:22:01",
   "@version": "2.7.0"
 }
diff --git a/ee/spec/frontend/analytics/cycle_analytics/components/base_spec.js b/ee/spec/frontend/analytics/cycle_analytics/components/base_spec.js
index dbb7954fe8bd49e8aabc92bd9a73c1ed0d41e79b..d643a81b5cabfabba62ae603227600f76be76f58 100644
--- a/ee/spec/frontend/analytics/cycle_analytics/components/base_spec.js
+++ b/ee/spec/frontend/analytics/cycle_analytics/components/base_spec.js
@@ -120,7 +120,7 @@ describe('Cycle Analytics component', () => {
       it('displays the groups filter', () => {
         expect(wrapper.find(GroupsDropdownFilter).exists()).toBe(true);
         expect(wrapper.find(GroupsDropdownFilter).props('queryParams')).toEqual(
-          wrapper.vm.groupsQueryParams,
+          wrapper.vm.$options.groupsQueryParams,
         );
       });
 
@@ -156,7 +156,7 @@ describe('Cycle Analytics component', () => {
 
           expect(wrapper.find(ProjectsDropdownFilter).props()).toEqual(
             expect.objectContaining({
-              queryParams: wrapper.vm.projectsQueryParams,
+              queryParams: wrapper.vm.$options.projectsQueryParams,
               groupId: mockData.group.id,
               multiSelect: wrapper.vm.multiProjectSelect,
             }),
diff --git a/ee/spec/frontend/analytics/cycle_analytics/store/actions_spec.js b/ee/spec/frontend/analytics/cycle_analytics/store/actions_spec.js
index ae503d2e71454e90fcce2909256fb58f88946a7a..2048a5c2f7f33d4dcdc581e109417550206e8ce8 100644
--- a/ee/spec/frontend/analytics/cycle_analytics/store/actions_spec.js
+++ b/ee/spec/frontend/analytics/cycle_analytics/store/actions_spec.js
@@ -2,6 +2,7 @@ import axios from 'axios';
 import MockAdapter from 'axios-mock-adapter';
 import testAction from 'helpers/vuex_action_helper';
 import { TEST_HOST } from 'helpers/test_constants';
+import createFlash from '~/flash';
 import * as actions from 'ee/analytics/cycle_analytics/store/actions';
 import * as types from 'ee/analytics/cycle_analytics/store/mutation_types';
 import {
@@ -17,12 +18,13 @@ const stageData = { events: [] };
 const error = new Error('Request failed with status code 404');
 const groupPath = 'cool-group';
 const groupLabelsEndpoint = `/groups/${groupPath}/-/labels`;
+const flashErrorMessage = 'There was an error while fetching cycle analytics data.';
 
 describe('Cycle analytics actions', () => {
   let state;
   let mock;
 
-  function shouldFlashAnError(msg = 'There was an error while fetching cycle analytics data.') {
+  function shouldFlashAnError(msg = flashErrorMessage) {
     expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(msg);
   }
 
@@ -303,6 +305,24 @@ describe('Cycle analytics actions', () => {
       );
     });
 
+    it('removes an existing flash error if present', () => {
+      const commit = jest.fn();
+      const dispatch = jest.fn();
+      const stateWithStages = {
+        ...state,
+        stages,
+      };
+      createFlash(flashErrorMessage);
+
+      const flashAlert = document.querySelector('.flash-alert');
+
+      expect(flashAlert).toBeVisible();
+
+      actions.receiveCycleAnalyticsDataSuccess({ commit, dispatch, state: stateWithStages });
+
+      expect(flashAlert.style.opacity).toBe('0');
+    });
+
     it("dispatches the 'setStageDataEndpoint' and 'fetchStageData' actions", done => {
       const { slug } = stages[0];
       const stateWithStages = {
diff --git a/ee/spec/frontend/design_management/components/delete_button_spec.js b/ee/spec/frontend/design_management/components/delete_button_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..b736958ce22823cb11a8b423b98e34aff10be08d
--- /dev/null
+++ b/ee/spec/frontend/design_management/components/delete_button_spec.js
@@ -0,0 +1,46 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
+import BatchDeleteButton from 'ee/design_management/components/delete_button.vue';
+
+describe('Batch delete button component', () => {
+  let wrapper;
+
+  const findButton = () => wrapper.find(GlButton);
+  const findModal = () => wrapper.find(GlModal);
+
+  function createComponent(isDeleting = false) {
+    wrapper = shallowMount(BatchDeleteButton, {
+      propsData: {
+        isDeleting,
+      },
+      sync: false,
+      directives: {
+        GlModalDirective,
+      },
+    });
+  }
+
+  afterEach(() => {
+    wrapper.destroy();
+  });
+
+  it('renders non-disabled button by default', () => {
+    createComponent();
+
+    expect(findButton().exists()).toBe(true);
+    expect(findButton().attributes('disabled')).toBeFalsy();
+  });
+
+  it('renders disabled button when design is deleting', () => {
+    createComponent(true);
+    expect(findButton().attributes('disabled')).toBeTruthy();
+  });
+
+  it('emits `deleteSelectedDesigns` event on modal ok click', () => {
+    createComponent();
+    findButton().vm.$emit('click');
+    findModal().vm.$emit('ok');
+
+    expect(wrapper.emitted().deleteSelectedDesigns).toBeTruthy();
+  });
+});
diff --git a/ee/spec/frontend/design_management/components/design_notes/design_discussion_spec.js b/ee/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
index 3ae6273bb475d38ec356910fa15c162a5ddba43c..5f1d0864741c79190b19e69d17e4c660e397cbf3 100644
--- a/ee/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
+++ b/ee/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
@@ -73,7 +73,7 @@ describe('Design discussions component', () => {
   it('hides reply placeholder and opens form on placeholder click', () => {
     findReplyPlaceholder().trigger('click');
 
-    wrapper.vm.$nextTick(() => {
+    return wrapper.vm.$nextTick().then(() => {
       expect(findReplyPlaceholder().exists()).toBe(false);
       expect(findReplyForm().exists()).toBe(true);
     });
@@ -85,16 +85,17 @@ describe('Design discussions component', () => {
       isFormRendered: true,
     });
 
-    wrapper.vm.$nextTick(() => {
-      findReplyForm().vm.$emit('submitForm');
+    return wrapper.vm
+      .$nextTick()
+      .then(() => {
+        findReplyForm().vm.$emit('submitForm');
 
-      expect(mutate).toHaveBeenCalledWith(mutationVariables);
+        expect(mutate).toHaveBeenCalledWith(mutationVariables);
 
-      const addComment = wrapper.vm.addDiscussionComment();
-
-      return addComment.then(() => {
+        return wrapper.vm.addDiscussionComment();
+      })
+      .then(() => {
         expect(findReplyForm().exists()).toBe(false);
       });
-    });
   });
 });
diff --git a/ee/spec/frontend/design_management/components/design_overlay_spec.js b/ee/spec/frontend/design_management/components/design_overlay_spec.js
index 5c63c775137a66839cada1a5080f9bf50eee55b7..8a7ebe2ce20431588742790dd92997fa67699796 100644
--- a/ee/spec/frontend/design_management/components/design_overlay_spec.js
+++ b/ee/spec/frontend/design_management/components/design_overlay_spec.js
@@ -106,7 +106,7 @@ describe('Design overlay component', () => {
       },
     });
 
-    wrapper.vm.$nextTick(() => {
+    return wrapper.vm.$nextTick().then(() => {
       expect(findFirstBadge().attributes().style).toBe('left: 20px; top: 30px;');
     });
   });
diff --git a/ee/spec/frontend/design_management/components/list/__snapshots__/index_spec.js.snap b/ee/spec/frontend/design_management/components/list/__snapshots__/index_spec.js.snap
deleted file mode 100644
index b13e24288bca0ae66f1ccd87cbd9c1c858fc8368..0000000000000000000000000000000000000000
--- a/ee/spec/frontend/design_management/components/list/__snapshots__/index_spec.js.snap
+++ /dev/null
@@ -1,32 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design management list component renders list 1`] = `
-<ol
-  class="list-unstyled row"
->
-  <li
-    class="col-md-6 col-lg-4 mb-3"
-  >
-    <design-stub
-      event="NONE"
-      id="1"
-      image="test"
-      name="test"
-      notescount="2"
-      updatedat="01-01-2019"
-    />
-  </li>
-  <li
-    class="col-md-6 col-lg-4 mb-3"
-  >
-    <design-stub
-      event="NONE"
-      id="2"
-      image="test"
-      name="test"
-      notescount="2"
-      updatedat="01-01-2019"
-    />
-  </li>
-</ol>
-`;
diff --git a/ee/spec/frontend/design_management/components/list/index_spec.js b/ee/spec/frontend/design_management/components/list/index_spec.js
deleted file mode 100644
index ecbb519a186e776fa5be3525de8eeed195b820dd..0000000000000000000000000000000000000000
--- a/ee/spec/frontend/design_management/components/list/index_spec.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import List from 'ee/design_management/components/list/index.vue';
-
-const createMockDesign = id => ({
-  id,
-  filename: 'test',
-  image: 'test',
-  event: 'NONE',
-  notesCount: 2,
-  updatedAt: '01-01-2019',
-});
-
-describe('Design management list component', () => {
-  let wrapper;
-
-  function createComponent() {
-    wrapper = shallowMount(List, {
-      sync: false,
-      propsData: {
-        designs: [createMockDesign(1), createMockDesign(2)],
-      },
-    });
-  }
-
-  afterEach(() => {
-    wrapper.destroy();
-  });
-
-  it('renders list', () => {
-    createComponent();
-
-    expect(wrapper.element).toMatchSnapshot();
-  });
-});
diff --git a/ee/spec/frontend/design_management/components/list/item_spec.js b/ee/spec/frontend/design_management/components/list/item_spec.js
index 5fb471509dfd66c2648964fb5879bf88e98fdf33..9bf5bd08d8ed1044cda2bd951024880593ba9a00 100644
--- a/ee/spec/frontend/design_management/components/list/item_spec.js
+++ b/ee/spec/frontend/design_management/components/list/item_spec.js
@@ -16,7 +16,7 @@ describe('Design management list item component', () => {
       router,
       propsData: {
         id: 1,
-        name: 'test',
+        filename: 'test',
         image: 'http://via.placeholder.com/300',
         event,
         notesCount,
diff --git a/ee/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap b/ee/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap
index 551d474a77527dd6acc4dd96da422e868c34622e..546a653b8a8db1d15376c5cb822c4b41727db55f 100644
--- a/ee/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap
+++ b/ee/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap
@@ -32,5 +32,16 @@ exports[`Design management toolbar component renders design and updated data 1`]
     class="ml-auto"
     id="1"
   />
+   
+  <deletebutton-stub
+    buttonclass=""
+    buttonvariant="danger"
+    hasselecteddesigns="true"
+  >
+    <icon-stub
+      name="remove"
+      size="18"
+    />
+  </deletebutton-stub>
 </header>
 `;
diff --git a/ee/spec/frontend/design_management/components/toolbar/index_spec.js b/ee/spec/frontend/design_management/components/toolbar/index_spec.js
index 9788f97e1d464c5aa103572002ba54db6c5b9f9d..44cdd66c64a906406cc7221cb97f13283f75ff63 100644
--- a/ee/spec/frontend/design_management/components/toolbar/index_spec.js
+++ b/ee/spec/frontend/design_management/components/toolbar/index_spec.js
@@ -1,6 +1,7 @@
 import { createLocalVue, shallowMount } from '@vue/test-utils';
 import VueRouter from 'vue-router';
 import Toolbar from 'ee/design_management/components/toolbar/index.vue';
+import DeleteButton from 'ee/design_management/components/delete_button.vue';
 
 const localVue = createLocalVue();
 localVue.use(VueRouter);
@@ -20,7 +21,7 @@ const RouterLinkStub = {
 describe('Design management toolbar component', () => {
   let wrapper;
 
-  function createComponent(isLoading = false) {
+  function createComponent(isLoading = false, createDesign = true, props) {
     const updatedAt = new Date();
     updatedAt.setHours(updatedAt.getHours() - 1);
 
@@ -30,23 +31,38 @@ describe('Design management toolbar component', () => {
       router,
       propsData: {
         id: '1',
+        isLatestVersion: true,
         isLoading,
+        isDeleting: false,
         name: 'test.jpg',
         updatedAt: updatedAt.toString(),
         updatedBy: {
           name: 'Test Name',
         },
+        ...props,
       },
       stubs: {
         'router-link': RouterLinkStub,
       },
     });
+
+    wrapper.setData({
+      permissions: {
+        createDesign,
+      },
+    });
   }
 
+  afterEach(() => {
+    wrapper.destroy();
+  });
+
   it('renders design and updated data', () => {
     createComponent();
 
-    expect(wrapper.element).toMatchSnapshot();
+    return wrapper.vm.$nextTick().then(() => {
+      expect(wrapper.element).toMatchSnapshot();
+    });
   });
 
   it('links back to designs list', () => {
@@ -61,4 +77,37 @@ describe('Design management toolbar component', () => {
       },
     });
   });
+
+  it('renders delete button on latest designs version with logged in user', () => {
+    createComponent();
+
+    return wrapper.vm.$nextTick().then(() => {
+      expect(wrapper.find(DeleteButton).exists()).toBe(true);
+    });
+  });
+
+  it('does not render delete button on non-latest version', () => {
+    createComponent(false, true, { isLatestVersion: false });
+
+    return wrapper.vm.$nextTick().then(() => {
+      expect(wrapper.find(DeleteButton).exists()).toBe(false);
+    });
+  });
+
+  it('does not render delete button when user is not logged in', () => {
+    createComponent(false, false);
+
+    return wrapper.vm.$nextTick().then(() => {
+      expect(wrapper.find(DeleteButton).exists()).toBe(false);
+    });
+  });
+
+  it('emits `delete` event on deleteButton `deleteSelectedDesigns` event', () => {
+    createComponent();
+
+    return wrapper.vm.$nextTick().then(() => {
+      wrapper.find(DeleteButton).vm.$emit('deleteSelectedDesigns');
+      expect(wrapper.emitted().delete).toBeTruthy();
+    });
+  });
 });
diff --git a/ee/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap b/ee/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap
index 7517ef6230a8fff1dfc71766f0290a444302e764..31940507a5f7cd6375050d1913edc1adb5b0569a 100644
--- a/ee/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap
+++ b/ee/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap
@@ -7,7 +7,7 @@ exports[`Design management upload button component renders inverted upload desig
   <glbutton-stub
     data-original-title="Adding a design with the same filename replaces the file in a new version."
     title=""
-    variant="primary"
+    variant="success"
   >
     
     Add designs
@@ -31,7 +31,7 @@ exports[`Design management upload button component renders loading icon 1`] = `
     data-original-title="Adding a design with the same filename replaces the file in a new version."
     disabled="true"
     title=""
-    variant="primary"
+    variant="success"
   >
     
     Add designs
@@ -60,7 +60,7 @@ exports[`Design management upload button component renders upload design button
   <glbutton-stub
     data-original-title="Adding a design with the same filename replaces the file in a new version."
     title=""
-    variant="primary"
+    variant="success"
   >
     
     Add designs
diff --git a/ee/spec/frontend/design_management/components/upload/__snapshots__/form_spec.js.snap b/ee/spec/frontend/design_management/components/upload/__snapshots__/form_spec.js.snap
deleted file mode 100644
index e9b9f4a8c6697dce628543d8cf2ea1468274d87e..0000000000000000000000000000000000000000
--- a/ee/spec/frontend/design_management/components/upload/__snapshots__/form_spec.js.snap
+++ /dev/null
@@ -1,33 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design management upload form component hides button if cant upload 1`] = `
-<header
-  class="row-content-block border-top-0 p-2 d-flex"
-  issueiid=""
-  projectpath=""
->
-  <div
-    class="d-flex justify-content-between align-items-center w-100"
-  >
-    <designversiondropdown-stub />
-     
-    <!---->
-  </div>
-</header>
-`;
-
-exports[`Design management upload form component renders upload design button 1`] = `
-<header
-  class="row-content-block border-top-0 p-2 d-flex"
-  issueiid=""
-  projectpath=""
->
-  <div
-    class="d-flex justify-content-between align-items-center w-100"
-  >
-    <designversiondropdown-stub />
-     
-    <uploadbutton-stub />
-  </div>
-</header>
-`;
diff --git a/ee/spec/frontend/design_management/components/upload/form_spec.js b/ee/spec/frontend/design_management/components/upload/form_spec.js
deleted file mode 100644
index 246ea94a28df862dc0789d79bb58814dc970aa42..0000000000000000000000000000000000000000
--- a/ee/spec/frontend/design_management/components/upload/form_spec.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import UploadForm from 'ee/design_management/components/upload/form.vue';
-
-describe('Design management upload form component', () => {
-  let wrapper;
-
-  function createComponent(isSaving = false, canUploadDesign = true) {
-    wrapper = shallowMount(UploadForm, {
-      sync: false,
-      propsData: {
-        isSaving,
-        canUploadDesign,
-        projectPath: '',
-        issueIid: '',
-      },
-    });
-  }
-
-  afterEach(() => {
-    wrapper.destroy();
-  });
-
-  it('renders upload design button', () => {
-    createComponent();
-
-    expect(wrapper.element).toMatchSnapshot();
-  });
-
-  it('hides button if cant upload', () => {
-    createComponent(false, false);
-
-    expect(wrapper.element).toMatchSnapshot();
-  });
-
-  describe('onFileUploadChange', () => {
-    it('emits upload event', () => {
-      createComponent();
-
-      wrapper.vm.onFileUploadChange('test');
-
-      expect(wrapper.emitted().upload[0]).toEqual(['test']);
-    });
-  });
-});
diff --git a/ee/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap b/ee/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
index 12940b985a2ad3d766b010109f930351657c363c..1aeee2bf310813159ea668de60607c0bc8708fee 100644
--- a/ee/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
+++ b/ee/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
@@ -1,21 +1,160 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Design management index page designs renders designs list 1`] = `
+exports[`Design management index page designs does not render toolbar when there is no permission 1`] = `
 <div>
-  <uploadform-stub
-    all-versions=""
-    canuploaddesign="true"
+  <!---->
+   
+  <div
+    class="mt-4"
+  >
+    <ol
+      class="list-unstyled row"
+    >
+      <li
+        class="col-md-6 col-lg-4 mb-3"
+      >
+        <design-stub
+          event="NONE"
+          filename="design-1-name"
+          id="design-1"
+          image="design-1-image"
+          notescount="0"
+        />
+         
+        <!---->
+      </li>
+      <li
+        class="col-md-6 col-lg-4 mb-3"
+      >
+        <design-stub
+          event="NONE"
+          filename="design-2-name"
+          id="design-2"
+          image="design-2-image"
+          notescount="1"
+        />
+         
+        <!---->
+      </li>
+      <li
+        class="col-md-6 col-lg-4 mb-3"
+      >
+        <design-stub
+          event="NONE"
+          filename="design-3-name"
+          id="design-3"
+          image="design-3-image"
+          notescount="0"
+        />
+         
+        <!---->
+      </li>
+    </ol>
+  </div>
+   
+  <routerview-stub
+    name="default"
   />
+</div>
+`;
+
+exports[`Design management index page designs renders designs list and header with upload button 1`] = `
+<div>
+  <header
+    class="row-content-block border-top-0 p-2 d-flex"
+  >
+    <div
+      class="d-flex justify-content-between align-items-center w-100"
+    >
+      <designversiondropdown-stub />
+       
+      <div
+        class="d-flex"
+      >
+        <glbutton-stub
+          class="mr-2 js-select-all"
+          variant="link"
+        >
+          Select all
+        </glbutton-stub>
+         
+        <div>
+          <deletebutton-stub
+            buttonclass="btn-danger btn-inverted mr-2"
+            buttonvariant=""
+          >
+            
+            Delete selected
+            
+            <!---->
+          </deletebutton-stub>
+        </div>
+         
+        <uploadbutton-stub />
+      </div>
+    </div>
+  </header>
    
   <div
     class="mt-4"
   >
-    <designlist-stub
-      designs="design"
-    />
+    <ol
+      class="list-unstyled row"
+    >
+      <li
+        class="col-md-6 col-lg-4 mb-3"
+      >
+        <design-stub
+          event="NONE"
+          filename="design-1-name"
+          id="design-1"
+          image="design-1-image"
+          notescount="0"
+        />
+         
+        <input
+          class="design-checkbox"
+          type="checkbox"
+        />
+      </li>
+      <li
+        class="col-md-6 col-lg-4 mb-3"
+      >
+        <design-stub
+          event="NONE"
+          filename="design-2-name"
+          id="design-2"
+          image="design-2-image"
+          notescount="1"
+        />
+         
+        <input
+          class="design-checkbox"
+          type="checkbox"
+        />
+      </li>
+      <li
+        class="col-md-6 col-lg-4 mb-3"
+      >
+        <design-stub
+          event="NONE"
+          filename="design-3-name"
+          id="design-3"
+          image="design-3-image"
+          notescount="0"
+        />
+         
+        <input
+          class="design-checkbox"
+          type="checkbox"
+        />
+      </li>
+    </ol>
   </div>
    
-  <router-view-stub />
+  <routerview-stub
+    name="default"
+  />
 </div>
 `;
 
@@ -32,7 +171,9 @@ exports[`Design management index page designs renders empty text 1`] = `
     />
   </div>
    
-  <router-view-stub />
+  <routerview-stub
+    name="default"
+  />
 </div>
 `;
 
@@ -52,7 +193,9 @@ exports[`Design management index page designs renders error 1`] = `
     </div>
   </div>
    
-  <router-view-stub />
+  <routerview-stub
+    name="default"
+  />
 </div>
 `;
 
@@ -70,6 +213,8 @@ exports[`Design management index page designs renders loading icon 1`] = `
     />
   </div>
    
-  <router-view-stub />
+  <routerview-stub
+    name="default"
+  />
 </div>
 `;
diff --git a/ee/spec/frontend/design_management/pages/design/index_spec.js b/ee/spec/frontend/design_management/pages/design/index_spec.js
index 701ba3ec17a1006b90686bf94902549d67e4f36c..58096c5a6275d3cfba6c24cf699c526b04577831 100644
--- a/ee/spec/frontend/design_management/pages/design/index_spec.js
+++ b/ee/spec/frontend/design_management/pages/design/index_spec.js
@@ -58,6 +58,10 @@ describe('Design management design index page', () => {
       propsData: { id: '1' },
       mocks: { $apollo },
     });
+
+    wrapper.setData({
+      issueIid: '1',
+    });
   }
 
   function setDesign() {
@@ -136,7 +140,7 @@ describe('Design management design index page', () => {
 
     wrapper.vm.openCommentForm({ x: 0, y: 0 });
 
-    wrapper.vm.$nextTick(() => {
+    return wrapper.vm.$nextTick().then(() => {
       expect(findDiscussionForm().exists()).toBe(true);
     });
   });
@@ -155,15 +159,16 @@ describe('Design management design index page', () => {
       comment: newComment,
     });
 
-    wrapper.vm.$nextTick(() => {
-      findDiscussionForm().vm.$emit('submitForm');
+    return wrapper.vm
+      .$nextTick()
+      .then(() => {
+        findDiscussionForm().vm.$emit('submitForm');
 
-      expect(mutate).toHaveBeenCalledWith(mutationVariables);
-      const addNote = wrapper.vm.addImageDiffNote();
-
-      return addNote.then(() => {
+        expect(mutate).toHaveBeenCalledWith(mutationVariables);
+        return wrapper.vm.addImageDiffNote();
+      })
+      .then(() => {
         expect(findDiscussionForm().exists()).toBe(false);
       });
-    });
   });
 });
diff --git a/ee/spec/frontend/design_management/pages/index_spec.js b/ee/spec/frontend/design_management/pages/index_spec.js
index d5d48a1807bf808bb63d0a32eb533dc5cb227315..4f1cb11deda1589ee114f9c49f9fd95f455eb222 100644
--- a/ee/spec/frontend/design_management/pages/index_spec.js
+++ b/ee/spec/frontend/design_management/pages/index_spec.js
@@ -1,8 +1,8 @@
 import { createLocalVue, shallowMount } from '@vue/test-utils';
 import VueRouter from 'vue-router';
 import Index from 'ee/design_management/pages/index.vue';
-import UploadForm from 'ee/design_management/components/upload/form.vue';
 import uploadDesignQuery from 'ee/design_management/graphql/mutations/uploadDesign.mutation.graphql';
+import DesignDestroyer from 'ee/design_management/components/design_destroyer.vue';
 
 const localVue = createLocalVue();
 localVue.use(VueRouter);
@@ -16,11 +16,50 @@ const router = new VueRouter({
   ],
 });
 
+const mockDesigns = [
+  {
+    id: 'design-1',
+    image: 'design-1-image',
+    filename: 'design-1-name',
+    event: 'NONE',
+    notesCount: 0,
+  },
+  {
+    id: 'design-2',
+    image: 'design-2-image',
+    filename: 'design-2-name',
+    event: 'NONE',
+    notesCount: 1,
+  },
+  {
+    id: 'design-3',
+    image: 'design-3-image',
+    filename: 'design-3-name',
+    event: 'NONE',
+    notesCount: 0,
+  },
+];
+
+const mockVersion = {
+  node: {
+    id: 'gid://gitlab/DesignManagement::Version/1',
+  },
+};
+
 describe('Design management index page', () => {
   let mutate;
-  let vm;
+  let wrapper;
+
+  const findDesignCheckboxes = () => wrapper.findAll('.design-checkbox');
+  const findSelectAllButton = () => wrapper.find('.js-select-all');
+  const findDeleteButton = () => wrapper.find('deletebutton-stub');
 
-  function createComponent(loading = false, designs = []) {
+  function createComponent({
+    loading = false,
+    designs = [],
+    allVersions = [],
+    createDesign = true,
+  } = {}) {
     mutate = jest.fn(() => Promise.resolve());
     const $apollo = {
       queries: {
@@ -34,64 +73,65 @@ describe('Design management index page', () => {
       mutate,
     };
 
-    vm = shallowMount(Index, {
+    wrapper = shallowMount(Index, {
+      sync: false,
       mocks: { $apollo },
-      stubs: ['router-view'],
       localVue,
       router,
+      stubs: { DesignDestroyer },
     });
 
-    vm.setData({
+    wrapper.setData({
       designs,
+      allVersions,
+      issueIid: '1',
       permissions: {
-        createDesign: true,
+        createDesign,
       },
     });
   }
 
   afterEach(() => {
-    vm.destroy();
+    wrapper.destroy();
   });
 
   describe('designs', () => {
     it('renders loading icon', () => {
-      createComponent(true);
+      createComponent({ loading: true });
 
-      expect(vm.element).toMatchSnapshot();
+      expect(wrapper.element).toMatchSnapshot();
     });
 
     it('renders error', () => {
       createComponent();
 
-      vm.setData({ error: true });
+      wrapper.setData({ error: true });
 
-      expect(vm.element).toMatchSnapshot();
+      return wrapper.vm.$nextTick().then(() => {
+        expect(wrapper.element).toMatchSnapshot();
+      });
     });
 
     it('renders empty text', () => {
       createComponent();
 
-      expect(vm.element).toMatchSnapshot();
-    });
-
-    it('renders designs list', () => {
-      createComponent(false, ['design']);
-
-      expect(vm.element).toMatchSnapshot();
+      expect(wrapper.element).toMatchSnapshot();
     });
-  });
 
-  describe('upload form', () => {
-    it('hides upload form', () => {
-      createComponent();
+    it('renders designs list and header with upload button', () => {
+      createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
 
-      expect(vm.find(UploadForm).exists()).toBe(false);
+      return wrapper.vm.$nextTick().then(() => {
+        expect(wrapper.element).toMatchSnapshot();
+      });
     });
 
-    it('renders upload form', () => {
-      createComponent(false, ['design']);
+    it('does not render toolbar when there is no permission', () => {
+      createComponent({ designs: mockDesigns, allVersions: [mockVersion], createDesign: false });
 
-      expect(vm.find(UploadForm).exists()).toBe(true);
+      return wrapper.vm.$nextTick().then(() => {
+        expect(wrapper.element).toMatchSnapshot();
+      });
     });
   });
 
@@ -99,7 +139,7 @@ describe('Design management index page', () => {
     it('calls apollo mutate', () => {
       createComponent();
 
-      return vm.vm
+      return wrapper.vm
         .onUploadDesign([
           {
             name: 'test',
@@ -114,7 +154,7 @@ describe('Design management index page', () => {
             variables: {
               files: [{ name: 'test' }],
               projectPath: '',
-              iid: null,
+              iid: '1',
             },
             update: expect.anything(),
             optimisticResponse: {
@@ -128,6 +168,8 @@ describe('Design management index page', () => {
                     image: '',
                     filename: 'test',
                     fullPath: '',
+                    event: 'NONE',
+                    notesCount: 0,
                     diffRefs: {
                       __typename: 'DiffRefs',
                       baseSha: '',
@@ -154,15 +196,9 @@ describe('Design management index page', () => {
     });
 
     it('does not call apollo mutate if createDesign is false', () => {
-      createComponent();
+      createComponent({ createDesign: false });
 
-      vm.setData({
-        permissions: {
-          createDesign: false,
-        },
-      });
-
-      vm.vm.onUploadDesign([]);
+      wrapper.vm.onUploadDesign([]);
 
       expect(mutate).not.toHaveBeenCalled();
     });
@@ -170,17 +206,96 @@ describe('Design management index page', () => {
     it('sets isSaving', () => {
       createComponent();
 
-      const uploadDesign = vm.vm.onUploadDesign([
+      const uploadDesign = wrapper.vm.onUploadDesign([
         {
           name: 'test',
         },
       ]);
 
-      expect(vm.vm.isSaving).toBe(true);
+      expect(wrapper.vm.isSaving).toBe(true);
 
       return uploadDesign.then(() => {
-        expect(vm.vm.isSaving).toBe(false);
+        expect(wrapper.vm.isSaving).toBe(false);
       });
     });
   });
+
+  describe('on latest version', () => {
+    beforeEach(() => {
+      createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
+    });
+
+    it('renders design checkboxes', () => {
+      expect(findDesignCheckboxes().length).toBe(mockDesigns.length);
+    });
+
+    it('renders a button with Select all text', () => {
+      expect(findSelectAllButton().exists()).toBe(true);
+      expect(findSelectAllButton().text()).toBe('Select all');
+    });
+
+    it('adds two designs to selected designs when their checkboxes are checked', () => {
+      findDesignCheckboxes()
+        .at(0)
+        .trigger('click');
+      findDesignCheckboxes()
+        .at(1)
+        .trigger('click');
+
+      return wrapper.vm.$nextTick().then(() => {
+        expect(findDeleteButton().exists()).toBe(true);
+        expect(findSelectAllButton().text()).toBe('Deselect all');
+        findDeleteButton().vm.$emit('deleteSelectedDesigns');
+        const [{ variables }] = mutate.mock.calls[0];
+        expect(variables.filenames).toStrictEqual([
+          mockDesigns[0].filename,
+          mockDesigns[1].filename,
+        ]);
+      });
+    });
+
+    it('adds all designs to selected designs when Select All button is clicked', () => {
+      findSelectAllButton().vm.$emit('click');
+
+      return wrapper.vm.$nextTick().then(() => {
+        expect(findDeleteButton().props().hasSelectedDesigns).toBe(true);
+        expect(findSelectAllButton().text()).toBe('Deselect all');
+        expect(wrapper.vm.selectedDesigns).toEqual(mockDesigns.map(design => design.filename));
+      });
+    });
+
+    it('removes all designs from selected designs when at least one design was selected', () => {
+      findDesignCheckboxes()
+        .at(0)
+        .trigger('click');
+      findSelectAllButton().vm.$emit('click');
+
+      return wrapper.vm.$nextTick().then(() => {
+        expect(findDeleteButton().props().hasSelectedDesigns).toBe(false);
+        expect(findSelectAllButton().text()).toBe('Select all');
+        expect(wrapper.vm.selectedDesigns).toEqual([]);
+      });
+    });
+  });
+
+  describe('on non-latest version', () => {
+    beforeEach(() => {
+      createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
+
+      router.replace({
+        name: 'designs',
+        query: {
+          version: '2',
+        },
+      });
+    });
+
+    it('does not render design checkboxes', () => {
+      expect(findDesignCheckboxes().length).toBe(0);
+    });
+
+    it('does not render Select All button', () => {
+      expect(findSelectAllButton().exists()).toBe(false);
+    });
+  });
 });
diff --git a/ee/spec/frontend/design_management/utils/design_management_utils_spec.js b/ee/spec/frontend/design_management/utils/design_management_utils_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..d351fd0e2317928de12a7c81e4d7c3e2160dfdd5
--- /dev/null
+++ b/ee/spec/frontend/design_management/utils/design_management_utils_spec.js
@@ -0,0 +1,54 @@
+import {
+  extractCurrentDiscussion,
+  extractDiscussions,
+} from 'ee/design_management/utils/design_management_utils';
+
+describe('extractCurrentDiscussion', () => {
+  let discussions;
+
+  beforeEach(() => {
+    discussions = {
+      edges: [
+        { node: { id: 101, payload: 'w' } },
+        { node: { id: 102, payload: 'x' } },
+        { node: { id: 103, payload: 'y' } },
+        { node: { id: 104, payload: 'z' } },
+      ],
+    };
+  });
+
+  it('finds the relevant discussion if it exists', () => {
+    const id = 103;
+    expect(extractCurrentDiscussion(discussions, id)).toEqual({
+      node: { id, payload: 'y' },
+    });
+  });
+
+  it('returns null if the relevant discussion does not exist', () => {
+    expect(extractCurrentDiscussion(discussions, 0)).not.toBeDefined();
+  });
+});
+
+describe('extractDiscussions', () => {
+  let discussions;
+
+  beforeEach(() => {
+    discussions = {
+      edges: [
+        { node: { id: 1, notes: { edges: [{ node: 'a' }] } } },
+        { node: { id: 2, notes: { edges: [{ node: 'b' }] } } },
+        { node: { id: 3, notes: { edges: [{ node: 'c' }] } } },
+        { node: { id: 4, notes: { edges: [{ node: 'd' }] } } },
+      ],
+    };
+  });
+
+  it('discards the edges.node artefacts of GraphQL', () => {
+    expect(extractDiscussions(discussions)).toEqual([
+      { id: 1, notes: ['a'] },
+      { id: 2, notes: ['b'] },
+      { id: 3, notes: ['c'] },
+      { id: 4, notes: ['d'] },
+    ]);
+  });
+});
diff --git a/ee/spec/frontend/license_management/mock_data.js b/ee/spec/frontend/license_management/mock_data.js
index ebf6fb8c69bf8089a4c18c4dd95056c48963ccd0..937dccd207dd97093d9c26f17fb39e47d3d19a30 100644
--- a/ee/spec/frontend/license_management/mock_data.js
+++ b/ee/spec/frontend/license_management/mock_data.js
@@ -1,5 +1,67 @@
 import { LICENSE_APPROVAL_STATUS } from 'ee/vue_shared/license_management/constants';
 
+const urlFor = ({ scheme = 'https', host = 'www.example.org', path = '/' }) =>
+  `${scheme}://${host}${path}`;
+const licenseUrlFor = name =>
+  urlFor({ host: 'opensource.org', path: `/licenses/${name.split(' ')[0]}` });
+const dependencyUrlFor = name => urlFor({ path: `/${name}` });
+const normalizeV1License = ({ name, url = licenseUrlFor(name) }) => ({ name, url });
+const V1 = {
+  normalizeLicenseSummary: ({ name, url = licenseUrlFor(name), count = 0 }) => ({
+    name,
+    url,
+    count,
+  }),
+  normalizeDependency: ({
+    name,
+    url = dependencyUrlFor(name),
+    description = name.toUpperCase(),
+    license = {},
+  }) => ({ dependency: { name, url, description }, license: normalizeV1License(license) }),
+};
+const V2 = {
+  normalizeLicenseSummary: ({ id, name, url = licenseUrlFor(id) }) => ({ id, name, url }),
+  normalizeDependency: ({
+    name,
+    url = dependencyUrlFor(name),
+    description = name.toUpperCase(),
+    licenses = [],
+  }) => ({ name, url, licenses, description }),
+};
+
+export class Builder {
+  static for(version, template = { licenses: [], dependencies: [] }) {
+    return new Builder(version, template);
+  }
+
+  static forV1() {
+    return this.for(V1);
+  }
+
+  static forV2() {
+    return this.for(V2, { version: '2.0', licenses: [], dependencies: [] });
+  }
+
+  constructor(version, template = { licenses: [], dependencies: [] }) {
+    this.report = template;
+    this.version = version;
+  }
+
+  addLicense(license) {
+    this.report.licenses.push(this.version.normalizeLicenseSummary(license));
+    return this;
+  }
+
+  addDependency(dependency) {
+    this.report.dependencies.push(this.version.normalizeDependency(dependency));
+    return this;
+  }
+
+  build(override = {}) {
+    return Object.assign(this.report, override);
+  }
+}
+
 export const approvedLicense = {
   id: 5,
   name: 'MIT',
@@ -12,94 +74,46 @@ export const blacklistedLicense = {
   approvalStatus: LICENSE_APPROVAL_STATUS.BLACKLISTED,
 };
 
-export const licenseBaseIssues = {
-  licenses: [
-    {
-      count: 1,
-      name: 'MIT',
-    },
-  ],
-  dependencies: [
-    {
-      license: {
-        name: 'MIT',
-        url: 'http://opensource.org/licenses/mit-license',
-      },
-      dependency: {
-        name: 'bundler',
-        url: 'http://bundler.io',
-        description: "The best way to manage your application's dependencies",
-        paths: ['.'],
-      },
-    },
-  ],
-};
+export const licenseBaseIssues = Builder.forV1()
+  .addLicense({ name: 'MIT', count: 1 })
+  .addDependency({
+    name: 'bundler',
+    url: 'http://bundler.io',
+    description: "The best way to manage your application's dependencies",
+    license: { name: 'MIT', url: 'http://opensource.org/licenses/mit-license' },
+  })
+  .build();
 
-export const licenseHeadIssues = {
-  licenses: [
-    {
-      count: 3,
-      name: 'New BSD',
-    },
-    {
-      count: 1,
-      name: 'MIT',
-    },
-  ],
-  dependencies: [
-    {
-      license: {
-        name: 'New BSD',
-        url: 'http://opensource.org/licenses/BSD-3-Clause',
-      },
-      dependency: {
-        name: 'pg',
-        url: 'https://bitbucket.org/ged/ruby-pg',
-        description:
-          'Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/]',
-        paths: ['.'],
-      },
-    },
-    {
-      license: {
-        name: 'New BSD',
-        url: 'http://opensource.org/licenses/BSD-3-Clause',
-      },
-      dependency: {
-        name: 'puma',
-        url: 'http://puma.io',
-        description:
-          'Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications',
-        paths: ['.'],
-      },
-    },
-    {
-      license: {
-        name: 'New BSD',
-        url: 'http://opensource.org/licenses/BSD-3-Clause',
-      },
-      dependency: {
-        name: 'foo',
-        url: 'http://foo.io',
-        description:
-          'Foo is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications',
-        paths: ['.'],
-      },
-    },
-    {
-      license: {
-        name: 'MIT',
-        url: 'http://opensource.org/licenses/mit-license',
-      },
-      dependency: {
-        name: 'execjs',
-        url: 'https://github.com/rails/execjs',
-        description: 'Run JavaScript code from Ruby',
-        paths: ['.'],
-      },
-    },
-  ],
-};
+export const licenseHeadIssues = Builder.forV1()
+  .addLicense({ name: 'New BSD', count: 3 })
+  .addLicense({ name: 'MIT', count: 1 })
+  .addDependency({
+    name: 'pg',
+    url: 'https://bitbucket.org/ged/ruby-pg',
+    description: 'Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/]',
+    license: { name: 'New BSD', url: 'http://opensource.org/licenses/BSD-3-Clause' },
+  })
+  .addDependency({
+    name: 'puma',
+    url: 'http://puma.io',
+    description:
+      'Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications',
+    license: { name: 'New BSD', url: 'http://opensource.org/licenses/BSD-3-Clause' },
+  })
+  .addDependency({
+    name: 'foo',
+    url: 'http://foo.io',
+    description:
+      'Foo is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications',
+    license: { name: 'New BSD', url: 'http://opensource.org/licenses/BSD-3-Clause' },
+  })
+  .addDependency({
+    name: 'execjs',
+    url: 'https://github.com/rails/execjs',
+    description: 'Run JavaScript code from Ruby',
+    license: { name: 'MIT', url: 'http://opensource.org/licenses/mit-license' },
+  })
+  .build();
 
 export const licenseReport = [
   {
diff --git a/ee/spec/frontend/security_dashboard/components/app_spec.js b/ee/spec/frontend/security_dashboard/components/app_spec.js
index c0870204d20369f2b8a33285d8dd69b5b8ab7fb5..e91569005bcc4f44077a7ad45eae40b109c05465 100644
--- a/ee/spec/frontend/security_dashboard/components/app_spec.js
+++ b/ee/spec/frontend/security_dashboard/components/app_spec.js
@@ -175,7 +175,7 @@ describe('Security Dashboard app', () => {
     `('$description', ({ getParameterValuesReturnValue, expected }) => {
       getParameterValues.mockImplementation(() => getParameterValuesReturnValue);
       createComponent();
-      expect(wrapper.vm.$store.state.filters.hide_dismissed).toBe(expected);
+      expect(wrapper.vm.$store.state.filters.hideDismissed).toBe(expected);
     });
   });
 });
diff --git a/ee/spec/frontend/security_dashboard/components/instance_security_dashboard_spec.js b/ee/spec/frontend/security_dashboard/components/instance_security_dashboard_spec.js
index a41e58ff063499114591c8b7f83de51bc6339e85..d1f239eafa3ccdbf64fc893062a422341322da38 100644
--- a/ee/spec/frontend/security_dashboard/components/instance_security_dashboard_spec.js
+++ b/ee/spec/frontend/security_dashboard/components/instance_security_dashboard_spec.js
@@ -3,6 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
 import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
 import InstanceSecurityDashboard from 'ee/security_dashboard/components/instance_security_dashboard.vue';
 import SecurityDashboard from 'ee/security_dashboard/components/app.vue';
+import ProjectManager from 'ee/security_dashboard/components/project_manager.vue';
 
 const localVue = createLocalVue();
 localVue.use(Vuex);
@@ -10,7 +11,8 @@ localVue.use(Vuex);
 const dashboardDocumentation = '/help/docs';
 const emptyStateSvgPath = '/svgs/empty.svg';
 const emptyDashboardStateSvgPath = '/svgs/empty-dash.svg';
-const projectsEndpoint = '/projects';
+const projectAddEndpoint = '/projects/add';
+const projectListEndpoint = '/projects/list';
 const vulnerabilitiesEndpoint = '/vulnerabilities';
 const vulnerabilitiesCountEndpoint = '/vulnerabilities_summary';
 const vulnerabilitiesHistoryEndpoint = '/vulnerabilities_history';
@@ -24,11 +26,11 @@ describe('Instance Security Dashboard component', () => {
   const factory = ({ projects = [] } = {}) => {
     store = new Vuex.Store({
       modules: {
-        projects: {
+        projectSelector: {
           namespaced: true,
           actions: {
             fetchProjects() {},
-            setProjectsEndpoint() {},
+            setProjectEndpoints() {},
           },
           state: {
             projects,
@@ -53,7 +55,8 @@ describe('Instance Security Dashboard component', () => {
         dashboardDocumentation,
         emptyStateSvgPath,
         emptyDashboardStateSvgPath,
-        projectsEndpoint,
+        projectAddEndpoint,
+        projectListEndpoint,
         vulnerabilitiesEndpoint,
         vulnerabilitiesCountEndpoint,
         vulnerabilitiesHistoryEndpoint,
@@ -85,6 +88,7 @@ describe('Instance Security Dashboard component', () => {
     expect(wrapper.find(GlEmptyState).exists()).toBe(false);
     expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
     expect(wrapper.find(SecurityDashboard).exists()).toBe(false);
+    expect(wrapper.find(ProjectManager).exists()).toBe(true);
   };
 
   afterEach(() => {
@@ -98,8 +102,14 @@ describe('Instance Security Dashboard component', () => {
 
     it('dispatches the expected actions', () => {
       expect(store.dispatch.mock.calls).toEqual([
-        ['projects/setProjectsEndpoint', projectsEndpoint],
-        ['projects/fetchProjects', undefined],
+        [
+          'projectSelector/setProjectEndpoints',
+          {
+            add: projectAddEndpoint,
+            list: projectListEndpoint,
+          },
+        ],
+        ['projectSelector/fetchProjects', undefined],
       ]);
     });
 
@@ -108,6 +118,7 @@ describe('Instance Security Dashboard component', () => {
       expect(wrapper.find(GlEmptyState).exists()).toBe(false);
       expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
       expect(wrapper.find(SecurityDashboard).exists()).toBe(false);
+      expect(wrapper.find(ProjectManager).exists()).toBe(false);
     });
   });
 
@@ -121,6 +132,7 @@ describe('Instance Security Dashboard component', () => {
       expect(findProjectSelectorToggleButton().exists()).toBe(true);
       expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
       expect(wrapper.find(SecurityDashboard).exists()).toBe(false);
+      expect(wrapper.find(ProjectManager).exists()).toBe(false);
 
       expectComponentWithProps(GlEmptyState, {
         svgPath: emptyStateSvgPath,
@@ -146,6 +158,7 @@ describe('Instance Security Dashboard component', () => {
       expect(findProjectSelectorToggleButton().exists()).toBe(true);
       expect(wrapper.find(GlEmptyState).exists()).toBe(false);
       expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+      expect(wrapper.find(ProjectManager).exists()).toBe(false);
 
       expectComponentWithProps(SecurityDashboard, {
         dashboardDocumentation,
diff --git a/ee/spec/frontend/security_dashboard/components/project_list_spec.js b/ee/spec/frontend/security_dashboard/components/project_list_spec.js
index 81ffe80da763ffb76ef17d140fec590f54fe1212..21956512fdb3b17f39d0a1ad0b04f1780c7ae03d 100644
--- a/ee/spec/frontend/security_dashboard/components/project_list_spec.js
+++ b/ee/spec/frontend/security_dashboard/components/project_list_spec.js
@@ -1,6 +1,6 @@
 import { shallowMount, createLocalVue } from '@vue/test-utils';
 
-import { GlBadge, GlButton } from '@gitlab/ui';
+import { GlBadge, GlButton, GlLoadingIcon } from '@gitlab/ui';
 import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
 
 import ProjectList from 'ee/security_dashboard/components/project_list.vue';
@@ -14,12 +14,13 @@ const generateMockProjects = (projectsCount, mockProject = {}) =>
 describe('Project List component', () => {
   let wrapper;
 
-  const factory = ({ projects = [], stubs = {} } = {}) => {
+  const factory = ({ projects = [], stubs = {}, showLoadingIndicator = false } = {}) => {
     wrapper = shallowMount(ProjectList, {
       stubs,
       localVue,
       propsData: {
         projects,
+        showLoadingIndicator,
       },
       sync: false,
     });
@@ -39,6 +40,18 @@ describe('Project List component', () => {
     );
   });
 
+  it('does not show a loading indicator when showLoadingIndicator = false', () => {
+    factory();
+
+    expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+  });
+
+  it('shows a loading indicator when showLoadingIndicator = true', () => {
+    factory({ showLoadingIndicator: true });
+
+    expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+  });
+
   it.each([0, 1, 2])(
     'renders a list of projects and displays a count of how many there are',
     projectsCount => {
diff --git a/ee/spec/frontend/security_dashboard/components/project_manager_spec.js b/ee/spec/frontend/security_dashboard/components/project_manager_spec.js
index f5e8bd1c0e554f7ececb52f0f86d0bdd35f6e3ef..4fe205946e6ad2fc193de1a46aa294826c122d6a 100644
--- a/ee/spec/frontend/security_dashboard/components/project_manager_spec.js
+++ b/ee/spec/frontend/security_dashboard/components/project_manager_spec.js
@@ -3,7 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
 
 import createDefaultState from 'ee/security_dashboard/store/modules/project_selector/state';
 
-import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
 
 import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
 import ProjectManager from 'ee/security_dashboard/components/project_manager.vue';
@@ -17,7 +17,12 @@ describe('Project Manager component', () => {
   let store;
   let wrapper;
 
-  const factory = ({ stateOverrides = {} } = {}) => {
+  const factory = ({
+    state = {},
+    canAddProjects = false,
+    isSearchingProjects = false,
+    isUpdatingProjects = false,
+  } = {}) => {
     storeOptions = {
       modules: {
         projectSelector: {
@@ -30,9 +35,14 @@ describe('Project Manager component', () => {
             toggleSelectedProject: jest.fn(),
             removeProject: jest.fn(),
           },
+          getters: {
+            canAddProjects: jest.fn().mockReturnValue(canAddProjects),
+            isSearchingProjects: jest.fn().mockReturnValue(isSearchingProjects),
+            isUpdatingProjects: jest.fn().mockReturnValue(isUpdatingProjects),
+          },
           state: {
             ...createDefaultState(),
-            ...stateOverrides,
+            ...state,
           },
         },
       },
@@ -51,7 +61,6 @@ describe('Project Manager component', () => {
   const getMockActionDispatchedPayload = actionName => getMockAction(actionName).mock.calls[0][1];
 
   const getAddProjectsButton = () => wrapper.find(GlButton);
-  const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
   const getProjectList = () => wrapper.find(ProjectList);
   const getProjectSelector = () => wrapper.find(ProjectSelector);
 
@@ -87,18 +96,11 @@ describe('Project Manager component', () => {
       expect(getAddProjectsButton().attributes('disabled')).toBe('true');
     });
 
-    it.each`
-      actionName              | payload
-      ${'addProjects'}        | ${undefined}
-      ${'clearSearchResults'} | ${undefined}
-    `(
-      'dispatches the correct actions when the add-projects button has been clicked',
-      ({ actionName, payload }) => {
-        getAddProjectsButton().vm.$emit('click');
+    it('dispatches the addProjects when the "Add projects" button has been clicked', () => {
+      getAddProjectsButton().vm.$emit('click');
 
-        expect(getMockActionDispatchedPayload(actionName)).toBe(payload);
-      },
-    );
+      expect(getMockAction('addProjects')).toHaveBeenCalled();
+    });
 
     it('contains a project-list component', () => {
       expect(getProjectList().exists()).toBe(true);
@@ -116,26 +118,26 @@ describe('Project Manager component', () => {
     });
   });
 
-  describe('given the state changes', () => {
+  describe('given the store state', () => {
     it.each`
-      state                                   | projectSelectorPropName            | expectedPropValue
-      ${{ searchCount: 1 }}                   | ${'showLoadingIndicator'}          | ${true}
-      ${{ selectedProjects: ['bar'] }}        | ${'selectedProjects'}              | ${['bar']}
-      ${{ projectSearchResults: ['foo'] }}    | ${'projectSearchResults'}          | ${['foo']}
-      ${{ messages: { noResults: true } }}    | ${'showNoResultsMessage'}          | ${true}
-      ${{ messages: { searchError: true } }}  | ${'showSearchErrorMessage'}        | ${true}
-      ${{ messages: { minimumQuery: true } }} | ${'showMinimumSearchQueryMessage'} | ${true}
+      config                                             | projectSelectorPropName            | expectedPropValue
+      ${{ isSearchingProjects: true }}                   | ${'showLoadingIndicator'}          | ${true}
+      ${{ state: { selectedProjects: ['bar'] } }}        | ${'selectedProjects'}              | ${['bar']}
+      ${{ state: { projectSearchResults: ['foo'] } }}    | ${'projectSearchResults'}          | ${['foo']}
+      ${{ state: { messages: { noResults: true } } }}    | ${'showNoResultsMessage'}          | ${true}
+      ${{ state: { messages: { searchError: true } } }}  | ${'showSearchErrorMessage'}        | ${true}
+      ${{ state: { messages: { minimumQuery: true } } }} | ${'showMinimumSearchQueryMessage'} | ${true}
     `(
-      'passes the correct prop-values to the project-selector',
-      ({ state, projectSelectorPropName, expectedPropValue }) => {
-        factory({ stateOverrides: state });
+      'passes $projectSelectorPropName = $expectedPropValue to the project-selector',
+      ({ config, projectSelectorPropName, expectedPropValue }) => {
+        factory(config);
 
         expect(getProjectSelector().props(projectSelectorPropName)).toEqual(expectedPropValue);
       },
     );
 
-    it('enables the add-projects button when at least one projects is selected', () => {
-      factory({ stateOverrides: { selectedProjects: [{}] } });
+    it('enables the add-projects button when projects can be added', () => {
+      factory({ canAddProjects: true });
 
       expect(getAddProjectsButton().attributes('disabled')).toBe(undefined);
     });
@@ -143,21 +145,18 @@ describe('Project Manager component', () => {
     it('passes the list of projects to the project-list component', () => {
       const projects = [{}];
 
-      factory({ stateOverrides: { projects } });
+      factory({ state: { projects } });
 
       expect(getProjectList().props('projects')).toBe(projects);
     });
 
-    it('toggles the loading icon when a project is being added', () => {
-      factory({ stateOverrides: { isAddingProjects: false } });
-
-      expect(getLoadingIcon().exists()).toBe(false);
-
-      store.state.projectSelector.isAddingProjects = true;
+    it.each([false, true])(
+      'passes showLoadingIndicator = %p to the project-list component',
+      isUpdatingProjects => {
+        factory({ isUpdatingProjects });
 
-      return wrapper.vm.$nextTick().then(() => {
-        expect(getLoadingIcon().exists()).toBe(true);
-      });
-    });
+        expect(getProjectList().props('showLoadingIndicator')).toBe(isUpdatingProjects);
+      },
+    );
   });
 });
diff --git a/ee/spec/frontend/security_dashboard/store/filters/utils_spec.js b/ee/spec/frontend/security_dashboard/store/filters/utils_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..ebde83ec0cd9b6ea644b6c70859e3e0e9eee0dc9
--- /dev/null
+++ b/ee/spec/frontend/security_dashboard/store/filters/utils_spec.js
@@ -0,0 +1,29 @@
+import { hasValidSelection } from 'ee/security_dashboard/store/modules/filters/utils';
+
+describe('filters module utils', () => {
+  describe('hasValidSelection', () => {
+    describe.each`
+      selection         | options           | expected
+      ${[]}             | ${[]}             | ${true}
+      ${[]}             | ${['foo']}        | ${true}
+      ${['foo']}        | ${['foo']}        | ${true}
+      ${['foo']}        | ${['foo', 'bar']} | ${true}
+      ${['bar', 'foo']} | ${['foo', 'bar']} | ${true}
+      ${['foo']}        | ${[]}             | ${false}
+      ${['foo']}        | ${['bar']}        | ${false}
+      ${['foo', 'bar']} | ${['foo']}        | ${false}
+    `('given selection $selection and options $options', ({ selection, options, expected }) => {
+      let filter;
+      beforeEach(() => {
+        filter = {
+          selection,
+          options: options.map(id => ({ id })),
+        };
+      });
+
+      it(`return ${expected}`, () => {
+        expect(hasValidSelection(filter)).toBe(expected);
+      });
+    });
+  });
+});
diff --git a/ee/spec/frontend/security_dashboard/store/modules/project_selector/actions_spec.js b/ee/spec/frontend/security_dashboard/store/modules/project_selector/actions_spec.js
index 56b1086200c83f4d1b628ac7fcc0af107e7fb918..a78fe967392748a217d9386cad60b7fb582b1423 100644
--- a/ee/spec/frontend/security_dashboard/store/modules/project_selector/actions_spec.js
+++ b/ee/spec/frontend/security_dashboard/store/modules/project_selector/actions_spec.js
@@ -91,6 +91,9 @@ describe('projectSelector actions', () => {
             type: 'receiveAddProjectsSuccess',
             payload: mockResponse,
           },
+          {
+            type: 'clearSearchResults',
+          },
         ],
       );
     });
@@ -103,7 +106,11 @@ describe('projectSelector actions', () => {
         null,
         state,
         [],
-        [{ type: 'requestAddProjects' }, { type: 'receiveAddProjectsError' }],
+        [
+          { type: 'requestAddProjects' },
+          { type: 'receiveAddProjectsError' },
+          { type: 'clearSearchResults' },
+        ],
       );
     });
   });
diff --git a/ee/spec/frontend/security_dashboard/store/modules/project_selector/getters_spec.js b/ee/spec/frontend/security_dashboard/store/modules/project_selector/getters_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..69f28b539c82550d3dd8dca22eb6befe5690fad2
--- /dev/null
+++ b/ee/spec/frontend/security_dashboard/store/modules/project_selector/getters_spec.js
@@ -0,0 +1,73 @@
+import createState from 'ee/security_dashboard/store/modules/project_selector/state';
+import * as getters from 'ee/security_dashboard/store/modules/project_selector/getters';
+
+describe('project selector module getters', () => {
+  let state;
+
+  beforeEach(() => {
+    state = createState();
+  });
+
+  describe('canAddProjects', () => {
+    describe.each`
+      isAddingProjects | selectedProjectCount | expected
+      ${true}          | ${0}                 | ${false}
+      ${true}          | ${1}                 | ${false}
+      ${false}         | ${0}                 | ${false}
+      ${false}         | ${1}                 | ${true}
+    `(
+      'given isAddingProjects = $isAddingProjects and $selectedProjectCount selected projects',
+      ({ isAddingProjects, selectedProjectCount, expected }) => {
+        beforeEach(() => {
+          state = {
+            ...state,
+            isAddingProjects,
+            selectedProjects: Array(selectedProjectCount).fill({}),
+          };
+        });
+
+        it(`returns ${expected}`, () => {
+          expect(getters.canAddProjects(state)).toBe(expected);
+        });
+      },
+    );
+  });
+
+  describe('isSearchingProjects', () => {
+    describe.each`
+      searchCount | expected
+      ${0}        | ${false}
+      ${1}        | ${true}
+      ${2}        | ${true}
+    `('given searchCount = $searchCount', ({ searchCount, expected }) => {
+      beforeEach(() => {
+        state = { ...state, searchCount };
+      });
+
+      it(`returns ${expected}`, () => {
+        expect(getters.isSearchingProjects(state)).toBe(expected);
+      });
+    });
+  });
+
+  describe('isUpdatingProjects', () => {
+    describe.each`
+      isAddingProjects | isRemovingProject | isLoadingProjects | expected
+      ${false}         | ${false}          | ${false}          | ${false}
+      ${true}          | ${false}          | ${false}          | ${true}
+      ${false}         | ${true}           | ${false}          | ${true}
+      ${false}         | ${false}          | ${true}           | ${true}
+    `(
+      'given isAddingProjects = $isAddingProjects, isRemovingProject = $isRemovingProject, isLoadingProjects = $isLoadingProjects',
+      ({ isAddingProjects, isRemovingProject, isLoadingProjects, expected }) => {
+        beforeEach(() => {
+          state = { ...state, isAddingProjects, isRemovingProject, isLoadingProjects };
+        });
+
+        it(`returns ${expected}`, () => {
+          expect(getters.isUpdatingProjects(state)).toBe(expected);
+        });
+      },
+    );
+  });
+});
diff --git a/ee/spec/frontend/security_dashboard/store/plugins/project_selector_spec.js b/ee/spec/frontend/security_dashboard/store/plugins/project_selector_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..6b16168f4b37f8df44490df812ac7aa2159034ab
--- /dev/null
+++ b/ee/spec/frontend/security_dashboard/store/plugins/project_selector_spec.js
@@ -0,0 +1,40 @@
+import Vuex from 'vuex';
+import createStore from 'ee/security_dashboard/store';
+import { BASE_FILTERS } from 'ee/security_dashboard/store/modules/filters/constants';
+import projectSelectorModule from 'ee/security_dashboard/store/modules/project_selector';
+import projectSelectorPlugin from 'ee/security_dashboard/store/plugins/project_selector';
+import * as projectSelectorMutationTypes from 'ee/security_dashboard/store/modules/projects/mutation_types';
+
+describe('project selector plugin', () => {
+  let store;
+
+  beforeEach(() => {
+    jest.spyOn(Vuex.Store.prototype, 'registerModule');
+    store = createStore({ plugins: [projectSelectorPlugin] });
+  });
+
+  it('registers the project selector module on the store', () => {
+    expect(Vuex.Store.prototype.registerModule).toHaveBeenCalledTimes(1);
+    expect(Vuex.Store.prototype.registerModule).toHaveBeenCalledWith(
+      'projectSelector',
+      projectSelectorModule(),
+    );
+  });
+
+  it('sets project filter options with lazy = true after projects have been received', () => {
+    jest.spyOn(store, 'dispatch').mockImplementation();
+    const projects = [{ name: 'foo', id: '1' }];
+
+    store.commit(
+      `projectSelector/${projectSelectorMutationTypes.RECEIVE_PROJECTS_SUCCESS}`,
+      projects,
+    );
+
+    expect(store.dispatch).toHaveBeenCalledTimes(1);
+    expect(store.dispatch).toHaveBeenCalledWith('filters/setFilterOptions', {
+      filterId: 'project_id',
+      options: [BASE_FILTERS.project_id, ...projects],
+      lazy: true,
+    });
+  });
+});
diff --git a/ee/spec/frontend/vue_shared/license_management/builder_spec.js b/ee/spec/frontend/vue_shared/license_management/builder_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..f000c30ce0ddd35e071a46102a6e2208c75235f5
--- /dev/null
+++ b/ee/spec/frontend/vue_shared/license_management/builder_spec.js
@@ -0,0 +1,32 @@
+import { Builder } from '../../license_management/mock_data';
+
+describe('build', () => {
+  it('creates a v1 report', () => {
+    const result = Builder.forV1()
+      .addLicense({ name: 'MIT License' })
+      .addDependency({ name: 'rails', license: { name: 'MIT License' } })
+      .build();
+
+    expect(result).toMatchObject({
+      licenses: [{ name: 'MIT License', url: 'https://opensource.org/licenses/MIT', count: 0 }],
+      dependencies: [
+        {
+          license: { name: 'MIT License', url: 'https://opensource.org/licenses/MIT' },
+          dependency: { name: 'rails', description: 'RAILS', url: 'https://www.example.org/rails' },
+        },
+      ],
+    });
+  });
+
+  it('creates a v2 report', () => {
+    const result = Builder.forV2()
+      .addLicense({ id: 'MIT', name: 'MIT License' })
+      .addDependency({ name: 'rails', licenses: ['MIT'] })
+      .build();
+    expect(result).toMatchObject({
+      version: '2.0',
+      licenses: [{ id: 'MIT', name: 'MIT License', url: 'https://opensource.org/licenses/MIT' }],
+      dependencies: [{ name: 'rails', description: 'RAILS', licenses: ['MIT'] }],
+    });
+  });
+});
diff --git a/ee/spec/frontend/vue_shared/license_management/report_mapper_spec.js b/ee/spec/frontend/vue_shared/license_management/report_mapper_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..113e03b4ace0b52acf5d1a57d92a1656b23838c2
--- /dev/null
+++ b/ee/spec/frontend/vue_shared/license_management/report_mapper_spec.js
@@ -0,0 +1,61 @@
+import ReportMapper from 'ee/vue_shared/license_management/report_mapper';
+import { Builder } from '../../license_management/mock_data';
+
+describe('mapFrom', () => {
+  let subject = null;
+
+  beforeEach(() => {
+    subject = new ReportMapper(true);
+  });
+
+  it('converts a v2 schema report to v1', () => {
+    const report = Builder.forV2()
+      .addLicense({ id: 'MIT', name: 'MIT License' })
+      .addLicense({ id: 'BSD', name: 'BSD License' })
+      .addDependency({ name: 'x', licenses: ['MIT'] })
+      .addDependency({ name: 'y', licenses: ['BSD'] })
+      .addDependency({ name: 'z', licenses: ['BSD', 'MIT'] })
+      .build();
+
+    const result = subject.mapFrom(report);
+    expect(result).toMatchObject(
+      Builder.forV1()
+        .addLicense({ name: 'BSD License', count: 2 })
+        .addLicense({ name: 'MIT License', count: 2 })
+        .addDependency({ name: 'x', license: { name: 'MIT License' } })
+        .addDependency({ name: 'y', license: { name: 'BSD License' } })
+        .addDependency({ name: 'z', license: { name: 'BSD License, MIT License', url: '' } })
+        .build(),
+    );
+  });
+
+  it('returns a v1 schema report', () => {
+    const report = Builder.forV1().build();
+
+    expect(subject.mapFrom(report)).toBe(report);
+  });
+
+  it('returns a v1.1 schema report', () => {
+    const report = Builder.forV1().build({ version: '1.1' });
+
+    expect(subject.mapFrom(report)).toBe(report);
+  });
+
+  it('ignores undefined versions', () => {
+    const report = {};
+
+    expect(subject.mapFrom(report)).toBe(report);
+  });
+
+  it('ignores undefined reports', () => {
+    const report = undefined;
+
+    expect(subject.mapFrom(report)).toBe(report);
+  });
+
+  it('ignores null reports', () => {
+    const report = null;
+
+    expect(subject.mapFrom(report)).toBe(report);
+  });
+});
diff --git a/ee/spec/graphql/types/epic_type_spec.rb b/ee/spec/graphql/types/epic_type_spec.rb
index e628de1423d0ca9dba739de8bd402eb056f95a63..930202ec43864226d90528eac98338a18b5c3c50 100644
--- a/ee/spec/graphql/types/epic_type_spec.rb
+++ b/ee/spec/graphql/types/epic_type_spec.rb
@@ -9,8 +9,8 @@
       start_date start_date_is_fixed start_date_fixed start_date_from_milestones
       due_date due_date_is_fixed due_date_fixed due_date_from_milestones
       closed_at created_at updated_at children has_children has_issues
-      web_path web_url relation_path reference issues
-      user_permissions notes discussions relative_position
+      web_path web_url relation_path reference issues user_permissions
+      notes discussions relative_position subscribed participants
     ]
   end
 
@@ -21,4 +21,8 @@
   it { expect(described_class).to require_graphql_authorizations(:read_epic) }
 
   it { expect(described_class).to have_graphql_fields(fields) }
+
+  it { is_expected.to have_graphql_field(:subscribed, complexity: 5) }
+
+  it { is_expected.to have_graphql_field(:participants, complexity: 5) }
 end
diff --git a/ee/spec/helpers/ee/user_callouts_helper_spec.rb b/ee/spec/helpers/ee/user_callouts_helper_spec.rb
index 61ba976515b13c6e9bd9feeee2b843f4fd3b7d0d..2c7d3c24a79d2120b3110f27c057a030dce261f2 100644
--- a/ee/spec/helpers/ee/user_callouts_helper_spec.rb
+++ b/ee/spec/helpers/ee/user_callouts_helper_spec.rb
@@ -55,6 +55,7 @@
 
   describe '.show_enable_hashed_storage_warning?' do
     subject { helper.show_enable_hashed_storage_warning? }
+
     let(:user) { create(:user) }
 
     context 'when hashed storage is disabled' do
@@ -87,6 +88,7 @@
 
   describe '.show_migrate_hashed_storage_warning?' do
     subject { helper.show_migrate_hashed_storage_warning? }
+
     let(:user) { create(:user) }
 
     context 'when hashed storage is disabled' do
diff --git a/ee/spec/javascripts/license_management/store/utils_spec.js b/ee/spec/javascripts/license_management/store/utils_spec.js
index 8776f385910e1a1b2eeb814b3507d588029949c9..f377a3fdaee1aafc74469b768b025a72f78967d6 100644
--- a/ee/spec/javascripts/license_management/store/utils_spec.js
+++ b/ee/spec/javascripts/license_management/store/utils_spec.js
@@ -9,6 +9,7 @@ import {
 import { LICENSE_APPROVAL_STATUS } from 'ee/vue_shared/license_management/constants';
 import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
 import {
+  Builder,
   approvedLicense,
   blacklistedLicense,
   licenseHeadIssues,
@@ -63,6 +64,71 @@ describe('utils', () => {
       expect(result[1].id).toBe(blacklistedLicense.id);
     });
 
+    it('compares a v2 report with a v2 report', () => {
+      const policies = [{ id: 100, name: 'BSD License', approvalStatus: 'blacklisted' }];
+      const baseReport = Builder.forV2()
+        .addLicense({ id: 'MIT', name: 'MIT License' })
+        .addDependency({ name: 'x', licenses: ['MIT'] })
+        .build();
+
+      const headReport = Builder.forV2()
+        .addLicense({ id: 'MIT', name: 'MIT License' })
+        .addLicense({ id: 'BSD', name: 'BSD License' })
+        .addDependency({ name: 'x', licenses: ['MIT'] })
+        .addDependency({ name: 'y', licenses: ['BSD'] })
+        .addDependency({ name: 'z', licenses: ['BSD', 'MIT'] })
+        .build();
+
+      const result = parseLicenseReportMetrics(headReport, baseReport, policies);
+
+      expect(result.length).toBe(1);
+      expect(result[0]).toEqual(
+        jasmine.objectContaining({
+          id: 100,
+          approvalStatus: 'blacklisted',
+          count: 2,
+          status: 'failed',
+          name: 'BSD License',
+          url: 'https://opensource.org/licenses/BSD',
+          packages: [{ name: 'y', url: 'https://www.example.org/y', description: 'Y' }],
+        }),
+      );
+    });
+
+    it('compares a v1 report with a v2 report', () => {
+      const policies = [{ id: 101, name: 'BSD License', approvalStatus: 'blacklisted' }];
+      const baseReport = Builder.forV1()
+        .addLicense({ name: 'MIT License' })
+        .addDependency({
+          name: 'x',
+          license: { name: 'MIT License', url: 'https://opensource.org/licenses/MIT' },
+        })
+        .build();
+
+      const headReport = Builder.forV2()
+        .addLicense({ id: 'MIT', name: 'MIT License' })
+        .addLicense({ id: 'BSD', name: 'BSD License' })
+        .addDependency({ name: 'x', licenses: ['MIT'] })
+        .addDependency({ name: 'y', licenses: ['BSD'] })
+        .addDependency({ name: 'z', licenses: ['BSD', 'MIT'] })
+        .build();
+
+      const result = parseLicenseReportMetrics(headReport, baseReport, policies);
+
+      expect(result.length).toBe(1);
+      expect(result[0]).toEqual(
+        jasmine.objectContaining({
+          id: 101,
+          approvalStatus: 'blacklisted',
+          count: 2,
+          status: 'failed',
+          name: 'BSD License',
+          url: 'https://opensource.org/licenses/BSD',
+          packages: [{ name: 'y', url: 'https://www.example.org/y', description: 'Y' }],
+        }),
+      );
+    });
+
     it('matches using a case insensitive match on license name', () => {
       const headReport = { licenses: [{ count: 1, name: 'BSD' }], dependencies: [] };
       const baseReport = { licenses: [{ count: 1, name: 'bsd' }], dependencies: [] };
diff --git a/ee/spec/javascripts/related_items_tree/components/tree_item_body_spec.js b/ee/spec/javascripts/related_items_tree/components/tree_item_body_spec.js
index b9e2e18f02ae3caf8c86d2249eae3462ea2543bc..d22499b174ddacbd11b6648d3c5e0e17748899bc 100644
--- a/ee/spec/javascripts/related_items_tree/components/tree_item_body_spec.js
+++ b/ee/spec/javascripts/related_items_tree/components/tree_item_body_spec.js
@@ -14,7 +14,7 @@ import createDefaultStore from 'ee/related_items_tree/store';
 import * as epicUtils from 'ee/related_items_tree/utils/epic_utils';
 import { ChildType, ChildState } from 'ee/related_items_tree/constants';
 
-import { mockParentItem, mockQueryResponse, mockIssue1 } from '../mock_data';
+import { mockParentItem, mockInitialConfig, mockQueryResponse, mockIssue1 } from '../mock_data';
 
 const mockItem = Object.assign({}, mockIssue1, {
   type: ChildType.Issue,
@@ -28,6 +28,7 @@ const createComponent = (parentItem = mockParentItem, item = mockItem) => {
   const children = epicUtils.processQueryResponse(mockQueryResponse.data.group);
 
   store.dispatch('setInitialParentItem', mockParentItem);
+  store.dispatch('setInitialConfig', mockInitialConfig);
   store.dispatch('setItemChildren', {
     parentItem: mockParentItem,
     isSubItem: false,
@@ -284,6 +285,10 @@ describe('RelatedItemsTree', () => {
     });
 
     describe('template', () => {
+      it('renders item body element without class `item-logged-out` when user is signed in', () => {
+        expect(wrapper.find('.item-body').classes()).not.toContain('item-logged-out');
+      });
+
       it('renders item state icon for large screens', () => {
         const statusIcon = wrapper.findAll(Icon).at(0);
 
diff --git a/ee/spec/javascripts/related_items_tree/components/tree_root_spec.js b/ee/spec/javascripts/related_items_tree/components/tree_root_spec.js
index 0112e86c6da37b9b45222a15217d48ae26626a5d..9fa72cf28bd31edadfb546a384e9c1d94a534531 100644
--- a/ee/spec/javascripts/related_items_tree/components/tree_root_spec.js
+++ b/ee/spec/javascripts/related_items_tree/components/tree_root_spec.js
@@ -1,12 +1,20 @@
 import { shallowMount, createLocalVue } from '@vue/test-utils';
 import { GlButton } from '@gitlab/ui';
 
+import Draggable from 'vuedraggable';
+
 import TreeRoot from 'ee/related_items_tree/components/tree_root.vue';
 
 import createDefaultStore from 'ee/related_items_tree/store';
 import * as epicUtils from 'ee/related_items_tree/utils/epic_utils';
 
-import { mockQueryResponse, mockParentItem, mockEpic1, mockIssue1 } from '../mock_data';
+import {
+  mockQueryResponse,
+  mockInitialConfig,
+  mockParentItem,
+  mockEpic1,
+  mockIssue1,
+} from '../mock_data';
 
 const { epic } = mockQueryResponse.data.group;
 
@@ -20,6 +28,7 @@ const createComponent = ({
   const children = epicUtils.processQueryResponse(mockQueryResponse.data.group);
 
   store.dispatch('setInitialParentItem', mockParentItem);
+  store.dispatch('setInitialConfig', mockInitialConfig);
   store.dispatch('setItemChildrenFlags', {
     isSubItem: false,
     children,
@@ -78,9 +87,24 @@ describe('RelatedItemsTree', () => {
         });
 
         describe('computed', () => {
-          describe('dragOptions', () => {
-            it('should return object containing Vue.Draggable config extended from `defaultSortableConfig`', () => {
-              expect(wrapper.vm.dragOptions).toEqual(
+          describe('treeRootWrapper', () => {
+            it('should return Draggable reference when userSignedIn prop is true', () => {
+              expect(wrapper.vm.treeRootWrapper).toBe(Draggable);
+            });
+
+            it('should return string "ul" when userSignedIn prop is false', () => {
+              wrapper.vm.$store.dispatch('setInitialConfig', {
+                ...mockInitialConfig,
+                userSignedIn: false,
+              });
+
+              expect(wrapper.vm.treeRootWrapper).toBe('ul');
+            });
+          });
+
+          describe('treeRootOptions', () => {
+            it('should return object containing Vue.Draggable config extended from `defaultSortableConfig` when userSignedIn prop is true', () => {
+              expect(wrapper.vm.treeRootOptions).toEqual(
                 jasmine.objectContaining({
                   animation: 200,
                   forceFallback: true,
@@ -88,9 +112,23 @@ describe('RelatedItemsTree', () => {
                   fallbackOnBody: false,
                   ghostClass: 'is-ghost',
                   group: mockParentItem.reference,
+                  tag: 'ul',
+                  'ghost-class': 'tree-item-drag-active',
+                  'data-parent-reference': mockParentItem.reference,
+                  value: wrapper.vm.children,
+                  move: wrapper.vm.handleDragOnMove,
                 }),
               );
             });
+
+            it('should return an empty object when userSignedIn prop is false', () => {
+              wrapper.vm.$store.dispatch('setInitialConfig', {
+                ...mockInitialConfig,
+                userSignedIn: false,
+              });
+
+              expect(wrapper.vm.treeRootOptions).toEqual(jasmine.objectContaining({}));
+            });
           });
         });
 
diff --git a/ee/spec/javascripts/related_items_tree/mock_data.js b/ee/spec/javascripts/related_items_tree/mock_data.js
index c3f169666b1770b7f881edad45225264cf4c07ef..06fab32fbc8ad35da7d1bce18328ef379d3b21ac 100644
--- a/ee/spec/javascripts/related_items_tree/mock_data.js
+++ b/ee/spec/javascripts/related_items_tree/mock_data.js
@@ -3,6 +3,7 @@ export const mockInitialConfig = {
   issuesEndpoint: 'http://test.host',
   autoCompleteEpics: true,
   autoCompleteIssues: false,
+  userSignedIn: true,
 };
 
 export const mockParentItem = {
diff --git a/ee/spec/javascripts/security_dashboard/store/filters/actions_spec.js b/ee/spec/javascripts/security_dashboard/store/filters/actions_spec.js
index ae5af7cc152388b9692e5c1479c38ab40cb454e7..6fec50fd811cc7a97b8fe7487bdee543b4156c1a 100644
--- a/ee/spec/javascripts/security_dashboard/store/filters/actions_spec.js
+++ b/ee/spec/javascripts/security_dashboard/store/filters/actions_spec.js
@@ -3,6 +3,7 @@ import Tracking from '~/tracking';
 import createState from 'ee/security_dashboard/store/modules/filters/state';
 import * as types from 'ee/security_dashboard/store/modules/filters/mutation_types';
 import module, * as actions from 'ee/security_dashboard/store/modules/filters/actions';
+import { ALL } from 'ee/security_dashboard/store/modules/filters/constants';
 
 describe('filters actions', () => {
   beforeEach(() => {
@@ -12,7 +13,26 @@ describe('filters actions', () => {
   describe('setFilter', () => {
     it('should commit the SET_FILTER mutuation', done => {
       const state = createState();
-      const payload = { filterId: 'type', optionId: 'sast' };
+      const payload = { filterId: 'report_type', optionId: 'sast' };
+
+      testAction(
+        actions.setFilter,
+        payload,
+        state,
+        [
+          {
+            type: types.SET_FILTER,
+            payload: { ...payload, lazy: false },
+          },
+        ],
+        [],
+        done,
+      );
+    });
+
+    it('should commit the SET_FILTER mutuation passing through lazy = true', done => {
+      const state = createState();
+      const payload = { filterId: 'report_type', optionId: 'sast', lazy: true };
 
       testAction(
         actions.setFilter,
@@ -33,7 +53,26 @@ describe('filters actions', () => {
   describe('setFilterOptions', () => {
     it('should commit the SET_FILTER_OPTIONS mutuation', done => {
       const state = createState();
-      const payload = { filterId: 'project', options: [] };
+      const payload = { filterId: 'project_id', options: [{ id: ALL }] };
+
+      testAction(
+        actions.setFilterOptions,
+        payload,
+        state,
+        [
+          {
+            type: types.SET_FILTER_OPTIONS,
+            payload,
+          },
+        ],
+        [],
+        done,
+      );
+    });
+
+    it('should commit the SET_FILTER_OPTIONS and SET_FILTER mutation when filter selection is invalid', done => {
+      const state = createState();
+      const payload = { filterId: 'project_id', options: [{ id: 'foo' }] };
 
       testAction(
         actions.setFilterOptions,
@@ -44,6 +83,40 @@ describe('filters actions', () => {
             type: types.SET_FILTER_OPTIONS,
             payload,
           },
+          {
+            type: types.SET_FILTER,
+            payload: jasmine.objectContaining({
+              filterId: 'project_id',
+              optionId: ALL,
+            }),
+          },
+        ],
+        [],
+        done,
+      );
+    });
+
+    it('should commit the SET_FILTER_OPTIONS and SET_FILTER mutation when filter selection is invalid, passing the lazy flag', done => {
+      const state = createState();
+      const payload = { filterId: 'project_id', options: [{ id: 'foo' }] };
+
+      testAction(
+        actions.setFilterOptions,
+        { ...payload, lazy: true },
+        state,
+        [
+          {
+            type: types.SET_FILTER_OPTIONS,
+            payload,
+          },
+          {
+            type: types.SET_FILTER,
+            payload: {
+              filterId: 'project_id',
+              optionId: ALL,
+              lazy: true,
+            },
+          },
         ],
         [],
         done,
@@ -75,17 +148,17 @@ describe('filters actions', () => {
   describe('setHideDismissedToggleInitialState', () => {
     [
       {
-        description: 'should set hide_dismissed to true if scope param is not present',
+        description: 'should set hideDismissed to true if scope param is not present',
         returnValue: [],
         hideDismissedValue: true,
       },
       {
-        description: 'should set hide_dismissed to false if scope param is "all"',
+        description: 'should set hideDismissed to false if scope param is "all"',
         returnValue: ['all'],
         hideDismissedValue: false,
       },
       {
-        description: 'should set hide_dismissed to true if scope param is "dismissed"',
+        description: 'should set hideDismissed to true if scope param is "dismissed"',
         returnValue: ['dismissed'],
         hideDismissedValue: true,
       },
@@ -101,7 +174,7 @@ describe('filters actions', () => {
             {
               type: types.SET_TOGGLE_VALUE,
               payload: {
-                key: 'hide_dismissed',
+                key: 'hideDismissed',
                 value: testCase.hideDismissedValue,
               },
             },
diff --git a/ee/spec/javascripts/security_dashboard/store/plugins/mediator_spec.js b/ee/spec/javascripts/security_dashboard/store/plugins/mediator_spec.js
index aa14edd3ffd8fb0955ad9b614337739449a897e8..514b9db12757f5264495d585012c9b562db20fa6 100644
--- a/ee/spec/javascripts/security_dashboard/store/plugins/mediator_spec.js
+++ b/ee/spec/javascripts/security_dashboard/store/plugins/mediator_spec.js
@@ -6,11 +6,10 @@ describe('mediator', () => {
 
   beforeEach(() => {
     store = createStore();
+    spyOn(store, 'dispatch');
   });
 
   it('triggers fetching vulnerabilities after one filter changes', () => {
-    spyOn(store, 'dispatch');
-
     const activeFilters = store.getters['filters/activeFilters'];
 
     store.commit(`filters/${filtersMutationTypes.SET_FILTER}`, {});
@@ -32,9 +31,13 @@ describe('mediator', () => {
     );
   });
 
-  it('triggers fetching vulnerabilities after filters change', () => {
-    spyOn(store, 'dispatch');
+  it('does not fetch vulnerabilities after one filter changes with lazy = true', () => {
+    store.commit(`filters/${filtersMutationTypes.SET_FILTER}`, { lazy: true });
+
+    expect(store.dispatch).not.toHaveBeenCalled();
+  });
 
+  it('triggers fetching vulnerabilities after filters change', () => {
     const payload = {
       ...store.getters['filters/activeFilters'],
       page: store.state.vulnerabilities.pageInfo.page,
@@ -57,8 +60,6 @@ describe('mediator', () => {
   });
 
   it('triggers fetching vulnerabilities after "Hide dismissed" toggle changes', () => {
-    spyOn(store, 'dispatch');
-
     const activeFilters = store.getters['filters/activeFilters'];
 
     store.commit(`filters/${filtersMutationTypes.SET_TOGGLE_VALUE}`, {});
@@ -79,4 +80,10 @@ describe('mediator', () => {
       activeFilters,
     );
   });
+
+  it('does not fetch vulnerabilities after "Hide dismissed" toggle changes with lazy = true', () => {
+    store.commit(`filters/${filtersMutationTypes.SET_TOGGLE_VALUE}`, { lazy: true });
+
+    expect(store.dispatch).not.toHaveBeenCalled();
+  });
 });
diff --git a/ee/spec/javascripts/security_dashboard/store/vulnerabilities/actions_spec.js b/ee/spec/javascripts/security_dashboard/store/vulnerabilities/actions_spec.js
index 27c4d890dad24814be3c94998b018fd8fc9b5d63..3ee7530145f7c45878c775e6dbd7dea15da6586b 100644
--- a/ee/spec/javascripts/security_dashboard/store/vulnerabilities/actions_spec.js
+++ b/ee/spec/javascripts/security_dashboard/store/vulnerabilities/actions_spec.js
@@ -758,6 +758,71 @@ describe('vulnerability dismissal', () => {
         );
       });
     });
+
+    describe('with dismissed vulnerabilities hidden', () => {
+      beforeEach(() => {
+        state = {
+          ...initialState(),
+          filters: {
+            hideDismissed: true,
+          },
+        };
+        mock
+          .onPost(vulnerability.create_vulnerability_feedback_dismissal_path)
+          .replyOnce(200, data);
+      });
+
+      it('should show the dismissal toast message and refresh vulnerabilities', done => {
+        spyOn(Vue.toasted, 'show').and.callThrough();
+
+        const checkToastMessage = () => {
+          const [message, options] = Vue.toasted.show.calls.argsFor(0);
+
+          expect(Vue.toasted.show).toHaveBeenCalledTimes(1);
+          expect(message).toContain('Turn off the hide dismissed toggle to view');
+          expect(options.action.length).toBe(2);
+          done();
+        };
+
+        testAction(
+          actions.dismissVulnerability,
+          { vulnerability, comment },
+          state,
+          [],
+          [
+            { type: 'requestDismissVulnerability' },
+            { type: 'closeDismissalCommentBox' },
+            {
+              type: 'receiveDismissVulnerabilitySuccess',
+              payload: { data, vulnerability },
+            },
+            { type: 'fetchVulnerabilities', payload: { page: 1 } },
+          ],
+          checkToastMessage,
+        );
+      });
+
+      it('should load the previous page if there is no more vulnerabiliy on the current one and page > 1', () => {
+        state.vulnerabilities = [mockDataVulnerabilities[0]];
+        state.pageInfo.page = 3;
+
+        testAction(
+          actions.dismissVulnerability,
+          { vulnerability, comment },
+          state,
+          [],
+          [
+            { type: 'requestDismissVulnerability' },
+            { type: 'closeDismissalCommentBox' },
+            {
+              type: 'receiveDismissVulnerabilitySuccess',
+              payload: { data, vulnerability },
+            },
+            { type: 'fetchVulnerabilities', payload: { page: 2 } },
+          ],
+        );
+      });
+    });
   });
 
   describe('receiveDismissVulnerabilitySuccess', () => {
diff --git a/ee/spec/lib/analytics/productivity_calculator_spec.rb b/ee/spec/lib/analytics/productivity_calculator_spec.rb
index 3b56c47776924c73f43f5b3822be1e273e628a80..2f5b55560d585285b8be808881b5db29909ddc5c 100644
--- a/ee/spec/lib/analytics/productivity_calculator_spec.rb
+++ b/ee/spec/lib/analytics/productivity_calculator_spec.rb
@@ -4,6 +4,7 @@
 
 describe Analytics::ProductivityCalculator do
   subject { described_class.new(merge_request) }
+
   let(:merge_request) { create(:merge_request_with_diff_notes, :merged, :with_diffs, created_at: 31.days.ago) }
 
   describe '#productivity_data' do
diff --git a/ee/spec/lib/ee/api/helpers_spec.rb b/ee/spec/lib/ee/api/helpers_spec.rb
index e6b66dea732d7d8d289a200eb64b07a804e7b240..61e57effd59ac71fdee548735d9ea76f453bf330 100644
--- a/ee/spec/lib/ee/api/helpers_spec.rb
+++ b/ee/spec/lib/ee/api/helpers_spec.rb
@@ -95,6 +95,7 @@ def app
 
   describe '#authorize_change_param' do
     subject { Class.new.include(described_class).new }
+
     let(:project) { create(:project) }
 
     before do
diff --git a/ee/spec/lib/ee/gitlab/auth/ldap/sync/group_spec.rb b/ee/spec/lib/ee/gitlab/auth/ldap/sync/group_spec.rb
index 83f32d396c7cda1f5ec6e4a5162eb62c49fdec17..59992a551029b12115cb64e594713f8714d8c11f 100644
--- a/ee/spec/lib/ee/gitlab/auth/ldap/sync/group_spec.rb
+++ b/ee/spec/lib/ee/gitlab/auth/ldap/sync/group_spec.rb
@@ -172,7 +172,6 @@ def execute
 
     let(:group) do
       create(:group_with_ldap_group_link,
-             :access_requestable,
              cn: 'ldap_group1',
              group_access: ::Gitlab::Access::DEVELOPER)
     end
@@ -388,7 +387,7 @@ def execute
       end
 
       context 'when user has a pending access request in a parent group' do
-        let(:parent_group) { create(:group, :access_requestable) }
+        let(:parent_group) { create(:group) }
         let(:ldap_group1) { ldap_group_entry(user_dn(user.username)) }
         let(:access_requester) { parent_group.request_access(user) }
         before do
@@ -578,7 +577,6 @@ def execute
     describe '#update_permissions' do
       let(:group) do
         create(:group_with_ldap_group_filter_link,
-               :access_requestable,
                group_access: ::Gitlab::Access::DEVELOPER)
       end
       let(:sync_group) { described_class.new(group, proxy(adapter)) }
diff --git a/ee/spec/lib/ee/gitlab/hook_data/issue_builder_spec.rb b/ee/spec/lib/ee/gitlab/hook_data/issue_builder_spec.rb
index f97dfa4dab4a09c020cb2c630650302c361bd4a6..cf6c1bae106c71e939a7b5aecfd6b6a4c8140c64 100644
--- a/ee/spec/lib/ee/gitlab/hook_data/issue_builder_spec.rb
+++ b/ee/spec/lib/ee/gitlab/hook_data/issue_builder_spec.rb
@@ -25,7 +25,7 @@
         moved_to_id
         project_id
         relative_position
-        state
+        state_id
         time_estimate
         title
         updated_at
diff --git a/ee/spec/lib/gitlab/analytics_spec.rb b/ee/spec/lib/gitlab/analytics_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..00dcf6b7ce595197c1f0f359a4978364d6169583
--- /dev/null
+++ b/ee/spec/lib/gitlab/analytics_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics do
+  describe '.productivity_analytics_enabled?' do
+    it 'is enabled by default' do
+      expect(described_class).to be_productivity_analytics_enabled
+    end
+  end
+end
diff --git a/ee/spec/lib/gitlab/ci/parsers/security/dast_spec.rb b/ee/spec/lib/gitlab/ci/parsers/security/dast_spec.rb
index e947c113bada15fbb5517e42f098d5b4870dc973..955541c275536d3cce46e6e8176d82953a953070 100644
--- a/ee/spec/lib/gitlab/ci/parsers/security/dast_spec.rb
+++ b/ee/spec/lib/gitlab/ci/parsers/security/dast_spec.rb
@@ -18,8 +18,8 @@
     end
 
     it 'parses all identifiers and occurrences' do
-      expect(report.occurrences.length).to eq(2)
-      expect(report.identifiers.length).to eq(3)
+      expect(report.occurrences.length).to eq(24)
+      expect(report.identifiers.length).to eq(15)
       expect(report.scanners.length).to eq(1)
     end
 
@@ -28,10 +28,9 @@
 
       expect(location).to be_a(::Gitlab::Ci::Reports::Security::Locations::Dast)
       expect(location).to have_attributes(
-        hostname: 'http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io',
+        hostname: 'http://goat:8080',
         method_name: 'GET',
-        param: 'X-Content-Type-Options',
-        path: ''
+        path: '/WebGoat/login?error'
       )
     end
 
@@ -40,8 +39,8 @@
 
       where(:attribute, :value) do
         :report_type | 'dast'
-        :severity | 'low'
-        :confidence | 'medium'
+        :severity | 'info'
+        :confidence | 'low'
       end
 
       with_them do
diff --git a/ee/spec/lib/gitlab/ci/parsers/security/dependency_list_spec.rb b/ee/spec/lib/gitlab/ci/parsers/security/dependency_list_spec.rb
index 64dd30cf36438e64f7ab3acfe1b728af50c8edd8..207a5fee61cf4d5acd1e0153cffc3c61ef8b6b22 100644
--- a/ee/spec/lib/gitlab/ci/parsers/security/dependency_list_spec.rb
+++ b/ee/spec/lib/gitlab/ci/parsers/security/dependency_list_spec.rb
@@ -55,7 +55,7 @@
 
   describe '#parse_licenses!' do
     let(:artifact) { create(:ee_ci_job_artifact, :license_management) }
-    let(:dependency_info) { build(:dependency, :with_vulnerabilities) }
+    let(:dependency_info) { build(:dependency, :nokogiri, :with_vulnerabilities) }
 
     before do
       report.add_dependency(dependency)
diff --git a/ee/spec/lib/gitlab/ci/parsers/security/dependency_scanning_spec.rb b/ee/spec/lib/gitlab/ci/parsers/security/dependency_scanning_spec.rb
index ca22482a381944ccdcd81cc95275ee0af95ba1c3..f80055354827a636afff1ee3a9e7a885b6e49f63 100644
--- a/ee/spec/lib/gitlab/ci/parsers/security/dependency_scanning_spec.rb
+++ b/ee/spec/lib/gitlab/ci/parsers/security/dependency_scanning_spec.rb
@@ -49,6 +49,26 @@
       end
     end
 
+    context "when parsing a vulnerability with a missing location" do
+      let(:report_hash) { JSON.parse(fixture_file('security_reports/master/gl-sast-report.json', dir: 'ee'), symbolize_names: true) }
+
+      before do
+        report_hash[:vulnerabilities][0][:location] = nil
+      end
+
+      it { expect { parser.parse!(report_hash.to_json, report) }.not_to raise_error }
+    end
+
+    context "when parsing a vulnerability with a missing cve" do
+      let(:report_hash) { JSON.parse(fixture_file('security_reports/master/gl-sast-report.json', dir: 'ee'), symbolize_names: true) }
+
+      before do
+        report_hash[:vulnerabilities][0][:cve] = nil
+      end
+
+      it { expect { parser.parse!(report_hash.to_json, report) }.not_to raise_error }
+    end
+
     context "when vulnerabilities have remediations" do
       let(:artifact) { create(:ee_ci_job_artifact, :dependency_scanning_remediation) }
 
diff --git a/ee/spec/lib/gitlab/ci/parsers/security/formatters/dast_spec.rb b/ee/spec/lib/gitlab/ci/parsers/security/formatters/dast_spec.rb
index 8901cc9fae7d419ad1868f7bf82df3212afa2c14..cfa363cdc524f91cfd0943a770546e8421a4fecc 100644
--- a/ee/spec/lib/gitlab/ci/parsers/security/formatters/dast_spec.rb
+++ b/ee/spec/lib/gitlab/ci/parsers/security/formatters/dast_spec.rb
@@ -16,7 +16,7 @@
 
   describe '#format_vulnerability' do
     let(:instance) { file_vulnerability['instances'][1] }
-    let(:hostname) { 'http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io' }
+    let(:hostname) { 'http://goat:8080' }
     let(:sanitized_desc) { file_vulnerability['desc'].gsub('<p>', '').gsub('</p>', '') }
     let(:sanitized_solution) { file_vulnerability['solution'].gsub('<p>', '').gsub('</p>', '') }
     let(:version) { parsed_report['@version'] }
@@ -25,38 +25,38 @@
       data = formatter.format(instance, hostname)
 
       expect(data['category']).to eq('dast')
-      expect(data['message']).to eq('X-Content-Type-Options Header Missing')
+      expect(data['message']).to eq('Anti CSRF Tokens Scanner')
       expect(data['description']).to eq(sanitized_desc)
-      expect(data['cve']).to eq('10021')
-      expect(data['severity']).to eq('low')
+      expect(data['cve']).to eq('20012')
+      expect(data['severity']).to eq('high')
       expect(data['confidence']).to eq('medium')
       expect(data['solution']).to eq(sanitized_solution)
       expect(data['scanner']).to eq({ 'id' => 'zaproxy', 'name' => 'ZAProxy' })
-      expect(data['links']).to eq([{ 'url' => 'http://msdn.microsoft.com/en-us/library/ie/gg622941%28v=vs.85%29.aspx' },
-                                   { 'url' => 'https://www.owasp.org/index.php/List_of_useful_HTTP_headers' }])
+      expect(data['links']).to eq([{ 'url' => 'http://projects.webappsec.org/Cross-Site-Request-Forgery' },
+                                   { 'url' => 'http://cwe.mitre.org/data/definitions/352.html' }])
       expect(data['identifiers'][0]).to eq({
                                              'type'  => 'ZAProxy_PluginId',
-                                             'name'  => 'X-Content-Type-Options Header Missing',
-                                             'value' => '10021',
+                                             'name'  => 'Anti CSRF Tokens Scanner',
+                                             'value' => '20012',
                                              'url'   => "https://github.com/zaproxy/zaproxy/blob/w2019-01-14/docs/scanners.md"
                                            })
       expect(data['identifiers'][1]).to eq({
                                              'type'  => 'CWE',
-                                             'name'  => "CWE-16",
-                                             'value' => '16',
-                                             'url'   => "https://cwe.mitre.org/data/definitions/16.html"
+                                             'name'  => "CWE-352",
+                                             'value' => '352',
+                                             'url'   => "https://cwe.mitre.org/data/definitions/352.html"
                                            })
       expect(data['identifiers'][2]).to eq({
                                              'type'  => 'WASC',
-                                             'name'  => "WASC-15",
-                                             'value' => '15',
+                                             'name'  => "WASC-9",
+                                             'value' => '9',
                                              'url'   => "http://projects.webappsec.org/w/page/13246974/Threat%20Classification%20Reference%20Grid"
                                            })
       expect(data['location']).to eq({
-                                       'param' => 'X-Content-Type-Options',
+                                       'param' => nil,
                                        'method' => 'GET',
                                        'hostname' => hostname,
-                                       'path' => '/'
+                                       'path' => '/WebGoat/login'
                                      })
     end
   end
diff --git a/ee/spec/lib/gitlab/ci/parsers/security/sast_spec.rb b/ee/spec/lib/gitlab/ci/parsers/security/sast_spec.rb
index 4ed552ae70bac06db1b7a4f48c9106924e923fac..e52b293462e9e75499f29318d8d19e8287c3b291 100644
--- a/ee/spec/lib/gitlab/ci/parsers/security/sast_spec.rb
+++ b/ee/spec/lib/gitlab/ci/parsers/security/sast_spec.rb
@@ -4,44 +4,53 @@
 
 describe Gitlab::Ci::Parsers::Security::Sast do
   describe '#parse!' do
-    let(:project) { artifact.project }
-    let(:pipeline) { artifact.job.pipeline }
-    let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline.sha) }
-    let(:parser) { described_class.new }
+    subject(:parser) { described_class.new }
 
-    where(report_format: %i(sast sast_deprecated))
+    let(:commit_sha) { "d8978e74745e18ce44d88814004d4255ac6a65bb" }
 
-    with_them do
-      let(:artifact) { create(:ee_ci_job_artifact, report_format) }
+    context "when parsing valid reports" do
+      where(report_format: %i(sast sast_deprecated))
 
-      before do
-        artifact.each_blob do |blob|
-          parser.parse!(blob, report)
+      with_them do
+        let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, commit_sha) }
+        let(:artifact) { create(:ee_ci_job_artifact, report_format) }
+
+        before do
+          artifact.each_blob do |blob|
+            parser.parse!(blob, report)
+          end
         end
-      end
 
-      it "parses all identifiers and occurrences" do
-        expect(report.occurrences.length).to eq(33)
-        expect(report.identifiers.length).to eq(17)
-        expect(report.scanners.length).to eq(3)
-      end
+        it "parses all identifiers and occurrences" do
+          expect(report.occurrences.length).to eq(33)
+          expect(report.identifiers.length).to eq(17)
+          expect(report.scanners.length).to eq(3)
+        end
 
-      it 'generates expected location' do
-        location = report.occurrences.first.location
-
-        expect(location).to be_a(::Gitlab::Ci::Reports::Security::Locations::Sast)
-        expect(location).to have_attributes(
-          file_path: 'python/hardcoded/hardcoded-tmp.py',
-          start_line: 1,
-          end_line: 1,
-          class_name: nil,
-          method_name: nil
-        )
-      end
+        it 'generates expected location' do
+          location = report.occurrences.first.location
+
+          expect(location).to be_a(::Gitlab::Ci::Reports::Security::Locations::Sast)
+          expect(location).to have_attributes(
+            file_path: 'python/hardcoded/hardcoded-tmp.py',
+            start_line: 1,
+            end_line: 1,
+            class_name: nil,
+            method_name: nil
+          )
+        end
 
-      it "generates expected metadata_version" do
-        expect(report.occurrences.first.metadata_version).to eq('1.2')
+        it "generates expected metadata_version" do
+          expect(report.occurrences.first.metadata_version).to eq('1.2')
+        end
       end
     end
+
+    context "when parsing an empty report" do
+      let(:report) { Gitlab::Ci::Reports::Security::Report.new('sast', commit_sha) }
+      let(:blob) { JSON.generate({}) }
+
+      it { expect(parser.parse!(blob, report)).to be_empty }
+    end
   end
 end
diff --git a/ee/spec/lib/gitlab/ci/reports/dependency_list/report_spec.rb b/ee/spec/lib/gitlab/ci/reports/dependency_list/report_spec.rb
index c86408951532d4b1c09019a2ad39d1bf94677473..29dd218934df95568bfc6a5613461f4edeea7b86 100644
--- a/ee/spec/lib/gitlab/ci/reports/dependency_list/report_spec.rb
+++ b/ee/spec/lib/gitlab/ci/reports/dependency_list/report_spec.rb
@@ -57,4 +57,41 @@
       end
     end
   end
+
+  describe '#dependencies_with_licenses' do
+    subject { report.dependencies_with_licenses }
+
+    context 'with found dependencies' do
+      let(:plain_dependency) { build :dependency }
+
+      before do
+        report.add_dependency(plain_dependency)
+      end
+
+      context 'with existing license' do
+        let(:dependency) { build :dependency, :with_licenses }
+
+        before do
+          report.add_dependency(dependency)
+        end
+
+        it 'returns only dependency with license' do
+          expect(subject.size).to eq(1)
+          expect(subject.first).to eq(dependency)
+        end
+      end
+
+      context 'without existing license' do
+        it 'returns empty array' do
+          expect(subject).to be_empty
+        end
+      end
+    end
+
+    context 'without found dependencies' do
+      it 'returns empty array' do
+        expect(subject).to be_empty
+      end
+    end
+  end
 end
diff --git a/ee/spec/lib/gitlab/ci/reports/license_scanning/report_spec.rb b/ee/spec/lib/gitlab/ci/reports/license_scanning/report_spec.rb
index d3512621a8a6e1ccfa2f72bdea9a5754cb0d36fb..51423b4dded6df9af223951ffc66f0649ba2df9f 100644
--- a/ee/spec/lib/gitlab/ci/reports/license_scanning/report_spec.rb
+++ b/ee/spec/lib/gitlab/ci/reports/license_scanning/report_spec.rb
@@ -3,19 +3,92 @@
 require 'spec_helper'
 
 describe Gitlab::Ci::Reports::LicenseScanning::Report do
-  subject { build(:ci_reports_license_scanning_report, :mit) }
+  include LicenseScanningReportHelpers
+
+  describe '#by_license_name' do
+    subject { report.by_license_name(name) }
+
+    let(:report) { build(:ci_reports_license_scanning_report, :report_2) }
+
+    context 'with existing license' do
+      let(:name) { 'MIT' }
+
+      it 'finds right name' do
+        is_expected.to be_a(Gitlab::Ci::Reports::LicenseScanning::License)
+        expect(subject.name).to eq('MIT')
+      end
+    end
+
+    context 'without existing license' do
+      let(:name) { 'TIM' }
+
+      it { is_expected.to be_nil }
+    end
+  end
+
+  describe '#merge_dependencies_info!' do
+    subject { report.merge_dependencies_info!(dependencies) }
+
+    let(:report) { build(:ci_reports_license_scanning_report, :report_2) }
+    let(:dependency_list_report) { Gitlab::Ci::Reports::DependencyList::Report.new }
+
+    context 'without licensed dependencies' do
+      let(:library1) { build(:dependency, name: 'Library1') }
+      let(:library3) { build(:dependency, name: 'Library3') }
+      let(:dependencies) { [library3, library1] }
+
+      before do
+        subject
+      end
+
+      it 'does not merge dependency path' do
+        paths = all_dependency_paths(report)
+
+        expect(paths).to be_empty
+      end
+    end
+
+    context 'with licensed dependencies' do
+      let(:library1) { build(:dependency, :with_licenses, name: 'Library1') }
+      let(:library3) { build(:dependency, :with_licenses, name: 'Library3') }
+      let(:library4) { build(:dependency, :with_licenses, name: 'Library4') }
+      let(:dependencies) { [library1, library3, library4] }
+
+      let(:mit_license) { report.by_license_name('MIT') }
+      let(:apache_license) { report.by_license_name('Apache 2.0') }
+
+      before do
+        mit_license.add_dependency('Library4')
+        apache_license.add_dependency('Library3')
+
+        subject
+      end
+
+      it 'merge path to matched dependencies' do
+        dep1 = dependency_by_name(mit_license, 'Library1')
+        dep4 = dependency_by_name(mit_license, 'Library4')
+        dep3 = dependency_by_name(apache_license, 'Library3')
+
+        expect(dep1.path).to eq(library1.dig(:location, :blob_path))
+        expect(dep4.path).to eq(library4.dig(:location, :blob_path))
+        expect(dep3.path).to be_nil
+      end
+    end
+  end
 
   describe '#violates?' do
+    subject { report.violates?(project.software_license_policies) }
+
     let(:project) { create(:project) }
 
     context "when checking for violations using v1 license scan report" do
-      subject { build(:license_scan_report) }
+      let(:report) { build(:license_scan_report) }
 
       let(:mit_license) { build(:software_license, :mit, spdx_identifier: nil) }
       let(:apache_license) { build(:software_license, :apache_2_0, spdx_identifier: nil) }
 
       before do
-        subject
+        report
           .add_license(id: nil, name: 'MIT')
           .add_dependency('rails')
       end
@@ -27,7 +100,7 @@
           project.software_license_policies << mit_blacklist
         end
 
-        specify { expect(subject.violates?(project.software_license_policies)).to be(true) }
+        it { is_expected.to be_truthy }
       end
 
       context 'when a blacklisted license is discovered with a different casing for the name' do
@@ -38,7 +111,7 @@
           project.software_license_policies << mit_blacklist
         end
 
-        specify { expect(subject.violates?(project.software_license_policies)).to be(true) }
+        it { is_expected.to be_truthy }
       end
 
       context 'when none of the licenses discovered in the report violate the blacklist policy' do
@@ -48,12 +121,12 @@
           project.software_license_policies << apache_blacklist
         end
 
-        specify { expect(subject.violates?(project.software_license_policies)).to be(false) }
+        it { is_expected.to be_falsey }
       end
     end
 
     context "when checking for violations using the v2 license scan reports" do
-      subject { build(:license_scan_report) }
+      let(:report) { build(:license_scan_report) }
 
       context "when a blacklisted license with a SPDX identifier is also in the report" do
         let(:mit_spdx_id) { 'MIT' }
@@ -61,11 +134,11 @@
         let(:mit_policy) { build(:software_license_policy, :blacklist, software_license: mit_license) }
 
         before do
-          subject.add_license(id: mit_spdx_id, name: 'MIT License')
+          report.add_license(id: mit_spdx_id, name: 'MIT License')
           project.software_license_policies << mit_policy
         end
 
-        specify { expect(subject.violates?(project.software_license_policies)).to be(true) }
+        it { is_expected.to be_truthy }
       end
 
       context "when a blacklisted license does not have an SPDX identifier because it was provided by an end user" do
@@ -73,11 +146,11 @@
         let(:custom_policy) { build(:software_license_policy, :blacklist, software_license: custom_license) }
 
         before do
-          subject.add_license(id: nil, name: 'Custom')
+          report.add_license(id: nil, name: 'Custom')
           project.software_license_policies << custom_policy
         end
 
-        specify { expect(subject.violates?(project.software_license_policies)).to be(true) }
+        it { is_expected.to be_truthy }
       end
 
       context "when none of the licenses discovered match any of the blacklisted software policies" do
@@ -85,12 +158,12 @@
         let(:apache_policy) { build(:software_license_policy, :blacklist, software_license: apache_license) }
 
         before do
-          subject.add_license(id: nil, name: 'Custom')
-          subject.add_license(id: 'MIT', name: 'MIT License')
+          report.add_license(id: nil, name: 'Custom')
+          report.add_license(id: 'MIT', name: 'MIT License')
           project.software_license_policies << apache_policy
         end
 
-        specify { expect(subject.violates?(project.software_license_policies)).to be(false) }
+        it { is_expected.to be_falsey }
       end
     end
   end
@@ -192,6 +265,7 @@ def names_from(licenses)
   describe '.parse_from' do
     context 'when parsing a v1 report' do
       subject { described_class.parse_from(v1_json) }
+
       let(:v1_json) { fixture_file('security_reports/master/gl-license-management-report.json', dir: 'ee') }
 
       it { expect(subject.version).to eql('1.0') }
@@ -200,6 +274,7 @@ def names_from(licenses)
 
     context 'when parsing a v2 report' do
       subject { described_class.parse_from(v2_json) }
+
       let(:v2_json) { fixture_file('security_reports/gl-license-management-report-v2.json', dir: 'ee') }
 
       it { expect(subject.version).to eql('2.0') }
diff --git a/ee/spec/lib/gitlab/ci/reports/security/occurrence_spec.rb b/ee/spec/lib/gitlab/ci/reports/security/occurrence_spec.rb
index f6d82cad26e91bc4f7f4fb920be74853804c0740..aca3d25d108182618631acd619e734dea9ab0083 100644
--- a/ee/spec/lib/gitlab/ci/reports/security/occurrence_spec.rb
+++ b/ee/spec/lib/gitlab/ci/reports/security/occurrence_spec.rb
@@ -63,6 +63,7 @@
 
   describe "delegation" do
     subject { create(:ci_reports_security_occurrence) }
+
     %i[file_path start_line end_line].each do |attribute|
       it "delegates attribute #{attribute} to location" do
         expect(subject.public_send(attribute)).to eq(subject.location.public_send(attribute))
diff --git a/ee/spec/lib/gitlab/ci/reports/security/report_spec.rb b/ee/spec/lib/gitlab/ci/reports/security/report_spec.rb
index 24f6d1160c6736e0b90c0803d902662355488b90..e7c467376272837b897599e2d9e41998e8508e4e 100644
--- a/ee/spec/lib/gitlab/ci/reports/security/report_spec.rb
+++ b/ee/spec/lib/gitlab/ci/reports/security/report_spec.rb
@@ -3,8 +3,8 @@
 require 'spec_helper'
 
 describe Gitlab::Ci::Reports::Security::Report do
-  let(:pipeline) { create(:ci_pipeline) }
-  let(:report) { described_class.new('sast', pipeline.sha) }
+  let(:report) { described_class.new('sast', commit_sha) }
+  let(:commit_sha) { "d8978e74745e18ce44d88814004d4255ac6a65bb" }
 
   it { expect(report.type).to eq('sast') }
 
@@ -111,7 +111,7 @@
       allow(report).to receive(:replace_with!)
     end
 
-    subject { report.merge!(described_class.new('sast', pipeline.sha)) }
+    subject { report.merge!(described_class.new('sast', commit_sha)) }
 
     it 'invokes the merge with other report and then replaces this report contents by merge result' do
       subject
@@ -119,4 +119,63 @@
       expect(report).to have_received(:replace_with!).with(merged_report)
     end
   end
+
+  describe "#safe?" do
+    subject { described_class.new('sast', commit_sha) }
+
+    context "when the sast report has an unsafe vulnerability" do
+      where(severity: %w[unknown Unknown high High critical Critical])
+      with_them do
+        let(:occurrence) { build(:ci_reports_security_occurrence, severity: severity) }
+
+        before do
+          subject.add_occurrence(occurrence)
+        end
+
+        it { expect(subject.unsafe_severity?).to be(true) }
+        it { expect(subject).not_to be_safe }
+      end
+    end
+
+    context "when the sast report has a medium to low severity vulnerability" do
+      where(severity: %w[medium Medium low Low])
+      with_them do
+        let(:occurrence) { build(:ci_reports_security_occurrence, severity: severity) }
+
+        before do
+          subject.add_occurrence(occurrence)
+        end
+
+        it { expect(subject.unsafe_severity?).to be(false) }
+        it { expect(subject).to be_safe }
+      end
+    end
+
+    context "when the sast report has a vulnerability with a `nil` severity" do
+      let(:occurrence) { build(:ci_reports_security_occurrence, severity: nil) }
+
+      before do
+        subject.add_occurrence(occurrence)
+      end
+
+      it { expect(subject.unsafe_severity?).to be(false) }
+      it { expect(subject).to be_safe }
+    end
+
+    context "when the sast report has a vulnerability with a blank severity" do
+      let(:occurrence) { build(:ci_reports_security_occurrence, severity: '') }
+
+      before do
+        subject.add_occurrence(occurrence)
+      end
+
+      it { expect(subject.unsafe_severity?).to be(false) }
+      it { expect(subject).to be_safe }
+    end
+
+    context "when the sast report has zero vulnerabilities" do
+      it { expect(subject.unsafe_severity?).to be(false) }
+      it { expect(subject).to be_safe }
+    end
+  end
 end
diff --git a/ee/spec/lib/gitlab/ci/reports/security/reports_spec.rb b/ee/spec/lib/gitlab/ci/reports/security/reports_spec.rb
index 6ed30bdfe2794cc092398e7214d47b62bc2c11f4..91ed3b23e8e22428bfda115aba708fb3fedcf959 100644
--- a/ee/spec/lib/gitlab/ci/reports/security/reports_spec.rb
+++ b/ee/spec/lib/gitlab/ci/reports/security/reports_spec.rb
@@ -35,4 +35,29 @@
       end
     end
   end
+
+  describe "#violates_default_policy?" do
+    subject { described_class.new(commit_sha) }
+
+    let(:low_severity) { build(:ci_reports_security_occurrence, severity: 'low') }
+    let(:high_severity) { build(:ci_reports_security_occurrence, severity: 'high') }
+
+    context "when a report has a high severity vulnerability" do
+      before do
+        subject.get_report('sast').add_occurrence(high_severity)
+        subject.get_report('dependency_scanning').add_occurrence(low_severity)
+      end
+
+      it { expect(subject.violates_default_policy?).to be(true) }
+    end
+
+    context "when none of the reports have a high severity vulnerability" do
+      before do
+        subject.get_report('sast').add_occurrence(low_severity)
+        subject.get_report('dependency_scanning').add_occurrence(low_severity)
+      end
+
+      it { expect(subject.violates_default_policy?).to be(false) }
+    end
+  end
 end
diff --git a/ee/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb b/ee/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb
index 08eda917e4560261e265ab363066dec10738f262..61f82a71ab7e84f4f4caa39aef49a0b6eabddfb8 100644
--- a/ee/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb
+++ b/ee/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb
@@ -3,9 +3,9 @@
 require 'spec_helper'
 
 describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
-  let!(:identifier) { create(:vulnerabilities_identifier) }
-  let!(:base_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '123') }
-  let!(:head_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '123') }
+  let!(:identifier) { build(:vulnerabilities_identifier) }
+  let!(:base_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '123', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:high], severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS[:critical]) }
+  let!(:head_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '123', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:high], severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS[:critical]) }
 
   before do
     allow(base_vulnerability).to receive(:location).and_return({})
@@ -14,40 +14,61 @@
 
   describe '#existing' do
     context 'with existing reports' do
+      let(:vuln) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '888', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:medium]) }
+      let(:low_vuln) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '888', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:low]) }
       let(:comparer) { described_class.new([base_vulnerability], [head_vulnerability]) }
 
       it 'points to source tree' do
-        allow(head_vulnerability).to receive(:raw_metadata).and_return('')
+        comparer = described_class.new([base_vulnerability], [head_vulnerability])
 
-        expect(comparer.existing.count).to eq(1)
         expect(comparer.existing).to eq([head_vulnerability])
       end
+
+      it 'does not change order' do
+        comparer = described_class.new([base_vulnerability, vuln], [head_vulnerability, vuln, low_vuln])
+
+        expect(comparer.existing).to eq([head_vulnerability, vuln])
+      end
     end
   end
 
   describe '#added' do
-    let(:vuln) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '888') }
+    let(:vuln) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '888', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:high], severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS[:critical]) }
+    let(:low_vuln) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '888', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:high], severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS[:low]) }
 
     context 'with new vulnerability' do
-      let(:comparer) { described_class.new([base_vulnerability], [head_vulnerability, vuln]) }
+      let(:comparer) { described_class.new([base_vulnerability], [vuln, low, head_vulnerability]) }
 
       it 'points to source tree' do
-        expect(comparer.added.count).to eq(1)
+        comparer = described_class.new([base_vulnerability], [head_vulnerability, vuln])
+
         expect(comparer.added).to eq([vuln])
       end
+
+      it 'does not change order' do
+        comparer = described_class.new([base_vulnerability], [head_vulnerability, vuln, low_vuln])
+
+        expect(comparer.added).to eq([vuln, low_vuln])
+      end
     end
   end
 
   describe '#fixed' do
     let(:vuln) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '888') }
+    let(:medium_vuln) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '888', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:high], severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS[:medium]) }
 
     context 'with fixed vulnerability' do
-      let(:comparer) { described_class.new([base_vulnerability, vuln], [head_vulnerability]) }
-
       it 'points to base tree' do
-        expect(comparer.fixed.count).to eq(1)
+        comparer = described_class.new([base_vulnerability, vuln], [head_vulnerability])
+
         expect(comparer.fixed).to eq([vuln])
       end
+
+      it 'does not change order' do
+        comparer = described_class.new([vuln, medium_vuln, base_vulnerability], [head_vulnerability])
+
+        expect(comparer.fixed).to eq([vuln, medium_vuln])
+      end
     end
   end
 
diff --git a/ee/spec/lib/gitlab/elastic/snippet_search_results_spec.rb b/ee/spec/lib/gitlab/elastic/snippet_search_results_spec.rb
index 36d27c47baa85d5354420832eb91ed2f52990b11..89e47783b063bbf73b601890a6977d5f14b81dd4 100644
--- a/ee/spec/lib/gitlab/elastic/snippet_search_results_spec.rb
+++ b/ee/spec/lib/gitlab/elastic/snippet_search_results_spec.rb
@@ -4,7 +4,7 @@
 
 describe Gitlab::Elastic::SnippetSearchResults, :elastic do
   let(:snippet) { create(:personal_snippet, content: 'foo', file_name: 'foo') }
-  let(:results) { described_class.new(snippet.author, 'foo') }
+  let(:results) { described_class.new(snippet.author, 'foo', []) }
 
   before do
     stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
@@ -26,7 +26,7 @@
   end
 
   context 'when user is not author' do
-    let(:results) { described_class.new(create(:user), 'foo') }
+    let(:results) { described_class.new(create(:user), 'foo', []) }
 
     it 'returns nothing' do
       expect(results.snippet_titles_count).to eq(0)
@@ -35,7 +35,7 @@
   end
 
   context 'when user is nil' do
-    let(:results) { described_class.new(nil, 'foo') }
+    let(:results) { described_class.new(nil, 'foo', []) }
 
     it 'returns nothing' do
       expect(results.snippet_titles_count).to eq(0)
@@ -54,7 +54,7 @@
 
   context 'when user has full_private_access' do
     let(:user) { create(:admin) }
-    let(:results) { described_class.new(user, 'foo') }
+    let(:results) { described_class.new(user, 'foo', :any) }
 
     it 'returns matched snippets' do
       expect(results.snippet_titles_count).to eq(1)
@@ -67,8 +67,8 @@
     let(:snippet) { create(:personal_snippet, :public, content: content) }
 
     it 'indexes up to a limit' do
-      expect(described_class.new(nil, 'abc').snippet_blobs_count).to eq(1)
-      expect(described_class.new(nil, 'xyz').snippet_blobs_count).to eq(0)
+      expect(described_class.new(nil, 'abc', []).snippet_blobs_count).to eq(1)
+      expect(described_class.new(nil, 'xyz', []).snippet_blobs_count).to eq(0)
     end
   end
 end
diff --git a/ee/spec/lib/omni_auth/strategies/kerberos_spnego_spec.rb b/ee/spec/lib/omni_auth/strategies/kerberos_spnego_spec.rb
index 15574c265d81dd3694052f21cd76485adf775974..497eb5d038a8f62b0f327b014314fc09b0f1f571 100644
--- a/ee/spec/lib/omni_auth/strategies/kerberos_spnego_spec.rb
+++ b/ee/spec/lib/omni_auth/strategies/kerberos_spnego_spec.rb
@@ -4,6 +4,7 @@
 
 describe OmniAuth::Strategies::KerberosSpnego do
   subject { described_class.new(:app) }
+
   let(:session) { {} }
 
   before do
diff --git a/ee/spec/mailers/emails/csv_export_spec.rb b/ee/spec/mailers/emails/csv_export_spec.rb
index a7143d90387ca1aec7f9f163b879a929dea200dc..8b8246c25a1f8b88f8dd39dee752a1d6cddeafe1 100644
--- a/ee/spec/mailers/emails/csv_export_spec.rb
+++ b/ee/spec/mailers/emails/csv_export_spec.rb
@@ -18,6 +18,7 @@
     let(:empty_project) { create(:project, path: 'myproject') }
     let(:export_status) { { truncated: false, rows_expected: 3, rows_written: 3 } }
     subject { Notify.issues_csv_email(user, empty_project, "dummy content", export_status) }
+
     let(:attachment) { subject.attachments.first }
 
     it 'attachment has csv mime type' do
diff --git a/ee/spec/migrations/set_self_monitoring_project_alerting_token_spec.rb b/ee/spec/migrations/set_self_monitoring_project_alerting_token_spec.rb
deleted file mode 100644
index cf016920883950433f6a6c95cbda899c6856d7c8..0000000000000000000000000000000000000000
--- a/ee/spec/migrations/set_self_monitoring_project_alerting_token_spec.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190809072552_set_self_monitoring_project_alerting_token.rb')
-
-describe SetSelfMonitoringProjectAlertingToken, :migration do
-  let(:application_settings) { table(:application_settings) }
-  let(:projects)             { table(:projects) }
-  let(:namespaces)           { table(:namespaces) }
-
-  let(:namespace) do
-    namespaces.create!(
-      path: 'gitlab-instance-administrators',
-      name: 'GitLab Instance Administrators'
-    )
-  end
-
-  let(:project) do
-    projects.create!(
-      namespace_id: namespace.id,
-      name: 'GitLab Instance Administration'
-    )
-  end
-
-  describe 'down' do
-    before do
-      application_settings.create!(instance_administration_project_id: project.id)
-
-      stub_licensed_features(prometheus_alerts: true)
-    end
-
-    it 'destroys token' do
-      migrate!
-
-      token = Alerting::ProjectAlertingSetting.where(project_id: project.id).first!.token
-      expect(token).to be_present
-
-      schema_migrate_down!
-
-      expect(Alerting::ProjectAlertingSetting.count).to eq(0)
-    end
-  end
-
-  describe 'up' do
-    context 'when instance administration project present' do
-      before do
-        application_settings.create!(instance_administration_project_id: project.id)
-
-        stub_licensed_features(prometheus_alerts: true)
-      end
-
-      it 'sets the alerting token' do
-        migrate!
-
-        token = Alerting::ProjectAlertingSetting.where(project_id: project.id).first!.token
-
-        expect(token).to be_present
-      end
-    end
-
-    context 'when instance administration project not present' do
-      it 'does not raise error' do
-        migrate!
-
-        expect(Alerting::ProjectAlertingSetting.count).to eq(0)
-      end
-    end
-  end
-end
diff --git a/ee/spec/models/approvable_spec.rb b/ee/spec/models/approvable_spec.rb
index 50fb1dba25f98319f0f6e0f1b58b4cf3709d531c..cf0377830148f2490c4f9d76b29ad178706f7f40 100644
--- a/ee/spec/models/approvable_spec.rb
+++ b/ee/spec/models/approvable_spec.rb
@@ -4,6 +4,7 @@
 
 describe Approvable do
   subject(:merge_request) { create(:merge_request) }
+
   let(:project) { merge_request.project }
   let(:author) { merge_request.author }
 
diff --git a/ee/spec/models/approval_merge_request_rule_spec.rb b/ee/spec/models/approval_merge_request_rule_spec.rb
index 64c16f525abd8ec0d6cb18911cffaca4ad830899..ba92bc95717cf25a777ca0a1ec6befa47db82c49 100644
--- a/ee/spec/models/approval_merge_request_rule_spec.rb
+++ b/ee/spec/models/approval_merge_request_rule_spec.rb
@@ -309,6 +309,7 @@
 
     context "when the rule is a `#{ApprovalRuleLike::DEFAULT_NAME_FOR_LICENSE_REPORT}` rule" do
       subject { create(:report_approver_rule, :requires_approval, :license_management, merge_request: open_merge_request) }
+
       let(:open_merge_request) { create(:merge_request, :opened, target_project: project, source_project: project) }
       let!(:project_approval_rule) { create(:approval_project_rule, :requires_approval, :license_management, project: project) }
       let(:project) { create(:project) }
diff --git a/ee/spec/models/approval_project_rule_spec.rb b/ee/spec/models/approval_project_rule_spec.rb
index f132f3b39cc26d029dd95e8059b01e542e619ba5..667585558fc2cd436ca3a1fb90cec39e19019c1f 100644
--- a/ee/spec/models/approval_project_rule_spec.rb
+++ b/ee/spec/models/approval_project_rule_spec.rb
@@ -80,6 +80,7 @@
     ApprovalProjectRule::REPORT_TYPES_BY_DEFAULT_NAME.each do |name, value|
       context "when the project rule is for a `#{name}`" do
         subject { create(:approval_project_rule, value, :requires_approval, project: project) }
+
         let!(:result) { subject.apply_report_approver_rules_to(merge_request) }
 
         specify { expect(merge_request.reload.approval_rules).to match_array([result]) }
diff --git a/ee/spec/models/ci/build_spec.rb b/ee/spec/models/ci/build_spec.rb
index 610dec4c0393768042d6cf46395d0732fbafdb37..458a7b2c27f37224d752f38dd94354729c8dea7f 100644
--- a/ee/spec/models/ci/build_spec.rb
+++ b/ee/spec/models/ci/build_spec.rb
@@ -3,7 +3,7 @@
 require 'spec_helper'
 
 describe Ci::Build do
-  set(:group) { create(:group, :access_requestable, plan: :bronze_plan) }
+  set(:group) { create(:group, plan: :bronze_plan) }
   let(:project) { create(:project, :repository, group: group) }
 
   let(:pipeline) do
@@ -15,8 +15,6 @@
 
   let(:job) { create(:ci_build, pipeline: pipeline) }
 
-  it { is_expected.to have_many(:sourced_pipelines) }
-
   describe '#shared_runners_minutes_limit_enabled?' do
     subject { job.shared_runners_minutes_limit_enabled? }
 
@@ -139,7 +137,7 @@
           expect(security_reports.get_report('sast').occurrences.size).to eq(33)
           expect(security_reports.get_report('dependency_scanning').occurrences.size).to eq(4)
           expect(security_reports.get_report('container_scanning').occurrences.size).to eq(8)
-          expect(security_reports.get_report('dast').occurrences.size).to eq(2)
+          expect(security_reports.get_report('dast').occurrences.size).to eq(20)
         end
       end
 
@@ -269,7 +267,7 @@
   describe '#collect_licenses_for_dependency_list!' do
     let!(:lm_artifact) { create(:ee_ci_job_artifact, :license_management, job: job, project: job.project) }
     let(:dependency_list_report) { Gitlab::Ci::Reports::DependencyList::Report.new }
-    let(:dependency) { build(:dependency) }
+    let(:dependency) { build(:dependency, :nokogiri) }
 
     subject { job.collect_licenses_for_dependency_list!(dependency_list_report) }
 
@@ -336,6 +334,7 @@
 
   describe '#retryable?' do
     subject { build.retryable? }
+
     let(:pipeline) { merge_request.all_pipelines.last }
     let!(:build) { create(:ci_build, :canceled, pipeline: pipeline) }
 
diff --git a/ee/spec/models/ci/pipeline_spec.rb b/ee/spec/models/ci/pipeline_spec.rb
index 4f421f659edd060fe608f091117bc2b41560de91..c2bba2619118967a396f5d55e7ec6076a883461e 100644
--- a/ee/spec/models/ci/pipeline_spec.rb
+++ b/ee/spec/models/ci/pipeline_spec.rb
@@ -10,10 +10,6 @@
     create(:ci_empty_pipeline, status: :created, project: project)
   end
 
-  it { is_expected.to have_one(:source_pipeline) }
-  it { is_expected.to have_many(:sourced_pipelines) }
-  it { is_expected.to have_one(:triggered_by_pipeline) }
-  it { is_expected.to have_many(:triggered_pipelines) }
   it { is_expected.to have_many(:downstream_bridges) }
   it { is_expected.to have_many(:job_artifacts).through(:builds) }
   it { is_expected.to have_many(:vulnerability_findings).through(:vulnerabilities_occurrence_pipelines).class_name('Vulnerabilities::Occurrence') }
@@ -530,6 +526,7 @@
 
   describe '#retryable?' do
     subject { pipeline.retryable? }
+
     let(:pipeline) { merge_request.all_pipelines.last }
     let!(:build) { create(:ci_build, :canceled, pipeline: pipeline) }
 
diff --git a/ee/spec/models/ci/sources/pipeline_spec.rb b/ee/spec/models/ci/sources/pipeline_spec.rb
index 63bee5bfb55890306f716a352ab71172771661ba..8e82adc1ee822659b12977b91d81f6caf41262b2 100644
--- a/ee/spec/models/ci/sources/pipeline_spec.rb
+++ b/ee/spec/models/ci/sources/pipeline_spec.rb
@@ -3,17 +3,5 @@
 require 'spec_helper'
 
 describe Ci::Sources::Pipeline do
-  it { is_expected.to belong_to(:project) }
-  it { is_expected.to belong_to(:pipeline) }
-
-  it { is_expected.to belong_to(:source_project) }
-  it { is_expected.to belong_to(:source_job) }
-  it { is_expected.to belong_to(:source_pipeline) }
-
-  it { is_expected.to validate_presence_of(:project) }
-  it { is_expected.to validate_presence_of(:pipeline) }
-
-  it { is_expected.to validate_presence_of(:source_project) }
-  it { is_expected.to validate_presence_of(:source_job) }
-  it { is_expected.to validate_presence_of(:source_pipeline) }
+  it { is_expected.to belong_to(:source_bridge) }
 end
diff --git a/ee/spec/models/concerns/ee/issuable_spec.rb b/ee/spec/models/concerns/ee/issuable_spec.rb
index 9df063da35d09b0eaf788a4e612ce353ec6aef6f..b6f658c09b6461c85d2e19b28a87c2c87c4da6a6 100644
--- a/ee/spec/models/concerns/ee/issuable_spec.rb
+++ b/ee/spec/models/concerns/ee/issuable_spec.rb
@@ -14,8 +14,8 @@
       it { is_expected.to validate_presence_of(:iid) }
       it { is_expected.to validate_presence_of(:author) }
       it { is_expected.to validate_presence_of(:title) }
-      it { is_expected.to validate_length_of(:title).is_at_most(255) }
-      it { is_expected.to validate_length_of(:description).is_at_most(16_000).on(:create) }
+      it { is_expected.to validate_length_of(:title).is_at_most(::Issuable::TITLE_LENGTH_MAX) }
+      it { is_expected.to validate_length_of(:description).is_at_most(::Issuable::DESCRIPTION_LENGTH_MAX).on(:create) }
 
       it_behaves_like 'validates description length with custom validation'
       it_behaves_like 'truncates the description to its allowed maximum length on import'
diff --git a/ee/spec/models/concerns/elastic/issue_spec.rb b/ee/spec/models/concerns/elastic/issue_spec.rb
index 30b185e3e17bb57f39a0b93ba76b6c03fb165209..151aeae90fc6eb1fb50987689522aa62b99e2969 100644
--- a/ee/spec/models/concerns/elastic/issue_spec.rb
+++ b/ee/spec/models/concerns/elastic/issue_spec.rb
@@ -101,16 +101,24 @@
     assignee = create(:user)
     issue = create :issue, project: project, assignees: [assignee]
 
-    expected_hash = issue.attributes.extract!('id', 'iid', 'title', 'description', 'created_at',
-                                                'updated_at', 'state', 'project_id', 'author_id',
-                                                'confidential')
-                                    .merge({
-                                            'join_field' => {
-                                              'name' => issue.es_type,
-                                              'parent' => issue.es_parent
-                                            },
-                                            'type' => issue.es_type
-                                           })
+    expected_hash = issue.attributes.extract!(
+      'id',
+      'iid',
+      'title',
+      'description',
+      'created_at',
+      'updated_at',
+      'project_id',
+      'author_id',
+      'confidential'
+    ).merge({
+      'type' => issue.es_type,
+      'state' => issue.state,
+      'join_field' => {
+        'name' => issue.es_type,
+        'parent' => issue.es_parent
+      }
+    })
 
     expected_hash['assignee_id'] = [assignee.id]
 
diff --git a/ee/spec/models/concerns/elastic/merge_request_spec.rb b/ee/spec/models/concerns/elastic/merge_request_spec.rb
index 1578cea3e0253158ddca69745328bc33ce9ff4ea..37132c612a88158055cb9b86aa4d502f131c5116 100644
--- a/ee/spec/models/concerns/elastic/merge_request_spec.rb
+++ b/ee/spec/models/concerns/elastic/merge_request_spec.rb
@@ -79,12 +79,12 @@
       'target_project_id',
       'author_id'
     ).merge({
-              'join_field' => {
-                'name' => merge_request.es_type,
-                'parent' => merge_request.es_parent
-              },
-              'type' => merge_request.es_type
-            })
+      'type' => merge_request.es_type,
+      'join_field' => {
+        'name' => merge_request.es_type,
+        'parent' => merge_request.es_parent
+      }
+    })
 
     expect(merge_request.__elasticsearch__.as_indexed_json).to eq(expected_hash)
   end
diff --git a/ee/spec/models/concerns/elastic/milestone_spec.rb b/ee/spec/models/concerns/elastic/milestone_spec.rb
index 48496a54a56e2c1c6a9f68dca1f7b282b62d8cb7..bfa01115a4d6d032f3873c0b19cba25052aac88e 100644
--- a/ee/spec/models/concerns/elastic/milestone_spec.rb
+++ b/ee/spec/models/concerns/elastic/milestone_spec.rb
@@ -48,11 +48,11 @@
       'created_at',
       'updated_at'
     ).merge({
+      'type' => milestone.es_type,
       'join_field' => {
         'name' => milestone.es_type,
         'parent' => milestone.es_parent
-      },
-      'type' => milestone.es_type
+      }
     })
 
     expect(milestone.__elasticsearch__.as_indexed_json).to eq(expected_hash)
diff --git a/ee/spec/models/concerns/elastic/note_spec.rb b/ee/spec/models/concerns/elastic/note_spec.rb
index d775556ead49495fe0a5a4ca06410b6630101525..b4bdb350cb65512678100ccb876284e253747ebb 100644
--- a/ee/spec/models/concerns/elastic/note_spec.rb
+++ b/ee/spec/models/concerns/elastic/note_spec.rb
@@ -75,22 +75,32 @@
   end
 
   it "returns json with all needed elements" do
-    note = create :note
-
-    expected_hash_keys = %w(
-      id
-      note
-      project_id
-      noteable_type
-      noteable_id
-      created_at
-      updated_at
-      issue
-      join_field
-      type
-    )
-
-    expect(note.__elasticsearch__.as_indexed_json.keys).to eq(expected_hash_keys)
+    assignee = create(:user)
+    issue = create(:issue, assignees: [assignee])
+    note = create(:note, noteable: issue, project: issue.project)
+
+    expected_hash = note.attributes.extract!(
+      'id',
+      'note',
+      'project_id',
+      'noteable_type',
+      'noteable_id',
+      'created_at',
+      'updated_at'
+    ).merge({
+      'issue' => {
+        'assignee_id' => issue.assignee_ids,
+        'author_id' => issue.author_id,
+        'confidential' => issue.confidential
+      },
+      'type' => note.es_type,
+      'join_field' => {
+        'name' => note.es_type,
+        'parent' => note.es_parent
+      }
+    })
+
+    expect(note.__elasticsearch__.as_indexed_json).to eq(expected_hash)
   end
 
   it "does not create ElasticIndexerWorker job for system messages" do
diff --git a/ee/spec/models/concerns/elastic/snippet_spec.rb b/ee/spec/models/concerns/elastic/snippet_spec.rb
index c532c9b62032caa28aa3616fbfb5ebceebf63f0f..165f8ef9c297c51564dd2be9696311cfa7c50032 100644
--- a/ee/spec/models/concerns/elastic/snippet_spec.rb
+++ b/ee/spec/models/concerns/elastic/snippet_spec.rb
@@ -34,7 +34,7 @@
     end
 
     it 'returns only public snippets when user is blank' do
-      result = described_class.elastic_search_code('password', options: { user: nil })
+      result = described_class.elastic_search_code('password', options: { current_user: nil })
 
       expect(result.total_count).to eq(1)
       expect(result.records).to match_array [public_snippet]
@@ -43,7 +43,7 @@
     it 'returns only public and internal personal snippets for non-members' do
       non_member = create(:user)
 
-      result = described_class.elastic_search_code('password', options: { user: non_member })
+      result = described_class.elastic_search_code('password', options: { current_user: non_member })
 
       expect(result.total_count).to eq(2)
       expect(result.records).to match_array [public_snippet, internal_snippet]
@@ -53,14 +53,14 @@
       member = create(:user)
       project.add_developer(member)
 
-      result = described_class.elastic_search_code('password', options: { user: member })
+      result = described_class.elastic_search_code('password', options: { current_user: member })
 
       expect(result.total_count).to eq(5)
       expect(result.records).to match_array [public_snippet, internal_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
     end
 
     it 'returns private snippets where the user is the author' do
-      result = described_class.elastic_search_code('password', options: { user: author })
+      result = described_class.elastic_search_code('password', options: { current_user: author })
 
       expect(result.total_count).to eq(3)
       expect(result.records).to match_array [public_snippet, internal_snippet, private_snippet]
@@ -70,7 +70,7 @@
       member = create(:user)
       project.add_reporter(member)
 
-      result = described_class.elastic_search_code('password +(123 | 789)', options: { user: member })
+      result = described_class.elastic_search_code('password +(123 | 789)', options: { current_user: member })
 
       expect(result.total_count).to eq(2)
       expect(result.records).to match_array [project_public_snippet, project_private_snippet]
@@ -80,7 +80,7 @@
       it "returns all snippets for #{user_type}" do
         superuser = create(user_type)
 
-        result = described_class.elastic_search_code('password', options: { user: superuser })
+        result = described_class.elastic_search_code('password', options: { current_user: superuser })
 
         expect(result.total_count).to eq(6)
         expect(result.records).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
@@ -97,7 +97,7 @@
         project.add_developer(member)
         expect(Ability).to receive(:allowed?).with(member, :read_cross_project) { false }
 
-        result = described_class.elastic_search_code('password', options: { user: member })
+        result = described_class.elastic_search_code('password', options: { current_user: member })
 
         expect(result.records).to match_array [public_snippet, internal_snippet]
       end
@@ -116,7 +116,7 @@
       Gitlab::Elastic::Helper.refresh_index
     end
 
-    options = { user: user }
+    options = { current_user: user }
 
     expect(described_class.elastic_search('home', options: options).total_count).to eq(1)
     expect(described_class.elastic_search('index.php', options: options).total_count).to eq(1)
@@ -136,7 +136,13 @@
       'project_id',
       'author_id',
       'visibility_level'
-    ).merge({ 'type' => snippet.es_type })
+    ).merge({
+      'type' => snippet.es_type,
+      'join_field' => {
+        'name' => snippet.es_type,
+        'parent' => snippet.es_parent
+      }
+    })
 
     expect(snippet.__elasticsearch__.as_indexed_json).to eq(expected_hash)
   end
diff --git a/ee/spec/models/ee/clusters/platforms/kubernetes_spec.rb b/ee/spec/models/ee/clusters/platforms/kubernetes_spec.rb
index 4bc7d56a54aef96f230930b49eafea13b2d25b24..9f9cea7e9ba3b7db2c0c2fbc47caf07771383816 100644
--- a/ee/spec/models/ee/clusters/platforms/kubernetes_spec.rb
+++ b/ee/spec/models/ee/clusters/platforms/kubernetes_spec.rb
@@ -150,7 +150,11 @@
       end
     end
 
-    shared_examples 'k8s responses' do
+    context 'with reactive cache' do
+      before do
+        synchronous_reactive_cache(service)
+      end
+
       context 'when kubernetes responds with valid logs' do
         before do
           stub_kubeclient_logs(pod_name, namespace, container: container)
@@ -201,34 +205,6 @@
 
         it_behaves_like 'resource not found error', 'Pod not found'
       end
-    end
-
-    context 'without pod_logs_reactive_cache feature flag' do
-      before do
-        stub_feature_flags(pod_logs_reactive_cache: false)
-      end
-
-      it_behaves_like 'k8s responses'
-
-      context 'when container name is not specified' do
-        subject { service.read_pod_logs(pod_name, namespace) }
-
-        before do
-          stub_kubeclient_logs(pod_name, namespace, container: nil)
-        end
-
-        include_examples 'successful log request'
-      end
-    end
-
-    context 'with pod_logs_reactive_cache feature flag' do
-      before do
-        stub_feature_flags(pod_logs_reactive_cache: true)
-
-        synchronous_reactive_cache(service)
-      end
-
-      it_behaves_like 'k8s responses'
 
       context 'when container name is not specified' do
         subject { service.read_pod_logs(pod_name, namespace) }
@@ -247,10 +223,6 @@
         ['get_pod_log', { 'pod_name' => pod_name, 'namespace' => namespace, 'container' => container }]
       end
 
-      before do
-        stub_feature_flags(pod_logs_reactive_cache: true)
-      end
-
       context 'result is cacheable' do
         before do
           stub_kubeclient_logs(pod_name, namespace, container: container)
diff --git a/ee/spec/models/ee/description_version_spec.rb b/ee/spec/models/ee/description_version_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f0c636f18506cc96e4a687f8b492079dc62effcf
--- /dev/null
+++ b/ee/spec/models/ee/description_version_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DescriptionVersion do
+  describe 'associations' do
+    it { is_expected.to belong_to :epic }
+  end
+
+  describe 'validations' do
+    it 'is valid when epic_id is set' do
+      expect(described_class.new(epic_id: 1)).to be_valid
+    end
+  end
+end
diff --git a/ee/spec/models/ee/note_spec.rb b/ee/spec/models/ee/note_spec.rb
index 9c9e52f941909016a7201b3323a6ec194b07045a..fc854e679d76d000998fc3bdb9093c4549c861b8 100644
--- a/ee/spec/models/ee/note_spec.rb
+++ b/ee/spec/models/ee/note_spec.rb
@@ -76,12 +76,12 @@
     end
   end
 
-  describe '#parent' do
+  describe '#resource_parent' do
     it 'returns group for epic notes' do
       group = create(:group)
       note = create(:note_on_epic, noteable: create(:epic, group: group))
 
-      expect(note.parent).to eq(group)
+      expect(note.resource_parent).to eq(group)
     end
   end
 
diff --git a/ee/spec/models/ee/notification_setting_spec.rb b/ee/spec/models/ee/notification_setting_spec.rb
index e7c0e394efb84d3ef75aff0409aa50a1d59abf32..b8988b7bdeff90b256e438062b18156146efbc71 100644
--- a/ee/spec/models/ee/notification_setting_spec.rb
+++ b/ee/spec/models/ee/notification_setting_spec.rb
@@ -12,6 +12,7 @@
       it 'appends EE specific events' do
         expect(subject).to eq(
           [
+            :new_release,
             :new_note,
             :new_issue,
             :reopen_issue,
@@ -38,6 +39,7 @@
       it 'returns CE list' do
         expect(subject).to eq(
           [
+            :new_release,
             :new_note,
             :new_issue,
             :reopen_issue,
@@ -63,6 +65,7 @@
       it 'appends EE specific events' do
         expect(subject).to eq(
           [
+            :new_release,
             :new_note,
             :new_issue,
             :reopen_issue,
diff --git a/ee/spec/models/ee/resource_label_event_spec.rb b/ee/spec/models/ee/resource_label_event_spec.rb
index c3a3aeee56dca96987c18978f008429bf2e071e8..68bc5b2a79506dc3b4ef9709e4456074ccbbcafb 100644
--- a/ee/spec/models/ee/resource_label_event_spec.rb
+++ b/ee/spec/models/ee/resource_label_event_spec.rb
@@ -4,6 +4,7 @@
 
 RSpec.describe ResourceLabelEvent, type: :model do
   subject { build(:resource_label_event) }
+
   let(:epic) { create(:epic) }
 
   describe 'validations' do
diff --git a/ee/spec/models/epic_spec.rb b/ee/spec/models/epic_spec.rb
index 70381670219ff7a611c3f7c6a48dcc62d5a80196..e353da2f5e698256d7f9a8c3e441b3ba776bfe52 100644
--- a/ee/spec/models/epic_spec.rb
+++ b/ee/spec/models/epic_spec.rb
@@ -794,4 +794,6 @@ def setup_control_group
       let(:default_params) { {} }
     end
   end
+
+  it_behaves_like 'versioned description'
 end
diff --git a/ee/spec/models/issue_spec.rb b/ee/spec/models/issue_spec.rb
index 68a721945a529b208814775d2c40f2e96c0c0870..1d36587b36ee23c938ec32a56b5f25efe15ff70d 100644
--- a/ee/spec/models/issue_spec.rb
+++ b/ee/spec/models/issue_spec.rb
@@ -46,6 +46,7 @@
     it { is_expected.to have_many(:designs) }
     it { is_expected.to have_many(:design_versions) }
     it { is_expected.to have_and_belong_to_many(:prometheus_alert_events) }
+    it { is_expected.to have_and_belong_to_many(:self_managed_prometheus_alert_events) }
     it { is_expected.to have_many(:prometheus_alerts) }
 
     describe 'versions.most_recent' do
diff --git a/ee/spec/models/merge_request/blocking_spec.rb b/ee/spec/models/merge_request/blocking_spec.rb
index b01d1290ddb7a9ac98e6983aec1fdd25d393abb9..7527f2e0cb012937626c45e082a932174cd11bd9 100644
--- a/ee/spec/models/merge_request/blocking_spec.rb
+++ b/ee/spec/models/merge_request/blocking_spec.rb
@@ -131,7 +131,7 @@
 
       context 'MR is merged' do
         before do
-          blocking_mr.update_columns(state: 'merged')
+          blocking_mr.update_columns(state_id: described_class.available_states[:merged])
         end
 
         it 'returns 0 by default' do
diff --git a/ee/spec/models/merge_request_spec.rb b/ee/spec/models/merge_request_spec.rb
index 13b89be5148e67dbd97c31e33b7d93c31cd0cfb6..ec8429d9e99a35c2ce4a8bd278e3efc6f8e71e82 100644
--- a/ee/spec/models/merge_request_spec.rb
+++ b/ee/spec/models/merge_request_spec.rb
@@ -162,6 +162,7 @@
 
   describe '#has_license_management_reports?' do
     subject { merge_request.has_license_management_reports? }
+
     let(:project) { create(:project, :repository) }
 
     before do
@@ -183,6 +184,7 @@
 
   describe '#has_dependency_scanning_reports?' do
     subject { merge_request.has_dependency_scanning_reports? }
+
     let(:project) { create(:project, :repository) }
 
     before do
@@ -204,6 +206,7 @@
 
   describe '#has_container_scanning_reports?' do
     subject { merge_request.has_container_scanning_reports? }
+
     let(:project) { create(:project, :repository) }
 
     before do
@@ -225,6 +228,7 @@
 
   describe '#has_sast_reports?' do
     subject { merge_request.has_sast_reports? }
+
     let(:project) { create(:project, :repository) }
 
     before do
@@ -246,6 +250,7 @@
 
   describe '#has_metrics_reports?' do
     subject { merge_request.has_metrics_reports? }
+
     let(:project) { create(:project, :repository) }
 
     before do
diff --git a/ee/spec/models/productivity_analytics_spec.rb b/ee/spec/models/productivity_analytics_spec.rb
index 5bf518a778a1e61a78d15135c033a5c2a58cb38c..e0d0e2664066dbefac00d2bedc04cf11150ce961 100644
--- a/ee/spec/models/productivity_analytics_spec.rb
+++ b/ee/spec/models/productivity_analytics_spec.rb
@@ -4,6 +4,7 @@
 
 describe ProductivityAnalytics do
   subject(:analytics) { described_class.new(merge_requests: MergeRequest.all, sort: custom_sort) }
+
   let(:custom_sort) { nil }
 
   let(:long_mr) do
diff --git a/ee/spec/models/project_ci_cd_setting_spec.rb b/ee/spec/models/project_ci_cd_setting_spec.rb
index 3a45f45a2186bc8a5cb7d56ffe536e19beec30c0..de717921194a05fee9f558b676d7d34a5351b7a9 100644
--- a/ee/spec/models/project_ci_cd_setting_spec.rb
+++ b/ee/spec/models/project_ci_cd_setting_spec.rb
@@ -72,19 +72,42 @@
 
     before do
       stub_licensed_features(merge_pipelines: true, merge_trains: true)
-      project.update(merge_pipelines_enabled: true)
     end
 
-    context 'when merge pipelines option is disabled' do
+    context 'when merge pipelines option was enabled' do
       before do
-        project.update(merge_pipelines_enabled: false)
+        project.update(merge_pipelines_enabled: true)
+      end
+
+      context 'when merge pipelines option is disabled' do
+        before do
+          project.update(merge_pipelines_enabled: false)
+        end
+
+        it { is_expected.to be true }
       end
 
-      it { is_expected.to be true }
+      context 'when merge pipelines option is intact' do
+        it { is_expected.to be false }
+      end
     end
 
-    context 'when merge pipelines option is intact' do
-      it { is_expected.to be false }
+    context 'when merge pipelines option was disabled' do
+      before do
+        project.update(merge_pipelines_enabled: false)
+      end
+
+      context 'when merge pipelines option is disabled' do
+        before do
+          project.update(merge_pipelines_enabled: true)
+        end
+
+        it { is_expected.to be false }
+      end
+
+      context 'when merge pipelines option is intact' do
+        it { is_expected.to be false }
+      end
     end
   end
 end
diff --git a/ee/spec/models/project_spec.rb b/ee/spec/models/project_spec.rb
index d66f258b94090d1ebcb6c8e3b478129992b7addb..afa423f1cf0a504f9fb2e568c0517086e5fbdd67 100644
--- a/ee/spec/models/project_spec.rb
+++ b/ee/spec/models/project_spec.rb
@@ -29,8 +29,6 @@
     it { is_expected.to have_many(:reviews).inverse_of(:project) }
     it { is_expected.to have_many(:path_locks) }
     it { is_expected.to have_many(:vulnerability_feedback) }
-    it { is_expected.to have_many(:sourced_pipelines) }
-    it { is_expected.to have_many(:source_pipelines) }
     it { is_expected.to have_many(:audit_events).dependent(false) }
     it { is_expected.to have_many(:protected_environments) }
     it { is_expected.to have_many(:approvers).dependent(:destroy) }
@@ -1137,20 +1135,6 @@
         it { is_expected.to include(*disabled_services) }
       end
     end
-
-    context 'when incident_management is available' do
-      before do
-        stub_licensed_features(incident_management: true)
-      end
-
-      context 'when feature flag generic_alert_endpoint is disabled' do
-        before do
-          stub_feature_flags(generic_alert_endpoint: false)
-        end
-
-        it { is_expected.to include('alerts') }
-      end
-    end
   end
 
   describe '#pull_mirror_available?' do
@@ -2131,6 +2115,7 @@ def stub_find_remote_root_ref(project, ref:)
   describe '#allowed_to_share_with_group?' do
     context 'for group related project' do
       subject(:project) { build_stubbed(:project, namespace: group, group: group) }
+
       let(:group) { build_stubbed :group }
 
       context 'with lock_memberships_to_ldap application setting enabled' do
@@ -2144,6 +2129,7 @@ def stub_find_remote_root_ref(project, ref:)
 
     context 'personal project' do
       subject(:project) { build_stubbed(:project, namespace: namespace) }
+
       let(:namespace) { build_stubbed :namespace }
 
       context 'with lock_memberships_to_ldap application setting enabled' do
diff --git a/ee/spec/models/prometheus_alert_event_spec.rb b/ee/spec/models/prometheus_alert_event_spec.rb
index d867da41a27e2aba51ca8a50e60ceddfd4e02eed..040113643ddb9f9ce376888dc31e0bcfea7ca43b 100644
--- a/ee/spec/models/prometheus_alert_event_spec.rb
+++ b/ee/spec/models/prometheus_alert_event_spec.rb
@@ -4,6 +4,7 @@
 
 describe PrometheusAlertEvent do
   subject { build(:prometheus_alert_event) }
+
   let(:alert) { subject.prometheus_alert }
 
   describe 'associations' do
@@ -85,7 +86,6 @@
           expect(result).to eq(true)
           expect(subject).to be_resolved
           expect(subject.ended_at).to be_like_time(ended_at)
-          expect(subject.payload_key).to be_nil
         end
       end
 
diff --git a/ee/spec/models/software_license_policy_spec.rb b/ee/spec/models/software_license_policy_spec.rb
index 436d43fc5a2daa496c86c75d63a5675d645a8af5..0b7dd5d4d1450abf75ef5253186b84174acace6a 100644
--- a/ee/spec/models/software_license_policy_spec.rb
+++ b/ee/spec/models/software_license_policy_spec.rb
@@ -15,6 +15,7 @@
 
   describe ".with_license_by_name" do
     subject { described_class }
+
     let!(:mit_policy) { create(:software_license_policy, software_license: mit) }
     let!(:mit) { create(:software_license, :mit) }
     let!(:apache_policy) { create(:software_license_policy, software_license: apache) }
diff --git a/ee/spec/models/software_license_spec.rb b/ee/spec/models/software_license_spec.rb
index c1729eb320a51a5559ecb821c48b5ae6d06d5e5b..d4165c1d75c9c77198c375b4a63c8f8b1b4ab28a 100644
--- a/ee/spec/models/software_license_spec.rb
+++ b/ee/spec/models/software_license_spec.rb
@@ -13,6 +13,7 @@
 
   describe '.create_policy_for!' do
     subject { described_class }
+
     let(:project) { create(:project) }
 
     context 'when a software license with a given name has already been created' do
diff --git a/ee/spec/models/user_spec.rb b/ee/spec/models/user_spec.rb
index d6c83a6f91e2e81b505ac32b905cb41f68527eb3..cf552a376735babd93ff19786ee53474ab43cb80 100644
--- a/ee/spec/models/user_spec.rb
+++ b/ee/spec/models/user_spec.rb
@@ -571,8 +571,9 @@
     let!(:ghost) { described_class.ghost }
     let!(:support_bot) { described_class.support_bot }
     let!(:alert_bot) { described_class.alert_bot }
+    let!(:visual_review_bot) { described_class.visual_review_bot }
     let!(:non_internal) { [user] }
-    let!(:internal) { [ghost, support_bot, alert_bot] }
+    let!(:internal) { [ghost, support_bot, alert_bot, visual_review_bot] }
 
     it 'returns non internal users' do
       expect(described_class.internal).to eq(internal)
@@ -591,6 +592,7 @@
 
         expect(support_bot.bot?).to eq(true)
         expect(alert_bot.bot?).to eq(true)
+        expect(visual_review_bot.bot?).to eq(true)
       end
     end
   end
diff --git a/ee/spec/models/vulnerabilities/occurrence_spec.rb b/ee/spec/models/vulnerabilities/occurrence_spec.rb
index f4e0b6867c71859053adfb3faeb37c76248d36ea..513546e12fabeecbd574d78da8622b10404d4333 100644
--- a/ee/spec/models/vulnerabilities/occurrence_spec.rb
+++ b/ee/spec/models/vulnerabilities/occurrence_spec.rb
@@ -11,6 +11,7 @@
     it { is_expected.to belong_to(:project) }
     it { is_expected.to belong_to(:primary_identifier).class_name('Vulnerabilities::Identifier') }
     it { is_expected.to belong_to(:scanner).class_name('Vulnerabilities::Scanner') }
+    it { is_expected.to belong_to(:vulnerability).inverse_of(:findings) }
     it { is_expected.to have_many(:pipelines).class_name('Ci::Pipeline') }
     it { is_expected.to have_many(:occurrence_pipelines).class_name('Vulnerabilities::OccurrencePipeline') }
     it { is_expected.to have_many(:identifiers).class_name('Vulnerabilities::Identifier') }
@@ -60,6 +61,16 @@
     end
   end
 
+  context 'order' do
+    let!(:occurrence1) { create(:vulnerabilities_occurrence, confidence: described_class::CONFIDENCE_LEVELS[:high], severity:   described_class::SEVERITY_LEVELS[:high]) }
+    let!(:occurrence2) { create(:vulnerabilities_occurrence, confidence: described_class::CONFIDENCE_LEVELS[:medium], severity: described_class::SEVERITY_LEVELS[:critical]) }
+    let!(:occurrence3) { create(:vulnerabilities_occurrence, confidence: described_class::CONFIDENCE_LEVELS[:high], severity:   described_class::SEVERITY_LEVELS[:critical]) }
+
+    it 'orders by severity and confidence' do
+      expect(described_class.all.ordered).to eq([occurrence3, occurrence2, occurrence1])
+    end
+  end
+
   describe '.report_type' do
     let(:report_type) { :sast }
 
diff --git a/ee/spec/models/vulnerability_spec.rb b/ee/spec/models/vulnerability_spec.rb
index 49a2c0783c68cbfb8358eb967ce62607dc006ff4..7b9ebd708930bc49d916b70a2df4c17834006d5f 100644
--- a/ee/spec/models/vulnerability_spec.rb
+++ b/ee/spec/models/vulnerability_spec.rb
@@ -33,7 +33,7 @@
     it { is_expected.to belong_to(:project) }
     it { is_expected.to belong_to(:milestone) }
     it { is_expected.to belong_to(:epic) }
-
+    it { is_expected.to have_many(:findings).class_name('Vulnerabilities::Occurrence').inverse_of(:vulnerability) }
     it { is_expected.to belong_to(:author).class_name('User') }
     it { is_expected.to belong_to(:updated_by).class_name('User') }
     it { is_expected.to belong_to(:last_edited_by).class_name('User') }
@@ -50,9 +50,9 @@
     it { is_expected.to validate_presence_of(:severity) }
     it { is_expected.to validate_presence_of(:confidence) }
 
-    it { is_expected.to validate_length_of(:title).is_at_most(255) }
-    it { is_expected.to validate_length_of(:title_html).is_at_most(800) }
-    it { is_expected.to validate_length_of(:description).is_at_most(16_000).allow_nil }
-    it { is_expected.to validate_length_of(:description_html).is_at_most(48_000).allow_nil }
+    it { is_expected.to validate_length_of(:title).is_at_most(::Issuable::TITLE_LENGTH_MAX) }
+    it { is_expected.to validate_length_of(:title_html).is_at_most(::Issuable::TITLE_HTML_LENGTH_MAX) }
+    it { is_expected.to validate_length_of(:description).is_at_most(::Issuable::DESCRIPTION_LENGTH_MAX).allow_nil }
+    it { is_expected.to validate_length_of(:description_html).is_at_most(::Issuable::DESCRIPTION_HTML_LENGTH_MAX).allow_nil }
   end
 end
diff --git a/ee/spec/policies/design_management/design_policy_spec.rb b/ee/spec/policies/design_management/design_policy_spec.rb
index 1bd52f2974cdde763d22a8eca17bd3f1503208a2..7d589c5e90aefd797f6032bf6db16512d37ddc54 100644
--- a/ee/spec/policies/design_management/design_policy_spec.rb
+++ b/ee/spec/policies/design_management/design_policy_spec.rb
@@ -94,6 +94,11 @@
     end
   end
 
+  shared_examples_for "read-only design abilities" do
+    it { is_expected.to be_allowed(:read_design) }
+    it { is_expected.to be_disallowed(:create_design, :destroy_design) }
+  end
+
   context "when the feature flag is off" do
     before do
       stub_licensed_features(design_management: true)
@@ -164,6 +169,20 @@
       end
     end
 
+    context "when the issue is locked" do
+      let(:current_user) { owner }
+      let(:issue) { create(:issue, :locked, project: project) }
+
+      it_behaves_like "read-only design abilities"
+    end
+
+    context "when the issue has moved" do
+      let(:current_user) { owner }
+      let(:issue) { create(:issue, project: project, moved_to: create(:issue)) }
+
+      it_behaves_like "read-only design abilities"
+    end
+
     context "when the project is archived" do
       let(:current_user) { owner }
 
@@ -171,10 +190,7 @@
         project.update!(archived: true)
       end
 
-      it "only allows reading designs" do
-        expect(design_policy).to be_allowed(:read_design)
-        expect(design_policy).to be_disallowed(:create_design, :destroy_design)
-      end
+      it_behaves_like "read-only design abilities"
     end
   end
 end
diff --git a/ee/spec/policies/global_policy_spec.rb b/ee/spec/policies/global_policy_spec.rb
index 09bf765cf42b40b5ae07fa3981bc9da5e611b11e..7a5572badc9038f4e6be120a05583f7b2e12059b 100644
--- a/ee/spec/policies/global_policy_spec.rb
+++ b/ee/spec/policies/global_policy_spec.rb
@@ -11,11 +11,19 @@
   subject { described_class.new(current_user, [user]) }
 
   describe 'reading operations dashboard' do
-    before do
-      stub_licensed_features(operations_dashboard: true)
-    end
+    context 'when licensed' do
+      before do
+        stub_licensed_features(operations_dashboard: true)
+      end
 
-    it { is_expected.to be_allowed(:read_operations_dashboard) }
+      it { is_expected.to be_allowed(:read_operations_dashboard) }
+
+      context 'and the user is not logged in' do
+        let(:current_user) { nil }
+
+        it { is_expected.not_to be_allowed(:read_operations_dashboard) }
+      end
+    end
 
     context 'when unlicensed' do
       before do
diff --git a/ee/spec/policies/project_policy_spec.rb b/ee/spec/policies/project_policy_spec.rb
index 548af049e003a2c695d7f969851f859047e60c56..f6ca81eb41594ac1784de3f6809513fd8e1023c2 100644
--- a/ee/spec/policies/project_policy_spec.rb
+++ b/ee/spec/policies/project_policy_spec.rb
@@ -30,7 +30,7 @@
       %i[read_issue_link read_software_license_policy]
     end
     let(:additional_reporter_permissions) { [:admin_issue_link] }
-    let(:additional_developer_permissions) { %i[admin_vulnerability_feedback read_project_security_dashboard read_feature_flag] }
+    let(:additional_developer_permissions) { %i[admin_vulnerability_feedback read_project_security_dashboard read_feature_flag dismiss_vulnerability] }
     let(:additional_maintainer_permissions) { %i[push_code_to_protected_branches admin_feature_flags_client] }
     let(:auditor_permissions) do
       %i[
@@ -43,6 +43,7 @@
         create_merge_request_in award_emoji
         read_project_security_dashboard
         read_vulnerability_feedback read_software_license_policy
+        dismiss_vulnerability
       ]
     end
 
@@ -466,16 +467,30 @@
     end
   end
 
+  shared_context 'when security dashboard feature is not available' do
+    before do
+      stub_licensed_features(security_dashboard: false)
+    end
+  end
+
   describe 'read_project_security_dashboard' do
     context 'with developer' do
       let(:current_user) { developer }
 
-      context 'when security dashboard features is not available' do
-        before do
-          stub_licensed_features(security_dashboard: false)
-        end
+      include_context 'when security dashboard feature is not available'
+
+      it { is_expected.to be_disallowed(:read_project_security_dashboard) }
+    end
+  end
+
+  describe 'vulnerability permissions' do
+    describe 'dismiss_vulnerability' do
+      context 'with developer' do
+        let(:current_user) { developer }
 
-        it { is_expected.to be_disallowed(:read_project_security_dashboard) }
+        include_context 'when security dashboard feature is not available'
+
+        it { is_expected.to be_disallowed(:dismiss_vulnerability) }
       end
     end
   end
@@ -965,6 +980,14 @@
     end
   end
 
+  context 'visual review bot' do
+    let(:current_user) { User.visual_review_bot }
+
+    it { expect_allowed(:create_note) }
+    it { expect_disallowed(:read_note) }
+    it { expect_disallowed(:resolve_note) }
+  end
+
   context 'commit_committer_check is not enabled by the current license' do
     before do
       stub_licensed_features(commit_committer_check: false)
diff --git a/ee/spec/requests/api/deployments_spec.rb b/ee/spec/requests/api/deployments_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d1928b410ba47aa57d9c8509012f7c9573e005d3
--- /dev/null
+++ b/ee/spec/requests/api/deployments_spec.rb
@@ -0,0 +1,184 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Deployments do
+  let(:user) { create(:user) }
+  let!(:project) { create(:project, :repository) }
+  let!(:environment) { create(:environment, project: project) }
+
+  before do
+    stub_licensed_features(protected_environments: true)
+  end
+
+  describe 'POST /projects/:id/deployments' do
+    context 'when deploying to a protected environment that requires maintainer access' do
+      before do
+        create(
+          :protected_environment,
+          :maintainers_can_deploy,
+          project: environment.project,
+          name: environment.name
+        )
+      end
+
+      it 'returns a 403 when the user is a developer' do
+        project.add_developer(user)
+
+        post(
+          api("/projects/#{project.id}/deployments", user),
+          params: {
+            environment: environment.name,
+            sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0',
+            ref: 'master',
+            tag: false,
+            status: 'success'
+          }
+        )
+
+        expect(response).to have_gitlab_http_status(403)
+      end
+
+      it 'creates the deployment when the user is a maintainer' do
+        project.add_maintainer(user)
+
+        post(
+          api("/projects/#{project.id}/deployments", user),
+          params: {
+            environment: environment.name,
+            sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0',
+            ref: 'master',
+            tag: false,
+            status: 'success'
+          }
+        )
+
+        expect(response).to have_gitlab_http_status(201)
+      end
+    end
+
+    context 'when deploying to a protected environment that requires developer access' do
+      before do
+        create(
+          :protected_environment,
+          :developers_can_deploy,
+          project: environment.project,
+          name: environment.name
+        )
+      end
+
+      it 'returns a 403 when the user is a guest' do
+        project.add_guest(user)
+
+        post(
+          api("/projects/#{project.id}/deployments", user),
+          params: {
+            environment: environment.name,
+            sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0',
+            ref: 'master',
+            tag: false,
+            status: 'success'
+          }
+        )
+
+        expect(response).to have_gitlab_http_status(403)
+      end
+
+      it 'creates the deployment when the user is a developer' do
+        project.add_developer(user)
+
+        post(
+          api("/projects/#{project.id}/deployments", user),
+          params: {
+            environment: environment.name,
+            sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0',
+            ref: 'master',
+            tag: false,
+            status: 'success'
+          }
+        )
+
+        expect(response).to have_gitlab_http_status(201)
+      end
+    end
+  end
+
+  describe 'PUT /projects/:id/deployments/:deployment_id' do
+    let(:project) { create(:project) }
+    let(:deploy) do
+      create(
+        :deployment,
+        :running,
+        project: project,
+        deployable: nil,
+        environment: environment
+      )
+    end
+
+    context 'when updating a deployment for a protected environment that requires maintainer access' do
+      before do
+        create(
+          :protected_environment,
+          :maintainers_can_deploy,
+          project: environment.project,
+          name: environment.name
+        )
+      end
+
+      it 'returns a 403 when the user is a developer' do
+        project.add_developer(user)
+
+        put(
+          api("/projects/#{project.id}/deployments/#{deploy.id}", user),
+          params: { status: 'success' }
+        )
+
+        expect(response).to have_gitlab_http_status(403)
+      end
+
+      it 'updates the deployment when the user is a maintainer' do
+        project.add_maintainer(user)
+
+        put(
+          api("/projects/#{project.id}/deployments/#{deploy.id}", user),
+          params: { status: 'success' }
+        )
+
+        expect(response).to have_gitlab_http_status(200)
+      end
+    end
+
+    context 'when updating a deployment for a protected environment that requires developer access' do
+      before do
+        create(
+          :protected_environment,
+          :developers_can_deploy,
+          project: environment.project,
+          name: environment.name
+        )
+      end
+
+      it 'returns a 403 when the user is a guest' do
+        project.add_guest(user)
+
+        put(
+          api("/projects/#{project.id}/deployments/#{deploy.id}", user),
+          params: { status: 'success' }
+        )
+
+        expect(response).to have_gitlab_http_status(403)
+      end
+
+      it 'updates the deployment when the user is a developer' do
+        project.add_developer(user)
+
+        put(
+          api("/projects/#{project.id}/deployments/#{deploy.id}", user),
+          params: { status: 'success' }
+        )
+
+        expect(response).to have_gitlab_http_status(200)
+      end
+    end
+  end
+end
diff --git a/ee/spec/requests/api/epics_spec.rb b/ee/spec/requests/api/epics_spec.rb
index 559b73773e6bb1ec3cb677588937c4af191b5c51..fc5887c8c59c0b59410599327ae343410f329096 100644
--- a/ee/spec/requests/api/epics_spec.rb
+++ b/ee/spec/requests/api/epics_spec.rb
@@ -422,6 +422,12 @@
         expect(response).to match_response_schema('public_api/v4/epic', dir: 'ee')
       end
 
+      it 'exposes subscribed field' do
+        get api(url, epic.author)
+
+        expect(json_response['subscribed']).to eq(true)
+      end
+
       it 'exposes closed_at attribute' do
         epic.close
 
@@ -485,6 +491,7 @@
           expect(epic.description).to eq('epic description')
           expect(epic.start_date_fixed).to eq(nil)
           expect(epic.start_date_is_fixed).to be_falsey
+          expect(epic.due_date).to eq(Date.new(2018, 7, 17))
           expect(epic.due_date_fixed).to eq(Date.new(2018, 7, 17))
           expect(epic.due_date_is_fixed).to eq(true)
           expect(epic.labels.first.title).to eq('label1')
diff --git a/ee/spec/requests/api/feature_flags_spec.rb b/ee/spec/requests/api/feature_flags_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6f1cad6b2443d49d2bc543868f8fd77219b42193
--- /dev/null
+++ b/ee/spec/requests/api/feature_flags_spec.rb
@@ -0,0 +1,204 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe API::FeatureFlags do
+  include FeatureFlagHelpers
+
+  let(:project) { create(:project, :repository) }
+  let(:developer) { create(:user) }
+  let(:reporter) { create(:user) }
+  let(:user) { developer }
+  let(:non_project_member) { create(:user) }
+
+  before do
+    stub_licensed_features(feature_flags: true)
+
+    project.add_developer(developer)
+    project.add_reporter(reporter)
+  end
+
+  shared_examples_for 'check user permission' do
+    context 'when user is reporter' do
+      let(:user) { reporter }
+
+      it 'forbids the request' do
+        subject
+
+        expect(response).to have_gitlab_http_status(:forbidden)
+      end
+    end
+  end
+
+  shared_examples_for 'not found' do
+    it 'returns Not Found' do
+      subject
+
+      expect(response).to have_gitlab_http_status(:not_found)
+    end
+  end
+
+  describe 'GET /projects/:id/feature_flags' do
+    subject { get api("/projects/#{project.id}/feature_flags", user) }
+
+    context 'when there are two feature flags' do
+      let!(:feature_flag_1) do
+        create(:operations_feature_flag, project: project)
+      end
+
+      let!(:feature_flag_2) do
+        create(:operations_feature_flag, project: project)
+      end
+
+      it 'returns feature flags ordered by name' do
+        subject
+
+        expect(response).to have_gitlab_http_status(:ok)
+        expect(response).to match_response_schema('public_api/v4/feature_flags', dir: 'ee')
+        expect(json_response.count).to eq(2)
+        expect(json_response.first['name']).to eq(feature_flag_1.name)
+        expect(json_response.second['name']).to eq(feature_flag_2.name)
+      end
+
+      it 'does not have N+1 problem' do
+        control_count = ActiveRecord::QueryRecorder.new { subject }
+
+        create_list(:operations_feature_flag, 3, project: project)
+
+        expect { get api("/projects/#{project.id}/feature_flags", user) }
+          .not_to exceed_query_limit(control_count)
+      end
+
+      it_behaves_like 'check user permission'
+    end
+  end
+
+  describe 'POST /projects/:id/feature_flags' do
+    subject do
+      post api("/projects/#{project.id}/feature_flags", user), params: params
+    end
+
+    let(:params) do
+      {
+        name: 'awesome-feature',
+        scopes: [default_scope]
+      }
+    end
+
+    it 'creates a new feature flag' do
+      subject
+
+      expect(response).to have_gitlab_http_status(:created)
+      expect(response).to match_response_schema('public_api/v4/feature_flag', dir: 'ee')
+
+      feature_flag = project.operations_feature_flags.last
+      expect(feature_flag.name).to eq(params[:name])
+      expect(feature_flag.description).to eq(params[:description])
+    end
+
+    it_behaves_like 'check user permission'
+
+    context 'when no scopes passed in parameters' do
+      let(:params) { { name: 'awesome-feature' } }
+
+      it 'creates a new feature flag with active default scope' do
+        subject
+
+        expect(response).to have_gitlab_http_status(:created)
+        feature_flag = project.operations_feature_flags.last
+        expect(feature_flag.default_scope).to be_active
+      end
+    end
+
+    context 'when there is a feature flag with the same name already' do
+      before do
+        create_flag(project, 'awesome-feature')
+      end
+
+      it 'fails to create a new feature flag' do
+        subject
+
+        expect(response).to have_gitlab_http_status(:bad_request)
+      end
+    end
+
+    context 'when create a feature flag with two scopes' do
+      let(:params) do
+        {
+          name: 'awesome-feature',
+          description: 'this is awesome',
+          scopes: [
+            default_scope,
+            scope_with_user_with_id
+          ]
+        }
+      end
+
+      let(:scope_with_user_with_id) do
+        {
+          environment_scope: 'production',
+          active: true,
+          strategies: [{
+            name: 'userWithId',
+            parameters: { userIds: 'user:1' }
+          }].to_json
+        }
+      end
+
+      it 'creates a new feature flag with two scopes' do
+        subject
+
+        expect(response).to have_gitlab_http_status(:created)
+
+        feature_flag = project.operations_feature_flags.last
+        feature_flag.scopes.ordered.each_with_index do |scope, index|
+          expect(scope.environment_scope).to eq(params[:scopes][index][:environment_scope])
+          expect(scope.active).to eq(params[:scopes][index][:active])
+          expect(scope.strategies).to eq(JSON.parse(params[:scopes][index][:strategies]))
+        end
+      end
+    end
+
+    def default_scope
+      {
+        environment_scope: '*',
+        active: false,
+        strategies: [{ name: 'default', parameters: {} }].to_json
+      }
+    end
+  end
+
+  describe 'GET /projects/:id/feature_flags/:name' do
+    subject { get api("/projects/#{project.id}/feature_flags/#{feature_flag.name}", user) }
+
+    context 'when there is a feature flag' do
+      let!(:feature_flag) { create_flag(project, 'awesome-feature') }
+
+      it 'returns a feature flag entry' do
+        subject
+
+        expect(response).to have_gitlab_http_status(:ok)
+        expect(response).to match_response_schema('public_api/v4/feature_flag', dir: 'ee')
+        expect(json_response['name']).to eq(feature_flag.name)
+        expect(json_response['description']).to eq(feature_flag.description)
+      end
+
+      it_behaves_like 'check user permission'
+    end
+  end
+
+  describe 'DELETE /projects/:id/feature_flags/:name' do
+    subject do
+      delete api("/projects/#{project.id}/feature_flags/#{feature_flag.name}", user),
+             params: params
+    end
+
+    let!(:feature_flag) { create(:operations_feature_flag, project: project) }
+    let(:params) { {} }
+
+    it 'destroys the feature flag' do
+      expect { subject }.to change { Operations::FeatureFlag.count }.by(-1)
+
+      expect(response).to have_gitlab_http_status(:ok)
+    end
+  end
+end
diff --git a/ee/spec/requests/api/graphql/mutations/epics/update_spec.rb b/ee/spec/requests/api/graphql/mutations/epics/update_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..73e3fd4db1a3801b8073150e4acc3b1ea4d63b2b
--- /dev/null
+++ b/ee/spec/requests/api/graphql/mutations/epics/update_spec.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Updating an Epic' do
+  include GraphqlHelpers
+
+  let_it_be(:current_user) { create(:user) }
+  let_it_be(:group) { create(:group) }
+  let(:epic) { create(:epic, group: group, title: 'original title') }
+
+  let(:attributes) do
+    {
+      title: 'updated title',
+      description: 'some description',
+      start_date_fixed: '2019-09-17',
+      due_date_fixed: '2019-09-18',
+      start_date_is_fixed: true,
+      due_date_is_fixed: true
+    }
+  end
+
+  let(:mutation) do
+    params = { group_path: group.full_path, iid: epic.iid.to_s }.merge(attributes)
+
+    graphql_mutation(:update_epic, params)
+  end
+
+  def mutation_response
+    graphql_mutation_response(:update_epic)
+  end
+
+  context 'when the user does not have permission' do
+    before do
+      stub_licensed_features(epics: true)
+    end
+
+    it_behaves_like 'a mutation that returns top-level errors',
+      errors: ['The resource that you are attempting to access does not exist '\
+               'or you don\'t have permission to perform this action']
+
+    it 'does not update the epic' do
+      post_graphql_mutation(mutation, current_user: current_user)
+
+      expect(epic.reload.title).to eq('original title')
+    end
+  end
+
+  context 'when the user has permission' do
+    before do
+      epic.group.add_developer(current_user)
+    end
+
+    context 'when epics are disabled' do
+      before do
+        stub_licensed_features(epics: false)
+      end
+
+      it_behaves_like 'a mutation that returns top-level errors',
+        errors: ['The resource that you are attempting to access does not '\
+                 'exist or you don\'t have permission to perform this action']
+    end
+
+    context 'when epics are enabled' do
+      before do
+        stub_licensed_features(epics: true)
+      end
+
+      it 'updates the epic' do
+        post_graphql_mutation(mutation, current_user: current_user)
+
+        epic_hash = mutation_response['epic']
+        expect(epic_hash['title']).to eq('updated title')
+        expect(epic_hash['description']).to eq('some description')
+        expect(epic_hash['startDateFixed']).to eq('2019-09-17')
+        expect(epic_hash['startDateIsFixed']).to eq(true)
+        expect(epic_hash['dueDateFixed']).to eq('2019-09-18')
+        expect(epic_hash['dueDateIsFixed']).to eq(true)
+      end
+
+      context 'when closing the epic' do
+        let(:attributes) { { state_event: 'CLOSE' } }
+
+        it 'closes open epic' do
+          post_graphql_mutation(mutation, current_user: current_user)
+
+          expect(epic.reload).to be_closed
+        end
+      end
+
+      context 'when reopening the epic' do
+        let(:attributes) { { state_event: 'REOPEN' } }
+
+        it 'allows epic to be reopend' do
+          epic.update!(state: 'closed')
+
+          post_graphql_mutation(mutation, current_user: current_user)
+
+          expect(epic.reload).to be_open
+        end
+      end
+
+      context 'when there are ActiveRecord validation errors' do
+        let(:attributes) { { title: '' } }
+
+        it_behaves_like 'a mutation that returns errors in the response',
+          errors: ["Title can't be blank"]
+
+        it 'does not update the epic' do
+          post_graphql_mutation(mutation, current_user: current_user)
+
+          expect(mutation_response['epic']['title']).to eq('original title')
+        end
+      end
+
+      context 'when the list of attributes is empty' do
+        let(:attributes) { {} }
+
+        it_behaves_like 'a mutation that returns top-level errors',
+          errors: ['The list of attributes to update is empty']
+      end
+    end
+  end
+end
diff --git a/ee/spec/requests/api/issues_spec.rb b/ee/spec/requests/api/issues_spec.rb
index 64f463118b3a6204bba4ff2f0638d9dd86eb66c7..7ea1c62b6039cce15591951672f260f2a8c86297 100644
--- a/ee/spec/requests/api/issues_spec.rb
+++ b/ee/spec/requests/api/issues_spec.rb
@@ -42,7 +42,7 @@
       it 'contains epic_iid in response' do
         subject
 
-        expect(response).to have_gitlab_http_status(200)
+        expect(response).to have_gitlab_http_status(:success)
         expect(epic_issue_response_for(epic_issue)['epic_iid']).to eq(epic.iid)
       end
     end
@@ -55,12 +55,57 @@
       it 'does not contain epic_iid in response' do
         subject
 
-        expect(response).to have_gitlab_http_status(200)
+        expect(response).to have_gitlab_http_status(:success)
         expect(epic_issue_response_for(epic_issue)).not_to have_key('epic_iid')
       end
     end
   end
 
+  shared_examples 'sets epic_iid' do
+    context 'with epics feature' do
+      before do
+        stub_licensed_features(epics: true)
+      end
+
+      it 'sets epic on issue' do
+        subject
+
+        expect(epic_issue.epic).to eq(epic)
+      end
+    end
+
+    context 'without epics feature' do
+      before do
+        stub_licensed_features(epics: false)
+      end
+
+      it 'does not set epic on issue' do
+        subject
+
+        expect(epic_issue.epic).not_to eq(epic)
+      end
+    end
+  end
+
+  shared_examples 'ignores epic_iid' do
+    before do
+      stub_licensed_features(epics: true)
+    end
+
+    it 'does not contain epic_iid in response' do
+      subject
+
+      expect(response).to have_gitlab_http_status(:success)
+      expect(epic_issue_response_for(epic_issue)).not_to have_key('epic_iid')
+    end
+
+    it 'does not set epic on issue' do
+      subject
+
+      expect(epic_issue.epic).not_to eq(epic)
+    end
+  end
+
   describe "GET /issues" do
     context "when authenticated" do
       it 'matches V4 response schema' do
@@ -221,6 +266,28 @@
       expect(json_response['assignee']['name']).to eq(user2.name)
       expect(json_response['assignees'].first['name']).to eq(user2.name)
     end
+
+    context 'with epic parameter' do
+      let(:epic_issue) { Issue.last }
+      let(:params) { { title: 'issue with epic', epic_iid: epic.iid } }
+
+      context 'for a group project' do
+        subject { post api("/projects/#{group_project.id}/issues", user), params: params }
+
+        before do
+          group.add_owner(user)
+        end
+
+        include_examples 'exposes epic_iid'
+        include_examples 'sets epic_iid'
+      end
+
+      context 'for a user project' do
+        subject { post api("/projects/#{project.id}/issues", user), params: params }
+
+        include_examples 'ignores epic_iid'
+      end
+    end
   end
 
   describe 'PUT /projects/:id/issues/:issue_id to update weight' do
@@ -271,6 +338,29 @@
     end
   end
 
+  describe 'PUT /projects/:id/issues/:issue_id to update epic' do
+    context 'for a group project' do
+      let!(:epic_issue) { create(:issue, project: group_project) }
+
+      subject { put api("/projects/#{group_project.id}/issues/#{epic_issue.iid}", user), params: { epic_iid: epic.iid } }
+
+      before do
+        group.add_owner(user)
+      end
+
+      include_examples 'exposes epic_iid'
+      include_examples 'sets epic_iid'
+    end
+
+    context 'for a user project' do
+      let!(:epic_issue) { create(:issue, project: project) }
+
+      subject { put api("/projects/#{project.id}/issues/#{epic_issue.iid}", user), params: { epic_iid: epic.iid } }
+
+      include_examples 'ignores epic_iid'
+    end
+  end
+
   private
 
   def epic_issue_response_for(epic_issue)
diff --git a/ee/spec/requests/api/vulnerabilities_spec.rb b/ee/spec/requests/api/vulnerabilities_spec.rb
index c93767ba603fcbf8111e95a7cd65f1d354ba2937..a036746eab1d07f808b224e0d7d1951a59762a64 100644
--- a/ee/spec/requests/api/vulnerabilities_spec.rb
+++ b/ee/spec/requests/api/vulnerabilities_spec.rb
@@ -7,7 +7,7 @@
     stub_licensed_features(security_dashboard: true)
   end
 
-  let_it_be(:project) { create(:project, :public, :with_vulnerabilities) }
+  let_it_be(:project) { create(:project, :with_vulnerabilities) }
   let_it_be(:user) { create(:user) }
 
   describe "GET /projects/:id/vulnerabilities" do
@@ -43,6 +43,108 @@
       end
     end
 
-    it_behaves_like 'forbids access to vulnerability-like endpoint in expected cases'
+    it_behaves_like 'forbids access to project vulnerabilities endpoint in expected cases'
+  end
+
+  describe "POST /vulnerabilities:id/dismiss" do
+    before do
+      create_list(:vulnerabilities_occurrence, 2, vulnerability: vulnerability, project: vulnerability.project)
+    end
+
+    let(:vulnerability) { project.vulnerabilities.first }
+
+    subject { post api("/vulnerabilities/#{vulnerability.id}/dismiss", user) }
+
+    context 'with an authorized user with proper permissions' do
+      before do
+        project.add_developer(user)
+      end
+
+      it 'dismisses a vulnerability and its associated findings' do
+        subject
+
+        expect(response).to have_gitlab_http_status(201)
+        expect(response).to match_response_schema('vulnerability', dir: 'ee')
+
+        expect(vulnerability.reload).to be_closed
+        expect(vulnerability.findings).to all have_vulnerability_dismissal_feedback
+      end
+
+      context 'when there is a dismissal error' do
+        before do
+          Grape::Endpoint.before_each do |endpoint|
+            allow(endpoint).to receive(:find_vulnerability!).and_wrap_original do |method, *args|
+              vulnerability = method.call(*args)
+
+              errors = ActiveModel::Errors.new(vulnerability)
+              errors.add(:base, 'something went wrong')
+
+              allow(vulnerability).to receive(:valid?).and_return(false)
+              allow(vulnerability).to receive(:errors).and_return(errors)
+
+              vulnerability
+            end
+          end
+        end
+
+        after do
+          # resetting according to the https://github.com/ruby-grape/grape#stubbing-helpers
+          Grape::Endpoint.before_each nil
+        end
+
+        it 'responds with error' do
+          subject
+
+          expect(response).to have_gitlab_http_status(400)
+          expect(json_response['message']).to eq('base' => ['something went wrong'])
+        end
+      end
+
+      context 'and when security dashboard feature is not available' do
+        before do
+          stub_licensed_features(security_dashboard: false)
+        end
+
+        it 'responds with 403 Forbidden' do
+          subject
+
+          expect(response).to have_gitlab_http_status(403)
+        end
+      end
+
+      context 'if a vulnerability is already dismissed' do
+        let(:vulnerability) { create(:vulnerability, :closed, project: project) }
+
+        it 'responds with 304 Not Modified' do
+          subject
+
+          expect(response).to have_gitlab_http_status(304)
+        end
+      end
+    end
+
+    context 'when user does not have permissions to create a dismissal feedback' do
+      before do
+        project.add_reporter(user)
+      end
+
+      it 'responds with 403 Forbidden' do
+        subject
+
+        expect(response).to have_gitlab_http_status(403)
+      end
+    end
+
+    context 'when first-class vulnerabilities feature is disabled' do
+      before do
+        stub_feature_flags(first_class_vulnerabilities: false)
+      end
+
+      it 'responds with 404 Not Found' do
+        subject
+
+        expect(response).to have_gitlab_http_status(404)
+      end
+    end
   end
 end
diff --git a/ee/spec/requests/api/vulnerability_findings_spec.rb b/ee/spec/requests/api/vulnerability_findings_spec.rb
index 0c0141bdc29ec8f84aabac54e790235ebdf99cd7..c4d138519e361213849d094a60ff910698b3fb10 100644
--- a/ee/spec/requests/api/vulnerability_findings_spec.rb
+++ b/ee/spec/requests/api/vulnerability_findings_spec.rb
@@ -10,7 +10,7 @@
     let(:project_vulnerabilities_path) { "/projects/#{project.id}/vulnerability_findings" }
 
     it_behaves_like 'getting list of vulnerability findings'
-    it_behaves_like 'forbids access to vulnerability-like endpoint in expected cases'
+    it_behaves_like 'forbids access to project vulnerabilities endpoint in expected cases'
 
     context 'with an authorized user with proper permissions' do
       before do
diff --git a/ee/spec/serializers/dependency_entity_spec.rb b/ee/spec/serializers/dependency_entity_spec.rb
index b90e2ccac03f14d9761eb942097c68e68ae2ca33..96b7cf15eb55ac7ff2adf530c728896884532e24 100644
--- a/ee/spec/serializers/dependency_entity_spec.rb
+++ b/ee/spec/serializers/dependency_entity_spec.rb
@@ -32,24 +32,24 @@
       end
 
       context 'with reporter' do
-        let(:dependency_info) { build(:dependency, :with_licenses) }
-
         before do
           project.add_reporter(user)
         end
 
-        it { is_expected.to eq(dependency_info) }
+        it 'includes license info and not vulnerabilities' do
+          is_expected.to eq(dependency.except(:vulnerabilities))
+        end
       end
     end
 
     context 'when all required features are unavailable' do
-      let(:dependency_info) { build(:dependency).except(:licenses) }
-
       before do
         project.add_developer(user)
       end
 
-      it { is_expected.to eq(dependency_info) }
+      it 'does not include licenses and vulnerabilities' do
+        is_expected.to eq(dependency.except(:vulnerabilities, :licenses))
+      end
     end
   end
 end
diff --git a/ee/spec/serializers/license_entity_spec.rb b/ee/spec/serializers/license_entity_spec.rb
index 644893ea7963147ef8b2d8d1087217aa11d2e207..1294f0a4d4b23bd2fad401955222fe02052b8a87 100644
--- a/ee/spec/serializers/license_entity_spec.rb
+++ b/ee/spec/serializers/license_entity_spec.rb
@@ -12,10 +12,17 @@
       {
         name:       'MIT',
         url:        'https://opensource.org/licenses/mit',
-        components: [{ name: 'rails' }]
+        components: [{
+                       name:     'rails',
+                       blob_path: 'some_path'
+                     }]
       }
     end
 
+    before do
+      license.dependencies.first.path = 'some_path'
+    end
+
     it { is_expected.to eq(assert_license) }
   end
 end
diff --git a/ee/spec/serializers/merge_request_widget_entity_spec.rb b/ee/spec/serializers/merge_request_widget_entity_spec.rb
index eb1eed31cbe2f0ce694346b184181920f6d5c8b9..7fa138e91f72d238519f34e019873ed5eb50af3a 100644
--- a/ee/spec/serializers/merge_request_widget_entity_spec.rb
+++ b/ee/spec/serializers/merge_request_widget_entity_spec.rb
@@ -271,7 +271,7 @@ def create_all_artifacts
       end
 
       it 'does not count a merged and hidden blocking MR' do
-        blocking_mr.update_columns(state: 'merged')
+        blocking_mr.update_columns(state_id: MergeRequest.available_states[:merged])
 
         is_expected.to eq(
           hidden_count: 0,
diff --git a/ee/spec/serializers/pipeline_details_entity_spec.rb b/ee/spec/serializers/pipeline_details_entity_spec.rb
deleted file mode 100644
index f6cb5e71c8defb1780f5472139d3019b7f896792..0000000000000000000000000000000000000000
--- a/ee/spec/serializers/pipeline_details_entity_spec.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe PipelineDetailsEntity do
-  let(:user) { create(:user) }
-  let(:request) { double('request') }
-
-  before do
-    stub_not_protect_default_branch
-
-    allow(request).to receive(:current_user).and_return(user)
-  end
-
-  let(:entity) do
-    described_class.represent(pipeline, request: request)
-  end
-
-  describe '#as_json' do
-    subject { entity.as_json }
-
-    context 'when pipeline is triggered by other pipeline' do
-      let(:pipeline) { create(:ci_empty_pipeline) }
-
-      before do
-        create(:ci_sources_pipeline, pipeline: pipeline)
-      end
-
-      it 'contains an information about depedent pipeline' do
-        expect(subject[:triggered_by]).to be_a(Hash)
-        expect(subject[:triggered_by][:path]).not_to be_nil
-        expect(subject[:triggered_by][:details]).not_to be_nil
-        expect(subject[:triggered_by][:details][:status]).not_to be_nil
-        expect(subject[:triggered_by][:project]).not_to be_nil
-      end
-    end
-
-    context 'when pipeline triggered other pipeline' do
-      let(:pipeline) { create(:ci_empty_pipeline) }
-      let(:build) { create(:ci_build, pipeline: pipeline) }
-
-      before do
-        create(:ci_sources_pipeline, source_job: build)
-        create(:ci_sources_pipeline, source_job: build)
-      end
-
-      it 'contains an information about depedent pipeline' do
-        expect(subject[:triggered]).to be_a(Array)
-        expect(subject[:triggered].length).to eq(2)
-        expect(subject[:triggered].first[:path]).not_to be_nil
-        expect(subject[:triggered].first[:details]).not_to be_nil
-        expect(subject[:triggered].first[:details][:status]).not_to be_nil
-        expect(subject[:triggered].first[:project]).not_to be_nil
-      end
-    end
-  end
-end
diff --git a/ee/spec/serializers/productivity_analytics_merge_request_entity_spec.rb b/ee/spec/serializers/productivity_analytics_merge_request_entity_spec.rb
index 51e25ab717653387ef1d92dae38c1584047d2fd6..c7ea50b116952f83625c0c647c2c83b8eeb90bc4 100644
--- a/ee/spec/serializers/productivity_analytics_merge_request_entity_spec.rb
+++ b/ee/spec/serializers/productivity_analytics_merge_request_entity_spec.rb
@@ -4,6 +4,7 @@
 
 describe ProductivityAnalyticsMergeRequestEntity do
   subject { described_class.represent(merge_request).as_json.with_indifferent_access }
+
   let(:merge_request) { create(:merge_request) }
 
   before do
diff --git a/ee/spec/services/analytics/cycle_analytics/stages/delete_service_spec.rb b/ee/spec/services/analytics/cycle_analytics/stages/delete_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e03f0e43117c35e907c275edd9d75a7af124984b
--- /dev/null
+++ b/ee/spec/services/analytics/cycle_analytics/stages/delete_service_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Analytics::CycleAnalytics::Stages::DeleteService do
+  let_it_be(:group, refind: true) { create(:group) }
+  let_it_be(:user, refind: true) { create(:user) }
+  let_it_be(:stage, refind: true) { create(:cycle_analytics_group_stage, group: group) }
+  let(:params) { { id: stage.id } }
+
+  subject { described_class.new(parent: group, params: params, current_user: user).execute }
+
+  before_all do
+    group.add_user(user, :reporter)
+  end
+
+  before do
+    stub_licensed_features(cycle_analytics_for_groups: true)
+  end
+
+  it_behaves_like 'permission check for cycle analytics stage services', :cycle_analytics_for_groups
+
+  context 'when persisted stage is given' do
+    it { expect(subject).to be_success }
+
+    it 'deletes the stage' do
+      subject
+
+      expect(group.cycle_analytics_stages.find_by(id: stage.id)).to be_nil
+    end
+  end
+
+  context 'disallows deletion when default stage is given' do
+    let_it_be(:stage, refind: true) { create(:cycle_analytics_group_stage, group: group, custom: false) }
+
+    it { expect(subject).not_to be_success }
+    it { expect(subject.http_status).to eq(:forbidden) }
+  end
+end
diff --git a/ee/spec/services/approval_rules/create_service_spec.rb b/ee/spec/services/approval_rules/create_service_spec.rb
index 8ec8467ae8c4debfc3bdbcea2dbafab07062d2f1..b55465f06a3cf51b3e3fdc0bcd76afc9cb2efdd9 100644
--- a/ee/spec/services/approval_rules/create_service_spec.rb
+++ b/ee/spec/services/approval_rules/create_service_spec.rb
@@ -86,6 +86,7 @@
     ApprovalProjectRule::REPORT_TYPES_BY_DEFAULT_NAME.keys.each do |rule_name|
       context "when the rule name is `#{rule_name}`" do
         subject { described_class.new(target, user, { name: rule_name, approvals_required: 1 }) }
+
         let(:result) { subject.execute }
 
         specify { expect(result[:status]).to eq(:success) }
diff --git a/ee/spec/services/boards/list_service_spec.rb b/ee/spec/services/boards/list_service_spec.rb
index 1ce3b396f3526bd30874881caa95897d01289198..e879159966910faeaa54d80616baf5f59b4cac5e 100644
--- a/ee/spec/services/boards/list_service_spec.rb
+++ b/ee/spec/services/boards/list_service_spec.rb
@@ -4,8 +4,8 @@
 
 describe Boards::ListService do
   shared_examples 'boards list service' do
-    let(:service) { described_class.new(parent, double) }
-    let!(:boards) { create_list(:board, 3, parent: parent) }
+    let(:service) { described_class.new(resource_parent, double) }
+    let!(:boards) { create_list(:board, 3, resource_parent: resource_parent) }
 
     describe '#execute' do
       it 'returns all issue boards when multiple issue boards is enabled' do
@@ -25,11 +25,11 @@
   end
 
   it_behaves_like 'boards list service' do
-    let(:parent) { create(:project, :empty_repo) }
+    let(:resource_parent) { create(:project, :empty_repo) }
   end
 
   it_behaves_like 'boards list service' do
-    let(:parent) { create(:group) }
+    let(:resource_parent) { create(:group) }
 
     it 'returns the first issue board when multiple issue boards is disabled' do
       stub_licensed_features(multiple_group_issue_boards: false)
diff --git a/ee/spec/services/boards/update_service_spec.rb b/ee/spec/services/boards/update_service_spec.rb
index 5bdd2cb7ed3d61528fac253954b3cea0a7867ef8..e301d3231b4a3dca9dfd67a0619fc281040a622f 100644
--- a/ee/spec/services/boards/update_service_spec.rb
+++ b/ee/spec/services/boards/update_service_spec.rb
@@ -68,7 +68,7 @@
 
     context '#set_labels' do
       def expect_label_assigned(user, board, input_labels, expected_labels)
-        service = described_class.new(board.parent, user, labels: input_labels.join(','))
+        service = described_class.new(board.resource_parent, user, labels: input_labels.join(','))
         service.execute(board)
 
         expect(board.reload.labels.map(&:title)).to contain_exactly(*expected_labels)
@@ -146,7 +146,7 @@ def expect_label_assigned(user, board, input_labels, expected_labels)
             other_group_label = create(:group_label, title: 'other_group_label')
             label_ids = [group_label.id, label.id, other_project_label.id, other_group_label.id]
 
-            described_class.new(board.parent, user, label_ids: label_ids).execute(board)
+            described_class.new(board.resource_parent, user, label_ids: label_ids).execute(board)
 
             expect(board.reload.labels).to contain_exactly(group_label, label)
           end
diff --git a/ee/spec/services/ci/pipeline_trigger_service_spec.rb b/ee/spec/services/ci/pipeline_trigger_service_spec.rb
deleted file mode 100644
index b598869dbd106eed983d04f91cad6d13b7152eff..0000000000000000000000000000000000000000
--- a/ee/spec/services/ci/pipeline_trigger_service_spec.rb
+++ /dev/null
@@ -1,96 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Ci::PipelineTriggerService do
-  let(:project) { create(:project, :repository) }
-
-  before do
-    stub_ci_pipeline_to_return_yaml_file
-  end
-
-  describe '#execute' do
-    let(:user) { create(:user) }
-    let!(:pipeline) { create(:ci_empty_pipeline, project: project) }
-    let(:job) { create(:ci_build, :running, pipeline: pipeline, user: user) }
-    let(:result) { described_class.new(project, user, params).execute }
-
-    before do
-      project.add_developer(user)
-    end
-
-    context 'when job user does not have a permission to read a project' do
-      let(:params) { { token: job.token, ref: 'master', variables: nil } }
-      let(:job) { create(:ci_build, pipeline: pipeline, user: create(:user)) }
-
-      it 'does nothing' do
-        expect { result }.not_to change { Ci::Pipeline.count }
-      end
-    end
-
-    context 'when job is not running' do
-      let(:params) { { token: job.token, ref: 'master', variables: nil } }
-      let(:job) { create(:ci_build, :success, pipeline: pipeline, user: user) }
-
-      it 'does nothing' do
-        expect { result }.not_to change { Ci::Pipeline.count }
-        expect(result[:message]).to eq('400 Job has to be running')
-      end
-    end
-
-    context 'when params have an existsed job token' do
-      context 'when params have an existsed ref' do
-        let(:params) { { token: job.token, ref: 'master', variables: nil } }
-
-        it 'triggers a pipeline' do
-          expect { result }.to change { Ci::Pipeline.count }.by(1)
-          expect(result[:pipeline].ref).to eq('master')
-          expect(result[:pipeline].project).to eq(project)
-          expect(result[:pipeline].user).to eq(job.user)
-          expect(result[:status]).to eq(:success)
-        end
-
-        context 'when commit message has [ci skip]' do
-          before do
-            allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { '[ci skip]' }
-          end
-
-          it 'ignores [ci skip] and create as general' do
-            expect { result }.to change { Ci::Pipeline.count }.by(1)
-            expect(result[:status]).to eq(:success)
-          end
-        end
-
-        context 'when params have a variable' do
-          let(:params) { { token: job.token, ref: 'master', variables: variables } }
-          let(:variables) { { 'AAA' => 'AAA123' } }
-
-          it 'has a variable' do
-            expect { result }.to change { Ci::PipelineVariable.count }.by(1)
-                             .and change { Ci::Sources::Pipeline.count }.by(1)
-            expect(result[:pipeline].variables.map { |v| { v.key => v.value } }.first).to eq(variables)
-            expect(job.sourced_pipelines.last.pipeline_id).to eq(result[:pipeline].id)
-          end
-        end
-      end
-
-      context 'when params have a non-existsed ref' do
-        let(:params) { { token: job.token, ref: 'invalid-ref', variables: nil } }
-
-        it 'does not job a pipeline' do
-          expect { result }.not_to change { Ci::Pipeline.count }
-          expect(result[:http_status]).to eq(400)
-        end
-      end
-    end
-
-    context 'when params have a non-existsed trigger token' do
-      let(:params) { { token: 'invalid-token', ref: nil, variables: nil } }
-
-      it 'does not trigger a pipeline' do
-        expect { result }.not_to change { Ci::Pipeline.count }
-        expect(result).to be_nil
-      end
-    end
-  end
-end
diff --git a/ee/spec/services/ee/boards/issues/create_service_spec.rb b/ee/spec/services/ee/boards/issues/create_service_spec.rb
index 0d28f08434ab0884f429c1673c485a4efc549895..9704f728072650d2f471bcd3f6dcad7ec6ee7ef3 100644
--- a/ee/spec/services/ee/boards/issues/create_service_spec.rb
+++ b/ee/spec/services/ee/boards/issues/create_service_spec.rb
@@ -10,7 +10,7 @@
     let(:label)   { create(:label, project: project, name: 'in-progress') }
 
     subject(:service) do
-      described_class.new(board.parent, project, user, board_id: board.id, list_id: list.id, title: 'New issue')
+      described_class.new(board.resource_parent, project, user, board_id: board.id, list_id: list.id, title: 'New issue')
     end
 
     before do
diff --git a/ee/spec/services/ee/boards/lists/list_service_spec.rb b/ee/spec/services/ee/boards/lists/list_service_spec.rb
index da2d474634b43eaaa131bab115976fbe434c5954..c4346093e06f6d1fbad487fc533f8d881a93303a 100644
--- a/ee/spec/services/ee/boards/lists/list_service_spec.rb
+++ b/ee/spec/services/ee/boards/lists/list_service_spec.rb
@@ -11,8 +11,8 @@
 
       context 'when the feature is enabled' do
         before do
-          allow(board.parent).to receive(:feature_available?).with(:board_assignee_lists).and_return(true)
-          allow(board.parent).to receive(:feature_available?).with(:board_milestone_lists).and_return(false)
+          allow(board.resource_parent).to receive(:feature_available?).with(:board_assignee_lists).and_return(true)
+          allow(board.resource_parent).to receive(:feature_available?).with(:board_milestone_lists).and_return(false)
         end
 
         it 'returns all lists' do
@@ -34,8 +34,8 @@
 
       context 'when the feature is enabled' do
         before do
-          allow(board.parent).to receive(:feature_available?).with(:board_assignee_lists).and_return(false)
-          allow(board.parent).to receive(:feature_available?).with(:board_milestone_lists).and_return(true)
+          allow(board.resource_parent).to receive(:feature_available?).with(:board_assignee_lists).and_return(false)
+          allow(board.resource_parent).to receive(:feature_available?).with(:board_milestone_lists).and_return(true)
         end
 
         it 'returns all lists' do
diff --git a/ee/spec/services/ee/update_deployment_service_spec.rb b/ee/spec/services/ee/deployments/after_create_service_spec.rb
similarity index 95%
rename from ee/spec/services/ee/update_deployment_service_spec.rb
rename to ee/spec/services/ee/deployments/after_create_service_spec.rb
index 6a9c9977cd6adf8768816e81e25d29bf375d2b94..c0905f82e0b72e2656c33edc34293d595c65e59f 100644
--- a/ee/spec/services/ee/update_deployment_service_spec.rb
+++ b/ee/spec/services/ee/deployments/after_create_service_spec.rb
@@ -2,7 +2,7 @@
 
 require 'spec_helper'
 
-describe UpdateDeploymentService do
+describe Deployments::AfterCreateService do
   include ::EE::GeoHelpers
 
   let(:primary) { create(:geo_node, :primary) }
diff --git a/ee/spec/services/elastic/index_record_service_spec.rb b/ee/spec/services/elastic/index_record_service_spec.rb
index e187cfe967f53b22f96ebf1d85a0d034a050ba72..1cade311c9d48d7bc0d539d3c45556776dd1d6be 100644
--- a/ee/spec/services/elastic/index_record_service_spec.rb
+++ b/ee/spec/services/elastic/index_record_service_spec.rb
@@ -78,8 +78,32 @@
       end
       Gitlab::Elastic::Helper.refresh_index
 
-      ## All database objects + data from repository. The absolute value does not matter
-      expect(Elasticsearch::Model.search('*').total_count).to be > 40
+      # Fetch all child documents
+      children = Elasticsearch::Model.search(
+        size: 100,
+        query: {
+          has_parent: {
+            parent_type: 'project',
+            query: {
+              term: { id: project.id }
+            }
+          }
+        }
+      )
+
+      # The absolute value does not matter
+      expect(children.total_count).to be > 40
+
+      # Make sure all types are present
+      expect(children.pluck(:_source).pluck(:type).uniq).to contain_exactly(
+        'blob',
+        'commit',
+        'issue',
+        'merge_request',
+        'milestone',
+        'note',
+        'snippet'
+      )
     end
 
     it 'does not index records not associated with the project' do
diff --git a/ee/spec/services/epics/create_service_spec.rb b/ee/spec/services/epics/create_service_spec.rb
index 1d0f0b97b9839e3b9b47064e55aa73175d2b7614..de2f5d9f039586ac1c5d1a6056d53855aa9c6577 100644
--- a/ee/spec/services/epics/create_service_spec.rb
+++ b/ee/spec/services/epics/create_service_spec.rb
@@ -25,4 +25,19 @@
       expect(NewEpicWorker).to have_received(:perform_async).with(epic.id, user.id)
     end
   end
+
+  context 'handling fixed dates' do
+    it 'sets the fixed date correctly' do
+      date = Date.new(2019, 10, 10)
+      params[:start_date_fixed] = date
+      params[:start_date_is_fixed] = true
+
+      subject
+
+      epic = Epic.last
+      expect(epic.start_date).to eq(date)
+      expect(epic.start_date_fixed).to eq(date)
+      expect(epic.start_date_is_fixed).to be_truthy
+    end
+  end
 end
diff --git a/ee/spec/services/feature_flags/create_service_spec.rb b/ee/spec/services/feature_flags/create_service_spec.rb
index 631a85524ac9ae538f3b7f71c666ec2e24eda7a6..ce68c3be2d371258a2ed9caf499ea77cb0831716 100644
--- a/ee/spec/services/feature_flags/create_service_spec.rb
+++ b/ee/spec/services/feature_flags/create_service_spec.rb
@@ -4,12 +4,21 @@
 
 describe FeatureFlags::CreateService do
   let(:project) { create(:project) }
-  let(:user) { create(:user) }
+  let(:developer) { create(:user) }
+  let(:reporter) { create(:user) }
+  let(:user) { developer }
+
+  before do
+    stub_licensed_features(feature_flags: true)
+    project.add_developer(developer)
+    project.add_reporter(reporter)
+  end
 
   describe '#execute' do
     subject do
       described_class.new(project, user, params).execute
     end
+
     let(:feature_flag) { subject[:feature_flag] }
 
     context 'when feature flag can not be created' do
@@ -57,6 +66,15 @@
         expect { subject }.to change { AuditEvent.count }.by(1)
         expect(AuditEvent.last.present.action).to eq(expected_message)
       end
+
+      context 'when user is reporter' do
+        let(:user) { reporter }
+
+        it 'returns error status' do
+          expect(subject[:status]).to eq(:error)
+          expect(subject[:message]).to eq('Access Denied')
+        end
+      end
     end
   end
 end
diff --git a/ee/spec/services/feature_flags/destroy_service_spec.rb b/ee/spec/services/feature_flags/destroy_service_spec.rb
index b7f0d336e15105ee5ddf1e50e7c1fbf74e3fcd86..03dfd73d3adb9284e835d8f3f0dd34abfd90d471 100644
--- a/ee/spec/services/feature_flags/destroy_service_spec.rb
+++ b/ee/spec/services/feature_flags/destroy_service_spec.rb
@@ -3,13 +3,25 @@
 require 'spec_helper'
 
 describe FeatureFlags::DestroyService do
+  include FeatureFlagHelpers
+
   let(:project) { create(:project) }
-  let(:user) { create(:user) }
-  let!(:feature_flag) { create(:operations_feature_flag) }
+  let(:developer) { create(:user) }
+  let(:reporter) { create(:user) }
+  let(:user) { developer }
+  let!(:feature_flag) { create(:operations_feature_flag, project: project) }
+
+  before do
+    stub_licensed_features(feature_flags: true)
+    project.add_developer(developer)
+    project.add_reporter(reporter)
+  end
 
   describe '#execute' do
-    subject { described_class.new(project, user).execute(feature_flag) }
+    subject { described_class.new(project, user, params).execute(feature_flag) }
+
     let(:audit_event_message) { AuditEvent.last.present.action }
+    let(:params) { {} }
 
     it 'returns status success' do
       expect(subject[:status]).to eq(:success)
@@ -24,6 +36,15 @@
       expect(audit_event_message).to eq("Deleted feature flag <strong>#{feature_flag.name}</strong>.")
     end
 
+    context 'when user is reporter' do
+      let(:user) { reporter }
+
+      it 'returns error status' do
+        expect(subject[:status]).to eq(:error)
+        expect(subject[:message]).to eq('Access Denied')
+      end
+    end
+
     context 'when feature flag can not be destroyed' do
       before do
         allow(feature_flag).to receive(:destroy).and_return(false)
diff --git a/ee/spec/services/feature_flags/update_service_spec.rb b/ee/spec/services/feature_flags/update_service_spec.rb
index b045d5b10ca2c7fa87faa07717e0b3c79d9e151f..6953b7a8afe7ff956c3e52671ded79207d5c0744 100644
--- a/ee/spec/services/feature_flags/update_service_spec.rb
+++ b/ee/spec/services/feature_flags/update_service_spec.rb
@@ -4,11 +4,20 @@
 
 describe FeatureFlags::UpdateService do
   let(:project) { create(:project) }
-  let(:user) { create(:user) }
-  let(:feature_flag) { create(:operations_feature_flag) }
+  let(:developer) { create(:user) }
+  let(:reporter) { create(:user) }
+  let(:user) { developer }
+  let(:feature_flag) { create(:operations_feature_flag, project: project) }
+
+  before do
+    stub_licensed_features(feature_flags: true)
+    project.add_developer(developer)
+    project.add_reporter(reporter)
+  end
 
   describe '#execute' do
     subject { described_class.new(project, user, params).execute(feature_flag) }
+
     let(:params) { { name: 'new_name' } }
     let(:audit_event_message) do
       AuditEvent.last.present.action
@@ -45,6 +54,15 @@
       end
     end
 
+    context 'when user is reporter' do
+      let(:user) { reporter }
+
+      it 'returns error status' do
+        expect(subject[:status]).to eq(:error)
+        expect(subject[:message]).to eq('Access Denied')
+      end
+    end
+
     context 'when nothing is changed' do
       let(:params) { {} }
 
diff --git a/ee/spec/services/geo/project_housekeeping_service_spec.rb b/ee/spec/services/geo/project_housekeeping_service_spec.rb
index c79311862d879142b084a0186ee0351aa2cb5458..4a27a65a572eec0481af0ae671e7f9cec1632f9b 100644
--- a/ee/spec/services/geo/project_housekeeping_service_spec.rb
+++ b/ee/spec/services/geo/project_housekeeping_service_spec.rb
@@ -7,6 +7,7 @@
   include ::EE::GeoHelpers
 
   subject(:service) { described_class.new(project) }
+
   set(:project) { create(:project, :repository) }
   let(:registry) { service.registry }
 
diff --git a/ee/spec/services/group_saml/group_managed_accounts/clean_up_members_service_spec.rb b/ee/spec/services/group_saml/group_managed_accounts/clean_up_members_service_spec.rb
index c8291de5544cc4c66d35bce3b78c70a11ce4d4f0..f43e491eea9e3a7bdeaf15207a65041df38f2996 100644
--- a/ee/spec/services/group_saml/group_managed_accounts/clean_up_members_service_spec.rb
+++ b/ee/spec/services/group_saml/group_managed_accounts/clean_up_members_service_spec.rb
@@ -4,6 +4,7 @@
 
 describe GroupSaml::GroupManagedAccounts::CleanUpMembersService do
   subject(:service) { described_class.new(current_user, group) }
+
   let(:group) { Group.new }
   let(:current_user) { instance_double('User') }
   let(:destroy_member_service_spy) { spy('Members::DestroyService') }
diff --git a/ee/spec/services/group_saml/saml_provider/update_service_spec.rb b/ee/spec/services/group_saml/saml_provider/update_service_spec.rb
index 0a77013ab2e7de2efe867637418e476094e371d7..14ca9cdf17cfca57c7b30a87af93ad8c8fc3bae2 100644
--- a/ee/spec/services/group_saml/saml_provider/update_service_spec.rb
+++ b/ee/spec/services/group_saml/saml_provider/update_service_spec.rb
@@ -4,6 +4,7 @@
 
 describe GroupSaml::SamlProvider::UpdateService do
   subject(:service) { described_class.new(nil, saml_provider, params: params) }
+
   let(:params) do
     {
       sso_url: 'https://test',
diff --git a/ee/spec/services/merge_requests/sync_report_approver_approval_rules_spec.rb b/ee/spec/services/merge_requests/sync_report_approver_approval_rules_spec.rb
index 6171c719c1e1c5ecf318e46393f7ae08836dce8b..575dbf689da5333b834dc7eeb13d9aac1e334cc2 100644
--- a/ee/spec/services/merge_requests/sync_report_approver_approval_rules_spec.rb
+++ b/ee/spec/services/merge_requests/sync_report_approver_approval_rules_spec.rb
@@ -4,6 +4,7 @@
 
 describe MergeRequests::SyncReportApproverApprovalRules do
   subject(:service) { described_class.new(merge_request) }
+
   let(:merge_request) { create(:merge_request) }
 
   describe '#execute' do
diff --git a/ee/spec/services/pod_logs_service_spec.rb b/ee/spec/services/pod_logs_service_spec.rb
index 6b4a15c621c4b7e3659560d73f933f5c4492b0d5..f41a128076be11289c7824bb2f7f764dbf8cb54e 100644
--- a/ee/spec/services/pod_logs_service_spec.rb
+++ b/ee/spec/services/pod_logs_service_spec.rb
@@ -79,7 +79,7 @@
     end
 
     context 'without deployment platform' do
-      it_behaves_like 'error', 'No deployment platform'
+      it_behaves_like 'error', 'No deployment platform available'
     end
 
     context 'with deployment platform' do
@@ -137,6 +137,18 @@
 
           subject.execute
         end
+
+        context 'when there are no pods' do
+          before do
+            allow_any_instance_of(Gitlab::Kubernetes::RolloutStatus).to receive(:instances)
+              .and_return([])
+          end
+
+          it 'returns error' do
+            expect(result[:status]).to eq(:error)
+            expect(result[:message]).to eq('No pods available')
+          end
+        end
       end
 
       context 'when error is returned' do
@@ -152,8 +164,8 @@
             .and_return(nil)
         end
 
-        it 'returns nil' do
-          expect(result).to eq(nil)
+        it 'returns processing' do
+          expect(result).to eq(status: :processing, last_step: :pod_logs)
         end
       end
     end
diff --git a/ee/spec/services/projects/alerting/notify_service_spec.rb b/ee/spec/services/projects/alerting/notify_service_spec.rb
index 6732ccf6392d05ff68086627e479c15f4d98d413..66a94e667718864031c09daf90af76a52a4b5f43 100644
--- a/ee/spec/services/projects/alerting/notify_service_spec.rb
+++ b/ee/spec/services/projects/alerting/notify_service_spec.rb
@@ -54,48 +54,34 @@
         stub_licensed_features(incident_management: true)
       end
 
-      context 'with Generic Alert Endpoint feature enabled' do
-        before do
-          stub_feature_flags(generic_alert_endpoint: true)
-        end
-
-        context 'with activated Alerts Service' do
-          let!(:alerts_service) { create(:alerts_service, project: project) }
+      context 'with activated Alerts Service' do
+        let!(:alerts_service) { create(:alerts_service, project: project) }
 
-          context 'with valid token' do
-            let(:token) { alerts_service.token }
-
-            context 'with a valid payload' do
-              it_behaves_like 'processes incident issues', 1
-            end
+        context 'with valid token' do
+          let(:token) { alerts_service.token }
 
-            context 'with an invalid payload' do
-              before do
-                allow(Gitlab::Alerting::NotificationPayloadParser)
-                  .to receive(:call)
-                  .and_raise(Gitlab::Alerting::NotificationPayloadParser::BadPayloadError)
-              end
+          context 'with a valid payload' do
+            it_behaves_like 'processes incident issues', 1
+          end
 
-              it_behaves_like 'does not process incident issues', http_status: 400
+          context 'with an invalid payload' do
+            before do
+              allow(Gitlab::Alerting::NotificationPayloadParser)
+                .to receive(:call)
+                .and_raise(Gitlab::Alerting::NotificationPayloadParser::BadPayloadError)
             end
-          end
 
-          context 'with invalid token' do
-            it_behaves_like 'does not process incident issues', http_status: 401
+            it_behaves_like 'does not process incident issues', http_status: 400
           end
         end
 
-        context 'with deactivated Alerts Service' do
-          let!(:alerts_service) { create(:alerts_service, :inactive, project: project) }
-
-          it_behaves_like 'does not process incident issues', http_status: 403
+        context 'with invalid token' do
+          it_behaves_like 'does not process incident issues', http_status: 401
         end
       end
 
-      context 'with Generic Alert Endpoint feature disabled' do
-        before do
-          stub_feature_flags(generic_alert_endpoint: false)
-        end
+      context 'with deactivated Alerts Service' do
+        let!(:alerts_service) { create(:alerts_service, :inactive, project: project) }
 
         it_behaves_like 'does not process incident issues', http_status: 403
       end
diff --git a/ee/spec/services/projects/prometheus/alerts/create_events_service_spec.rb b/ee/spec/services/projects/prometheus/alerts/create_events_service_spec.rb
index 071e631af77153d7144024f352c68c797e306b93..dcc0ea8716625f3e07cc7b9fd49ea3cbdd00bb26 100644
--- a/ee/spec/services/projects/prometheus/alerts/create_events_service_spec.rb
+++ b/ee/spec/services/projects/prometheus/alerts/create_events_service_spec.rb
@@ -32,6 +32,18 @@
     end
   end
 
+  shared_examples 'self managed events persisted' do
+    subject { service.execute }
+
+    it 'returns created events' do
+      expect(subject).not_to be_empty
+    end
+
+    it 'does change self managed event count' do
+      expect { subject }.to change { SelfManagedPrometheusAlertEvent.count }
+    end
+  end
+
   context 'with valid alerts_payload' do
     let!(:alert) { create(:prometheus_alert, prometheus_metric: metric, project: project) }
 
@@ -221,14 +233,14 @@
       end
 
       describe '`ended_at`' do
-        context 'is missing' do
-          let(:alerts_payload) { { 'alerts' => [alert_payload(ended_at: nil)] } }
+        context 'is missing and status is resolved' do
+          let(:alerts_payload) { { 'alerts' => [alert_payload(ended_at: nil, status: 'resolved')] } }
 
           it_behaves_like 'no events persisted'
         end
 
-        context 'is invalid' do
-          let(:alerts_payload) { { 'alerts' => [alert_payload(ended_at: 'invalid date')] } }
+        context 'is invalid and status is resolved' do
+          let(:alerts_payload) { { 'alerts' => [alert_payload(ended_at: 'invalid date', status: 'resolved')] } }
 
           it_behaves_like 'no events persisted'
         end
@@ -242,6 +254,25 @@
             it_behaves_like 'no events persisted'
           end
 
+          context 'is missing but title is given' do
+            let(:alerts_payload) { { 'alerts' => [alert_payload(gitlab_alert_id: nil, title: 'alert')] } }
+
+            it_behaves_like 'self managed events persisted'
+          end
+
+          context 'is missing and environment name is given' do
+            let(:environment) { create(:environment, project: project) }
+            let(:alerts_payload) { { 'alerts' => [alert_payload(gitlab_alert_id: nil, title: 'alert', environment: environment.name)] } }
+
+            it_behaves_like 'self managed events persisted'
+
+            it 'associates the environment to the alert event' do
+              service.execute
+
+              expect(SelfManagedPrometheusAlertEvent.last.environment).to eq environment
+            end
+          end
+
           context 'is invalid' do
             let(:alerts_payload) { { 'alerts' => [alert_payload(gitlab_alert_id: '-1')] } }
 
@@ -254,13 +285,16 @@
 
   private
 
-  def alert_payload(status: 'firing', started_at: Time.now, ended_at: Time.now, gitlab_alert_id: alert.prometheus_metric_id)
+  def alert_payload(status: 'firing', started_at: Time.now, ended_at: Time.now, gitlab_alert_id: alert.prometheus_metric_id, title: nil, environment: nil)
     payload = {}
 
     payload['status'] = status if status
     payload['startsAt'] = utc_rfc3339(started_at) if started_at
     payload['endsAt'] = utc_rfc3339(ended_at) if ended_at
-    payload['labels'] = { 'gitlab_alert_id' => gitlab_alert_id.to_s } if gitlab_alert_id
+    payload['labels'] = {}
+    payload['labels']['gitlab_alert_id'] = gitlab_alert_id.to_s if gitlab_alert_id
+    payload['labels']['alertname'] = title if title
+    payload['labels']['gitlab_environment_name'] = environment if environment
 
     payload
   end
diff --git a/ee/spec/services/security/licenses_list_service_spec.rb b/ee/spec/services/security/licenses_list_service_spec.rb
index 0505c328d1b0a2018a26721d756d063777daabe5..3dd55ac7423546d066cc2a2d3c5fcebc6ef4ddb1 100644
--- a/ee/spec/services/security/licenses_list_service_spec.rb
+++ b/ee/spec/services/security/licenses_list_service_spec.rb
@@ -3,17 +3,43 @@
 require 'spec_helper'
 
 describe Security::LicensesListService do
-  describe '#execute' do
-    let!(:pipeline) { create(:ee_ci_pipeline, :with_license_management_report) }
+  include LicenseScanningReportHelpers
 
+  describe '#execute' do
     subject { described_class.new(pipeline: pipeline).execute }
 
+    let!(:pipeline) { create(:ee_ci_pipeline, :with_license_management_report) }
+    let(:project) { pipeline.project }
+    let(:mit_license) { find_license_by_name(subject, 'MIT') }
+
     before do
-      stub_licensed_features(license_management: true)
+      stub_licensed_features(license_management: true, dependency_list: true)
     end
 
-    it 'returns array of Licenses' do
-      is_expected.to be_an(Array)
+    context 'with matching dependency list' do
+      let!(:build) { create(:ci_build, :success, name: 'dependency_list', pipeline: pipeline, project: project) }
+      let!(:artifact) { create(:ee_ci_job_artifact, :dependency_list, job: build, project: project) }
+
+      it 'merges dependency location for found dependencies' do
+        nokogiri = dependency_by_name(mit_license, 'nokogiri')
+        actioncable = dependency_by_name(mit_license, 'actioncable')
+        nokogiri_path = "/#{project.full_path}/blob/#{pipeline.sha}/rails/Gemfile.lock"
+
+        expect(nokogiri.path).to eq(nokogiri_path)
+        expect(actioncable.path).to be_nil
+      end
+    end
+
+    context 'without matching dependency list' do
+      it 'returns array of Licenses' do
+        is_expected.to be_an(Array)
+      end
+
+      it 'returns empty path in dependencies' do
+        nokogiri = dependency_by_name(mit_license, 'nokogiri')
+
+        expect(nokogiri.path).to be_nil
+      end
     end
   end
 end
diff --git a/ee/spec/services/security/merge_reports_service_spec.rb b/ee/spec/services/security/merge_reports_service_spec.rb
index b2638e5e660219a3499b6b994dc0a9ae5c561326..f4820f1f72f76ccc1fe1330bafef21f0ef113f0d 100644
--- a/ee/spec/services/security/merge_reports_service_spec.rb
+++ b/ee/spec/services/security/merge_reports_service_spec.rb
@@ -12,6 +12,7 @@
   let(:identifier_2_primary) { build(:ci_reports_security_identifier, external_id: 'VULN-2', external_type: 'scanner-2') }
   let(:identifier_2_cve) { build(:ci_reports_security_identifier, external_id: 'CVE-2019-456', external_type: 'cve') }
   let(:identifier_cwe) { build(:ci_reports_security_identifier, external_id: '789', external_type: 'cwe') }
+  let(:identifier_wasc) { build(:ci_reports_security_identifier, external_id: '13', external_type: 'wasc') }
 
   let(:occurrence_id_1) do
     build(:ci_reports_security_occurrence,
@@ -63,7 +64,23 @@
          )
   end
 
-  let(:report_1_occurrences) { [occurrence_id_1, occurrence_id_2_loc_1, occurrence_cwe_2] }
+  let(:occurrence_wasc_1) do
+    build(:ci_reports_security_occurrence,
+          identifiers: [identifier_wasc],
+          scanner: scanner_1,
+          severity: :medium
+         )
+  end
+
+  let(:occurrence_wasc_2) do
+    build(:ci_reports_security_occurrence,
+          identifiers: [identifier_wasc],
+          scanner: scanner_2,
+          severity: :critical
+         )
+  end
+
+  let(:report_1_occurrences) { [occurrence_id_1, occurrence_id_2_loc_1, occurrence_cwe_2, occurrence_wasc_1] }
 
   let(:report_1) do
     build(
@@ -74,11 +91,13 @@
     )
   end
 
+  let(:report_2_occurrences) { [occurrence_id_2_loc_2, occurrence_wasc_2] }
+
   let(:report_2) do
     build(
       :ci_reports_security_report,
       scanners: [scanner_2],
-      occurrences: [occurrence_id_2_loc_2],
+      occurrences: report_2_occurrences,
       identifiers: occurrence_id_2_loc_2.identifiers
     )
   end
@@ -109,14 +128,23 @@
         identifier_1_cve,
         identifier_2_primary,
         identifier_2_cve,
-        identifier_cwe
+        identifier_cwe,
+        identifier_wasc
       )
     )
   end
 
-  it 'deduplicates and sorts the vulnerabilities by severity (desc) then by compare key' do
+  it 'deduplicates (except cwe and wasc) and sorts the vulnerabilities by severity (desc) then by compare key' do
     expect(subject.occurrences).to(
-      eq([occurrence_cwe_2, occurrence_cwe_1, occurrence_id_2_loc_2, occurrence_id_2_loc_1, occurrence_id_1])
+      eq([
+          occurrence_cwe_2,
+          occurrence_wasc_2,
+          occurrence_cwe_1,
+          occurrence_id_2_loc_2,
+          occurrence_id_2_loc_1,
+          occurrence_wasc_1,
+          occurrence_id_1
+      ])
     )
   end
 end
diff --git a/ee/spec/services/security/sync_reports_to_approval_rules_service_spec.rb b/ee/spec/services/security/sync_reports_to_approval_rules_service_spec.rb
index 15726b7a2371693a8d9641d2a55ca9d977f3e046..1447c07f4789bf68ba407eb5d07551902b940ac5 100644
--- a/ee/spec/services/security/sync_reports_to_approval_rules_service_spec.rb
+++ b/ee/spec/services/security/sync_reports_to_approval_rules_service_spec.rb
@@ -35,7 +35,7 @@
 
       context 'when only low-severity vulnerabilities are present' do
         before do
-          create(:ee_ci_build, :success, :dast, name: 'dast_job', pipeline: pipeline, project: project)
+          create(:ee_ci_build, :success, :low_severity_dast_report, name: 'dast_job', pipeline: pipeline, project: project)
         end
 
         it 'lowers approvals_required count to zero' do
@@ -56,10 +56,6 @@
         end
 
         it "won't change approvals_required count" do
-          expect(
-            pipeline.security_reports.reports.values.all?(&:unsafe_severity?)
-          ).to be false
-
           expect { subject }
             .not_to change { report_approver_rule.reload.approvals_required }
         end
@@ -119,7 +115,7 @@
 
       context 'when only low-severity vulnerabilities are present' do
         before do
-          create(:ee_ci_build, :success, :dast, name: 'dast_job', pipeline: pipeline, project: project)
+          create(:ee_ci_build, :success, :low_severity_dast_report, name: 'dast_job', pipeline: pipeline, project: project)
         end
 
         it 'lowers approvals_required count to zero' do
@@ -135,13 +131,14 @@
   end
 
   context 'without reports' do
+    let(:pipeline) { create(:ci_pipeline, :running, project: project, merge_requests_as_head_pipeline: [merge_request]) }
+
     it "won't change approvals_required count" do
       expect { subject }
         .not_to change { report_approver_rule.reload.approvals_required }
     end
 
     context "license compliance policy" do
-      let(:pipeline) { create(:ee_ci_pipeline, :running, project: project, merge_requests_as_head_pipeline: [merge_request]) }
       let!(:software_license_policy) { create(:software_license_policy, :blacklist, project: project, software_license: blacklisted_license) }
       let!(:license_compliance_rule) { create(:report_approver_rule, :license_management, merge_request: merge_request, approvals_required: 1) }
       let!(:blacklisted_license) { create(:software_license) }
diff --git a/ee/spec/services/system_note_service_spec.rb b/ee/spec/services/system_note_service_spec.rb
index 4b7f3cce70bcfe033d400468ed9fb952b95bf5aa..d1781431d0d984a455ca3397c01f85d496b71fe5 100644
--- a/ee/spec/services/system_note_service_spec.rb
+++ b/ee/spec/services/system_note_service_spec.rb
@@ -99,6 +99,32 @@
       end
     end
 
+    describe 'icons' do
+      where(:action) do
+        [
+          [:creation],
+          [:modification],
+          [:deletion]
+        ]
+      end
+
+      with_them do
+        before do
+          version.actions.update_all(event: action)
+        end
+
+        subject(:metadata) do
+          described_class.design_version_added(version)
+            .first.system_note_metadata
+        end
+
+        it 'has a valid action' do
+          expect(EE::SystemNoteHelper::EE_ICON_NAMES_BY_ACTION)
+            .to include(metadata.action)
+        end
+      end
+    end
+
     context 'it succeeds' do
       where(:action, :icon, :human_description) do
         [
diff --git a/ee/spec/services/vulnerabilities/dismiss_service_spec.rb b/ee/spec/services/vulnerabilities/dismiss_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e70fc493050d902c42eaec0d5fa5e7333de7162a
--- /dev/null
+++ b/ee/spec/services/vulnerabilities/dismiss_service_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Vulnerabilities::DismissService do
+  before do
+    stub_licensed_features(security_dashboard: true)
+  end
+
+  let_it_be(:user) { create(:user) }
+  let(:project) { create(:project) } # cannot use let_it_be here: caching causes problems with permission-related tests
+  let(:vulnerability) { create(:vulnerability, :with_findings, project: project) }
+  let(:service) { described_class.new(user, vulnerability) }
+
+  subject { service.execute }
+
+  context 'with an authorized user with proper permissions' do
+    before do
+      project.add_developer(user)
+    end
+
+    it 'dismisses a vulnerability and its associated findings' do
+      subject
+
+      expect(vulnerability.reload).to be_closed
+      expect(vulnerability.findings).to all have_vulnerability_dismissal_feedback
+    end
+
+    context 'when there is a finding dismissal error' do
+      before do
+        allow(service).to receive(:dismiss_findings).and_return(
+          described_class::FindingsDismissResult.new(false, broken_finding, 'something went wrong'))
+      end
+
+      let(:broken_finding) { vulnerability.findings.first }
+
+      it 'responds with error' do
+        expect(subject.errors.messages).to eq(
+          base: ["failed to dismiss associated finding(id=#{broken_finding.id}): something went wrong"])
+      end
+    end
+
+    context 'when security dashboard feature is disabled' do
+      before do
+        stub_licensed_features(security_dashboard: false)
+      end
+
+      it 'raises an "access denied" error' do
+        expect { subject }.to raise_error(Gitlab::Access::AccessDeniedError)
+      end
+    end
+  end
+
+  context 'when user does not have rights to dismiss a vulnerability' do
+    before do
+      project.add_reporter(user)
+    end
+
+    it 'raises an "access denied" error' do
+      expect { subject }.to raise_error(Gitlab::Access::AccessDeniedError)
+    end
+  end
+end
diff --git a/ee/spec/support/helpers/feature_flag_helpers.rb b/ee/spec/support/helpers/feature_flag_helpers.rb
index e8b7cd3e58c6c8ebaedd77e90c22e38349fcf3fd..5d5c1e7170cc8ee6e878af3cf84b872110abe961 100644
--- a/ee/spec/support/helpers/feature_flag_helpers.rb
+++ b/ee/spec/support/helpers/feature_flag_helpers.rb
@@ -1,12 +1,12 @@
 # frozen_string_literal: true
 
 module FeatureFlagHelpers
-  def create_flag(project, name, active, description: nil)
+  def create_flag(project, name, active = true, description: nil)
     create(:operations_feature_flag, name: name, active: active,
                                      description: description, project: project)
   end
 
-  def create_scope(feature_flag, environment_scope, active, strategies = [{ name: "default", parameters: {} }])
+  def create_scope(feature_flag, environment_scope, active = true, strategies = [{ name: "default", parameters: {} }])
     create(:operations_feature_flag_scope,
       feature_flag: feature_flag,
       environment_scope: environment_scope,
diff --git a/ee/spec/support/helpers/license_scanning_report_helpers.rb b/ee/spec/support/helpers/license_scanning_report_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c09c6c6ddb247ddcd3f92630e98035dab9a303ed
--- /dev/null
+++ b/ee/spec/support/helpers/license_scanning_report_helpers.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module LicenseScanningReportHelpers
+  def all_dependency_paths(report)
+    report.licenses.map { |license| license.dependencies.map(&:path) }.flatten.compact
+  end
+
+  def dependency_by_name(license, name)
+    license.dependencies.find { |dep| dep.name == name }
+  end
+
+  def find_license_by_name(licenses, name)
+    licenses.find { |license| license.name == name }
+  end
+end
diff --git a/ee/spec/support/helpers/vulnerability_helpers.rb b/ee/spec/support/helpers/vulnerability_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b7fb2502bba1071da5f1d2d923c9aaeb02609310
--- /dev/null
+++ b/ee/spec/support/helpers/vulnerability_helpers.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define :have_vulnerability_dismissal_feedback do
+  match do |finding|
+    expect(finding.dismissal_feedback).to have_attributes(project: finding.vulnerability.project,
+                                                          category: finding.report_type,
+                                                          project_fingerprint: finding.project_fingerprint)
+  end
+end
diff --git a/ee/spec/support/shared_examples/controllers/analytics/cycle_analytics/shared_stage_examples.rb b/ee/spec/support/shared_examples/controllers/analytics/cycle_analytics/shared_stage_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..67326b71460cca5215f3b1b4f803a2ede2a9d1a8
--- /dev/null
+++ b/ee/spec/support/shared_examples/controllers/analytics/cycle_analytics/shared_stage_examples.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+shared_examples 'group permission check on the controller level' do
+  context 'when `group_id` is not provided' do
+    before do
+      params[:group_id] = nil
+    end
+
+    it 'renders `not_found` when group_id is not provided' do
+      subject
+
+      expect(response).to have_gitlab_http_status(:not_found)
+    end
+  end
+
+  context 'when `group_id` is not found' do
+    before do
+      params[:group_id] = 'missing_group'
+    end
+
+    it 'renders `not_found` when group is missing' do
+      subject
+
+      expect(response).to have_gitlab_http_status(:not_found)
+    end
+  end
+
+  context 'when feature flag is disabled' do
+    before do
+      stub_feature_flags(Gitlab::Analytics::CYCLE_ANALYTICS_FEATURE_FLAG => false)
+    end
+
+    it 'renders `not_found` response' do
+      subject
+
+      expect(response).to have_gitlab_http_status(:not_found)
+    end
+  end
+
+  context 'when user has no lower access level than `reporter`' do
+    before do
+      GroupMember.where(user: user).delete_all
+      group.add_guest(user)
+    end
+
+    it 'renders `forbidden` response' do
+      subject
+
+      expect(response).to have_gitlab_http_status(:forbidden)
+    end
+  end
+
+  context 'when feature is not available for the group' do
+    before do
+      stub_licensed_features(cycle_analytics_for_groups: false)
+    end
+
+    it 'renders `forbidden` response' do
+      subject
+
+      expect(response).to have_gitlab_http_status(:forbidden)
+    end
+  end
+end
+
+shared_context 'when invalid stage parameters are given' do
+  before do
+    params[:name] = ''
+  end
+
+  it 'renders the validation errors' do
+    subject
+
+    expect(response).to have_gitlab_http_status(:unprocessable_entity)
+    expect(response).to match_response_schema('analytics/cycle_analytics/validation_error', dir: 'ee')
+  end
+end
diff --git a/ee/spec/support/shared_examples/controllers/multiple_issue_board_show.rb b/ee/spec/support/shared_examples/controllers/multiple_issue_board_show.rb
index 8f270b3f8a24b1f54b68c8a84fa27dcb3cd882c0..022a685fb8b0e808b235d3c3e5d7f96b288c97d7 100644
--- a/ee/spec/support/shared_examples/controllers/multiple_issue_board_show.rb
+++ b/ee/spec/support/shared_examples/controllers/multiple_issue_board_show.rb
@@ -3,8 +3,8 @@
 require 'spec_helper'
 
 shared_examples 'multiple issue boards show' do
-  let!(:board1) { create(:board, parent: parent, name: 'b') }
-  let!(:board2) { create(:board, parent: parent, name: 'a') }
+  let!(:board1) { create(:board, resource_parent: parent, name: 'b') }
+  let!(:board2) { create(:board, resource_parent: parent, name: 'a') }
 
   context 'when multiple issue boards is enabled' do
     it 'lets user view board1' do
diff --git a/ee/spec/support/shared_examples/controllers/recent_boards.rb b/ee/spec/support/shared_examples/controllers/recent_boards.rb
index bc3694dd7136beb770b74ebca41cbb4502cb8714..ed24c6a06100673cce9ec504be71f4391075674b 100644
--- a/ee/spec/support/shared_examples/controllers/recent_boards.rb
+++ b/ee/spec/support/shared_examples/controllers/recent_boards.rb
@@ -3,7 +3,7 @@
 require 'spec_helper'
 
 shared_examples 'returns recently visited boards' do
-  let(:boards) { create_list(:board, 8, parent: parent) }
+  let(:boards) { create_list(:board, 8, resource_parent: parent) }
 
   context 'unauthenticated' do
     it 'returns a 401' do
@@ -28,7 +28,7 @@
 end
 
 shared_examples 'redirects to last visited board' do
-  let(:boards) { create_list(:board, 3, parent: parent) }
+  let(:boards) { create_list(:board, 3, resource_parent: parent) }
 
   before do
     visit_board(boards[2], Time.now + 1.minute)
diff --git a/ee/spec/support/shared_examples/requests/api/vulnerabilities_shared_examples.rb b/ee/spec/support/shared_examples/requests/api/vulnerabilities_shared_examples.rb
index f71cc233c9c3684495a4208ec1efaa2509927860..02e3b404d36287b61366466681ddeb5d78fd556f 100644
--- a/ee/spec/support/shared_examples/requests/api/vulnerabilities_shared_examples.rb
+++ b/ee/spec/support/shared_examples/requests/api/vulnerabilities_shared_examples.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-shared_examples 'forbids access to vulnerability-like endpoint in expected cases' do
+shared_examples 'forbids access to project vulnerabilities endpoint in expected cases' do
   context 'with authorized user without read permissions' do
     before do
       project.add_reporter(user)
@@ -125,7 +125,7 @@
 
         # occurrences are implicitly sorted by Security::MergeReportsService,
         # occurrences order differs from what is present in fixture file
-        expect(json_response.first['name']).to eq 'ECB mode is insecure'
+        expect(json_response.first['name']).to eq 'Consider possible security implications associated with Popen module.'
       end
 
       it 'returns vulnerabilities with dependency_scanning report_type' do
diff --git a/ee/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/ee/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
index 72786d467e533588ad1f962ad542eee8b8d651ae..8345399a6ebf549147a5f37d32b41236382e8b0d 100644
--- a/ee/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/ee/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -73,40 +73,12 @@
       expect(rendered).not_to have_text 'Tracing'
     end
 
-    context 'with project.tracing_external_url' do
-      let(:tracing_url) { 'https://tracing.url' }
-      let(:tracing_settings) { create(:project_tracing_setting, project: project, external_url: tracing_url) }
-
-      before do
-        allow(view).to receive(:can?).and_return(true)
-      end
-
-      it 'links to project.tracing_external_url' do
-        expect(tracing_settings.external_url).to eq(tracing_url)
-        expect(project.tracing_external_url).to eq(tracing_url)
-
-        render
-
-        expect(rendered).to have_link('Tracing', href: tracing_url)
-      end
-
-      context 'with malicious external_url' do
-        let(:malicious_tracing_url) { "https://replaceme.com/'><script>alert(document.cookie)</script>" }
-        let(:cleaned_url) { "https://replaceme.com/'>" }
-
-        before do
-          tracing_settings.update_column(:external_url, malicious_tracing_url)
-        end
-
-        it 'sanitizes external_url' do
-          expect(project.tracing_external_url).to eq(malicious_tracing_url)
+    it 'links to Tracing page' do
+      allow(view).to receive(:can?).and_return(true)
 
-          render
+      render
 
-          expect(tracing_settings.external_url).to eq(malicious_tracing_url)
-          expect(rendered).to have_link('Tracing', href: cleaned_url)
-        end
-      end
+      expect(rendered).to have_link('Tracing', href: project_tracing_path(project))
     end
 
     context 'without project.tracing_external_url' do
diff --git a/ee/spec/views/projects/tracing/show.html.haml_spec.rb b/ee/spec/views/projects/tracing/show.html.haml_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..39154d1249faef1709f225f4903ffe13316df72e
--- /dev/null
+++ b/ee/spec/views/projects/tracing/show.html.haml_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'projects/tracings/show' do
+  let(:project) { create(:project, :repository) }
+  let(:error_tracking_setting) { create(:project_error_tracking_setting, project: project) }
+
+  before do
+    assign(:project, project)
+    assign(:repository, project.repository)
+    allow(view).to receive(:current_ref).and_return('master')
+    allow(view).to receive(:error_tracking_setting).and_return(error_tracking_setting)
+    allow(view).to receive(:incident_management_available?) { false }
+    stub_licensed_features(tracing: true)
+  end
+
+  context 'with project.tracing_external_url' do
+    let(:tracing_url) { 'https://tracing.url' }
+    let(:tracing_setting) { create(:project_tracing_setting, project: project, external_url: tracing_url) }
+
+    before do
+      allow(view).to receive(:can?).and_return(true)
+      allow(view).to receive(:tracing_setting).and_return(tracing_setting)
+    end
+
+    it 'renders iframe' do
+      render
+
+      expect(rendered).to match(/iframe/)
+    end
+
+    context 'with malicious external_url' do
+      let(:malicious_tracing_url) { "https://replaceme.com/'><script>alert(document.cookie)</script>" }
+      let(:cleaned_url) { "https://replaceme.com/'&gt;" }
+
+      before do
+        tracing_setting.update_column(:external_url, malicious_tracing_url)
+      end
+
+      it 'sanitizes external_url' do
+        render
+
+        expect(tracing_setting.external_url).to eq(malicious_tracing_url)
+        expect(rendered).to have_xpath("//iframe[@src=\"#{cleaned_url}\"]")
+      end
+    end
+  end
+
+  context 'without project.tracing_external_url' do
+    before do
+      allow(view).to receive(:can?).and_return(true)
+    end
+
+    it 'renders empty state' do
+      render
+
+      expect(rendered).to have_link('Add Jaeger URL')
+      expect(rendered).not_to match(/iframe/)
+    end
+  end
+end
diff --git a/ee/spec/workers/elastic_batch_project_indexer_worker_spec.rb b/ee/spec/workers/elastic_batch_project_indexer_worker_spec.rb
index 05a3c52444509f8431bd17d1805ef10ce19ff822..e22e39beeefd12ebb2a515bec09b12af8454ee62 100644
--- a/ee/spec/workers/elastic_batch_project_indexer_worker_spec.rb
+++ b/ee/spec/workers/elastic_batch_project_indexer_worker_spec.rb
@@ -4,6 +4,7 @@
 
 describe ElasticBatchProjectIndexerWorker do
   subject(:worker) { described_class.new }
+
   let(:projects) { create_list(:project, 2) }
 
   describe '#perform' do
diff --git a/ee/spec/workers/incident_management/process_prometheus_alert_worker_spec.rb b/ee/spec/workers/incident_management/process_prometheus_alert_worker_spec.rb
index bb3e004669ede9d19f5ecb62a53697c88665d9bc..77aec8c6f7b2c0df6de7bafaae7c148a2cb4973c 100644
--- a/ee/spec/workers/incident_management/process_prometheus_alert_worker_spec.rb
+++ b/ee/spec/workers/incident_management/process_prometheus_alert_worker_spec.rb
@@ -29,7 +29,9 @@
 
     it 'relates issue to an event' do
       expect { subject.perform(project.id, alert_params) }
-        .to change(prometheus_alert.related_issues, :count).from(0).to(1)
+        .to change(prometheus_alert.related_issues, :count)
+        .from(0)
+        .to(1)
     end
 
     context 'when project could not be found' do
@@ -56,7 +58,7 @@
 
       it 'does not relate issue to an event' do
         expect { subject.perform(project.id, alert_params) }
-          .not_to change(Issue, :count)
+          .not_to change(prometheus_alert.related_issues, :count)
       end
     end
 
@@ -72,5 +74,55 @@
           .not_to change(prometheus_alert.related_issues, :count)
       end
     end
+
+    context 'self-managed alert' do
+      let(:alert_name) { 'alert' }
+      let(:starts_at) { Time.now.rfc3339 }
+
+      let!(:prometheus_alert) do
+        payload_key = SelfManagedPrometheusAlertEvent.payload_key_for(starts_at, alert_name, 'vector(1)')
+        create(:self_managed_prometheus_alert_event, project: project, payload_key: payload_key)
+      end
+
+      let(:alert_params) do
+        {
+          startsAt: starts_at,
+          generatorURL: 'http://localhost:9090/graph?g0.expr=vector%281%29&g0.tab=1',
+          labels: {
+            alertname: alert_name
+          }
+        }.with_indifferent_access
+      end
+
+      it 'creates an issue' do
+        expect { subject.perform(project.id, alert_params) }
+          .to change(Issue, :count)
+          .by(1)
+      end
+
+      it 'relates issue to an event' do
+        expect { subject.perform(project.id, alert_params) }
+          .to change(prometheus_alert.related_issues, :count)
+          .from(0)
+          .to(1)
+      end
+
+      context 'when event could not be found' do
+        before do
+          alert_params[:generatorURL] = 'http://somethingelse.com'
+        end
+
+        it 'creates an issue' do
+          expect { subject.perform(project.id, alert_params) }
+            .to change(Issue, :count)
+            .by(1)
+        end
+
+        it 'does not relate issue to an event' do
+          expect { subject.perform(project.id, alert_params) }
+            .not_to change(prometheus_alert.related_issues, :count)
+        end
+      end
+    end
   end
 end
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index d58a5e214ed09998235b7d5d8ee462102b4982b0..d108c811f4b41a1ea1dd38634d612951e3b975fe 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -58,7 +58,6 @@ class CommitStatuses < Grape::API
       post ':id/statuses/:sha' do
         authorize! :create_commit_status, user_project
 
-        commit = @project.commit(params[:sha])
         not_found! 'Commit' unless commit
 
         # Since the CommitStatus is attached to Ci::Pipeline (in the future Pipeline)
@@ -68,14 +67,15 @@ class CommitStatuses < Grape::API
         # If we don't receive it, we will attach the CommitStatus to
         # the first found branch on that commit
 
+        pipeline = all_matching_pipelines.first
+
         ref = params[:ref]
+        ref ||= pipeline&.ref
         ref ||= @project.repository.branch_names_contains(commit.sha).first
         not_found! 'References for commit' unless ref
 
         name = params[:name] || params[:context] || 'default'
 
-        pipeline = @project.pipeline_for(ref, commit.sha, params[:pipeline_id])
-
         unless pipeline
           pipeline = @project.ci_pipelines.create!(
             source: :external,
@@ -126,6 +126,20 @@ class CommitStatuses < Grape::API
         end
       end
       # rubocop: enable CodeReuse/ActiveRecord
+      helpers do
+        def commit
+          strong_memoize(:commit) do
+            user_project.commit(params[:sha])
+          end
+        end
+
+        def all_matching_pipelines
+          pipelines = user_project.ci_pipelines.newest_first(sha: commit.sha)
+          pipelines = pipelines.for_ref(params[:ref]) if params[:ref]
+          pipelines = pipelines.for_id(params[:pipeline_id]) if params[:pipeline_id]
+          pipelines
+        end
+      end
     end
   end
 end
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index df6d27219770e8bf64239bf0d1c6023b907fdc50..e86bcc19b2b07f80ed4ff62383c20cb2fc01efb9 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -115,14 +115,20 @@ def find_by_deploy_key(project, key_id)
       put ":id/deploy_keys/:key_id" do
         deploy_keys_project = find_by_deploy_key(user_project, params[:key_id])
 
-        authorize!(:update_deploy_key, deploy_keys_project.deploy_key)
+        if !can?(current_user, :update_deploy_key, deploy_keys_project.deploy_key) &&
+            !can?(current_user, :update_deploy_keys_project, deploy_keys_project)
+          forbidden!(nil)
+        end
+
+        update_params = {}
+        update_params[:can_push] = params[:can_push] if params.key?(:can_push)
+        update_params[:deploy_key_attributes] = { id: params[:key_id] }
 
-        can_push = params[:can_push].nil? ? deploy_keys_project.can_push : params[:can_push]
-        title = params[:title] || deploy_keys_project.deploy_key.title
+        if can?(current_user, :update_deploy_key, deploy_keys_project.deploy_key)
+          update_params[:deploy_key_attributes][:title] = params[:title] if params.key?(:title)
+        end
 
-        result = deploy_keys_project.update(can_push: can_push,
-                                            deploy_key_attributes: { id: params[:key_id],
-                                                                     title: title })
+        result = deploy_keys_project.update(update_params)
 
         if result
           present deploy_keys_project, with: Entities::DeployKeysProject
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
index eb45df31ff9d951aacf0547ca57314ed2ebccedb..da8825470710c648263418ba9d04bc543ce75571 100644
--- a/lib/api/deployments.rb
+++ b/lib/api/deployments.rb
@@ -42,6 +42,88 @@ class Deployments < Grape::API
 
         present deployment, with: Entities::Deployment
       end
+
+      desc 'Creates a new deployment' do
+        detail 'This feature was introduced in GitLab 12.4'
+        success Entities::Deployment
+      end
+      params do
+        requires :environment,
+          type: String,
+          desc: 'The name of the environment to deploy to'
+
+        requires :sha,
+          type: String,
+          desc: 'The SHA of the commit that was deployed'
+
+        requires :ref,
+          type: String,
+          desc: 'The name of the branch or tag that was deployed'
+
+        requires :tag,
+          type: Boolean,
+          desc: 'A boolean indicating if the deployment ran for a tag'
+
+        requires :status,
+          type: String,
+          desc: 'The status of the deployment',
+          values: %w[running success failed canceled]
+      end
+      post ':id/deployments' do
+        authorize!(:create_deployment, user_project)
+        authorize!(:create_environment, user_project)
+
+        environment = user_project
+          .environments
+          .find_or_create_by_name(params[:environment])
+
+        unless environment.persisted?
+          render_validation_error!(deployment)
+        end
+
+        authorize!(:create_deployment, environment)
+
+        service = ::Deployments::CreateService
+          .new(environment, current_user, declared_params)
+
+        deployment = service.execute
+
+        if deployment.persisted?
+          present(deployment, with: Entities::Deployment, current_user: current_user)
+        else
+          render_validation_error!(deployment)
+        end
+      end
+
+      desc 'Updates an existing deployment' do
+        detail 'This feature was introduced in GitLab 12.4'
+        success Entities::Deployment
+      end
+      params do
+        requires :status,
+          type: String,
+          desc: 'The new status of the deployment',
+          values: %w[running success failed canceled]
+      end
+      put ':id/deployments/:deployment_id' do
+        authorize!(:read_deployment, user_project)
+
+        deployment = user_project.deployments.find(params[:deployment_id])
+
+        authorize!(:update_deployment, deployment)
+
+        if deployment.deployable
+          forbidden!('Deployments created using GitLab CI can not be updated using the API')
+        end
+
+        service = ::Deployments::UpdateService.new(deployment, declared_params)
+
+        if service.execute
+          present(deployment, with: Entities::Deployment, current_user: current_user)
+        else
+          render_validation_error!(deployment)
+        end
+      end
     end
   end
 end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 522f3ed5565c788e867de485e8af0af38e0d32a3..91811efacd72f79026d6a4c97fc35f4e3713e172 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -933,8 +933,8 @@ class CommitStatus < Grape::Entity
     end
 
     class PushEventPayload < Grape::Entity
-      expose :commit_count, :action, :ref_type, :commit_from, :commit_to
-      expose :ref, :commit_title
+      expose :commit_count, :action, :ref_type, :commit_from, :commit_to, :ref,
+             :commit_title, :ref_count
     end
 
     class Event < Grape::Entity
@@ -988,11 +988,11 @@ def todo_target_class(target_type)
 
       def todo_target_url(todo)
         target_type = todo.target_type.underscore
-        target_url = "#{todo.parent.class.to_s.underscore}_#{target_type}_url"
+        target_url = "#{todo.resource_parent.class.to_s.underscore}_#{target_type}_url"
 
         Gitlab::Routing
           .url_helpers
-          .public_send(target_url, todo.parent, todo.target, anchor: todo_target_anchor(todo)) # rubocop:disable GitlabSecurity/PublicSend
+          .public_send(target_url, todo.resource_parent, todo.target, anchor: todo_target_anchor(todo)) # rubocop:disable GitlabSecurity/PublicSend
       end
 
       def todo_target_anchor(todo)
@@ -1315,8 +1315,8 @@ class Release < Grape::Entity
         end
       end
       expose :_links do
-        expose :merge_requests_url
-        expose :issues_url
+        expose :merge_requests_url, if: -> (_) { release_mr_issue_urls_available? }
+        expose :issues_url, if: -> (_) { release_mr_issue_urls_available? }
       end
 
       private
@@ -1347,6 +1347,10 @@ def params_for_issues_and_mrs
         { scope: 'all', state: 'opened', release_tag: object.tag }
       end
 
+      def release_mr_issue_urls_available?
+        ::Feature.enabled?(:release_mr_issue_urls, project)
+      end
+
       def project
         @project ||= object.project
       end
diff --git a/lib/api/members.rb b/lib/api/members.rb
index 461ffe71a621b9b904d6f18985e9da3291506cad..1d4616fed52fa2a12fd6f7d4189d82fcf5cbe8e7 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -18,6 +18,7 @@ class Members < Grape::API
         end
         params do
           optional :query, type: String, desc: 'A query string to search for members'
+          optional :user_ids, type: Array[Integer], desc: 'Array of user ids to look up for membership'
           use :pagination
         end
         # rubocop: disable CodeReuse/ActiveRecord
@@ -26,6 +27,7 @@ class Members < Grape::API
 
           members = source.members.where.not(user_id: nil).includes(:user)
           members = members.joins(:user).merge(User.search(params[:query])) if params[:query].present?
+          members = members.where(user_id: params[:user_ids]) if params[:user_ids].present?
           members = paginate(members)
 
           present members, with: Entities::Member
@@ -37,6 +39,7 @@ class Members < Grape::API
         end
         params do
           optional :query, type: String, desc: 'A query string to search for members'
+          optional :user_ids, type: Array[Integer], desc: 'Array of user ids to look up for membership'
           use :pagination
         end
         # rubocop: disable CodeReuse/ActiveRecord
@@ -45,6 +48,7 @@ class Members < Grape::API
 
           members = find_all_members(source_type, source)
           members = members.includes(:user).references(:user).merge(User.search(params[:query])) if params[:query].present?
+          members = members.where(user_id: params[:user_ids]) if params[:user_ids].present?
           members = paginate(members)
 
           present members, with: Entities::Member
@@ -68,6 +72,23 @@ class Members < Grape::API
         end
         # rubocop: enable CodeReuse/ActiveRecord
 
+        desc 'Gets a member of a group or project, including those who gained membership through ancestor group' do
+          success Entities::Member
+        end
+        params do
+          requires :user_id, type: Integer, desc: 'The user ID of the member'
+        end
+        # rubocop: disable CodeReuse/ActiveRecord
+        get ":id/members/all/:user_id" do
+          source = find_source(source_type, params[:id])
+
+          members = find_all_members(source_type, source)
+          member = members.find_by!(user_id: params[:user_id])
+
+          present member, with: Entities::Member
+        end
+        # rubocop: enable CodeReuse/ActiveRecord
+
         desc 'Adds a member to a group or project.' do
           success Entities::Member
         end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 16fca9acccb83b5fb3d863a59fb2b5d1e3bf530f..89e4da5a42ea888006abe74b09f3815909e498e8 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -80,7 +80,7 @@ class Notes < Grape::API
           note = create_note(noteable, opts)
 
           if note.valid?
-            present note, with: Entities.const_get(note.class.name)
+            present note, with: Entities.const_get(note.class.name, false)
           else
             bad_request!("Note #{note.errors.messages}")
           end
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index e4ef507228ba49559223588f39321b598a6d6a96..c90ba0c9b5db61aed9ae2891d88974d053956b19 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -101,6 +101,8 @@ def filter_attributes_using_license(attrs)
       optional :polling_interval_multiplier, type: BigDecimal, desc: 'Interval multiplier used by endpoints that perform polling. Set to 0 to disable polling.'
       optional :project_export_enabled, type: Boolean, desc: 'Enable project export'
       optional :prometheus_metrics_enabled, type: Boolean, desc: 'Enable Prometheus metrics'
+      optional :push_event_hooks_limit, type: Integer, desc: "Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value."
+      optional :push_event_activities_limit, type: Integer, desc: 'Number of changes (branches or tags) in a single push to determine whether individual push events or bulk push event will be created. Bulk push event will be created if it surpasses that value.'
       optional :recaptcha_enabled, type: Boolean, desc: 'Helps prevent bots from creating accounts'
       given recaptcha_enabled: ->(val) { val } do
         requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha'
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index 404675bfaec6d98317e3bd39f7c556661b2b7251..e3f3aca27dfa3c070e69dc7b807e3a956717542b 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -49,7 +49,7 @@ def find_todos
     resource :todos do
       helpers do
         def issuable_and_awardable?(type)
-          obj_type = Object.const_get(type)
+          obj_type = Object.const_get(type, false)
 
           (obj_type < Issuable) && (obj_type < Awardable)
         rescue NameError
diff --git a/lib/banzai/filter.rb b/lib/banzai/filter.rb
index 7d9766c906c886427759dabfa07924cb424fac89..2438cb3c1666007ed033a75c1ed6c8dbf2236ae4 100644
--- a/lib/banzai/filter.rb
+++ b/lib/banzai/filter.rb
@@ -3,7 +3,7 @@
 module Banzai
   module Filter
     def self.[](name)
-      const_get("#{name.to_s.camelize}Filter")
+      const_get("#{name.to_s.camelize}Filter", false)
     end
   end
 end
diff --git a/lib/banzai/filter/audio_link_filter.rb b/lib/banzai/filter/audio_link_filter.rb
index 83aa520dc4bd2f196433ddc2b146aec718045267..50472c3cf8115dc5409711f9f09f82ca61811e4f 100644
--- a/lib/banzai/filter/audio_link_filter.rb
+++ b/lib/banzai/filter/audio_link_filter.rb
@@ -3,63 +3,15 @@
 # Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/audio.js
 module Banzai
   module Filter
-    # Find every image that isn't already wrapped in an `a` tag, and that has
-    # a `src` attribute ending with an audio extension, add a new audio node and
-    # a "Download" link in the case the audio cannot be played.
-    class AudioLinkFilter < HTML::Pipeline::Filter
-      def call
-        doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |el|
-          el.replace(audio_node(doc, el)) if has_audio_extension?(el)
-        end
-
-        doc
-      end
-
+    class AudioLinkFilter < PlayableLinkFilter
       private
 
-      def has_audio_extension?(element)
-        src = element.attr('data-canonical-src').presence || element.attr('src')
-
-        return unless src.present?
-
-        src_ext = File.extname(src).sub('.', '').downcase
-        Gitlab::FileTypeDetection::SAFE_AUDIO_EXT.include?(src_ext)
+      def media_type
+        "audio"
       end
 
-      def audio_node(doc, element)
-        container = doc.document.create_element(
-          'div',
-          class: 'audio-container'
-        )
-
-        audio = doc.document.create_element(
-          'audio',
-          src: element['src'],
-          controls: true,
-          'data-setup' => '{}',
-          'data-title' => element['title'] || element['alt'])
-
-        link = doc.document.create_element(
-          'a',
-          element['title'] || element['alt'],
-          href: element['src'],
-          target: '_blank',
-          rel: 'noopener noreferrer',
-          title: "Download '#{element['title'] || element['alt']}'")
-
-        # make sure the original non-proxied src carries over
-        if element['data-canonical-src']
-          audio['data-canonical-src'] = element['data-canonical-src']
-          link['data-canonical-src']  = element['data-canonical-src']
-        end
-
-        download_paragraph = doc.document.create_element('p')
-        download_paragraph.children = link
-
-        container.add_child(audio)
-        container.add_child(download_paragraph)
-
-        container
+      def safe_media_ext
+        Gitlab::FileTypeDetection::SAFE_AUDIO_EXT
       end
     end
   end
diff --git a/lib/banzai/filter/playable_link_filter.rb b/lib/banzai/filter/playable_link_filter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0a043aa809cb9aefde14e597b192b47fbe266f9c
--- /dev/null
+++ b/lib/banzai/filter/playable_link_filter.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+module Banzai
+  module Filter
+    # Find every image that isn't already wrapped in an `a` tag, and that has
+    # a `src` attribute ending with an audio or video extension, add a new audio or video node and
+    # a "Download" link in the case the media cannot be played.
+    class PlayableLinkFilter < HTML::Pipeline::Filter
+      def call
+        doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |el|
+          el.replace(media_node(doc, el)) if has_media_extension?(el)
+        end
+
+        doc
+      end
+
+      private
+
+      def media_type
+        raise NotImplementedError
+      end
+
+      def safe_media_ext
+        raise NotImplementedError
+      end
+
+      def extra_element_attrs
+        {}
+      end
+
+      def has_media_extension?(element)
+        src = element.attr('data-canonical-src').presence || element.attr('src')
+
+        return unless src.present?
+
+        src_ext = File.extname(src).sub('.', '').downcase
+        safe_media_ext.include?(src_ext)
+      end
+
+      def media_element(doc, element)
+        media_element_attrs = {
+            src: element['src'],
+            controls: true,
+            'data-setup': '{}',
+            'data-title': element['title'] || element['alt']
+        }.merge!(extra_element_attrs)
+
+        if element['data-canonical-src']
+          media_element_attrs['data-canonical-src'] = element['data-canonical-src']
+        end
+
+        doc.document.create_element(media_type, media_element_attrs)
+      end
+
+      def download_paragraph(doc, element)
+        link_content = element['title'] || element['alt']
+
+        link_element_attrs = {
+          href: element['src'],
+          target: '_blank',
+          rel: 'noopener noreferrer',
+          title: "Download '#{link_content}'"
+        }
+
+        # make sure the original non-proxied src carries over
+        if element['data-canonical-src']
+          link_element_attrs['data-canonical-src'] = element['data-canonical-src']
+        end
+
+        link = doc.document.create_element('a', link_content, link_element_attrs)
+
+        doc.document.create_element('p').tap do |paragraph|
+          paragraph.children = link
+        end
+      end
+
+      def media_node(doc, element)
+        container_element_attrs = { class: "#{media_type}-container" }
+
+        doc.document.create_element( "div", container_element_attrs).tap do |container|
+          container.add_child(media_element(doc, element))
+          container.add_child(download_paragraph(doc, element))
+        end
+      end
+    end
+  end
+end
diff --git a/lib/banzai/filter/video_link_filter.rb b/lib/banzai/filter/video_link_filter.rb
index 0e3293394745221c4b6f55a4abc1a7eae294b53b..ed82fbc1f94692dddaba8df7e87cc9c0a3510f43 100644
--- a/lib/banzai/filter/video_link_filter.rb
+++ b/lib/banzai/filter/video_link_filter.rb
@@ -3,64 +3,19 @@
 # Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/video.js
 module Banzai
   module Filter
-    # Find every image that isn't already wrapped in an `a` tag, and that has
-    # a `src` attribute ending with a video extension, add a new video node and
-    # a "Download" link in the case the video cannot be played.
-    class VideoLinkFilter < HTML::Pipeline::Filter
-      def call
-        doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |el|
-          el.replace(video_node(doc, el)) if has_video_extension?(el)
-        end
-
-        doc
-      end
-
+    class VideoLinkFilter < PlayableLinkFilter
       private
 
-      def has_video_extension?(element)
-        src = element.attr('data-canonical-src').presence || element.attr('src')
-
-        return unless src.present?
-
-        src_ext = File.extname(src).sub('.', '').downcase
-        Gitlab::FileTypeDetection::SAFE_VIDEO_EXT.include?(src_ext)
+      def media_type
+        "video"
       end
 
-      def video_node(doc, element)
-        container = doc.document.create_element(
-          'div',
-          class: 'video-container'
-        )
-
-        video = doc.document.create_element(
-          'video',
-          src: element['src'],
-          width: '100%',
-          controls: true,
-          'data-setup' => '{}',
-          'data-title' => element['title'] || element['alt'])
-
-        link = doc.document.create_element(
-          'a',
-          element['title'] || element['alt'],
-          href: element['src'],
-          target: '_blank',
-          rel: 'noopener noreferrer',
-          title: "Download '#{element['title'] || element['alt']}'")
-
-        # make sure the original non-proxied src carries over
-        if element['data-canonical-src']
-          video['data-canonical-src'] = element['data-canonical-src']
-          link['data-canonical-src']  = element['data-canonical-src']
-        end
-
-        download_paragraph = doc.document.create_element('p')
-        download_paragraph.children = link
-
-        container.add_child(video)
-        container.add_child(download_paragraph)
+      def safe_media_ext
+        Gitlab::FileTypeDetection::SAFE_VIDEO_EXT
+      end
 
-        container
+      def extra_element_attrs
+        { width: "100%" }
       end
     end
   end
diff --git a/lib/banzai/pipeline.rb b/lib/banzai/pipeline.rb
index e8a81bebaa9f49f78b820063d2dd6448031d3553..497d3f27542f54ffdbc9abaf14e77a8d8486b94d 100644
--- a/lib/banzai/pipeline.rb
+++ b/lib/banzai/pipeline.rb
@@ -4,7 +4,7 @@ module Banzai
   module Pipeline
     def self.[](name)
       name ||= :full
-      const_get("#{name.to_s.camelize}Pipeline")
+      const_get("#{name.to_s.camelize}Pipeline", false)
     end
   end
 end
diff --git a/lib/banzai/pipeline/ascii_doc_pipeline.rb b/lib/banzai/pipeline/ascii_doc_pipeline.rb
index 82b99d3de4aba38e449b0ea49df5bb400aa1e6ac..2e8d2bd23b061e68b14bf448d3312539e193b514 100644
--- a/lib/banzai/pipeline/ascii_doc_pipeline.rb
+++ b/lib/banzai/pipeline/ascii_doc_pipeline.rb
@@ -10,6 +10,7 @@ def self.filters
           Filter::SyntaxHighlightFilter,
           Filter::ExternalLinkFilter,
           Filter::PlantumlFilter,
+          Filter::ColorFilter,
           Filter::AsciiDocPostProcessingFilter
         ]
       end
diff --git a/lib/banzai/reference_parser.rb b/lib/banzai/reference_parser.rb
index efe15096f08505ba00a881e134fc753f1d89b808..c08d3364a875dc1f0c8bca740fe27b51989ca099 100644
--- a/lib/banzai/reference_parser.rb
+++ b/lib/banzai/reference_parser.rb
@@ -10,7 +10,7 @@ module ReferenceParser
     #
     # This would return the `Banzai::ReferenceParser::IssueParser` class.
     def self.[](name)
-      const_get("#{name.to_s.camelize}Parser")
+      const_get("#{name.to_s.camelize}Parser", false)
     end
   end
 end
diff --git a/lib/banzai/reference_parser/mentioned_user_parser.rb b/lib/banzai/reference_parser/mentioned_user_parser.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4b1bcb3ca09fd868d24ab1adfa3599a8354e2d6a
--- /dev/null
+++ b/lib/banzai/reference_parser/mentioned_user_parser.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Banzai
+  module ReferenceParser
+    class MentionedUserParser < BaseParser
+      self.reference_type = :user
+
+      def references_relation
+        User
+      end
+
+      # any user can be mentioned by username
+      def can_read_reference?(user, ref_attr, node)
+        true
+      end
+    end
+  end
+end
diff --git a/lib/banzai/reference_parser/mentioned_users_by_group_parser.rb b/lib/banzai/reference_parser/mentioned_users_by_group_parser.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d4ff6a12cd08cc5c977072cf90331ad3d3b4ae6e
--- /dev/null
+++ b/lib/banzai/reference_parser/mentioned_users_by_group_parser.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Banzai
+  module ReferenceParser
+    class MentionedUsersByGroupParser < BaseParser
+      GROUP_ATTR = 'data-group'
+
+      self.reference_type = :user
+
+      def self.data_attribute
+        @data_attribute ||= GROUP_ATTR
+      end
+
+      def references_relation
+        Group
+      end
+
+      def nodes_visible_to_user(user, nodes)
+        groups = lazy { grouped_objects_for_nodes(nodes, Group, GROUP_ATTR) }
+
+        nodes.select do |node|
+          node.has_attribute?(GROUP_ATTR) && can_read_group_reference?(node, user, groups)
+        end
+      end
+
+      def can_read_group_reference?(node, user, groups)
+        node_group = groups[node]
+
+        node_group && can?(user, :read_group, node_group)
+      end
+    end
+  end
+end
diff --git a/lib/banzai/reference_parser/mentioned_users_by_project_parser.rb b/lib/banzai/reference_parser/mentioned_users_by_project_parser.rb
new file mode 100644
index 0000000000000000000000000000000000000000..79258d81cc351d57407756367807042d4857b6c6
--- /dev/null
+++ b/lib/banzai/reference_parser/mentioned_users_by_project_parser.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Banzai
+  module ReferenceParser
+    class MentionedUsersByProjectParser < ProjectParser
+      PROJECT_ATTR = 'data-project'
+
+      self.reference_type = :user
+
+      def self.data_attribute
+        @data_attribute ||= PROJECT_ATTR
+      end
+
+      def references_relation
+        Project
+      end
+    end
+  end
+end
diff --git a/lib/bitbucket/page.rb b/lib/bitbucket/page.rb
index 7cc1342ad6509455ede7dc72bf5f728cef23ccf5..38c689628dd3c0885632a98406f6e149ec6f72c3 100644
--- a/lib/bitbucket/page.rb
+++ b/lib/bitbucket/page.rb
@@ -30,7 +30,7 @@ def parse_values(raw, bitbucket_rep_class)
     end
 
     def representation_class(type)
-      Bitbucket::Representation.const_get(type.to_s.camelize)
+      Bitbucket::Representation.const_get(type.to_s.camelize, false)
     end
   end
 end
diff --git a/lib/bitbucket_server/page.rb b/lib/bitbucket_server/page.rb
index 5d9a3168876c57346cf8061c8efcbaec37a8fb52..304f7cd9d725dbcc621898ea35e54a2dbe396c37 100644
--- a/lib/bitbucket_server/page.rb
+++ b/lib/bitbucket_server/page.rb
@@ -30,7 +30,7 @@ def parse_values(raw, bitbucket_rep_class)
     end
 
     def representation_class(type)
-      BitbucketServer::Representation.const_get(type.to_s.camelize)
+      BitbucketServer::Representation.const_get(type.to_s.camelize, false)
     end
   end
 end
diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb
index 2bd8eb65306ccfff6c4d91452412fde0c3b5784f..92861c567a8ecf9c3113d45e2d1c27d309ae1ea9 100644
--- a/lib/container_registry/client.rb
+++ b/lib/container_registry/client.rb
@@ -36,7 +36,9 @@ def repository_tag_digest(name, reference)
     end
 
     def delete_repository_tag(name, reference)
-      faraday.delete("/v2/#{name}/manifests/#{reference}").success?
+      result = faraday.delete("/v2/#{name}/manifests/#{reference}")
+
+      result.success? || result.status == 404
     end
 
     def upload_raw_blob(path, blob)
@@ -84,7 +86,9 @@ def blob(name, digest, type = nil)
     end
 
     def delete_blob(name, digest)
-      faraday.delete("/v2/#{name}/blobs/#{digest}").success?
+      result = faraday.delete("/v2/#{name}/blobs/#{digest}")
+
+      result.success? || result.status == 404
     end
 
     def put_tag(name, reference, manifest)
diff --git a/lib/feature/gitaly.rb b/lib/feature/gitaly.rb
index 81f8ba5c8c3dd0efa7d83182c691009670648ba8..c23d7025d0f0eb2df84f71b4ee0e19b21eb2d2bc 100644
--- a/lib/feature/gitaly.rb
+++ b/lib/feature/gitaly.rb
@@ -7,7 +7,6 @@ class Gitaly
     # Server feature flags should use '_' to separate words.
     SERVER_FEATURE_FLAGS =
       %w[
-        cache_invalidator
         inforef_uploadpack_cache
         get_all_lfs_pointers_go
       ].freeze
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index 0cc9a6a5fb12e396e1c72f9e755bcf4e7e709378..ad8e693ccbc071e4cfc0725829d5ff44f90c9535 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -69,14 +69,14 @@ def self.ee?
       # means that checking the presence of the License class could result in
       # this method returning `false`, even for an EE installation.
       #
-      # The `IS_GITLAB_EE` is always `string` or `nil`
+      # The `FOSS_ONLY` is always `string` or `nil`
       # Thus the nil or empty string will result
-      # in using default value: true
+      # in using default value: false
       #
       # The behavior needs to be synchronised with
       # config/helpers/is_ee_env.js
       root.join('ee/app/models/license.rb').exist? &&
-        (ENV['IS_GITLAB_EE'].to_s.empty? || Gitlab::Utils.to_boolean(ENV['IS_GITLAB_EE']))
+        !%w[true 1].include?(ENV['FOSS_ONLY'].to_s)
   end
 
   def self.ee
diff --git a/lib/gitlab/background_migration.rb b/lib/gitlab/background_migration.rb
index 2e3a4f3b86955a7830188c75b2db2da76718a2bb..61e0a075018bb33fb35c0dcf4d50716621bda77a 100644
--- a/lib/gitlab/background_migration.rb
+++ b/lib/gitlab/background_migration.rb
@@ -78,7 +78,7 @@ def self.retrying_jobs?(migration_class)
     end
 
     def self.migration_class_for(class_name)
-      const_get(class_name)
+      const_get(class_name, false)
     end
 
     def self.enqueued_job?(queues, migration_class)
diff --git a/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config.rb b/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config.rb
index 29fa0f18448694048a2f5b4389afe878b02929d2..3c142327e94989448bfa7a13076cd57890d7a650 100644
--- a/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config.rb
+++ b/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config.rb
@@ -171,7 +171,11 @@ def safe_perform_one(project, retry_count = 0)
         end
 
         def schedule_retry(project, retry_count)
-          BackgroundMigrationWorker.perform_in(RETRY_DELAY, self.class::RetryOne.name, [project.id, retry_count])
+          # Constants provided to BackgroundMigrationWorker must be within the
+          # scope of Gitlab::BackgroundMigration
+          retry_class_name = self.class::RetryOne.name.sub('Gitlab::BackgroundMigration::', '')
+
+          BackgroundMigrationWorker.perform_in(RETRY_DELAY, retry_class_name, [project.id, retry_count])
         end
       end
 
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index 24bc73e0de58a8b9cd49b433583132adedce27b6..e01ffb631ba031184b36065caaa72f8db4438948 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -104,7 +104,7 @@ def import_issues
             iid: issue.iid,
             title: issue.title,
             description: description,
-            state: issue.state,
+            state_id: Issue.available_states[issue.state],
             author_id: gitlab_user_id(project, issue.author),
             milestone: milestone,
             created_at: issue.created_at,
diff --git a/lib/gitlab/cache/request_cache.rb b/lib/gitlab/cache/request_cache.rb
index 4c658dc0b8d57cd8af89fdc3ea202e326a4de250..6e48ca90054b36d6adae96d40a17477bc49a932e 100644
--- a/lib/gitlab/cache/request_cache.rb
+++ b/lib/gitlab/cache/request_cache.rb
@@ -23,7 +23,7 @@ def request_cache_key(&block)
       end
 
       def request_cache(method_name, &method_key_block)
-        const_get(:RequestCacheExtension).module_eval do
+        const_get(:RequestCacheExtension, false).module_eval do
           cache_key_method_name = "#{method_name}_cache_key"
 
           define_method(method_name) do |*args|
diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb
index b7886114e9c0b6165277f41028ef799dc1f303d3..eb5d78ebcd41aee923ad02c8fe4b340d87254d59 100644
--- a/lib/gitlab/ci/ansi2html.rb
+++ b/lib/gitlab/ci/ansi2html.rb
@@ -178,6 +178,8 @@ def convert(stream, new_state)
 
           close_open_tags
 
+          # TODO: replace OpenStruct with a better type
+          # https://gitlab.com/gitlab-org/gitlab/issues/34305
           OpenStruct.new(
             html: @out.force_encoding(Encoding.default_external),
             state: state,
diff --git a/lib/gitlab/ci/ansi2json.rb b/lib/gitlab/ci/ansi2json.rb
new file mode 100644
index 0000000000000000000000000000000000000000..79114d35916cc46aff83b225c3fcef3e47fe0719
--- /dev/null
+++ b/lib/gitlab/ci/ansi2json.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+# Convert terminal stream to JSON
+module Gitlab
+  module Ci
+    module Ansi2json
+      def self.convert(ansi, state = nil)
+        Converter.new.convert(ansi, state)
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/ci/ansi2json/converter.rb b/lib/gitlab/ci/ansi2json/converter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8d25b66af9c72cf6e152cc75240082345cfc5396
--- /dev/null
+++ b/lib/gitlab/ci/ansi2json/converter.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Ci
+    module Ansi2json
+      class Converter
+        def convert(stream, new_state)
+          @lines = []
+          @state = State.new(new_state, stream.size)
+
+          append = false
+          truncated = false
+
+          cur_offset = stream.tell
+          if cur_offset > @state.offset
+            @state.offset = cur_offset
+            truncated = true
+          else
+            stream.seek(@state.offset)
+            append = @state.offset > 0
+          end
+
+          start_offset = @state.offset
+
+          @state.set_current_line!(style: Style.new(@state.inherited_style))
+
+          stream.each_line do |line|
+            s = StringScanner.new(line)
+            convert_line(s)
+          end
+
+          # This must be assigned before flushing the current line
+          # or the @current_line.offset will advance to the very end
+          # of the trace. Instead we want @last_line_offset to always
+          # point to the beginning of last line.
+          @state.set_last_line_offset
+
+          flush_current_line
+
+          # TODO: replace OpenStruct with a better type
+          # https://gitlab.com/gitlab-org/gitlab/issues/34305
+          OpenStruct.new(
+            lines: @lines,
+            state: @state.encode,
+            append: append,
+            truncated: truncated,
+            offset: start_offset,
+            size: stream.tell - start_offset,
+            total: stream.size
+          )
+        end
+
+        private
+
+        def convert_line(scanner)
+          until scanner.eos?
+
+            if scanner.scan(Gitlab::Regex.build_trace_section_regex)
+              handle_section(scanner)
+            elsif scanner.scan(/\e([@-_])(.*?)([@-~])/)
+              handle_sequence(scanner)
+            elsif scanner.scan(/\e(([@-_])(.*?)?)?$/)
+              break
+            elsif scanner.scan(/</)
+              @state.current_line << '&lt;'
+            elsif scanner.scan(/\r?\n/)
+              # we advance the offset of the next current line
+              # so it does not start from \n
+              flush_current_line(advance_offset: scanner.matched_size)
+            else
+              @state.current_line << scanner.scan(/./m)
+            end
+
+            @state.offset += scanner.matched_size
+          end
+        end
+
+        def handle_sequence(scanner)
+          indicator = scanner[1]
+          commands = scanner[2].split ';'
+          terminator = scanner[3]
+
+          # We are only interested in color and text style changes - triggered by
+          # sequences starting with '\e[' and ending with 'm'. Any other control
+          # sequence gets stripped (including stuff like "delete last line")
+          return unless indicator == '[' && terminator == 'm'
+
+          @state.update_style(commands)
+        end
+
+        def handle_section(scanner)
+          action = scanner[1]
+          timestamp = scanner[2]
+          section = scanner[3]
+
+          section_name = sanitize_section_name(section)
+
+          if action == "start"
+            handle_section_start(section_name, timestamp)
+          elsif action == "end"
+            handle_section_end(section_name, timestamp)
+          end
+        end
+
+        def handle_section_start(section, timestamp)
+          flush_current_line unless @state.current_line.empty?
+          @state.open_section(section, timestamp)
+        end
+
+        def handle_section_end(section, timestamp)
+          return unless @state.section_open?(section)
+
+          flush_current_line unless @state.current_line.empty?
+          @state.close_section(section, timestamp)
+
+          # ensure that section end is detached from the last
+          # line in the section
+          flush_current_line
+        end
+
+        def flush_current_line(advance_offset: 0)
+          @lines << @state.current_line.to_h
+
+          @state.set_current_line!(advance_offset: advance_offset)
+        end
+
+        def sanitize_section_name(section)
+          section.to_s.downcase.gsub(/[^a-z0-9]/, '-')
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/ci/ansi2json/line.rb b/lib/gitlab/ci/ansi2json/line.rb
new file mode 100644
index 0000000000000000000000000000000000000000..173fb1df88ed98da4ad39570e4bb53f337020b7e
--- /dev/null
+++ b/lib/gitlab/ci/ansi2json/line.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Ci
+    module Ansi2json
+      # Line class is responsible for keeping the internal state of
+      # a log line and to finally serialize it as Hash.
+      class Line
+        # Line::Segment is a portion of a line that has its own style
+        # and text. Multiple segments make the line content.
+        class Segment
+          attr_accessor :text, :style
+
+          def initialize(style:)
+            @text = +''
+            @style = style
+          end
+
+          def empty?
+            text.empty?
+          end
+
+          def to_h
+            # Without force encoding to UTF-8 we could get an error
+            # when serializing the Hash to JSON.
+            # Encoding::UndefinedConversionError:
+            #   "\xE2" from ASCII-8BIT to UTF-8
+            { text: text.force_encoding('UTF-8') }.tap do |result|
+              result[:style] = style.to_s if style.set?
+            end
+          end
+        end
+
+        attr_reader :offset, :sections, :segments, :current_segment,
+                    :section_header, :section_duration
+
+        def initialize(offset:, style:, sections: [])
+          @offset = offset
+          @segments = []
+          @sections = sections
+          @section_header = false
+          @duration = nil
+          @current_segment = Segment.new(style: style)
+        end
+
+        def <<(data)
+          @current_segment.text << data
+        end
+
+        def style
+          @current_segment.style
+        end
+
+        def empty?
+          @segments.empty? && @current_segment.empty?
+        end
+
+        def update_style(ansi_commands)
+          @current_segment.style.update(ansi_commands)
+        end
+
+        def add_section(section)
+          @sections << section
+        end
+
+        def set_as_section_header
+          @section_header = true
+        end
+
+        def set_section_duration(duration)
+          @section_duration = Time.at(duration.to_i).strftime('%M:%S')
+        end
+
+        def flush_current_segment!
+          return if @current_segment.empty?
+
+          @segments << @current_segment.to_h
+          @current_segment = Segment.new(style: @current_segment.style)
+        end
+
+        def to_h
+          flush_current_segment!
+
+          { offset: offset, content: @segments }.tap do |result|
+            result[:section] = sections.last if sections.any?
+            result[:section_header] = true if @section_header
+            result[:section_duration] = @section_duration if @section_duration
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/ci/ansi2json/parser.rb b/lib/gitlab/ci/ansi2json/parser.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d428680fb2a8d4e70c5139f1d09c95f5bad7028b
--- /dev/null
+++ b/lib/gitlab/ci/ansi2json/parser.rb
@@ -0,0 +1,200 @@
+# frozen_string_literal: true
+
+# This Parser translates ANSI escape codes into human readable format.
+# It considers color and format changes.
+# Inspired by http://en.wikipedia.org/wiki/ANSI_escape_code
+module Gitlab
+  module Ci
+    module Ansi2json
+      class Parser
+        # keys represent the trailing digit in color changing command (30-37, 40-47, 90-97. 100-107)
+        COLOR = {
+          0 => 'black', # not that this is gray in the intense color table
+          1 => 'red',
+          2 => 'green',
+          3 => 'yellow',
+          4 => 'blue',
+          5 => 'magenta',
+          6 => 'cyan',
+          7 => 'white' # not that this is gray in the dark (aka default) color table
+        }.freeze
+
+        STYLE_SWITCHES = {
+          bold:       0x01,
+          italic:     0x02,
+          underline:  0x04,
+          conceal:    0x08,
+          cross:      0x10
+        }.freeze
+
+        def self.bold?(mask)
+          mask & STYLE_SWITCHES[:bold] != 0
+        end
+
+        def self.matching_formats(mask)
+          formats = []
+          STYLE_SWITCHES.each do |text_format, flag|
+            formats << "term-#{text_format}" if mask & flag != 0
+          end
+
+          formats
+        end
+
+        def initialize(command, ansi_stack = nil)
+          @command = command
+          @ansi_stack = ansi_stack
+        end
+
+        def changes
+          if self.respond_to?("on_#{@command}")
+            send("on_#{@command}", @ansi_stack) # rubocop:disable GitlabSecurity/PublicSend
+          end
+        end
+
+        # rubocop:disable Style/SingleLineMethods
+        def on_0(_) { reset: true } end
+
+        def on_1(_) { enable: STYLE_SWITCHES[:bold] } end
+
+        def on_3(_) { enable: STYLE_SWITCHES[:italic] } end
+
+        def on_4(_) { enable: STYLE_SWITCHES[:underline] } end
+
+        def on_8(_) { enable: STYLE_SWITCHES[:conceal] } end
+
+        def on_9(_) { enable: STYLE_SWITCHES[:cross] } end
+
+        def on_21(_) { disable: STYLE_SWITCHES[:bold] } end
+
+        def on_22(_) { disable: STYLE_SWITCHES[:bold] } end
+
+        def on_23(_) { disable: STYLE_SWITCHES[:italic] } end
+
+        def on_24(_) { disable: STYLE_SWITCHES[:underline] } end
+
+        def on_28(_) { disable: STYLE_SWITCHES[:conceal] } end
+
+        def on_29(_) { disable: STYLE_SWITCHES[:cross] } end
+
+        def on_30(_) { fg: fg_color(0) } end
+
+        def on_31(_) { fg: fg_color(1) } end
+
+        def on_32(_) { fg: fg_color(2) } end
+
+        def on_33(_) { fg: fg_color(3) } end
+
+        def on_34(_) { fg: fg_color(4) } end
+
+        def on_35(_) { fg: fg_color(5) } end
+
+        def on_36(_) { fg: fg_color(6) } end
+
+        def on_37(_) { fg: fg_color(7) } end
+
+        def on_38(stack) { fg: fg_color_256(stack) } end
+
+        def on_39(_) { fg: fg_color(9) } end
+
+        def on_40(_) { bg: bg_color(0) } end
+
+        def on_41(_) { bg: bg_color(1) } end
+
+        def on_42(_) { bg: bg_color(2) } end
+
+        def on_43(_) { bg: bg_color(3) } end
+
+        def on_44(_) { bg: bg_color(4) } end
+
+        def on_45(_) { bg: bg_color(5) } end
+
+        def on_46(_) { bg: bg_color(6) } end
+
+        def on_47(_) { bg: bg_color(7) } end
+
+        def on_48(stack) { bg: bg_color_256(stack) } end
+
+        # TODO: all the x9 never get called?
+        def on_49(_) { fg: fg_color(9) } end
+
+        def on_90(_) { fg: fg_color(0, 'l') } end
+
+        def on_91(_) { fg: fg_color(1, 'l') } end
+
+        def on_92(_) { fg: fg_color(2, 'l') } end
+
+        def on_93(_) { fg: fg_color(3, 'l') } end
+
+        def on_94(_) { fg: fg_color(4, 'l') } end
+
+        def on_95(_) { fg: fg_color(5, 'l') } end
+
+        def on_96(_) { fg: fg_color(6, 'l') } end
+
+        def on_97(_) { fg: fg_color(7, 'l') } end
+
+        def on_99(_) { fg: fg_color(9, 'l') } end
+
+        def on_100(_) { fg: bg_color(0, 'l') } end
+
+        def on_101(_) { fg: bg_color(1, 'l') } end
+
+        def on_102(_) { fg: bg_color(2, 'l') } end
+
+        def on_103(_) { fg: bg_color(3, 'l') } end
+
+        def on_104(_) { fg: bg_color(4, 'l') } end
+
+        def on_105(_) { fg: bg_color(5, 'l') } end
+
+        def on_106(_) { fg: bg_color(6, 'l') } end
+
+        def on_107(_) { fg: bg_color(7, 'l') } end
+
+        def on_109(_) { fg: bg_color(9, 'l') } end
+        # rubocop:enable Style/SingleLineMethods
+
+        def fg_color(color_index, prefix = nil)
+          term_color_class(color_index, ['fg', prefix])
+        end
+
+        def fg_color_256(command_stack)
+          xterm_color_class(command_stack, 'fg')
+        end
+
+        def bg_color(color_index, prefix = nil)
+          term_color_class(color_index, ['bg', prefix])
+        end
+
+        def bg_color_256(command_stack)
+          xterm_color_class(command_stack, 'bg')
+        end
+
+        def term_color_class(color_index, prefix)
+          color_name = COLOR[color_index]
+          return if color_name.nil?
+
+          color_class(['term', prefix, color_name])
+        end
+
+        def xterm_color_class(command_stack, prefix)
+          # the 38 and 48 commands have to be followed by "5" and the color index
+          return unless command_stack.length >= 2
+          return unless command_stack[0] == "5"
+
+          command_stack.shift # ignore the "5" command
+          color_index = command_stack.shift.to_i
+
+          return unless color_index >= 0
+          return unless color_index <= 255
+
+          color_class(["xterm", prefix, color_index])
+        end
+
+        def color_class(segments)
+          [segments].flatten.compact.join('-')
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/ci/ansi2json/state.rb b/lib/gitlab/ci/ansi2json/state.rb
new file mode 100644
index 0000000000000000000000000000000000000000..db7a9035b8b4ca25e329f6b995b9a5cbe2caa919
--- /dev/null
+++ b/lib/gitlab/ci/ansi2json/state.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+# In this class we keep track of the state changes that the
+# Converter makes as it scans through the log stream.
+module Gitlab
+  module Ci
+    module Ansi2json
+      class State
+        attr_accessor :offset, :current_line, :inherited_style, :open_sections, :last_line_offset
+
+        def initialize(new_state, stream_size)
+          @offset = 0
+          @inherited_style = {}
+          @open_sections = {}
+          @stream_size = stream_size
+
+          restore_state!(new_state)
+        end
+
+        def encode
+          state = {
+            offset: @last_line_offset,
+            style: @current_line.style.to_h,
+            open_sections: @open_sections
+          }
+          Base64.urlsafe_encode64(state.to_json)
+        end
+
+        def open_section(section, timestamp)
+          @open_sections[section] = timestamp
+
+          @current_line.add_section(section)
+          @current_line.set_as_section_header
+        end
+
+        def close_section(section, timestamp)
+          return unless section_open?(section)
+
+          duration = timestamp.to_i - @open_sections[section].to_i
+          @current_line.set_section_duration(duration)
+
+          @open_sections.delete(section)
+        end
+
+        def section_open?(section)
+          @open_sections.key?(section)
+        end
+
+        def set_current_line!(style: nil, advance_offset: 0)
+          new_line = Line.new(
+            offset: @offset + advance_offset,
+            style: style || @current_line.style,
+            sections: @open_sections.keys
+          )
+          @current_line = new_line
+        end
+
+        def set_last_line_offset
+          @last_line_offset = @current_line.offset
+        end
+
+        def update_style(commands)
+          @current_line.flush_current_segment!
+          @current_line.update_style(commands)
+        end
+
+        private
+
+        def restore_state!(encoded_state)
+          state = decode_state(encoded_state)
+
+          return unless state
+          return if state['offset'].to_i > @stream_size
+
+          @offset = state['offset'].to_i if state['offset']
+          @open_sections = state['open_sections'] if state['open_sections']
+
+          if state['style']
+            @inherited_style = {
+              fg: state.dig('style', 'fg'),
+              bg: state.dig('style', 'bg'),
+              mask: state.dig('style', 'mask')
+            }
+          end
+        end
+
+        def decode_state(state)
+          return unless state.present?
+
+          decoded_state = Base64.urlsafe_decode64(state)
+          return unless decoded_state.present?
+
+          JSON.parse(decoded_state)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/ci/ansi2json/style.rb b/lib/gitlab/ci/ansi2json/style.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2739ffdfa5d9a9bf1414c3792eaafa2a981ebed4
--- /dev/null
+++ b/lib/gitlab/ci/ansi2json/style.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Ci
+    module Ansi2json
+      class Style
+        attr_reader :fg, :bg, :mask
+
+        def initialize(fg: nil, bg: nil, mask: 0)
+          @fg = fg
+          @bg = bg
+          @mask = mask
+
+          update_formats
+        end
+
+        def update(ansi_commands)
+          command = ansi_commands.shift
+          return unless command
+
+          if changes = Gitlab::Ci::Ansi2json::Parser.new(command, ansi_commands).changes
+            apply_changes(changes)
+          end
+
+          update(ansi_commands)
+        end
+
+        def set?
+          @fg || @bg || @formats.any?
+        end
+
+        def reset!
+          @fg = nil
+          @bg = nil
+          @mask = 0
+          @formats = []
+        end
+
+        def ==(other)
+          self.to_h == other.to_h
+        end
+
+        def to_s
+          [@fg, @bg, @formats].flatten.compact.join(' ')
+        end
+
+        def to_h
+          { fg: @fg, bg: @bg, mask: @mask }
+        end
+
+        private
+
+        def apply_changes(changes)
+          case
+          when changes[:reset]
+            reset!
+          when changes[:fg]
+            @fg = changes[:fg]
+          when changes[:bg]
+            @bg = changes[:bg]
+          when changes[:enable]
+            @mask |= changes[:enable]
+          when changes[:disable]
+            @mask &= ~changes[:disable]
+          else
+            return
+          end
+
+          update_formats
+        end
+
+        def update_formats
+          # Most terminals show bold colored text in the light color variant
+          # Let's mimic that here
+          if @fg.present? && Gitlab::Ci::Ansi2json::Parser.bold?(@mask)
+            @fg = @fg.sub(/fg-([a-z]{2,}+)/, 'fg-l-\1')
+          end
+
+          @formats = Gitlab::Ci::Ansi2json::Parser.matching_formats(@mask)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/ci/build/policy.rb b/lib/gitlab/ci/build/policy.rb
index 43c46ad74af3fc9beff93e8fca2d9ed340f107e5..ebeebe7fb5b000236266cf37e8aee6c347ec4717 100644
--- a/lib/gitlab/ci/build/policy.rb
+++ b/lib/gitlab/ci/build/policy.rb
@@ -6,7 +6,7 @@ module Build
       module Policy
         def self.fabricate(specs)
           specifications = specs.to_h.map do |spec, value|
-            self.const_get(spec.to_s.camelize).new(value)
+            self.const_get(spec.to_s.camelize, false).new(value)
           end
 
           specifications.compact
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index 342dcb2f784375394e2b154a69e27430c81364b6..9c1e6277e95ee74775a45cadd3628bc05e777660 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -78,8 +78,13 @@ def expand_config(config)
       def build_config(config)
         initial_config = Gitlab::Config::Loader::Yaml.new(config).load!
         initial_config = Config::External::Processor.new(initial_config, @context).perform
+        initial_config = Config::Extendable.new(initial_config).to_hash
 
-        Config::Extendable.new(initial_config).to_hash
+        if Feature.enabled?(:ci_pre_post_pipeline_stages, @context.project, default_enabled: true)
+          initial_config = Config::EdgeStagesInjector.new(initial_config).to_hash
+        end
+
+        initial_config
       end
 
       def build_context(project:, sha:, user:)
diff --git a/lib/gitlab/ci/config/edge_stages_injector.rb b/lib/gitlab/ci/config/edge_stages_injector.rb
new file mode 100644
index 0000000000000000000000000000000000000000..64ff9f951e4390a18d8306b114c6922a332e8dc2
--- /dev/null
+++ b/lib/gitlab/ci/config/edge_stages_injector.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Ci
+    class Config
+      class EdgeStagesInjector
+        PRE_PIPELINE = '.pre'
+        POST_PIPELINE = '.post'
+        EDGES = [PRE_PIPELINE, POST_PIPELINE].freeze
+
+        def self.wrap_stages(stages)
+          stages = stages.to_a - EDGES
+          stages.unshift PRE_PIPELINE
+          stages.push POST_PIPELINE
+
+          stages
+        end
+
+        def initialize(config)
+          @config = config.to_h.deep_dup
+        end
+
+        def to_hash
+          if config.key?(:stages)
+            process(:stages)
+          elsif config.key?(:types)
+            process(:types)
+          else
+            config
+          end
+        end
+
+        private
+
+        attr_reader :config
+
+        delegate :wrap_stages, to: :class
+
+        def process(keyword)
+          stages = extract_stages(keyword)
+          return config if stages.empty?
+
+          stages = wrap_stages(stages)
+          config[keyword] = stages
+          config
+        end
+
+        def extract_stages(keyword)
+          stages = config[keyword]
+          return [] unless stages.is_a?(Array)
+
+          stages
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/ci/config/entry/artifacts.rb b/lib/gitlab/ci/config/entry/artifacts.rb
index 41613369ca2e85cb69d7ac1bcbd68afff06e2c62..9d8d7675234297f435d8502b61366f357c9fa451 100644
--- a/lib/gitlab/ci/config/entry/artifacts.rb
+++ b/lib/gitlab/ci/config/entry/artifacts.rb
@@ -12,7 +12,9 @@ class Artifacts < ::Gitlab::Config::Entry::Node
           include ::Gitlab::Config::Entry::Validatable
           include ::Gitlab::Config::Entry::Attributable
 
-          ALLOWED_KEYS = %i[name untracked paths reports when expire_in].freeze
+          ALLOWED_KEYS = %i[name untracked paths reports when expire_in expose_as].freeze
+          EXPOSE_AS_REGEX = /\A\w[-\w ]*\z/.freeze
+          EXPOSE_AS_ERROR_MESSAGE = "can contain only letters, digits, '-', '_' and spaces"
 
           attributes ALLOWED_KEYS
 
@@ -21,11 +23,18 @@ class Artifacts < ::Gitlab::Config::Entry::Node
           validations do
             validates :config, type: Hash
             validates :config, allowed_keys: ALLOWED_KEYS
+            validates :paths, presence: true, if: :expose_as_present?
 
             with_options allow_nil: true do
               validates :name, type: String
               validates :untracked, boolean: true
               validates :paths, array_of_strings: true
+              validates :paths, array_of_strings: {
+                with: /\A[^*]*\z/,
+                message: "can't contain '*' when used with 'expose_as'"
+              }, if: :expose_as_present?
+              validates :expose_as, type: String, length: { maximum: 100 }, if: :expose_as_present?
+              validates :expose_as, format: { with: EXPOSE_AS_REGEX, message: EXPOSE_AS_ERROR_MESSAGE }, if: :expose_as_present?
               validates :reports, type: Hash
               validates :when,
                 inclusion: { in: %w[on_success on_failure always],
@@ -41,6 +50,12 @@ def value
             @config[:reports] = reports_value if @config.key?(:reports)
             @config
           end
+
+          def expose_as_present?
+            return false unless Feature.enabled?(:ci_expose_arbitrary_artifacts_in_mr, default_enabled: true)
+
+            !@config[:expose_as].nil?
+          end
         end
       end
     end
diff --git a/lib/gitlab/ci/config/entry/stages.rb b/lib/gitlab/ci/config/entry/stages.rb
index 2d715cbc6bb3977f619650be26c117e8294a6e42..7e431f0f8bb123ad62c638692f812ca15048b0bb 100644
--- a/lib/gitlab/ci/config/entry/stages.rb
+++ b/lib/gitlab/ci/config/entry/stages.rb
@@ -15,7 +15,7 @@ class Stages < ::Gitlab::Config::Entry::Node
           end
 
           def self.default
-            %w[build test deploy]
+            Config::EdgeStagesInjector.wrap_stages %w[build test deploy]
           end
         end
       end
diff --git a/lib/gitlab/ci/pipeline/seed/deployment.rb b/lib/gitlab/ci/pipeline/seed/deployment.rb
index 238cd845a0e2900cf675b7ae0bc6daad9b13f972..8c90f03cb1de5301e6f516c0382a4b9f2d355bc8 100644
--- a/lib/gitlab/ci/pipeline/seed/deployment.rb
+++ b/lib/gitlab/ci/pipeline/seed/deployment.rb
@@ -22,7 +22,7 @@ def to_resource
             # If there is a validation error on environment creation, such as
             # the name contains invalid character, the job will fall back to a
             # non-environment job.
-            return unless deployment.valid? && deployment.environment.valid?
+            return unless deployment.valid? && deployment.environment.persisted?
 
             deployment.cluster_id =
               deployment.environment.deployment_platform&.cluster_id
diff --git a/lib/gitlab/ci/pipeline/seed/environment.rb b/lib/gitlab/ci/pipeline/seed/environment.rb
index 813a9e9e399a6b0a492ffed337da24a5cc1f4c51..2d3a1e702f9ea0310e80499dca69cc4724de939f 100644
--- a/lib/gitlab/ci/pipeline/seed/environment.rb
+++ b/lib/gitlab/ci/pipeline/seed/environment.rb
@@ -12,7 +12,7 @@ def initialize(job)
           end
 
           def to_resource
-            find_environment || ::Environment.new(attributes)
+            find_environment || ::Environment.create(attributes)
           end
 
           private
diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb
index 2a0bf060c9b2e53a96e78203ee9ccaf71a487734..c29dc51f076de527e00610ccfc2028eacac2f7b5 100644
--- a/lib/gitlab/ci/status/factory.rb
+++ b/lib/gitlab/ci/status/factory.rb
@@ -20,7 +20,7 @@ def fabricate!
 
         def core_status
           Gitlab::Ci::Status
-            .const_get(@status.capitalize)
+            .const_get(@status.capitalize, false)
             .new(@subject, @user)
             .extend(self.class.common_helpers)
         end
diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
index 1ad9dd2913e19a6c3a88bc3f1d3eea7f8d8018a1..5a7642d24ee9fe7ba5cfb259a8573089b47d9075 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -77,15 +77,10 @@ include:
   - template: Jobs/Test.gitlab-ci.yml  # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
   - template: Jobs/Code-Quality.gitlab-ci.yml  # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
   - template: Jobs/Deploy.gitlab-ci.yml  # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+  - template: Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml  # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
   - template: Jobs/Browser-Performance-Testing.gitlab-ci.yml  # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
   - template: Security/DAST.gitlab-ci.yml  # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
   - template: Security/Container-Scanning.gitlab-ci.yml  # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
   - template: Security/Dependency-Scanning.gitlab-ci.yml  # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
   - template: Security/License-Management.gitlab-ci.yml  # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml
   - template: Security/SAST.gitlab-ci.yml  # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
-
-# Override DAST job to exclude master branch
-dast:
-  except:
-    refs:
-      - master
diff --git a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ae2ff9992f9573eb9c0b64cbb384640c3a1f6c5a
--- /dev/null
+++ b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
@@ -0,0 +1,55 @@
+.auto-deploy:
+  image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.1.0"
+
+dast_environment_deploy:
+  extends: .auto-deploy
+  stage: review
+  script:
+    - auto-deploy check_kube_domain
+    - auto-deploy download_chart
+    - auto-deploy ensure_namespace
+    - auto-deploy initialize_tiller
+    - auto-deploy create_secret
+    - auto-deploy deploy
+    - auto-deploy persist_environment_url
+  environment:
+    name: dast-default
+    url: http://dast-$CI_PROJECT_ID-$CI_ENVIRONMENT_SLUG.$KUBE_INGRESS_BASE_DOMAIN
+    on_stop: stop_dast_environment
+  artifacts:
+    paths: [environment_url.txt]
+  only:
+    refs:
+      - branches
+    variables:
+      - $GITLAB_FEATURES =~ /\bdast\b/
+    kubernetes: active
+  except:
+    variables:
+      - $CI_DEFAULT_BRANCH != $CI_COMMIT_REF_NAME
+      - $DAST_DISABLED || $DAST_DISABLED_FOR_DEFAULT_BRANCH
+      - $DAST_WEBSITE # we don't need to create a review app if a URL is already given
+
+stop_dast_environment:
+  extends: .auto-deploy
+  stage: cleanup
+  variables:
+    GIT_STRATEGY: none
+  script:
+    - auto-deploy initialize_tiller
+    - auto-deploy delete
+  environment:
+    name: dast-default
+    action: stop
+  needs: ["dast"]
+  only:
+    refs:
+      - branches
+    variables:
+      - $GITLAB_FEATURES =~ /\bdast\b/
+    kubernetes: active
+  except:
+    variables:
+      - $CI_DEFAULT_BRANCH != $CI_COMMIT_REF_NAME
+      - $DAST_DISABLED || $DAST_DISABLED_FOR_DEFAULT_BRANCH
+      - $DAST_WEBSITE
diff --git a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
index 4b55ffd37716190d8c20cb3db6e957e32aa4f730..23c65a0cb67b8d5904fc466191d8e8a6a076e421 100644
--- a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
@@ -46,3 +46,4 @@ dast:
   except:
     variables:
       - $DAST_DISABLED
+      - $DAST_DISABLED_FOR_DEFAULT_BRANCH && $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb
index e61fb50a3034089c787ebd21a7d872883a98f2f7..20f5620dd64c941e06e8f79329fb2eaaa66e936a 100644
--- a/lib/gitlab/ci/trace/stream.rb
+++ b/lib/gitlab/ci/trace/stream.rb
@@ -63,10 +63,6 @@ def raw(last_lines: nil)
           end.force_encoding(Encoding.default_external)
         end
 
-        def html_with_state(state = nil)
-          ::Gitlab::Ci::Ansi2html.convert(stream, state)
-        end
-
         def html(last_lines: nil)
           text = raw(last_lines: last_lines)
           buffer = StringIO.new(text)
diff --git a/lib/gitlab/cluster/lifecycle_events.rb b/lib/gitlab/cluster/lifecycle_events.rb
index 3fbbccbf56e33d767cc8caa1b1958f537c4a83e3..294ffad02ce4d04f1a998be9da09ded2c7593b8b 100644
--- a/lib/gitlab/cluster/lifecycle_events.rb
+++ b/lib/gitlab/cluster/lifecycle_events.rb
@@ -8,14 +8,50 @@ module Cluster
     # watchdog threads. This lets us abstract away the Unix process
     # lifecycles of Unicorn, Sidekiq, Puma, Puma Cluster, etc.
     #
-    # We have three lifecycle events.
+    # We have the following lifecycle events.
     #
-    # - before_fork (only in forking processes)
-    #     In forking processes (Unicorn and Puma in multiprocess mode) this
-    #     will be called exactly once, on startup, before the workers are
-    #     forked. This will be called in the parent process.
-    # - worker_start
-    # - before_master_restart (only in forking processes)
+    # - on_master_start:
+    #
+    #     Unicorn/Puma Cluster: This will be called exactly once,
+    #       on startup, before the workers are forked. This is
+    #       called in the PARENT/MASTER process.
+    #
+    #     Sidekiq/Puma Single: This is called immediately.
+    #
+    # - on_before_fork:
+    #
+    #     Unicorn/Puma Cluster: This will be called exactly once,
+    #       on startup, before the workers are forked. This is
+    #       called in the PARENT/MASTER process.
+    #
+    #     Sidekiq/Puma Single: This is not called.
+    #
+    # - on_worker_start:
+    #
+    #     Unicorn/Puma Cluster: This is called in the worker process
+    #       exactly once before processing requests.
+    #
+    #     Sidekiq/Puma Single: This is called immediately.
+    #
+    # - on_before_phased_restart:
+    #
+    #     Unicorn/Puma Cluster: This will be called before a graceful
+    #       shutdown of workers starts happening.
+    #       This is called on `master` process.
+    #
+    #     Sidekiq/Puma Single: This is not called.
+    #
+    # - on_before_master_restart:
+    #
+    #     Unicorn: This will be called before a new master is spun up.
+    #       This is called on forked master before `execve` to become
+    #       a new masterfor Unicorn. This means that this does not really
+    #       affect old master process.
+    #
+    #     Puma Cluster: This will be called before a new master is spun up.
+    #       This is called on `master` process.
+    #
+    #     Sidekiq/Puma Single: This is not called.
     #
     # Blocks will be executed in the order in which they are registered.
     #
@@ -34,15 +70,17 @@ def on_worker_start(&block)
         end
 
         def on_before_fork(&block)
-          return unless in_clustered_environment?
-
           # Defer block execution
           (@before_fork_hooks ||= []) << block
         end
 
-        def on_before_master_restart(&block)
-          return unless in_clustered_environment?
+        # Read the config/initializers/cluster_events_before_phased_restart.rb
+        def on_before_phased_restart(&block)
+          # Defer block execution
+          (@master_phased_restart ||= []) << block
+        end
 
+        def on_before_master_restart(&block)
           # Defer block execution
           (@master_restart_hooks ||= []) << block
         end
@@ -70,8 +108,14 @@ def do_before_fork
           end
         end
 
+        def do_before_phased_restart
+          @master_phased_restart&.each do |block|
+            block.call
+          end
+        end
+
         def do_before_master_restart
-          @master_restart_hooks && @master_restart_hooks.each do |block|
+          @master_restart_hooks&.each do |block|
             block.call
           end
         end
diff --git a/lib/gitlab/cluster/mixins/puma_cluster.rb b/lib/gitlab/cluster/mixins/puma_cluster.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e9157d9f1e47db5459e623bd6aa0f9cf8e44a334
--- /dev/null
+++ b/lib/gitlab/cluster/mixins/puma_cluster.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Cluster
+    module Mixins
+      module PumaCluster
+        def self.prepended(base)
+          raise 'missing method Puma::Cluster#stop_workers' unless base.method_defined?(:stop_workers)
+        end
+
+        def stop_workers
+          Gitlab::Cluster::LifecycleEvents.do_before_phased_restart
+
+          super
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/cluster/mixins/unicorn_http_server.rb b/lib/gitlab/cluster/mixins/unicorn_http_server.rb
new file mode 100644
index 0000000000000000000000000000000000000000..765fd0c2baa1a4f459a55c4d0ead98d7473b3f15
--- /dev/null
+++ b/lib/gitlab/cluster/mixins/unicorn_http_server.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Cluster
+    module Mixins
+      module UnicornHttpServer
+        def self.prepended(base)
+          raise 'missing method Unicorn::HttpServer#reexec' unless base.method_defined?(:reexec)
+        end
+
+        def reexec
+          Gitlab::Cluster::LifecycleEvents.do_before_phased_restart
+
+          super
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/config/entry/simplifiable.rb b/lib/gitlab/config/entry/simplifiable.rb
index a56a89adb35fe90892b8ee71be183f273d29969f..d58aba07d15f82a020b39b7ff34944c0259ea03b 100644
--- a/lib/gitlab/config/entry/simplifiable.rb
+++ b/lib/gitlab/config/entry/simplifiable.rb
@@ -37,7 +37,7 @@ def self.strategies
 
         def self.entry_class(strategy)
           if strategy.present?
-            self.const_get(strategy.name)
+            self.const_get(strategy.name, false)
           else
             self::UnknownStrategy
           end
diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb
index 374f929878e2fddd2929985d17b91c8da5368200..8a04cca60d7e874b9cba9a3e594fca6c8511be0a 100644
--- a/lib/gitlab/config/entry/validators.rb
+++ b/lib/gitlab/config/entry/validators.rb
@@ -61,8 +61,15 @@ class ArrayOfStringsValidator < ActiveModel::EachValidator
           include LegacyValidationHelpers
 
           def validate_each(record, attribute, value)
-            unless validate_array_of_strings(value)
-              record.errors.add(attribute, 'should be an array of strings')
+            valid = validate_array_of_strings(value)
+
+            record.errors.add(attribute, 'should be an array of strings') unless valid
+
+            if valid && options[:with]
+              unless value.all? { |v| v =~ options[:with] }
+                message = options[:message] || 'contains elements that do not match the format'
+                record.errors.add(attribute, message)
+              end
             end
           end
         end
diff --git a/lib/gitlab/cycle_analytics/event_fetcher.rb b/lib/gitlab/cycle_analytics/event_fetcher.rb
index 98a30a8fc97b3926b5a7081c44cb0d3f41a6af00..04f4b4f053f0eb5b63909742a329280b87d46fc5 100644
--- a/lib/gitlab/cycle_analytics/event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/event_fetcher.rb
@@ -4,7 +4,7 @@ module Gitlab
   module CycleAnalytics
     module EventFetcher
       def self.[](stage_name)
-        CycleAnalytics.const_get("#{stage_name.to_s.camelize}EventFetcher")
+        CycleAnalytics.const_get("#{stage_name.to_s.camelize}EventFetcher", false)
       end
     end
   end
diff --git a/lib/gitlab/cycle_analytics/stage.rb b/lib/gitlab/cycle_analytics/stage.rb
index 1bd40a7aa18482854d8c5b6280158b6b7cd50e16..5cfd9ea473018e63f5c801b211648d1f1ff52b87 100644
--- a/lib/gitlab/cycle_analytics/stage.rb
+++ b/lib/gitlab/cycle_analytics/stage.rb
@@ -4,7 +4,7 @@ module Gitlab
   module CycleAnalytics
     module Stage
       def self.[](stage_name)
-        CycleAnalytics.const_get("#{stage_name.to_s.camelize}Stage")
+        CycleAnalytics.const_get("#{stage_name.to_s.camelize}Stage", false)
       end
     end
   end
diff --git a/lib/gitlab/cycle_analytics/summary/deploy.rb b/lib/gitlab/cycle_analytics/summary/deploy.rb
index 0691f3cd13139aea85e93b651b2bfc1e1b96002f..5ff8d881143ed94a78ca3bcbccf74d9414917ed1 100644
--- a/lib/gitlab/cycle_analytics/summary/deploy.rb
+++ b/lib/gitlab/cycle_analytics/summary/deploy.rb
@@ -12,7 +12,7 @@ def title
 
         def value
           strong_memoize(:value) do
-            query = @project.deployments.where("created_at >= ?", @from)
+            query = @project.deployments.success.where("created_at >= ?", @from)
             query = query.where("created_at <= ?", @to) if @to
             query.count
           end
diff --git a/lib/gitlab/daemon.rb b/lib/gitlab/daemon.rb
index 43c159fee27b5da2e21d191d6ba4cca370c340a1..8a2538938926e222f51884341ed6799c13fa039a 100644
--- a/lib/gitlab/daemon.rb
+++ b/lib/gitlab/daemon.rb
@@ -34,7 +34,9 @@ def start
       @mutex.synchronize do
         break thread if thread?
 
-        @thread = Thread.new { start_working }
+        if start_working
+          @thread = Thread.new { run_thread }
+        end
       end
     end
 
@@ -57,10 +59,18 @@ def stop
 
     private
 
+    # Executed in lock context before starting thread
+    # Needs to return success
     def start_working
+      true
+    end
+
+    # Executed in separate thread
+    def run_thread
       raise NotImplementedError
     end
 
+    # Executed in lock context
     def stop_working
       # no-ops
     end
diff --git a/lib/gitlab/danger/request_helper.rb b/lib/gitlab/danger/request_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..06da4ed9ad36b71a850b193e3adf3c4da758fd1b
--- /dev/null
+++ b/lib/gitlab/danger/request_helper.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'net/http'
+require 'json'
+
+module Gitlab
+  module Danger
+    module RequestHelper
+      HTTPError = Class.new(RuntimeError)
+
+      # @param [String] url
+      def self.http_get_json(url)
+        rsp = Net::HTTP.get_response(URI.parse(url))
+
+        unless rsp.is_a?(Net::HTTPOK)
+          raise HTTPError, "Failed to read #{url}: #{rsp.code} #{rsp.message}"
+        end
+
+        JSON.parse(rsp.body)
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/danger/roulette.rb b/lib/gitlab/danger/roulette.rb
index 25de0a87c9dfa2fcbc20af36296bb9d2331e96d4..dbf42912882a0a8a03f4a6175b57ead1f43561b5 100644
--- a/lib/gitlab/danger/roulette.rb
+++ b/lib/gitlab/danger/roulette.rb
@@ -1,16 +1,11 @@
 # frozen_string_literal: true
 
-require 'net/http'
-require 'json'
-require 'cgi'
-
 require_relative 'teammate'
 
 module Gitlab
   module Danger
     module Roulette
       ROULETTE_DATA_URL = 'https://about.gitlab.com/roulette.json'
-      HTTPError = Class.new(RuntimeError)
 
       # Looks up the current list of GitLab team members and parses it into a
       # useful form
@@ -19,7 +14,7 @@ module Roulette
       def team
         @team ||=
           begin
-            data = http_get_json(ROULETTE_DATA_URL)
+            data = Gitlab::Danger::RequestHelper.http_get_json(ROULETTE_DATA_URL)
             data.map { |hash| ::Gitlab::Danger::Teammate.new(hash) }
           rescue JSON::ParserError
             raise "Failed to parse JSON response from #{ROULETTE_DATA_URL}"
@@ -44,6 +39,7 @@ def new_random(seed)
 
       # Known issue: If someone is rejected due to OOO, and then becomes not OOO, the
       # selection will change on next spin
+      # @param [Array<Teammate>] people
       def spin_for_person(people, random:)
         people.shuffle(random: random)
           .find(&method(:valid_person?))
@@ -51,32 +47,17 @@ def spin_for_person(people, random:)
 
       private
 
+      # @param [Teammate] person
+      # @return [Boolean]
       def valid_person?(person)
-        !mr_author?(person) && !out_of_office?(person)
+        !mr_author?(person) && person.available?
       end
 
+      # @param [Teammate] person
+      # @return [Boolean]
       def mr_author?(person)
         person.username == gitlab.mr_author
       end
-
-      def out_of_office?(person)
-        username = CGI.escape(person.username)
-        api_endpoint = "https://gitlab.com/api/v4/users/#{username}/status"
-        response = http_get_json(api_endpoint)
-        response["message"]&.match?(/OOO/i)
-      rescue HTTPError, JSON::ParserError
-        false # this is no worse than not checking for OOO
-      end
-
-      def http_get_json(url)
-        rsp = Net::HTTP.get_response(URI.parse(url))
-
-        unless rsp.is_a?(Net::HTTPSuccess)
-          raise HTTPError, "Failed to read #{url}: #{rsp.code} #{rsp.message}"
-        end
-
-        JSON.parse(rsp.body)
-      end
     end
   end
 end
diff --git a/lib/gitlab/danger/teammate.rb b/lib/gitlab/danger/teammate.rb
index 4ad66f61c2b3029a960599179fa603b678915847..5c2324836d76ee8231783caf80da2b79da60553e 100644
--- a/lib/gitlab/danger/teammate.rb
+++ b/lib/gitlab/danger/teammate.rb
@@ -1,5 +1,7 @@
 # frozen_string_literal: true
 
+require 'cgi'
+
 module Gitlab
   module Danger
     class Teammate
@@ -34,8 +36,30 @@ def maintainer?(project, category, labels)
         has_capability?(project, category, :maintainer, labels)
       end
 
+      def status
+        api_endpoint = "https://gitlab.com/api/v4/users/#{CGI.escape(username)}/status"
+        @status ||= Gitlab::Danger::RequestHelper.http_get_json(api_endpoint)
+      rescue Gitlab::Danger::RequestHelper::HTTPError, JSON::ParserError
+        nil # better no status than a crashing Danger
+      end
+
+      # @return [Boolean]
+      def available?
+        !out_of_office? && has_capacity?
+      end
+
       private
 
+      # @return [Boolean]
+      def out_of_office?
+        status&.dig("message")&.match?(/OOO/i) || false
+      end
+
+      # @return [Boolean]
+      def has_capacity?
+        status&.dig("emoji") != 'red_circle'
+      end
+
       def has_capability?(project, category, kind, labels)
         case category
         when :test
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index 3460e07fdc52d73bdf1ad3050072a2abf62322a8..a83b03f540c9e4f314b090ea2cfeed38bb2c2b71 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -107,6 +107,14 @@ def build(
         }
       end
 
+      def build_bulk(action:, ref_type:, changes:)
+        {
+          action: action,
+          ref_count: changes.count,
+          ref_type: ref_type
+        }
+      end
+
       # This method provides a sample data generated with
       # existing project and commits to test webhooks
       def build_sample(project, user)
diff --git a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
index 5422a8631a001be9852199005f6d429e7466e284..dfef158cc1d53754ccedb5d1254264eb9bbdb4b2 100644
--- a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
+++ b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
@@ -33,7 +33,7 @@ def execute!
 
             if result[:status] == :success
               result
-            elsif STEPS_ALLOWED_TO_FAIL.include?(result[:failed_step])
+            elsif STEPS_ALLOWED_TO_FAIL.include?(result[:last_step])
               success
             else
               raise StandardError, result[:message]
@@ -42,121 +42,124 @@ def execute!
 
           private
 
-          def validate_application_settings
+          def validate_application_settings(_result)
             return success if application_settings
 
             log_error('No application_settings found')
             error(_('No application_settings found'))
           end
 
-          def validate_project_created
-            return success unless project_created?
+          def validate_project_created(result)
+            return success(result) unless project_created?
 
             log_error('Project already created')
             error(_('Project already created'))
           end
 
-          def validate_admins
+          def validate_admins(result)
             unless instance_admins.any?
               log_error('No active admin user found')
               return error(_('No active admin user found'))
             end
 
-            success
+            success(result)
           end
 
-          def create_group
+          def create_group(result)
             if project_created?
               log_info(_('Instance administrators group already exists'))
-              @group = application_settings.instance_administration_project.owner
-              return success(group: @group)
+              result[:group] = application_settings.instance_administration_project.owner
+              return success(result)
             end
 
-            @group = ::Groups::CreateService.new(group_owner, create_group_params).execute
+            result[:group] = ::Groups::CreateService.new(group_owner, create_group_params).execute
 
-            if @group.persisted?
-              success(group: @group)
+            if result[:group].persisted?
+              success(result)
             else
               error(_('Could not create group'))
             end
           end
 
-          def create_project
+          def create_project(result)
             if project_created?
               log_info('Instance administration project already exists')
-              @project = application_settings.instance_administration_project
-              return success(project: project)
+              result[:project] = application_settings.instance_administration_project
+              return success(result)
             end
 
-            @project = ::Projects::CreateService.new(group_owner, create_project_params).execute
+            result[:project] = ::Projects::CreateService.new(group_owner, create_project_params(result[:group])).execute
 
-            if project.persisted?
-              success(project: project)
+            if result[:project].persisted?
+              success(result)
             else
-              log_error("Could not create instance administration project. Errors: %{errors}" % { errors: project.errors.full_messages })
+              log_error("Could not create instance administration project. Errors: %{errors}" % { errors: result[:project].errors.full_messages })
               error(_('Could not create project'))
             end
           end
 
-          def save_project_id
+          def save_project_id(result)
             return success if project_created?
 
-            result = application_settings.update(instance_administration_project_id: @project.id)
+            response = application_settings.update(
+              instance_administration_project_id: result[:project].id
+            )
 
-            if result
-              success
+            if response
+              success(result)
             else
               log_error("Could not save instance administration project ID, errors: %{errors}" % { errors: application_settings.errors.full_messages })
               error(_('Could not save project ID'))
             end
           end
 
-          def add_group_members
-            members = @group.add_users(members_to_add, Gitlab::Access::MAINTAINER)
+          def add_group_members(result)
+            group = result[:group]
+            members = group.add_users(members_to_add(group), Gitlab::Access::MAINTAINER)
             errors = members.flat_map { |member| member.errors.full_messages }
 
             if errors.any?
               log_error('Could not add admins as members to self-monitoring project. Errors: %{errors}' % { errors: errors })
               error(_('Could not add admins as members'))
             else
-              success
+              success(result)
             end
           end
 
-          def add_to_whitelist
-            return success unless prometheus_enabled?
-            return success unless prometheus_listen_address.present?
+          def add_to_whitelist(result)
+            return success(result) unless prometheus_enabled?
+            return success(result) unless prometheus_listen_address.present?
 
             uri = parse_url(internal_prometheus_listen_address_uri)
             return error(_('Prometheus listen_address in config/gitlab.yml is not a valid URI')) unless uri
 
             application_settings.add_to_outbound_local_requests_whitelist([uri.normalized_host])
-            result = application_settings.save
+            response = application_settings.save
 
-            if result
+            if response
               # Expire the Gitlab::CurrentSettings cache after updating the whitelist.
               # This happens automatically in an after_commit hook, but in migrations,
               # the after_commit hook only runs at the end of the migration.
               Gitlab::CurrentSettings.expire_current_application_settings
-              success
+              success(result)
             else
               log_error("Could not add prometheus URL to whitelist, errors: %{errors}" % { errors: application_settings.errors.full_messages })
               error(_('Could not add prometheus URL to whitelist'))
             end
           end
 
-          def add_prometheus_manual_configuration
-            return success unless prometheus_enabled?
-            return success unless prometheus_listen_address.present?
+          def add_prometheus_manual_configuration(result)
+            return success(result) unless prometheus_enabled?
+            return success(result) unless prometheus_listen_address.present?
 
-            service = project.find_or_initialize_service('prometheus')
+            service = result[:project].find_or_initialize_service('prometheus')
 
             unless service.update(prometheus_service_attributes)
               log_error('Could not save prometheus manual configuration for self-monitoring project. Errors: %{errors}' % { errors: service.errors.full_messages })
               return error(_('Could not save prometheus manual configuration'))
             end
 
-            success
+            success(result)
           end
 
           def application_settings
@@ -196,11 +199,11 @@ def group_owner
             instance_admins.first
           end
 
-          def members_to_add
+          def members_to_add(group)
             # Exclude admins who are already members of group because
-            # `@group.add_users(users)` returns an error if the users parameter contains
+            # `group.add_users(users)` returns an error if the users parameter contains
             # users who are already members of the group.
-            instance_admins - @group.members.collect(&:user)
+            instance_admins - group.members.collect(&:user)
           end
 
           def create_group_params
@@ -217,13 +220,13 @@ def docs_path
             )
           end
 
-          def create_project_params
+          def create_project_params(group)
             {
               initialize_with_readme: true,
               visibility_level: VISIBILITY_LEVEL,
               name: PROJECT_NAME,
               description: "This project is automatically generated and will be used to help monitor this GitLab instance. [More information](#{docs_path})",
-              namespace_id: @group.id
+              namespace_id: group.id
             }
           end
 
diff --git a/lib/gitlab/diff/position_collection.rb b/lib/gitlab/diff/position_collection.rb
index 59c60f77aaaeb0be9ad8b8749bb744fb5a448cc2..2112d347678e28798cf8b92d9c541536aeb75deb 100644
--- a/lib/gitlab/diff/position_collection.rb
+++ b/lib/gitlab/diff/position_collection.rb
@@ -6,13 +6,13 @@ class PositionCollection
       include Enumerable
 
       # collection - An array of Gitlab::Diff::Position
-      def initialize(collection, diff_head_sha)
+      def initialize(collection, diff_head_sha = nil)
         @collection = collection
         @diff_head_sha = diff_head_sha
       end
 
       def each(&block)
-        @collection.each(&block)
+        filtered_positions.each(&block)
       end
 
       def concat(positions)
@@ -23,9 +23,21 @@ def concat(positions)
       # positions (https://gitlab.com/gitlab-org/gitlab/issues/33271).
       def unfoldable
         select do |position|
-          position.unfoldable? && position.head_sha == @diff_head_sha
+          position.unfoldable? && valid_head_sha?(position)
         end
       end
+
+      private
+
+      def filtered_positions
+        @collection.select { |item| item.is_a?(Position) }
+      end
+
+      def valid_head_sha?(position)
+        return true unless @diff_head_sha
+
+        position.head_sha == @diff_head_sha
+      end
     end
   end
 end
diff --git a/lib/gitlab/discussions_diff/file_collection.rb b/lib/gitlab/discussions_diff/file_collection.rb
index 6692dd764381707308df505f68c2ccc9de067ac3..7a9d4c5c0c27452af883087586d41e7def7785fb 100644
--- a/lib/gitlab/discussions_diff/file_collection.rb
+++ b/lib/gitlab/discussions_diff/file_collection.rb
@@ -27,12 +27,14 @@ def find_by_id(id)
       # - The cache content is not updated (there's no need to do so)
       def load_highlight
         ids = highlightable_collection_ids
+        return if ids.empty?
+
         cached_content = read_cache(ids)
 
         uncached_ids = ids.select.each_with_index { |_, i| cached_content[i].nil? }
         mapping = highlighted_lines_by_ids(uncached_ids)
 
-        HighlightCache.write_multiple(mapping)
+        HighlightCache.write_multiple(mapping) if mapping.any?
 
         diffs = diff_files_indexed_by_id.values_at(*ids)
 
diff --git a/lib/gitlab/downtime_check.rb b/lib/gitlab/downtime_check.rb
index 31bb681039153cc5ebd930a25599ea8527dec2ff..457a3c122069b9939405dc69b0ebd55422fb4261 100644
--- a/lib/gitlab/downtime_check.rb
+++ b/lib/gitlab/downtime_check.rb
@@ -58,13 +58,13 @@ def class_for_migration_file(path)
 
     # Returns true if the given migration can be performed without downtime.
     def online?(migration)
-      migration.const_get(DOWNTIME_CONST) == false
+      migration.const_get(DOWNTIME_CONST, false) == false
     end
 
     # Returns the downtime reason, or nil if none was defined.
     def downtime_reason(migration)
       if migration.const_defined?(DOWNTIME_REASON_CONST)
-        migration.const_get(DOWNTIME_REASON_CONST)
+        migration.const_get(DOWNTIME_REASON_CONST, false)
       else
         nil
       end
diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb
index 678d47150e869890f0738957993b3992522cdf96..895755376ee40bfb48a20eb1859d0c9ddada8b7d 100644
--- a/lib/gitlab/experimentation.rb
+++ b/lib/gitlab/experimentation.rb
@@ -40,8 +40,8 @@ def set_experimentation_subject_id_cookie
         }
       end
 
-      def experiment_enabled?(experiment)
-        Experimentation.enabled?(experiment, experimentation_subject_index)
+      def experiment_enabled?(experiment_key)
+        Experimentation.enabled?(experiment_key, experimentation_subject_index)
       end
 
       private
@@ -55,10 +55,14 @@ def experimentation_subject_index
     end
 
     class << self
+      def experiment(key)
+        Experiment.new(EXPERIMENTS[key].merge(key: key))
+      end
+
       def enabled?(experiment_key, experimentation_subject_index)
         return false unless EXPERIMENTS.key?(experiment_key)
 
-        experiment = Experiment.new(EXPERIMENTS[experiment_key].merge(key: experiment_key))
+        experiment = experiment(experiment_key)
 
         experiment.feature_toggle_enabled? &&
           experiment.enabled_for_environment? &&
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 8fac3621df9c8ffe85d579eb157c38687a44195d..6210223917b4a1273547f5a0cf5ad6c2b74366b1 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -155,10 +155,6 @@ def batch_by_oid(repo, oids)
           end
         end
 
-        def extract_signature(repository, commit_id)
-          repository.gitaly_commit_client.extract_signature(commit_id)
-        end
-
         def extract_signature_lazily(repository, commit_id)
           BatchLoader.for(commit_id).batch(key: repository) do |commit_ids, loader, args|
             batch_signature_extraction(args[:key], commit_ids).each do |commit_id, signature_data|
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index befcc004ee47ff079e8f2d8ccb442713596b7850..b0f29d22ad4c3faf173708e2623b471b539efa83 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -86,7 +86,7 @@ def self.stub_class(name)
       if name == :health_check
         Grpc::Health::V1::Health::Stub
       else
-        Gitaly.const_get(name.to_s.camelcase.to_sym).const_get(:Stub)
+        Gitaly.const_get(name.to_s.camelcase.to_sym, false).const_get(:Stub, false)
       end
     end
 
diff --git a/lib/gitlab/gitaly_client/attributes_bag.rb b/lib/gitlab/gitaly_client/attributes_bag.rb
index 3f1a0ef488865138d9665baef752326ab0e73b8e..f935281ac2ed94656f106d54b4c04ed7af6f91af 100644
--- a/lib/gitlab/gitaly_client/attributes_bag.rb
+++ b/lib/gitlab/gitaly_client/attributes_bag.rb
@@ -8,7 +8,7 @@ module AttributesBag
       extend ActiveSupport::Concern
 
       included do
-        attr_accessor(*const_get(:ATTRS))
+        attr_accessor(*const_get(:ATTRS, false))
       end
 
       def initialize(params)
@@ -26,7 +26,7 @@ def ==(other)
       end
 
       def attributes
-        self.class.const_get(:ATTRS)
+        self.class.const_get(:ATTRS, false)
       end
     end
   end
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 2eaf52355dd639030a486ad38461a879d3b3a280..dca55091be61aa2d436bc7fa2c86caeb2464112e 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -298,18 +298,6 @@ def find_commit(revision)
         Gitlab::SafeRequestStore[key] = commit
       end
 
-      # rubocop: disable CodeReuse/ActiveRecord
-      def patch(revision)
-        request = Gitaly::CommitPatchRequest.new(
-          repository: @gitaly_repo,
-          revision: encode_binary(revision)
-        )
-        response = GitalyClient.call(@repository.storage, :diff_service, :commit_patch, request, timeout: GitalyClient.medium_timeout)
-
-        response.sum(&:data)
-      end
-      # rubocop: enable CodeReuse/ActiveRecord
-
       def commit_stats(revision)
         request = Gitaly::CommitStatsRequest.new(
           repository: @gitaly_repo,
@@ -360,25 +348,6 @@ def filter_shas_with_signatures(shas)
         end
       end
 
-      def extract_signature(commit_id)
-        request = Gitaly::ExtractCommitSignatureRequest.new(repository: @gitaly_repo, commit_id: commit_id)
-        response = GitalyClient.call(@repository.storage, :commit_service, :extract_commit_signature, request, timeout: GitalyClient.fast_timeout)
-
-        signature = +''.b
-        signed_text = +''.b
-
-        response.each do |message|
-          signature << message.signature
-          signed_text << message.signed_text
-        end
-
-        return if signature.blank? && signed_text.blank?
-
-        [signature, signed_text]
-      rescue GRPC::InvalidArgument => ex
-        raise ArgumentError, ex
-      end
-
       def get_commit_signatures(commit_ids)
         request = Gitaly::GetCommitSignaturesRequest.new(repository: @gitaly_repo, commit_ids: commit_ids)
         response = GitalyClient.call(@repository.storage, :commit_service, :get_commit_signatures, request, timeout: GitalyClient.fast_timeout)
diff --git a/lib/gitlab/gitaly_client/conflict_files_stitcher.rb b/lib/gitlab/gitaly_client/conflict_files_stitcher.rb
index 0e00f6e8c445c4265cca3f4a45fa58548374dece..38ec910111ce2b758bfe7750a26baa12e1c9de84 100644
--- a/lib/gitlab/gitaly_client/conflict_files_stitcher.rb
+++ b/lib/gitlab/gitaly_client/conflict_files_stitcher.rb
@@ -5,8 +5,11 @@ module GitalyClient
     class ConflictFilesStitcher
       include Enumerable
 
-      def initialize(rpc_response)
+      attr_reader :gitaly_repo
+
+      def initialize(rpc_response, gitaly_repo)
         @rpc_response = rpc_response
+        @gitaly_repo = gitaly_repo
       end
 
       def each
@@ -31,7 +34,7 @@ def each
 
       def file_from_gitaly_header(header)
         Gitlab::Git::Conflict::File.new(
-          Gitlab::GitalyClient::Util.git_repository(header.repository),
+          Gitlab::GitalyClient::Util.git_repository(gitaly_repo),
           header.commit_oid,
           conflict_from_gitaly_file_header(header),
           ''
diff --git a/lib/gitlab/gitaly_client/conflicts_service.rb b/lib/gitlab/gitaly_client/conflicts_service.rb
index c497bd12738a22df68fb4fbb511ec0ff252a235c..f7eb4b45197ebb1525bb6dfffa523049913e918d 100644
--- a/lib/gitlab/gitaly_client/conflicts_service.rb
+++ b/lib/gitlab/gitaly_client/conflicts_service.rb
@@ -22,7 +22,7 @@ def list_conflict_files
         )
         response = GitalyClient.call(@repository.storage, :conflicts_service, :list_conflict_files, request, timeout: GitalyClient.long_timeout)
 
-        GitalyClient::ConflictFilesStitcher.new(response)
+        GitalyClient::ConflictFilesStitcher.new(response, @gitaly_repo)
       end
 
       def conflicts?
diff --git a/lib/gitlab/gitaly_client/storage_service.rb b/lib/gitlab/gitaly_client/storage_service.rb
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb
index 1e7203cb82a504686a43ead34c544adf71a35300..4da2004b74f09767cd7ad23c7f8a0339ed682531 100644
--- a/lib/gitlab/google_code_import/importer.rb
+++ b/lib/gitlab/google_code_import/importer.rb
@@ -117,7 +117,7 @@ def import_issues
             description:  body,
             author_id:    project.creator_id,
             assignee_ids: [assignee_id],
-            state:        raw_issue['state'] == 'closed' ? 'closed' : 'opened'
+            state_id:     raw_issue['state'] == 'closed' ? Issue.available_states[:closed] : Issue.available_states[:opened]
           )
 
           issue_labels = ::LabelsFinder.new(nil, project_id: project.id, title: labels).execute(skip_authorization: true)
diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb
index 4b797a0e39725294d440a8b16ce4f2f4f58d53e4..dc71d0b427a28db959600163b59ffc53e82a1d13 100644
--- a/lib/gitlab/gpg/commit.rb
+++ b/lib/gitlab/gpg/commit.rb
@@ -10,6 +10,8 @@ def initialize(commit)
 
         repo = commit.project.repository.raw_repository
         @signature_data = Gitlab::Git::Commit.extract_signature_lazily(repo, commit.sha || commit.id)
+
+        lazy_signature
       end
 
       def signature_text
@@ -28,18 +30,16 @@ def has_signature?
         !!(signature_text && signed_text)
       end
 
-      # rubocop: disable CodeReuse/ActiveRecord
       def signature
         return unless has_signature?
 
         return @signature if @signature
 
-        cached_signature = GpgSignature.find_by(commit_sha: @commit.sha)
+        cached_signature = lazy_signature&.itself
         return @signature = cached_signature if cached_signature.present?
 
         @signature = create_cached_signature!
       end
-      # rubocop: enable CodeReuse/ActiveRecord
 
       def update_signature!(cached_signature)
         using_keychain do |gpg_key|
@@ -50,6 +50,14 @@ def update_signature!(cached_signature)
 
       private
 
+      def lazy_signature
+        BatchLoader.for(@commit.sha).batch do |shas, loader|
+          GpgSignature.by_commit_sha(shas).each do |signature|
+            loader.call(signature.commit_sha, signature)
+          end
+        end
+      end
+
       def using_keychain
         Gitlab::Gpg.using_tmp_keychain do
           # first we need to get the fingerprint from the signature to query the gpg
diff --git a/lib/gitlab/graphql/docs/renderer.rb b/lib/gitlab/graphql/docs/renderer.rb
index f47a372aa19ef0b80fad07154ebacf03c6c53f47..41aef64f683c7cd5fd387a3b64331247842dc47a 100644
--- a/lib/gitlab/graphql/docs/renderer.rb
+++ b/lib/gitlab/graphql/docs/renderer.rb
@@ -23,15 +23,12 @@ def initialize(schema, output_dir:, template:)
           @parsed_schema = GraphQLDocs::Parser.new(schema, {}).parse
         end
 
-        def render
-          contents = @layout.render(self)
-
-          write_file(contents)
+        def contents
+          # Render and remove an extra trailing new line
+          @contents ||= @layout.render(self).sub!(/\n(?=\Z)/, '')
         end
 
-        private
-
-        def write_file(contents)
+        def write
           filename = File.join(@output_dir, 'index.md')
 
           FileUtils.mkdir_p(@output_dir)
diff --git a/lib/gitlab/graphql/docs/templates/default.md.haml b/lib/gitlab/graphql/docs/templates/default.md.haml
index cc22d43ab4f844549912d6a398fb1d8379cf1ba0..33acff38ef4c3bb1c54e61101424a0437b82f07e 100644
--- a/lib/gitlab/graphql/docs/templates/default.md.haml
+++ b/lib/gitlab/graphql/docs/templates/default.md.haml
@@ -20,6 +20,3 @@
     - type[:fields].each do |field|
       = "| `#{field[:name]}` | #{render_field_type(field[:type][:info])} | #{field[:description]} |"
     \
-
-
-
diff --git a/lib/gitlab/health_checks/checks.rb b/lib/gitlab/health_checks/checks.rb
deleted file mode 100644
index c4016c5fffd2d8f4525b197a6755119390da196b..0000000000000000000000000000000000000000
--- a/lib/gitlab/health_checks/checks.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
-  module HealthChecks
-    CHECKS = [
-      Gitlab::HealthChecks::DbCheck,
-      Gitlab::HealthChecks::Redis::RedisCheck,
-      Gitlab::HealthChecks::Redis::CacheCheck,
-      Gitlab::HealthChecks::Redis::QueuesCheck,
-      Gitlab::HealthChecks::Redis::SharedStateCheck,
-      Gitlab::HealthChecks::GitalyCheck
-    ].freeze
-  end
-end
diff --git a/lib/gitlab/health_checks/gitaly_check.rb b/lib/gitlab/health_checks/gitaly_check.rb
index f5f142c251fdb236f7e70edb30bcd9223bea46b8..e780bf8a986da0928a79fea19fc8312817a756d1 100644
--- a/lib/gitlab/health_checks/gitaly_check.rb
+++ b/lib/gitlab/health_checks/gitaly_check.rb
@@ -5,7 +5,7 @@ module HealthChecks
     class GitalyCheck
       extend BaseAbstractCheck
 
-      METRIC_PREFIX = 'gitaly_health_check'
+      METRIC_PREFIX = 'gitaly_health_check'.freeze
 
       class << self
         def readiness
diff --git a/lib/gitlab/health_checks/probes/readiness.rb b/lib/gitlab/health_checks/probes/collection.rb
similarity index 78%
rename from lib/gitlab/health_checks/probes/readiness.rb
rename to lib/gitlab/health_checks/probes/collection.rb
index b789cbe1ae6848c72600c5cbd6b0e2a7f8e39ad7..db3ef4834c2d8470c95da77de1882e49c9507658 100644
--- a/lib/gitlab/health_checks/probes/readiness.rb
+++ b/lib/gitlab/health_checks/probes/collection.rb
@@ -3,14 +3,13 @@
 module Gitlab
   module HealthChecks
     module Probes
-      class Readiness
+      class Collection
         attr_reader :checks
 
-        # This accepts an array of Proc
+        # This accepts an array of objects implementing `:readiness`
         # that returns `::Gitlab::HealthChecks::Result`
-        def initialize(*additional_checks)
-          @checks = ::Gitlab::HealthChecks::CHECKS.map { |check| check.method(:readiness) }
-          @checks += additional_checks
+        def initialize(*checks)
+          @checks = checks
         end
 
         def execute
@@ -43,7 +42,7 @@ def payload(readiness)
 
         def probe_readiness
           checks
-            .flat_map(&:call)
+            .flat_map(&:readiness)
             .compact
             .group_by(&:name)
         end
diff --git a/lib/gitlab/health_checks/probes/liveness.rb b/lib/gitlab/health_checks/probes/liveness.rb
deleted file mode 100644
index b4d346e945e5be83dfaa010225a42db709166eed..0000000000000000000000000000000000000000
--- a/lib/gitlab/health_checks/probes/liveness.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
-  module HealthChecks
-    module Probes
-      class Liveness
-        def execute
-          Probes::Status.new(200, status: 'ok')
-        end
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/health_checks/puma_check.rb b/lib/gitlab/health_checks/puma_check.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7aafe29fbae742e51edc1dcf3e16d606429dcbc8
--- /dev/null
+++ b/lib/gitlab/health_checks/puma_check.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module HealthChecks
+    # This check can only be run on Puma `master` process
+    class PumaCheck
+      extend SimpleAbstractCheck
+
+      class << self
+        private
+
+        def metric_prefix
+          'puma_check'
+        end
+
+        def successful?(result)
+          result > 0
+        end
+
+        def check
+          return unless defined?(::Puma)
+
+          stats = Puma.stats
+          stats = JSON.parse(stats)
+
+          # If `workers` is missing this means that
+          # Puma server is running in single mode
+          stats.fetch('workers', 1)
+        rescue NoMethodError
+          # server is not ready
+          0
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/health_checks/simple_abstract_check.rb b/lib/gitlab/health_checks/simple_abstract_check.rb
index 959f28791c3dce676571cd0337237af09ea54085..4e0b929681983111c41522f828649f9f6d2101f5 100644
--- a/lib/gitlab/health_checks/simple_abstract_check.rb
+++ b/lib/gitlab/health_checks/simple_abstract_check.rb
@@ -7,6 +7,8 @@ module SimpleAbstractCheck
 
       def readiness
         check_result = check
+        return if check_result.nil?
+
         if successful?(check_result)
           HealthChecks::Result.new(name, true)
         elsif check_result.is_a?(Timeout::Error)
@@ -20,6 +22,8 @@ def readiness
 
       def metrics
         result, elapsed = with_timing(&method(:check))
+        return if result.nil?
+
         Rails.logger.error("#{human_name} check returned unexpected result #{result}") unless successful?(result) # rubocop:disable Gitlab/RailsLogger
         [
           metric("#{metric_prefix}_timeout", result.is_a?(Timeout::Error) ? 1 : 0),
diff --git a/lib/gitlab/health_checks/unicorn_check.rb b/lib/gitlab/health_checks/unicorn_check.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a30ae0152574edf90019c3ae0201bfdc000650fa
--- /dev/null
+++ b/lib/gitlab/health_checks/unicorn_check.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module HealthChecks
+    # This check can only be run on Unicorn `master` process
+    class UnicornCheck
+      extend SimpleAbstractCheck
+
+      class << self
+        include Gitlab::Utils::StrongMemoize
+
+        private
+
+        def metric_prefix
+          'unicorn_check'
+        end
+
+        def successful?(result)
+          result > 0
+        end
+
+        def check
+          return unless http_servers
+
+          http_servers.sum(&:worker_processes) # rubocop: disable CodeReuse/ActiveRecord
+        end
+
+        # Traversal of ObjectSpace is expensive, on fully loaded application
+        # it takes around 80ms. The instances of HttpServers are not a subject
+        # to change so we can cache the list of servers.
+        def http_servers
+          strong_memoize(:http_servers) do
+            next unless defined?(::Unicorn::HttpServer)
+
+            ObjectSpace.each_object(::Unicorn::HttpServer).to_a
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/hook_data/issue_builder.rb b/lib/gitlab/hook_data/issue_builder.rb
index 1f64e440141c2885befa1480aef79737a5f89377..9d9db6cf94f7a93c70def43d9467949eb9c0739c 100644
--- a/lib/gitlab/hook_data/issue_builder.rb
+++ b/lib/gitlab/hook_data/issue_builder.rb
@@ -27,7 +27,7 @@ def self.safe_hook_attributes
           duplicated_to_id
           project_id
           relative_position
-          state
+          state_id
           time_estimate
           title
           updated_at
@@ -46,7 +46,8 @@ def build
             human_time_estimate: issue.human_time_estimate,
             assignee_ids: issue.assignee_ids,
             assignee_id: issue.assignee_ids.first, # This key is deprecated
-            labels: issue.labels_hook_attrs
+            labels: issue.labels_hook_attrs,
+            state: issue.state
         }
 
         issue.attributes.with_indifferent_access.slice(*self.class.safe_hook_attributes)
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 9ec244b0960b11c01363ec0438ac34582cf8152a..cb85af91f75ed70924ea16751f0a320240a05328 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -292,7 +292,11 @@ def existing_or_new_object
 
             existing_object
           else
-            relation_class.new(parsed_relation_hash)
+            object = relation_class.new
+
+            # Use #assign_attributes here to call object custom setters
+            object.assign_attributes(parsed_relation_hash)
+            object
           end
         end
       end
diff --git a/lib/gitlab/metrics/dashboard/errors.rb b/lib/gitlab/metrics/dashboard/errors.rb
index d41bd2c43c7cadb0bc29b49a18f1e2e19b791e32..264ea0488e77f6686c7ef46b7a4018d0aad26dd2 100644
--- a/lib/gitlab/metrics/dashboard/errors.rb
+++ b/lib/gitlab/metrics/dashboard/errors.rb
@@ -9,6 +9,7 @@ module Dashboard
       module Errors
         DashboardProcessingError = Class.new(StandardError)
         PanelNotFoundError = Class.new(StandardError)
+        MissingIntegrationError = Class.new(StandardError)
         LayoutError = Class.new(DashboardProcessingError)
         MissingQueryError = Class.new(DashboardProcessingError)
 
@@ -22,6 +23,10 @@ def handle_errors(error)
             error("#{dashboard_path} could not be found.", :not_found)
           when PanelNotFoundError
             error(error.message, :not_found)
+          when ::Grafana::Client::Error
+            error(error.message, :service_unavailable)
+          when MissingIntegrationError
+            error('Proxy support for this API is not available currently', :bad_request)
           else
             raise error
           end
diff --git a/lib/gitlab/metrics/dashboard/processor.rb b/lib/gitlab/metrics/dashboard/processor.rb
index bfdee76a818e7c9647e0fe903418e85e3d203a13..9566e5afb9a49915437bc3c4f1772f332f437753 100644
--- a/lib/gitlab/metrics/dashboard/processor.rb
+++ b/lib/gitlab/metrics/dashboard/processor.rb
@@ -17,7 +17,10 @@ def initialize(project, dashboard, sequence, params)
 
         # Returns a new dashboard hash with the results of
         # running transforms on the dashboard.
+        # @return [Hash, nil]
         def process
+          return unless @dashboard
+
           @dashboard.deep_symbolize_keys.tap do |dashboard|
             @sequence.each do |stage|
               stage.new(@project, dashboard, @params).transform!
diff --git a/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb b/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ce75c54d01497919d40b5b243567ec76002cbc89
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb
@@ -0,0 +1,224 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Metrics
+    module Dashboard
+      module Stages
+        class GrafanaFormatter < BaseStage
+          include Gitlab::Utils::StrongMemoize
+
+          CHART_TYPE = 'area-chart'
+          PROXY_PATH = 'api/v1/query_range'
+
+          # Reformats the specified panel in the Gitlab
+          # dashboard-yml format
+          def transform!
+            InputFormatValidator.new(
+              grafana_dashboard,
+              datasource,
+              panel,
+              query_params
+            ).validate!
+
+            new_dashboard = formatted_dashboard
+
+            dashboard.clear
+            dashboard.merge!(new_dashboard)
+          end
+
+          private
+
+          def formatted_dashboard
+            { panel_groups: [{ panels: [formatted_panel] }] }
+          end
+
+          def formatted_panel
+            {
+              title:   panel[:title],
+              type:    CHART_TYPE,
+              y_label: '', # Grafana panels do not include a Y-Axis label
+              metrics: panel[:targets].map.with_index do |target, idx|
+                formatted_metric(target, idx)
+              end
+            }
+          end
+
+          def formatted_metric(metric, idx)
+            {
+              id:                       "#{metric[:legendFormat]}_#{idx}",
+              query_range:              format_query(metric),
+              label:                    replace_variables(metric[:legendFormat]),
+              prometheus_endpoint_path: prometheus_endpoint_for_metric(metric)
+            }.compact
+          end
+
+          # Panel specified by the url from the Grafana dashboard
+          def panel
+            strong_memoize(:panel) do
+              grafana_dashboard[:dashboard][:panels].find do |panel|
+                panel[:id].to_s == query_params[:panelId]
+              end
+            end
+          end
+
+          # Grafana url query parameters. Includes information
+          # on which panel to select and time range.
+          def query_params
+            strong_memoize(:query_params) do
+              Gitlab::Metrics::Dashboard::Url.parse_query(grafana_url)
+            end
+          end
+
+          # Endpoint which will return prometheus metric data
+          # for the metric
+          def prometheus_endpoint_for_metric(metric)
+            Gitlab::Routing.url_helpers.project_grafana_api_path(
+              project,
+              datasource_id: datasource[:id],
+              proxy_path: PROXY_PATH,
+              query: format_query(metric)
+            )
+          end
+
+          # Reformats query for compatibility with prometheus api.
+          def format_query(metric)
+            expression = remove_new_lines(metric[:expr])
+            expression = replace_variables(expression)
+            expression = replace_global_variables(expression, metric)
+
+            expression
+          end
+
+          # Accomodates instance-defined Grafana variables.
+          # These are variables defined by users, and values
+          # must be provided in the query parameters.
+          def replace_variables(expression)
+            return expression unless grafana_dashboard[:dashboard][:templating]
+
+            grafana_dashboard[:dashboard][:templating][:list]
+              .sort_by { |variable| variable[:name].length }
+              .each do |variable|
+                variable_value = query_params[:"var-#{variable[:name]}"]
+
+                expression = expression.gsub("$#{variable[:name]}", variable_value)
+                expression = expression.gsub("[[#{variable[:name]}]]", variable_value)
+                expression = expression.gsub("{{#{variable[:name]}}}", variable_value)
+              end
+
+            expression
+          end
+
+          # Replaces Grafana global built-in variables with values.
+          # Only $__interval and $__from and $__to are supported.
+          #
+          # See https://grafana.com/docs/reference/templating/#global-built-in-variables
+          def replace_global_variables(expression, metric)
+            expression = expression.gsub('$__interval', metric[:interval]) if metric[:interval]
+            expression = expression.gsub('$__from', query_params[:from])
+            expression = expression.gsub('$__to', query_params[:to])
+
+            expression
+          end
+
+          # Removes new lines from expression.
+          def remove_new_lines(expression)
+            expression.gsub(/\R+/, '')
+          end
+
+          # Grafana datasource object corresponding to the
+          # specified dashboard
+          def datasource
+            params[:datasource]
+          end
+
+          # The specified Grafana dashboard
+          def grafana_dashboard
+            params[:grafana_dashboard]
+          end
+
+          # The URL specifying which Grafana panel to embed
+          def grafana_url
+            params[:grafana_url]
+          end
+        end
+
+        class InputFormatValidator
+          include ::Gitlab::Metrics::Dashboard::Errors
+
+          attr_reader :grafana_dashboard, :datasource, :panel, :query_params
+
+          UNSUPPORTED_GRAFANA_GLOBAL_VARS = %w(
+            $__interval_ms
+            $__timeFilter
+            $__name
+            $timeFilter
+            $interval
+          ).freeze
+
+          def initialize(grafana_dashboard, datasource, panel, query_params)
+            @grafana_dashboard = grafana_dashboard
+            @datasource = datasource
+            @panel = panel
+            @query_params = query_params
+          end
+
+          def validate!
+            validate_query_params!
+            validate_datasource!
+            validate_panel_type!
+            validate_variable_definitions!
+            validate_global_variables!
+          end
+
+          private
+
+          def validate_datasource!
+            return if datasource[:access] == 'proxy' && datasource[:type] == 'prometheus'
+
+            raise_error 'Only Prometheus datasources with proxy access in Grafana are supported.'
+          end
+
+          def validate_query_params!
+            return if [:panelId, :from, :to].all? { |param| query_params.include?(param) }
+
+            raise_error 'Grafana query parameters must include panelId, from, and to.'
+          end
+
+          def validate_panel_type!
+            return if panel[:type] == 'graph' && panel[:lines]
+
+            raise_error 'Panel type must be a line graph.'
+          end
+
+          def validate_variable_definitions!
+            return unless grafana_dashboard[:dashboard][:templating]
+
+            return if grafana_dashboard[:dashboard][:templating][:list].all? do |variable|
+              query_params[:"var-#{variable[:name]}"].present?
+            end
+
+            raise_error 'All Grafana variables must be defined in the query parameters.'
+          end
+
+          def validate_global_variables!
+            return unless panel_contains_unsupported_vars?
+
+            raise_error 'Prometheus must not include'
+          end
+
+          def panel_contains_unsupported_vars?
+            panel[:targets].any? do |target|
+              UNSUPPORTED_GRAFANA_GLOBAL_VARS.any? do |variable|
+                target[:expr].include?(variable)
+              end
+            end
+          end
+
+          def raise_error(message)
+            raise DashboardProcessingError.new(message)
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/metrics/exporter/base_exporter.rb b/lib/gitlab/metrics/exporter/base_exporter.rb
index b56770e224b81923525331c4b590590eb2bb37bb..7111835c85a903dc671818a80fd4ce778b54fd41 100644
--- a/lib/gitlab/metrics/exporter/base_exporter.rb
+++ b/lib/gitlab/metrics/exporter/base_exporter.rb
@@ -6,6 +6,8 @@ module Exporter
       class BaseExporter < Daemon
         attr_reader :server
 
+        attr_accessor :readiness_checks
+
         def enabled?
           settings.enabled
         end
@@ -32,21 +34,31 @@ def start_working
             Port: settings.port, BindAddress: settings.address,
             Logger: logger, AccessLog: access_log)
           server.mount_proc '/readiness' do |req, res|
-            render_probe(
-              ::Gitlab::HealthChecks::Probes::Readiness.new, req, res)
+            render_probe(readiness_probe, req, res)
           end
           server.mount_proc '/liveness' do |req, res|
-            render_probe(
-              ::Gitlab::HealthChecks::Probes::Liveness.new, req, res)
+            render_probe(liveness_probe, req, res)
           end
           server.mount '/', Rack::Handler::WEBrick, rack_app
-          server.start
+
+          true
+        end
+
+        def run_thread
+          server&.start
+        rescue IOError
+          # ignore forcibily closed servers
         end
 
         def stop_working
           if server
-            server.shutdown
-            server.listeners.each(&:close)
+            # we close sockets if thread is not longer running
+            # this happens, when the process forks
+            if thread.alive?
+              server.shutdown
+            else
+              server.listeners.each(&:close)
+            end
           end
 
           @server = nil
@@ -60,6 +72,14 @@ def rack_app
           end
         end
 
+        def readiness_probe
+          ::Gitlab::HealthChecks::Probes::Collection.new(*readiness_checks)
+        end
+
+        def liveness_probe
+          ::Gitlab::HealthChecks::Probes::Collection.new
+        end
+
         def render_probe(probe, req, res)
           result = probe.execute
 
diff --git a/lib/gitlab/metrics/exporter/sidekiq_exporter.rb b/lib/gitlab/metrics/exporter/sidekiq_exporter.rb
index 4de95edfc181d357bde8bcaf3d2136ba5aeca3f7..5ba7b29734be1d0957f07e770d109a7fd85da6ed 100644
--- a/lib/gitlab/metrics/exporter/sidekiq_exporter.rb
+++ b/lib/gitlab/metrics/exporter/sidekiq_exporter.rb
@@ -14,6 +14,29 @@ def settings
         def log_filename
           File.join(Rails.root, 'log', 'sidekiq_exporter.log')
         end
+
+        private
+
+        # Sidekiq Exporter does not work properly in sidekiq-cluster
+        # mode. It tries to start the service on the same port for
+        # each of the cluster workers, this results in failure
+        # due to duplicate binding.
+        #
+        # For now we ignore this error, as metrics are still "kind of"
+        # valid as they are rendered from shared directory.
+        #
+        # Issue: https://gitlab.com/gitlab-org/gitlab/issues/5714
+        def start_working
+          super
+        rescue Errno::EADDRINUSE => e
+          Sidekiq.logger.error(
+            class: self.class.to_s,
+            message: 'Cannot start sidekiq_exporter',
+            exception: e.message
+          )
+
+          false
+        end
       end
     end
   end
diff --git a/lib/gitlab/metrics/exporter/web_exporter.rb b/lib/gitlab/metrics/exporter/web_exporter.rb
index fac7043352a0f35e2631bc28a98c99d6d1d4e2df..3940f6fa155a3956da8f7c9d9479e4b08a21dc90 100644
--- a/lib/gitlab/metrics/exporter/web_exporter.rb
+++ b/lib/gitlab/metrics/exporter/web_exporter.rb
@@ -7,13 +7,60 @@ module Gitlab
   module Metrics
     module Exporter
       class WebExporter < BaseExporter
+        ExporterCheck = Struct.new(:exporter) do
+          def readiness
+            Gitlab::HealthChecks::Result.new(
+              'web_exporter', exporter.running)
+          end
+        end
+
+        attr_reader :running
+
+        # This exporter is always run on master process
+        def initialize
+          super
+
+          self.readiness_checks = [
+            WebExporter::ExporterCheck.new(self),
+            Gitlab::HealthChecks::PumaCheck,
+            Gitlab::HealthChecks::UnicornCheck
+          ]
+        end
+
         def settings
-          Settings.monitoring.web_exporter
+          Gitlab.config.monitoring.web_exporter
         end
 
         def log_filename
           File.join(Rails.root, 'log', 'web_exporter.log')
         end
+
+        private
+
+        def start_working
+          @running = true
+          super
+        end
+
+        def stop_working
+          @running = false
+          wait_in_blackout_period if server && thread.alive?
+          super
+        end
+
+        def wait_in_blackout_period
+          return unless blackout_seconds > 0
+
+          @server.logger.info(
+            message: 'starting blackout...',
+            duration_s: blackout_seconds)
+
+          sleep(blackout_seconds)
+        end
+
+        def blackout_seconds
+          settings['blackout_seconds'].to_i
+        end
       end
     end
   end
diff --git a/lib/gitlab/metrics/samplers/base_sampler.rb b/lib/gitlab/metrics/samplers/base_sampler.rb
index d7d848d2833dedfcce6cb875fd6e116640c50b96..90051f85f311be2f96713e6e06c40b8777723ee4 100644
--- a/lib/gitlab/metrics/samplers/base_sampler.rb
+++ b/lib/gitlab/metrics/samplers/base_sampler.rb
@@ -50,6 +50,11 @@ def sleep_interval
 
         def start_working
           @running = true
+
+          true
+        end
+
+        def run_thread
           sleep(sleep_interval)
           while running
             safe_sample
diff --git a/lib/gitlab/metrics/samplers/puma_sampler.rb b/lib/gitlab/metrics/samplers/puma_sampler.rb
index 8a24d4f3663facaedf7f426eeeba754c46858020..f788f51b1ce155ecdb378682310e48ef1fc755e0 100644
--- a/lib/gitlab/metrics/samplers/puma_sampler.rb
+++ b/lib/gitlab/metrics/samplers/puma_sampler.rb
@@ -1,7 +1,5 @@
 # frozen_string_literal: true
 
-require 'puma/state_file'
-
 module Gitlab
   module Metrics
     module Samplers
diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb
index 51f48095cb57f0a32891a7c1c12caf9babcb733c..2a61b3de405d5e94678ddbb0ef065b5f96fb0581 100644
--- a/lib/gitlab/metrics/system.rb
+++ b/lib/gitlab/metrics/system.rb
@@ -63,6 +63,21 @@ def self.real_time(precision = :float_second)
       def self.monotonic_time
         Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second)
       end
+
+      def self.thread_cpu_time
+        # Not all OS kernels are supporting `Process::CLOCK_THREAD_CPUTIME_ID`
+        # Refer: https://gitlab.com/gitlab-org/gitlab/issues/30567#note_221765627
+        return unless defined?(Process::CLOCK_THREAD_CPUTIME_ID)
+
+        Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :float_second)
+      end
+
+      def self.thread_cpu_duration(start_time)
+        end_time = thread_cpu_time
+        return unless start_time && end_time
+
+        end_time - start_time
+      end
     end
   end
 end
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index ba2a0b2ecf8a61dc36cfe993ecc27bd663540c20..115368c8bc6fb8e9f9cf68d1be372a0e2de3b981 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -44,6 +44,10 @@ def duration_milliseconds
         duration.in_milliseconds.to_i
       end
 
+      def thread_cpu_duration
+        System.thread_cpu_duration(@thread_cputime_start)
+      end
+
       def allocated_memory
         @memory_after - @memory_before
       end
@@ -53,12 +57,14 @@ def run
 
         @memory_before = System.memory_usage
         @started_at = System.monotonic_time
+        @thread_cputime_start = System.thread_cpu_time
 
         yield
       ensure
         @memory_after = System.memory_usage
         @finished_at = System.monotonic_time
 
+        self.class.gitlab_transaction_cputime_seconds.observe(labels, thread_cpu_duration)
         self.class.gitlab_transaction_duration_seconds.observe(labels, duration)
         self.class.gitlab_transaction_allocated_memory_bytes.observe(labels, allocated_memory * 1024.0)
 
@@ -142,6 +148,12 @@ def action
         "#{labels[:controller]}##{labels[:action]}" if labels && !labels.empty?
       end
 
+      define_histogram :gitlab_transaction_cputime_seconds do
+        docstring 'Transaction thread cputime'
+        base_labels BASE_LABELS
+        buckets [0.1, 0.25, 0.5, 1.0, 2.5, 5.0]
+      end
+
       define_histogram :gitlab_transaction_duration_seconds do
         docstring 'Transaction duration'
         base_labels BASE_LABELS
diff --git a/lib/gitlab/patch/prependable.rb b/lib/gitlab/patch/prependable.rb
index a9f6cfb19cbf52937a19244e6cb8ea9819a18205..22ece0a6a8b2f04a3d47558f2cc0d5c219addfb8 100644
--- a/lib/gitlab/patch/prependable.rb
+++ b/lib/gitlab/patch/prependable.rb
@@ -24,7 +24,7 @@ def prepend_features(base)
         super
 
         if const_defined?(:ClassMethods)
-          klass_methods = const_get(:ClassMethods)
+          klass_methods = const_get(:ClassMethods, false)
           base.singleton_class.prepend klass_methods
           base.instance_variable_set(:@_prepended_class_methods, klass_methods)
         end
@@ -40,7 +40,7 @@ def class_methods
         super
 
         if instance_variable_defined?(:@_prepended_class_methods)
-          const_get(:ClassMethods).prepend @_prepended_class_methods
+          const_get(:ClassMethods, false).prepend @_prepended_class_methods
         end
       end
 
diff --git a/lib/gitlab/phabricator_import/base_worker.rb b/lib/gitlab/phabricator_import/base_worker.rb
index b69c65e78f8976aaef0a16d657360e0c467a072b..d2c2ef8db48ac7c8bc71df252dcbd78652c56822 100644
--- a/lib/gitlab/phabricator_import/base_worker.rb
+++ b/lib/gitlab/phabricator_import/base_worker.rb
@@ -23,6 +23,8 @@ class BaseWorker
       include ProjectImportOptions # This marks the project as failed after too many tries
       include Gitlab::ExclusiveLeaseHelpers
 
+      feature_category :importers
+
       class << self
         def schedule(project_id, *args)
           perform_async(project_id, *args)
diff --git a/lib/gitlab/quick_actions/extractor.rb b/lib/gitlab/quick_actions/extractor.rb
index ff9bb293b47063694b0683c092fc6e8ee657fda6..e04d6f250b1aa407f217a5989dbe8f1fb825634a 100644
--- a/lib/gitlab/quick_actions/extractor.rb
+++ b/lib/gitlab/quick_actions/extractor.rb
@@ -50,7 +50,7 @@ def extract_commands(content, only: nil)
 
         content, commands = perform_substitutions(content, commands)
 
-        [content.strip, commands]
+        [content.rstrip, commands]
       end
 
       private
@@ -109,7 +109,7 @@ def commands_regex(only:)
                 [ ]
                 (?<arg>[^\n]*)
               )?
-              (?:\n|$)
+              (?:\s*\n|$)
             )
         }mix
       end
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index 00f817c23998e6066b7d20a4e4e1908f178f4976..ea2b03b42c17533ff087c465a83ea65bbe64b2ef 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -3,7 +3,8 @@
 module Gitlab
   # Extract possible GFM references from an arbitrary String for further processing.
   class ReferenceExtractor < Banzai::ReferenceExtractor
-    REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range directly_addressed_user epic).freeze
+    REFERABLES = %i(user issue label milestone
+                    merge_request snippet commit commit_range directly_addressed_user epic).freeze
     attr_accessor :project, :current_user, :author
 
     def initialize(project, current_user = nil)
@@ -54,9 +55,9 @@ def all
     def self.references_pattern
       return @pattern if @pattern
 
-      patterns = REFERABLES.map do |ref|
-        ref.to_s.classify.constantize.try(:reference_pattern)
-      end
+      patterns = REFERABLES.map do |type|
+        Banzai::ReferenceParser[type].reference_type.to_s.classify.constantize.try(:reference_pattern)
+      end.uniq
 
       @pattern = Regexp.union(patterns.compact)
     end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 4bfa6f7e9a5921268ac48bfe2533770a787f5c69..3d1f15c72ae82a1d4d2141026ea862d418a10281 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -119,6 +119,15 @@ def jira_transition_id_regex
     def breakline_regex
       @breakline_regex ||= /\r\n|\r|\n/
     end
+
+    # https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html
+    def aws_arn_regex
+      /\Aarn:\S+\z/
+    end
+
+    def aws_arn_regex_message
+      "must be a valid Amazon Resource Name"
+    end
   end
 end
 
diff --git a/lib/gitlab/request_context.rb b/lib/gitlab/request_context.rb
index ab2549d5e6820cecac2c8cdd67d2d5b1e30fd2a4..13187836e025047eef9f4e4200f6a41b0bb85aa1 100644
--- a/lib/gitlab/request_context.rb
+++ b/lib/gitlab/request_context.rb
@@ -6,6 +6,10 @@ class << self
       def client_ip
         Gitlab::SafeRequestStore[:client_ip]
       end
+
+      def start_thread_cpu_time
+        Gitlab::SafeRequestStore[:start_thread_cpu_time]
+      end
     end
 
     def initialize(app)
@@ -23,6 +27,8 @@ def call(env)
 
       Gitlab::SafeRequestStore[:client_ip] = req.ip
 
+      Gitlab::SafeRequestStore[:start_thread_cpu_time] = Gitlab::Metrics::System.thread_cpu_time
+
       @app.call(env)
     end
   end
diff --git a/lib/gitlab/sidekiq_daemon/memory_killer.rb b/lib/gitlab/sidekiq_daemon/memory_killer.rb
index eb58435e3f1384385b4cb450e842da592422581e..9d0d67a488ffee69b2b153ed26107ace4ddc2bcf 100644
--- a/lib/gitlab/sidekiq_daemon/memory_killer.rb
+++ b/lib/gitlab/sidekiq_daemon/memory_killer.rb
@@ -21,15 +21,47 @@ class MemoryKiller < Daemon
       # In case not set, default to 300M. This is for extra-safe.
       DEFAULT_MAX_MEMORY_GROWTH_KB = 300_000
 
+      # Phases of memory killer
+      PHASE = {
+        running: 1,
+        above_soft_limit: 2,
+        stop_fetching_new_jobs: 3,
+        shutting_down: 4,
+        killing_sidekiq: 5
+      }.freeze
+
       def initialize
         super
 
         @enabled = true
+        @metrics = init_metrics
       end
 
       private
 
-      def start_working
+      def init_metrics
+        {
+          sidekiq_current_rss:                  ::Gitlab::Metrics.gauge(:sidekiq_current_rss, 'Current RSS of Sidekiq Worker'),
+          sidekiq_memory_killer_soft_limit_rss: ::Gitlab::Metrics.gauge(:sidekiq_memory_killer_soft_limit_rss, 'Current soft_limit_rss of Sidekiq Worker'),
+          sidekiq_memory_killer_hard_limit_rss: ::Gitlab::Metrics.gauge(:sidekiq_memory_killer_hard_limit_rss, 'Current hard_limit_rss of Sidekiq Worker'),
+          sidekiq_memory_killer_phase:          ::Gitlab::Metrics.gauge(:sidekiq_memory_killer_phase, 'Current phase of Sidekiq Worker')
+        }
+      end
+
+      def refresh_state(phase)
+        @phase = PHASE.fetch(phase)
+        @current_rss = get_rss
+        @soft_limit_rss = get_soft_limit_rss
+        @hard_limit_rss = get_hard_limit_rss
+
+        # track the current state as prometheus gauges
+        @metrics[:sidekiq_memory_killer_phase].set({}, @phase)
+        @metrics[:sidekiq_current_rss].set({}, @current_rss)
+        @metrics[:sidekiq_memory_killer_soft_limit_rss].set({}, @soft_limit_rss)
+        @metrics[:sidekiq_memory_killer_hard_limit_rss].set({}, @hard_limit_rss)
+      end
+
+      def run_thread
         Sidekiq.logger.info(
           class: self.class.to_s,
           action: 'start',
@@ -77,41 +109,51 @@ def restart_sidekiq
         # Tell Sidekiq to stop fetching new jobs
         # We first SIGNAL and then wait given time
         # We also monitor a number of running jobs and allow to restart early
+        refresh_state(:stop_fetching_new_jobs)
         signal_and_wait(SHUTDOWN_TIMEOUT_SECONDS, 'SIGTSTP', 'stop fetching new jobs')
         return unless enabled?
 
         # Tell sidekiq to restart itself
         # Keep extra safe to wait `Sidekiq.options[:timeout] + 2` seconds before SIGKILL
+        refresh_state(:shutting_down)
         signal_and_wait(Sidekiq.options[:timeout] + 2, 'SIGTERM', 'gracefully shut down')
         return unless enabled?
 
         # Ideally we should never reach this condition
         # Wait for Sidekiq to shutdown gracefully, and kill it if it didn't
         # Kill the whole pgroup, so we can be sure no children are left behind
+        refresh_state(:killing_sidekiq)
         signal_pgroup('SIGKILL', 'die')
       end
 
       def rss_within_range?
-        current_rss = nil
+        refresh_state(:running)
+
         deadline = Gitlab::Metrics::System.monotonic_time + GRACE_BALLOON_SECONDS.seconds
         loop do
           return true unless enabled?
 
-          current_rss = get_rss
-
           # RSS go above hard limit should trigger forcible shutdown right away
-          break if current_rss > hard_limit_rss
+          break if @current_rss > @hard_limit_rss
 
           # RSS go below the soft limit
-          return true if current_rss < soft_limit_rss
+          return true if @current_rss < @soft_limit_rss
 
           # RSS did not go below the soft limit within deadline, restart
           break if Gitlab::Metrics::System.monotonic_time > deadline
 
           sleep(CHECK_INTERVAL_SECONDS)
+
+          refresh_state(:above_soft_limit)
         end
 
-        log_rss_out_of_range(current_rss, hard_limit_rss, soft_limit_rss)
+        # There are two chances to break from loop:
+        #   - above hard limit, or
+        #   - above soft limit after deadline
+        # When `above hard limit`, it immediately go to `stop_fetching_new_jobs`
+        # So ignore `above hard limit` and always set `above_soft_limit` here
+        refresh_state(:above_soft_limit)
+        log_rss_out_of_range(@current_rss, @hard_limit_rss, @soft_limit_rss)
 
         false
       end
@@ -143,11 +185,11 @@ def get_rss
         output.to_i
       end
 
-      def soft_limit_rss
+      def get_soft_limit_rss
         SOFT_LIMIT_RSS_KB + rss_increase_by_jobs
       end
 
-      def hard_limit_rss
+      def get_hard_limit_rss
         HARD_LIMIT_RSS_KB
       end
 
diff --git a/lib/gitlab/sidekiq_daemon/monitor.rb b/lib/gitlab/sidekiq_daemon/monitor.rb
index 09f30837cd2d3f96156b25aebae2d4aeceb5a4b7..a3d61c69ae1989c93d41275248bf4eb7b962cf7f 100644
--- a/lib/gitlab/sidekiq_daemon/monitor.rb
+++ b/lib/gitlab/sidekiq_daemon/monitor.rb
@@ -61,7 +61,7 @@ def self.cancel_job(jid)
 
       private
 
-      def start_working
+      def run_thread
         return unless notification_channel_enabled?
 
         begin
diff --git a/lib/gitlab/submodule_links.rb b/lib/gitlab/submodule_links.rb
index 18fd604a3b03335ad019c88b62af697a6fd98670..b0ee0877f304a6f2b7f81e9dc339fadc2b69cb7e 100644
--- a/lib/gitlab/submodule_links.rb
+++ b/lib/gitlab/submodule_links.rb
@@ -6,6 +6,7 @@ class SubmoduleLinks
 
     def initialize(repository)
       @repository = repository
+      @cache_store = {}
     end
 
     def for(submodule, sha)
@@ -18,8 +19,9 @@ def for(submodule, sha)
     attr_reader :repository
 
     def submodule_urls_for(sha)
-      strong_memoize(:"submodule_urls_for_#{sha}") do
-        repository.submodule_urls_for(sha)
+      @cache_store.fetch(sha) do
+        submodule_urls = repository.submodule_urls_for(sha)
+        @cache_store[sha] = submodule_urls
       end
     end
 
diff --git a/lib/gitlab/tracking/incident_management.rb b/lib/gitlab/tracking/incident_management.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bd8d1669dd36f4847d7d6485fcdc758755cf90e4
--- /dev/null
+++ b/lib/gitlab/tracking/incident_management.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Tracking
+    module IncidentManagement
+      class << self
+        def track_from_params(incident_params)
+          return if incident_params.blank?
+
+          incident_params.each do |k, v|
+            prefix = ['', '0'].include?(v.to_s) ? 'disabled' : 'enabled'
+
+            key = tracking_keys.dig(k, :name)
+            label = tracking_keys.dig(k, :label)
+
+            next if key.blank?
+
+            details = label ? { label: label, property: v } : {}
+
+            ::Gitlab::Tracking.event('IncidentManagement::Settings', "#{prefix}_#{key}", **details )
+          end
+        end
+
+        def tracking_keys
+          {
+            create_issue: {
+              name: 'issue_auto_creation_on_alerts'
+            },
+            issue_template_key: {
+              name: 'issue_template_on_alerts',
+              label: 'Template name'
+            },
+            send_email: {
+              name: 'sending_emails'
+            }
+          }.with_indifferent_access.freeze
+        end
+      end
+    end
+  end
+end
diff --git a/lib/grafana/client.rb b/lib/grafana/client.rb
index 0765630f9bb41f917caae1287f03f15096135ef1..b419f79bace6ba27855099a8f971f8c3bb6ef9fa 100644
--- a/lib/grafana/client.rb
+++ b/lib/grafana/client.rb
@@ -11,6 +11,18 @@ def initialize(api_url:, token:)
       @token = token
     end
 
+    # @param uid [String] Unique identifier for a Grafana dashboard
+    def get_dashboard(uid:)
+      http_get("#{@api_url}/api/dashboards/uid/#{uid}")
+    end
+
+    # @param name [String] Unique identifier for a Grafana datasource
+    def get_datasource(name:)
+      # CGI#escape formats strings such that the Grafana endpoint
+      # will not recognize the dashboard name. Preferring URI#escape.
+      http_get("#{@api_url}/api/datasources/name/#{URI.escape(name)}") # rubocop:disable Lint/UriEscapeUnescape
+    end
+
     # @param datasource_id [String] Grafana ID for the datasource
     # @param proxy_path [String] Path to proxy - ex) 'api/v1/query_range'
     def proxy_datasource(datasource_id:, proxy_path:, query: {})
@@ -57,7 +69,7 @@ def handle_request_exceptions
     def handle_response(response)
       return response if response.code == 200
 
-      raise_error "Grafana response status code: #{response.code}"
+      raise_error "Grafana response status code: #{response.code}, Message: #{response.body}"
     end
 
     def raise_error(message)
diff --git a/lib/tasks/gitlab/graphql.rake b/lib/tasks/gitlab/graphql.rake
index fd8df015903020917656dc1f20fd6776f9bd29ff..902f22684eeb6f430857a1ad8c639abceb0aeab6 100644
--- a/lib/tasks/gitlab/graphql.rake
+++ b/lib/tasks/gitlab/graphql.rake
@@ -11,10 +11,28 @@ namespace :gitlab do
     task compile_docs: :environment do
       renderer = Gitlab::Graphql::Docs::Renderer.new(GitlabSchema.graphql_definition, render_options)
 
-      renderer.render
+      renderer.write
 
       puts "Documentation compiled."
     end
+
+    desc 'GitLab | Check if GraphQL docs are up to date'
+    task check_docs: :environment do
+      renderer = Gitlab::Graphql::Docs::Renderer.new(GitlabSchema.graphql_definition, render_options)
+
+      doc = File.read(Rails.root.join(OUTPUT_DIR, 'index.md'))
+
+      if doc == renderer.contents
+        puts "GraphQL documentation is up to date"
+      else
+        puts '#' * 10
+        puts '#'
+        puts '# GraphQL documentation is outdated! Please update it by running `bundle exec rake gitlab:graphql:compile_docs`.'
+        puts '#'
+        puts '#' * 10
+        abort
+      end
+    end
   end
 end
 
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index abd47f018f1c6d3ca4927aca311ba9c553dbd768..a592015963d7d40c7cd17e9cb9b116c62621177b 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -43,7 +43,7 @@ namespace :gitlab do
 
         [
           %w(bin/install) + repository_storage_paths_args,
-          %w(bin/compile)
+          %w(make build)
         ].each do |cmd|
           unless Kernel.system(*cmd)
             raise "command failed: #{cmd.join(' ')}"
diff --git a/lib/uploaded_file.rb b/lib/uploaded_file.rb
index aae542f02acfe9c0a315439040ac12a4ee7e3aba..424db653fb85d3c83d3c6f0091f4f44fa4833c49 100644
--- a/lib/uploaded_file.rb
+++ b/lib/uploaded_file.rb
@@ -6,6 +6,7 @@
 
 class UploadedFile
   InvalidPathError = Class.new(StandardError)
+  UnknownSizeError = Class.new(StandardError)
 
   # The filename, *not* including the path, of the "uploaded" file
   attr_reader :original_filename
@@ -18,37 +19,50 @@ class UploadedFile
 
   attr_reader :remote_id
   attr_reader :sha256
-
-  def initialize(path, filename: nil, content_type: "application/octet-stream", sha256: nil, remote_id: nil)
-    raise InvalidPathError, "#{path} file does not exist" unless ::File.exist?(path)
+  attr_reader :size
+
+  def initialize(path, filename: nil, content_type: "application/octet-stream", sha256: nil, remote_id: nil, size: nil)
+    if path.present?
+      raise InvalidPathError, "#{path} file does not exist" unless ::File.exist?(path)
+
+      @tempfile = File.new(path, 'rb')
+      @size = @tempfile.size
+    else
+      begin
+        @size = Integer(size)
+      rescue ArgumentError, TypeError
+        raise UnknownSizeError, 'Unable to determine file size'
+      end
+    end
 
     @content_type = content_type
-    @original_filename = sanitize_filename(filename || path)
+    @original_filename = sanitize_filename(filename || path || '')
     @content_type = content_type
     @sha256 = sha256
     @remote_id = remote_id
-    @tempfile = File.new(path, 'rb')
   end
 
   def self.from_params(params, field, upload_paths)
-    unless params["#{field}.path"]
-      raise InvalidPathError, "file is invalid" if params["#{field}.remote_id"]
-
-      return
-    end
-
-    file_path = File.realpath(params["#{field}.path"])
-
-    paths = Array(upload_paths) << Dir.tmpdir
-    unless self.allowed_path?(file_path, paths.compact)
-      raise InvalidPathError, "insecure path used '#{file_path}'"
+    path = params["#{field}.path"]
+    remote_id = params["#{field}.remote_id"]
+    return if path.blank? && remote_id.blank?
+
+    file_path = nil
+    if path
+      file_path = File.realpath(path)
+
+      paths = Array(upload_paths) << Dir.tmpdir
+      unless self.allowed_path?(file_path, paths.compact)
+        raise InvalidPathError, "insecure path used '#{file_path}'"
+      end
     end
 
     UploadedFile.new(file_path,
       filename: params["#{field}.name"],
       content_type: params["#{field}.type"] || 'application/octet-stream',
       sha256: params["#{field}.sha256"],
-      remote_id: params["#{field}.remote_id"])
+      remote_id: remote_id,
+      size: params["#{field}.size"])
   end
 
   def self.allowed_path?(file_path, paths)
@@ -68,7 +82,11 @@ def sanitize_filename(name)
   end
 
   def path
-    @tempfile.path
+    @tempfile&.path
+  end
+
+  def close
+    @tempfile&.close
   end
 
   alias_method :local_path, :path
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 3b3eb4d649e89fc0835259d0e090741a471197dd..6f66b51951d788d0676bd558e8fbca767597a434 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -62,6 +62,9 @@ msgstr ""
 msgid " or references (e.g. path/to/project!merge_request_id)"
 msgstr ""
 
+msgid "\"%{path}\" did not exist on \"%{ref}\""
+msgstr ""
+
 msgid "%d comment"
 msgid_plural "%d comments"
 msgstr[0] ""
@@ -250,6 +253,9 @@ msgstr ""
 msgid "%{firstLabel} +%{labelCount} more"
 msgstr ""
 
+msgid "%{from} to %{to}"
+msgstr ""
+
 msgid "%{gitlab_ci_yml} not found in this commit"
 msgstr ""
 
@@ -659,6 +665,12 @@ msgstr ""
 msgid "A merge request approval is required when the license compliance report contains a blacklisted license."
 msgstr ""
 
+msgid "A new Release %{tag} for %{name} was published. Visit the %{release_link_start}Releases page%{release_link_end} to read more about it."
+msgstr ""
+
+msgid "A new Release %{tag} for %{name} was published. Visit the Releases page to read more about it:"
+msgstr ""
+
 msgid "A new branch will be created in your fork and a new merge request will be started."
 msgstr ""
 
@@ -677,7 +689,7 @@ msgstr ""
 msgid "A ready-to-go template for use with iOS Swift apps."
 msgstr ""
 
-msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgid "A regular expression that will be used to find the test coverage output in the job log. Leave blank to disable"
 msgstr ""
 
 msgid "A secure token that identifies an external storage request."
@@ -1413,10 +1425,7 @@ msgstr ""
 msgid "Allow users to register any application to use GitLab as an OAuth provider"
 msgstr ""
 
-msgid "Allow users to request access"
-msgstr ""
-
-msgid "Allow users to request access if visibility is public or internal."
+msgid "Allow users to request access (if visibility is public or internal)"
 msgstr ""
 
 msgid "Allowed email domain restriction only permitted for top-level groups"
@@ -1509,6 +1518,9 @@ msgstr ""
 msgid "An error occurred while fetching environments."
 msgstr ""
 
+msgid "An error occurred while fetching exposed artifacts."
+msgstr ""
+
 msgid "An error occurred while fetching folder content."
 msgstr ""
 
@@ -1773,6 +1785,9 @@ msgstr ""
 msgid "Applied"
 msgstr ""
 
+msgid "Apply"
+msgstr ""
+
 msgid "Apply a label"
 msgstr ""
 
@@ -2012,6 +2027,9 @@ msgstr ""
 msgid "Assets"
 msgstr ""
 
+msgid "Assets:"
+msgstr ""
+
 msgid "Assign"
 msgstr ""
 
@@ -2379,21 +2397,15 @@ msgstr ""
 msgid "Billing"
 msgstr ""
 
-msgid "BillingPlans|%{group_name} is currently using the %{plan_link} plan."
+msgid "BillingPlans|%{group_name} is currently using the %{plan_name} plan."
 msgstr ""
 
-msgid "BillingPlans|@%{user_name} you are currently using the %{plan_link} plan."
+msgid "BillingPlans|@%{user_name} you are currently using the %{plan_name} plan."
 msgstr ""
 
 msgid "BillingPlans|Congratulations, your new trial is activated"
 msgstr ""
 
-msgid "BillingPlans|Current plan"
-msgstr ""
-
-msgid "BillingPlans|Downgrade"
-msgstr ""
-
 msgid "BillingPlans|If you would like to downgrade your plan please contact %{support_link_start}Customer Support%{support_link_end}."
 msgstr ""
 
@@ -2418,15 +2430,15 @@ msgstr ""
 msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
 msgstr ""
 
-msgid "BillingPlans|Upgrade"
-msgstr ""
-
 msgid "BillingPlans|Your GitLab.com trial expired on %{expiration_date}. %{learn_more_text}"
 msgstr ""
 
 msgid "BillingPlans|Your GitLab.com trial will <strong>expire after %{expiration_date}</strong>. You can learn more about GitLab.com Gold by reading about our %{features_link}."
 msgstr ""
 
+msgid "BillingPlans|billed annually at %{price_per_year}"
+msgstr ""
+
 msgid "BillingPlans|features"
 msgstr ""
 
@@ -2436,13 +2448,10 @@ msgstr ""
 msgid "BillingPlans|monthly"
 msgstr ""
 
-msgid "BillingPlans|paid annually at %{price_per_year}"
-msgstr ""
-
 msgid "BillingPlans|per user"
 msgstr ""
 
-msgid "BillingPlan|Upgrade plan"
+msgid "BillingPlan|Upgrade"
 msgstr ""
 
 msgid "Bitbucket Server Import"
@@ -3069,6 +3078,9 @@ msgstr ""
 msgid "Choose a type..."
 msgstr ""
 
+msgid "Choose an existing tag, or create a new one"
+msgstr ""
+
 msgid "Choose any color."
 msgstr ""
 
@@ -3348,9 +3360,6 @@ msgstr ""
 msgid "Closes this %{quick_action_target}."
 msgstr ""
 
-msgid "Cluster %{cluster} was used."
-msgstr ""
-
 msgid "Cluster Health"
 msgstr ""
 
@@ -3531,6 +3540,15 @@ msgstr ""
 msgid "ClusterIntegration|Create cluster on"
 msgstr ""
 
+msgid "ClusterIntegration|Create new Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Create new Cluster on EKS"
+msgstr ""
+
+msgid "ClusterIntegration|Create new Cluster on GKE"
+msgstr ""
+
 msgid "ClusterIntegration|Did you know?"
 msgstr ""
 
@@ -3543,6 +3561,9 @@ msgstr ""
 msgid "ClusterIntegration|Enable this setting if using role-based access control (RBAC)."
 msgstr ""
 
+msgid "ClusterIntegration|Enter the details for your Amazon EKS Kubernetes cluster"
+msgstr ""
+
 msgid "ClusterIntegration|Enter the details for your Kubernetes cluster"
 msgstr ""
 
@@ -3804,7 +3825,7 @@ msgstr ""
 msgid "ClusterIntegration|RBAC-enabled cluster"
 msgstr ""
 
-msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
+msgid "ClusterIntegration|Read our %{link_start}help page%{link_end} on Kubernetes cluster integration."
 msgstr ""
 
 msgid "ClusterIntegration|Region"
@@ -4020,9 +4041,6 @@ msgstr ""
 msgid "ClusterIntegration|documentation"
 msgstr ""
 
-msgid "ClusterIntegration|help page"
-msgstr ""
-
 msgid "ClusterIntegration|installed via %{installed_via}"
 msgstr ""
 
@@ -4355,16 +4373,19 @@ msgstr ""
 msgid "ContainerRegistry|Copy build command"
 msgstr ""
 
+msgid "ContainerRegistry|Copy login command"
+msgstr ""
+
 msgid "ContainerRegistry|Copy push command"
 msgstr ""
 
 msgid "ContainerRegistry|Docker connection error"
 msgstr ""
 
-msgid "ContainerRegistry|Last Updated"
+msgid "ContainerRegistry|If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have %{twofaDocLinkStart}Two-Factor Authentication%{twofaDocLinkEnd} enabled, use a %{personalAccessTokensDocLinkStart}Personal Access Token%{personalAccessTokensDocLinkEnd} instead of a password."
 msgstr ""
 
-msgid "ContainerRegistry|No tags in Container Registry for this container image."
+msgid "ContainerRegistry|Last Updated"
 msgstr ""
 
 msgid "ContainerRegistry|Quick Start"
@@ -4390,15 +4411,27 @@ msgstr ""
 msgid "ContainerRegistry|Tag ID"
 msgstr ""
 
+msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
+msgstr ""
+
+msgid "ContainerRegistry|There are no container images available in this group"
+msgstr ""
+
 msgid "ContainerRegistry|There are no container images stored for this project"
 msgstr ""
 
+msgid "ContainerRegistry|This image has no active tags"
+msgstr ""
+
 msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
 msgstr ""
 
 msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
 msgstr ""
 
+msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. Push at least one Docker image in one of this group's projects in order to show up here. %{docLinkStart}More Information%{docLinkEnd}"
+msgstr ""
+
 msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
 msgstr ""
 
@@ -4795,6 +4828,9 @@ msgstr ""
 msgid "Current Branch"
 msgstr ""
 
+msgid "Current Plan"
+msgstr ""
+
 msgid "Current Project"
 msgstr ""
 
@@ -4810,7 +4846,10 @@ msgstr ""
 msgid "CurrentUser|Settings"
 msgstr ""
 
-msgid "Custom CI config path"
+msgid "Custom CI configuration path"
+msgstr ""
+
+msgid "Custom Git clone URL for HTTP(S)"
 msgstr ""
 
 msgid "Custom hostname (for private commit emails)"
@@ -4828,6 +4867,9 @@ msgstr ""
 msgid "Custom project templates have not been set up for groups that you are a member of. They are enabled from a group’s settings page. Contact your group’s Owner or Maintainer to setup custom project templates."
 msgstr ""
 
+msgid "Custom range"
+msgstr ""
+
 msgid "CustomCycleAnalytics|Add a stage"
 msgstr ""
 
@@ -5409,6 +5451,27 @@ msgstr ""
 msgid "Deploying to"
 msgstr ""
 
+msgid "Deployment|API"
+msgstr ""
+
+msgid "Deployment|This deployment was created using the API"
+msgstr ""
+
+msgid "Deployment|canceled"
+msgstr ""
+
+msgid "Deployment|created"
+msgstr ""
+
+msgid "Deployment|failed"
+msgstr ""
+
+msgid "Deployment|running"
+msgstr ""
+
+msgid "Deployment|success"
+msgstr ""
+
 msgid "Deprioritize label"
 msgstr ""
 
@@ -5448,6 +5511,9 @@ msgstr ""
 msgid "DesignManagement|An error occurred while loading designs. Please try again."
 msgstr ""
 
+msgid "DesignManagement|Are you sure you want to delete the selected designs?"
+msgstr ""
+
 msgid "DesignManagement|Could not add a new comment. Please try again"
 msgstr ""
 
@@ -5457,6 +5523,18 @@ msgstr ""
 msgid "DesignManagement|Could not find design, please try again."
 msgstr ""
 
+msgid "DesignManagement|Delete"
+msgstr ""
+
+msgid "DesignManagement|Delete designs confirmation"
+msgstr ""
+
+msgid "DesignManagement|Delete selected"
+msgstr ""
+
+msgid "DesignManagement|Deselect all"
+msgstr ""
+
 msgid "DesignManagement|Error uploading a new design. Please try again"
 msgstr ""
 
@@ -5475,6 +5553,9 @@ msgstr ""
 msgid "DesignManagement|Requested design version does not exist. Showing latest version instead"
 msgstr ""
 
+msgid "DesignManagement|Select all"
+msgstr ""
+
 msgid "DesignManagement|The maximum number of designs allowed to be uploaded is %{upload_limit}. Please try again."
 msgstr ""
 
@@ -5484,6 +5565,9 @@ msgstr ""
 msgid "DesignManagement|Upload and view the latest designs for this issue. Consistent and easy to find, so everyone is up to date."
 msgstr ""
 
+msgid "DesignManagement|We could not delete design(s). Please try again."
+msgstr ""
+
 msgid "Designs"
 msgstr ""
 
@@ -5664,6 +5748,12 @@ msgstr ""
 msgid "Download"
 msgstr ""
 
+msgid "Download %{format}"
+msgstr ""
+
+msgid "Download %{format}:"
+msgstr ""
+
 msgid "Download CSV"
 msgstr ""
 
@@ -5745,6 +5835,9 @@ msgstr ""
 msgid "Edit Pipeline Schedule %{id}"
 msgstr ""
 
+msgid "Edit Release"
+msgstr ""
+
 msgid "Edit Snippet"
 msgstr ""
 
@@ -5787,6 +5880,9 @@ msgstr ""
 msgid "Edit stage"
 msgstr ""
 
+msgid "Edit this release"
+msgstr ""
+
 msgid "Edit wiki page"
 msgstr ""
 
@@ -6009,13 +6105,13 @@ msgstr ""
 msgid "Ensure your %{linkStart}environment is part of the deploy stage%{linkEnd} of your CI pipeline to track deployments to your cluster."
 msgstr ""
 
-msgid "Enter IP address range"
+msgid "Enter Admin Mode"
 msgstr ""
 
-msgid "Enter a number"
+msgid "Enter IP address range"
 msgstr ""
 
-msgid "Enter admin mode"
+msgid "Enter a number"
 msgstr ""
 
 msgid "Enter at least three characters to search"
@@ -7229,6 +7325,9 @@ msgstr ""
 msgid "Fork project"
 msgstr ""
 
+msgid "Fork project?"
+msgstr ""
+
 msgid "ForkedFromProjectPath|Forked from"
 msgstr ""
 
@@ -7247,6 +7346,9 @@ msgstr ""
 msgid "Format"
 msgstr ""
 
+msgid "Format: %{dateFormat}"
+msgstr ""
+
 msgid "Forward external support email address to"
 msgstr ""
 
@@ -7745,6 +7847,9 @@ msgstr ""
 msgid "Get started with performance monitoring"
 msgstr ""
 
+msgid "Get started!"
+msgstr ""
+
 msgid "Getting started with releases"
 msgstr ""
 
@@ -7823,6 +7928,9 @@ msgstr ""
 msgid "GitLab single sign on URL"
 msgstr ""
 
+msgid "GitLab uses %{jaeger_link} to monitor distributed systems."
+msgstr ""
+
 msgid "GitLab will run a background job that will produce pseudonymized CSVs of the GitLab database that will be uploaded to your configured object storage directory."
 msgstr ""
 
@@ -8258,6 +8366,9 @@ msgstr ""
 msgid "GroupSettings|Be careful. Changing a group's parent can have unintended %{side_effects_link_start}side effects%{side_effects_link_end}."
 msgstr ""
 
+msgid "GroupSettings|Cannot update the path because there are projects under this group that contain Docker images in their Container Registry. Please remove the images from your projects first and try again."
+msgstr ""
+
 msgid "GroupSettings|Change group path"
 msgstr ""
 
@@ -8818,6 +8929,9 @@ msgstr ""
 msgid "In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index."
 msgstr ""
 
+msgid "In order to tailor your experience with GitLab<br>we would like to know a bit more about you."
+msgstr ""
+
 msgid "In the next step, you'll be able to select the projects you want to import."
 msgstr ""
 
@@ -9231,7 +9345,7 @@ msgstr ""
 msgid "Job is stuck. Check runners."
 msgstr ""
 
-msgid "Job traces and artifacts"
+msgid "Job logs and artifacts"
 msgstr ""
 
 msgid "Job was retried"
@@ -9572,7 +9686,7 @@ msgstr ""
 msgid "Leave"
 msgstr ""
 
-msgid "Leave admin mode"
+msgid "Leave Admin Mode"
 msgstr ""
 
 msgid "Leave edit mode? All unsaved changes will be lost."
@@ -10325,6 +10439,9 @@ msgstr ""
 msgid "Metrics|Legend label (optional)"
 msgstr ""
 
+msgid "Metrics|Link contains an invalid time window."
+msgstr ""
+
 msgid "Metrics|Max"
 msgstr ""
 
@@ -10606,6 +10723,9 @@ msgstr ""
 msgid "Name has already been taken"
 msgstr ""
 
+msgid "Name is too long (maximum is %{max_length} characters)."
+msgstr ""
+
 msgid "Name new label"
 msgstr ""
 
@@ -10845,6 +10965,9 @@ msgstr ""
 msgid "No data to display"
 msgstr ""
 
+msgid "No deployment platform available"
+msgstr ""
+
 msgid "No deployments found"
 msgstr ""
 
@@ -10875,7 +10998,7 @@ msgstr ""
 msgid "No issues for the selected time period."
 msgstr ""
 
-msgid "No job trace"
+msgid "No job log"
 msgstr ""
 
 msgid "No jobs to show"
@@ -10911,6 +11034,9 @@ msgstr ""
 msgid "No parent group"
 msgstr ""
 
+msgid "No pods available"
+msgstr ""
+
 msgid "No preview for this file type"
 msgstr ""
 
@@ -11082,6 +11208,9 @@ msgstr ""
 msgid "NotificationEvent|New note"
 msgstr ""
 
+msgid "NotificationEvent|New release"
+msgstr ""
+
 msgid "NotificationEvent|Reassign issue"
 msgstr ""
 
@@ -11142,6 +11271,12 @@ msgstr ""
 msgid "Number of LOCs per commit"
 msgstr ""
 
+msgid "Number of changes (branches or tags) in a single push to determine whether individual push events or bulk push event will be created. Bulk push event will be created if it surpasses that value."
+msgstr ""
+
+msgid "Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value."
+msgstr ""
+
 msgid "Number of commits per MR"
 msgstr ""
 
@@ -11906,6 +12041,9 @@ msgstr ""
 msgid "Please provide a valid email address."
 msgstr ""
 
+msgid "Please refer to <a href=\"%{docs_url}\">%{docs_url}</a>"
+msgstr ""
+
 msgid "Please retype the email address."
 msgstr ""
 
@@ -13088,10 +13226,10 @@ msgstr ""
 msgid "ProtectedBranch|Allowed to push:"
 msgstr ""
 
-msgid "ProtectedBranch|Code owner approval"
+msgid "ProtectedBranch|Branch"
 msgstr ""
 
-msgid "ProtectedBranch|Last commit"
+msgid "ProtectedBranch|Code owner approval"
 msgstr ""
 
 msgid "ProtectedBranch|Protect"
@@ -13280,6 +13418,9 @@ msgstr ""
 msgid "Quick actions can be used in the issues description and comment boxes."
 msgstr ""
 
+msgid "Quick range"
+msgstr ""
+
 msgid "README"
 msgstr ""
 
@@ -13390,7 +13531,7 @@ msgstr ""
 msgid "Register and see your runners for this project."
 msgstr ""
 
-msgid "Register for GitLab.com"
+msgid "Register for GitLab"
 msgstr ""
 
 msgid "Register now"
@@ -13426,12 +13567,30 @@ msgstr ""
 msgid "Release"
 msgstr ""
 
+msgid "Release notes"
+msgstr ""
+
+msgid "Release notes:"
+msgstr ""
+
+msgid "Release title"
+msgstr ""
+
 msgid "Releases"
 msgstr ""
 
+msgid "Releases are based on Git tags. We recommend naming tags that fit within semantic versioning, for example %{codeStart}v1.0%{codeEnd}, %{codeStart}v2.0-pre%{codeEnd}."
+msgstr ""
+
 msgid "Releases mark specific points in a project's development history, communicate information about the type of change, and deliver on prepared, often compiled, versions of the software to be reused elsewhere. Currently, releases can only be created through the API."
 msgstr ""
 
+msgid "Release|Something went wrong while getting the release details"
+msgstr ""
+
+msgid "Release|Something went wrong while saving the release details"
+msgstr ""
+
 msgid "Remember me"
 msgstr ""
 
@@ -13633,6 +13792,9 @@ msgstr ""
 msgid "Replaced all labels with %{label_references} %{label_text}."
 msgstr ""
 
+msgid "Replaces the clone URL root."
+msgstr ""
+
 msgid "Reply by email"
 msgstr ""
 
@@ -13948,6 +14110,9 @@ msgstr ""
 msgid "Roadmap"
 msgstr ""
 
+msgid "Role"
+msgstr ""
+
 msgid "Rollback"
 msgstr ""
 
@@ -14369,6 +14534,9 @@ msgstr ""
 msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
 msgstr ""
 
+msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgstr ""
+
 msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
 msgstr ""
 
@@ -15910,6 +16078,9 @@ msgstr ""
 msgid "Tag list:"
 msgstr ""
 
+msgid "Tag name"
+msgstr ""
+
 msgid "Tag this commit."
 msgstr ""
 
@@ -16087,6 +16258,9 @@ msgstr ""
 msgid "The \"%{group_path}\" group allows you to sign in with your Single Sign-On Account"
 msgstr ""
 
+msgid "The \"Require approval from CODEOWNERS\" setting was moved to %{banner_link_start}Protected Branches%{banner_link_end}"
+msgstr ""
+
 msgid "The %{type} contains the following error:"
 msgid_plural "The %{type} contains the following errors:"
 msgstr[0] ""
@@ -16236,7 +16410,7 @@ msgstr ""
 msgid "The passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
 msgstr ""
 
-msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
+msgid "The path to the CI configuration file. Defaults to <code>.gitlab-ci.yml</code>"
 msgstr ""
 
 msgid "The phase of the development lifecycle."
@@ -16644,6 +16818,9 @@ msgstr ""
 msgid "This issue is locked."
 msgstr ""
 
+msgid "This job depends on other jobs with expired/erased artifacts: %{invalid_dependencies}"
+msgstr ""
+
 msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
 msgstr ""
 
@@ -16662,21 +16839,36 @@ msgstr ""
 msgid "This job has not started yet"
 msgstr ""
 
+msgid "This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink}."
+msgstr ""
+
+msgid "This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink}. View the %{deploymentLink}."
+msgstr ""
+
 msgid "This job is an out-of-date deployment to %{environmentLink}."
 msgstr ""
 
-msgid "This job is an out-of-date deployment to %{environmentLink}. View the most recent deployment %{deploymentLink}."
+msgid "This job is an out-of-date deployment to %{environmentLink}. View the %{deploymentLink}."
 msgstr ""
 
 msgid "This job is archived. Only the complete pipeline can be retried."
 msgstr ""
 
-msgid "This job is creating a deployment to %{environmentLink} and will overwrite the %{deploymentLink}."
+msgid "This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink}. This will overwrite the %{deploymentLink}."
 msgstr ""
 
 msgid "This job is creating a deployment to %{environmentLink}."
 msgstr ""
 
+msgid "This job is creating a deployment to %{environmentLink}. This will overwrite the %{deploymentLink}."
+msgstr ""
+
+msgid "This job is deployed to %{environmentLink} using cluster %{clusterNameOrLink}."
+msgstr ""
+
+msgid "This job is deployed to %{environmentLink}."
+msgstr ""
+
 msgid "This job is in pending state and is waiting to be picked by a runner"
 msgstr ""
 
@@ -16692,9 +16884,6 @@ msgstr ""
 msgid "This job is stuck because you don't have any active runners that can run this job."
 msgstr ""
 
-msgid "This job is the most recent deployment to %{link}."
-msgstr ""
-
 msgid "This job requires a manual action"
 msgstr ""
 
@@ -17011,6 +17200,9 @@ msgstr ""
 msgid "Titles and Filenames"
 msgstr ""
 
+msgid "To"
+msgstr ""
+
 msgid "To %{link_to_help} of your domain, add the above key to a TXT record within to your DNS configuration."
 msgstr ""
 
@@ -17251,6 +17443,9 @@ msgstr ""
 msgid "Transfer project"
 msgstr ""
 
+msgid "TransferGroup|Cannot update the path because there are projects under this group that contain Docker images in their Container Registry. Please remove the images from your projects first and try again."
+msgstr ""
+
 msgid "TransferGroup|Database is not supported."
 msgstr ""
 
@@ -18201,7 +18396,7 @@ msgstr ""
 msgid "View job"
 msgstr ""
 
-msgid "View job trace"
+msgid "View job log"
 msgstr ""
 
 msgid "View jobs"
@@ -18635,6 +18830,9 @@ msgstr ""
 msgid "Write milestone description..."
 msgstr ""
 
+msgid "Write your release notes or drag your files here…"
+msgstr ""
+
 msgid "Wrong extern UID provided. Make sure Auth0 is configured correctly."
 msgstr ""
 
@@ -19100,6 +19298,9 @@ msgstr ""
 msgid "Your new personal access token has been created."
 msgstr ""
 
+msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
+msgstr ""
+
 msgid "Your password reset token has expired."
 msgstr ""
 
@@ -19571,6 +19772,9 @@ msgstr ""
 msgid "failed"
 msgstr ""
 
+msgid "failed to dismiss associated finding(id=%{finding_id}): %{message}"
+msgstr ""
+
 msgid "for %{link_to_merge_request} with %{link_to_merge_request_source_branch}"
 msgstr ""
 
@@ -19713,6 +19917,9 @@ msgstr ""
 msgid "missing"
 msgstr ""
 
+msgid "most recent deployment"
+msgstr ""
+
 msgid "mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}."
 msgstr ""
 
diff --git a/locale/unfound_translations.rb b/locale/unfound_translations.rb
index 0826d64049b9575485a3b5700ec9dee90d3c8ab0..1ae0958c43cb5b9ed5adca20a7f23c22c39928db 100644
--- a/locale/unfound_translations.rb
+++ b/locale/unfound_translations.rb
@@ -14,3 +14,4 @@
 N_('NotificationEvent|Reassign merge request')
 N_('NotificationEvent|Merge merge request')
 N_('NotificationEvent|Failed pipeline')
+N_('NotificationEvent|New release')
diff --git a/package.json b/package.json
index 29fb22d4caa948611793f133664d620aca11ba01..0d951a58406848712efd35cd99e68efe69a450b4 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,7 @@
     "@babel/plugin-syntax-import-meta": "^7.2.0",
     "@babel/preset-env": "^7.6.2",
     "@gitlab/svgs": "^1.78.0",
-    "@gitlab/ui": "5.32.0",
+    "@gitlab/ui": "5.36.0",
     "@gitlab/visual-review-tools": "1.0.3",
     "apollo-cache-inmemory": "^1.5.1",
     "apollo-client": "^2.5.1",
diff --git a/qa/qa.rb b/qa/qa.rb
index 85ee017ccd444d9847208b65fbb430d6df45417a..f9fba28bacb836a505863ea1f40a182a443e475e 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -407,6 +407,7 @@ module ClusterProvider
     module DockerRun
       autoload :Base, 'qa/service/docker_run/base'
       autoload :LDAP, 'qa/service/docker_run/ldap'
+      autoload :NodeJs, 'qa/service/docker_run/node_js'
       autoload :GitlabRunner, 'qa/service/docker_run/gitlab_runner'
     end
   end
@@ -418,6 +419,7 @@ module Specs
     autoload :Config, 'qa/specs/config'
     autoload :Runner, 'qa/specs/runner'
     autoload :ParallelRunner, 'qa/specs/parallel_runner'
+    autoload :LoopRunner, 'qa/specs/loop_runner'
 
     module Helpers
       autoload :Quarantine, 'qa/specs/helpers/quarantine'
diff --git a/qa/qa/ee.rb b/qa/qa/ee.rb
index 948e72b97482d5f9e30a463364a8135433787de3..87be8f2a9da97822494f29c1a215b9afa86b1b6f 100644
--- a/qa/qa/ee.rb
+++ b/qa/qa/ee.rb
@@ -85,6 +85,7 @@ module Project
         autoload :Menu, 'qa/ee/page/project/menu'
 
         module SubMenus
+          autoload :Packages, 'qa/ee/page/project/sub_menus/packages'
           autoload :SecurityCompliance, 'qa/ee/page/project/sub_menus/security_compliance'
           autoload :Repository, 'qa/ee/page/project/sub_menus/repository'
           autoload :Settings, 'qa/ee/page/project/sub_menus/settings'
@@ -118,6 +119,11 @@ module Kubernetes
           end
         end
 
+        module Packages
+          autoload :Index, 'qa/ee/page/project/packages/index'
+          autoload :Show, 'qa/ee/page/project/packages/show'
+        end
+
         module Pipeline
           autoload :Show, 'qa/ee/page/project/pipeline/show'
         end
diff --git a/qa/qa/ee/page/group/roadmap.rb b/qa/qa/ee/page/group/roadmap.rb
index 6ff3c4884e87db07329ccb7d84172ef0379d5048..7a18d4764080a65dd4660955910a6245d2320db1 100644
--- a/qa/qa/ee/page/group/roadmap.rb
+++ b/qa/qa/ee/page/group/roadmap.rb
@@ -22,8 +22,10 @@ def epic_present?(epic)
             group_relative_url = uri.path
             epic_href_selector = "a[href='#{group_relative_url}/-/epics/#{epic.iid}']"
 
-            find_element(:roadmap_shell).find("#{epic_details_cell} #{epic_href_selector}") &&
-            find_element(:roadmap_shell).find("#{epic_timeline_cell} #{epic_href_selector}")
+            within_element(:roadmap_shell) do
+              find("[data-qa-selector='epic_details_cell'] #{epic_href_selector}") &&
+              find("[data-qa-selector='epic_timeline_cell'] #{epic_href_selector}")
+            end
           end
         end
       end
diff --git a/qa/qa/ee/page/group/settings/general.rb b/qa/qa/ee/page/group/settings/general.rb
index 4c92a4da895e54e804a8b9648f94ad25365234ac..20ae60133aeee3516b4fc620b1e82efed6806e37 100644
--- a/qa/qa/ee/page/group/settings/general.rb
+++ b/qa/qa/ee/page/group/settings/general.rb
@@ -28,6 +28,12 @@ def self.included(base)
                 view 'ee/app/views/shared/_repository_size_limit_setting.html.haml' do
                   element :repository_size_limit_field
                 end
+
+                view 'ee/app/views/groups/_templates_setting.html.haml' do
+                  element :file_template_repository_dropdown
+                  element :file_template_repositories
+                  element :save_changes_button
+                end
               end
             end
 
@@ -72,6 +78,25 @@ def set_membership_lock_disabled
             def set_repository_size_limit(limit)
               find_element(:repository_size_limit_field).set limit
             end
+
+            def current_file_template_repository
+              expand_section(:file_template_repositories)
+
+              within_element(:file_template_repository_dropdown) do
+                current_selection
+              end
+            end
+
+            def choose_file_template_repository(path)
+              expand_section(:file_template_repositories)
+
+              within_element(:file_template_repository_dropdown) do
+                clear_current_selection_if_present
+              end
+              click_element :file_template_repository_dropdown
+              search_and_select(path)
+              click_element :save_changes_button
+            end
           end
         end
       end
diff --git a/qa/qa/ee/page/project/menu.rb b/qa/qa/ee/page/project/menu.rb
index bc28bea9e6ca26a197c6dae7a8eacf9c8b08cfb3..5c9b8be147c240825ee8501ba4fd253f343ba595 100644
--- a/qa/qa/ee/page/project/menu.rb
+++ b/qa/qa/ee/page/project/menu.rb
@@ -7,6 +7,7 @@ module Project
         module Menu
           prepend QA::Page::Project::SubMenus::Common
           prepend SubMenus::SecurityCompliance
+          prepend SubMenus::Packages
           prepend SubMenus::Project
           prepend SubMenus::Settings
         end
diff --git a/qa/qa/ee/page/project/packages/index.rb b/qa/qa/ee/page/project/packages/index.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2afa312ced33e6ce5e4e72522751cab728959c2b
--- /dev/null
+++ b/qa/qa/ee/page/project/packages/index.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module QA
+  module EE
+    module Page
+      module Project
+        module Packages
+          class Index < QA::Page::Base
+            view 'ee/app/views/projects/packages/packages/index.html.haml' do
+              element :package_row
+              element :package_link
+            end
+
+            def click_package(name)
+              click_element(:package_link, text: name)
+            end
+
+            def has_package?(name)
+              has_element?(:package_link, text: name)
+            end
+
+            def has_no_package?(name)
+              has_no_element?(:package_link, text: name)
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/ee/page/project/packages/show.rb b/qa/qa/ee/page/project/packages/show.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6643e4a6c485c7db8c2c241944b243b6582269fe
--- /dev/null
+++ b/qa/qa/ee/page/project/packages/show.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module QA
+  module EE
+    module Page
+      module Project
+        module Packages
+          class Show < QA::Page::Base
+            view 'ee/app/assets/javascripts/packages/components/app.vue' do
+              element :delete_button
+              element :delete_modal_button
+              element :package_information_content
+            end
+
+            def has_package_info?(name, version)
+              has_element?(:package_information_content, text: /#{name}.*#{version}/)
+            end
+
+            def click_delete
+              click_element(:delete_button)
+              wait_for_animated_element(:delete_modal_button)
+              click_element(:delete_modal_button)
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/ee/page/project/sub_menus/packages.rb b/qa/qa/ee/page/project/sub_menus/packages.rb
new file mode 100644
index 0000000000000000000000000000000000000000..34e08730f4b31d50c00d59e99b470ac4219576c2
--- /dev/null
+++ b/qa/qa/ee/page/project/sub_menus/packages.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module QA
+  module EE
+    module Page
+      module Project
+        module SubMenus
+          module Packages
+            def self.included(page)
+              page.class_eval do
+                view 'ee/app/views/layouts/nav/sidebar/_project_packages_link.html.haml' do
+                  element :packages_link
+                end
+              end
+            end
+
+            def click_packages_link
+              within_sidebar do
+                click_element :packages_link
+              end
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/page/component/select2.rb b/qa/qa/page/component/select2.rb
index d05c44d22b2c8f461d1ecba4a8a1d32a1d9c67a7..8fe6a4a75b37f152d0a185403e506db569561bbb 100644
--- a/qa/qa/page/component/select2.rb
+++ b/qa/qa/page/component/select2.rb
@@ -20,12 +20,20 @@ def clear_current_selection_if_present
 
         def search_and_select(item_text)
           find('.select2-input').set(item_text)
+
+          wait_for_search_to_complete
+
           select_item(item_text)
         end
 
         def expand_select_list
           find('span.select2-arrow').click
         end
+
+        def wait_for_search_to_complete
+          has_css?('.select2-active')
+          has_no_css?('.select2-active', wait: 30)
+        end
       end
     end
   end
diff --git a/qa/qa/page/file/shared/commit_button.rb b/qa/qa/page/file/shared/commit_button.rb
index d8e751dd7b6fc4baa76fbbb0ab02047dd4d6813f..559b4c6ceea8f73666e9ef72334fcde2b06c1c55 100644
--- a/qa/qa/page/file/shared/commit_button.rb
+++ b/qa/qa/page/file/shared/commit_button.rb
@@ -13,6 +13,10 @@ def self.included(base)
 
           def commit_changes
             click_element(:commit_button)
+
+            wait(reload: false, max: 60) do
+              finished_loading?
+            end
           end
         end
       end
diff --git a/qa/qa/page/validator.rb b/qa/qa/page/validator.rb
index 9b2d0a1a41d2491d2673fee3c59e4e4992b4abe9..75e48b5785e6a311026d5b3c20e028229f462532 100644
--- a/qa/qa/page/validator.rb
+++ b/qa/qa/page/validator.rb
@@ -17,7 +17,7 @@ def initialize(constant)
 
       def constants
         @consts ||= @module.constants.map do |const|
-          @module.const_get(const)
+          @module.const_get(const, false)
         end
       end
 
diff --git a/qa/qa/resource/runner.rb b/qa/qa/resource/runner.rb
index 1be2429bc0491e41c65a72b5739e63fc114aaf29..102c1ec83f5e5fc770117956ff2c7022063adf16 100644
--- a/qa/qa/resource/runner.rb
+++ b/qa/qa/resource/runner.rb
@@ -36,7 +36,6 @@ def fabricate_via_api!
           runner.tags = tags
           runner.image = image
           runner.config = config if config
-          runner.run_untagged = true
           runner.register!
         end
       end
diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb
index 77d04aa7594bedec8a2bd8ccd3119e7127f7bc02..4789b380377ff11ee65762aaed1b2a94f80fb26a 100644
--- a/qa/qa/runtime/browser.rb
+++ b/qa/qa/runtime/browser.rb
@@ -65,7 +65,7 @@ def self.configure!
 
           # QA::Runtime::Env.browser.capitalize will work for every driver type except PhantomJS.
           # We will have no use to use PhantomJS so this shouldn't be a problem.
-          options = Selenium::WebDriver.const_get(QA::Runtime::Env.browser.capitalize)::Options.new
+          options = Selenium::WebDriver.const_get(QA::Runtime::Env.browser.capitalize, false)::Options.new
 
           if QA::Runtime::Env.browser == :chrome
             options.add_argument("window-size=1480,2200")
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index b4047ef508867d911ae5da234a519a3689e29033..bcd2a2254696157894ce50937d4ec27abea68d9a 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -261,6 +261,10 @@ def runtime_scenario_attributes
         ENV['QA_RUNTIME_SCENARIO_ATTRIBUTES']
       end
 
+      def gitlab_qa_loop_runner_minutes
+        ENV.fetch('GITLAB_QA_LOOP_RUNNER_MINUTES', 1).to_i
+      end
+
       private
 
       def remote_grid_credentials
diff --git a/qa/qa/runtime/fixtures.rb b/qa/qa/runtime/fixtures.rb
index 02cecffd4dff94592cb93fc6822518e23eedfaf6..f91218ea0b589f6f75c6fa6faea4c24e3b356348 100644
--- a/qa/qa/runtime/fixtures.rb
+++ b/qa/qa/runtime/fixtures.rb
@@ -1,5 +1,7 @@
 # frozen_string_literal: true
 
+require 'tmpdir'
+
 module QA
   module Runtime
     module Fixtures
@@ -18,6 +20,19 @@ def fetch_template_from_api(api_path, key)
         parse_body(response)[:content]
       end
 
+      def with_fixtures(fixtures)
+        dir = Dir.mktmpdir
+        fixtures.each do |file_def|
+          path = File.join(dir, file_def[:file_path])
+          FileUtils.mkdir_p(File.dirname(path))
+          File.write(path, file_def[:content])
+        end
+
+        yield dir
+      ensure
+        FileUtils.remove_entry(dir)
+      end
+
       private
 
       def api_client
diff --git a/qa/qa/runtime/release.rb b/qa/qa/runtime/release.rb
index 4f96e0cf44b8b7679fb30a26226437e5921bf7fe..18a6736afcf3857989c380ae4889d8ab4f2d71a9 100644
--- a/qa/qa/runtime/release.rb
+++ b/qa/qa/runtime/release.rb
@@ -19,7 +19,7 @@ def version
       end
 
       def strategy
-        QA.const_get("QA::#{version}::Strategy")
+        Object.const_get("QA::#{version}::Strategy", false)
       end
 
       def self.method_missing(name, *args)
diff --git a/qa/qa/scenario/shared_attributes.rb b/qa/qa/scenario/shared_attributes.rb
index 52f50ec8c277c2d47cd9ae5050e3edcfb9d6941d..bb45c4ce4cb75269d06a935b5de63c0b25745e52 100644
--- a/qa/qa/scenario/shared_attributes.rb
+++ b/qa/qa/scenario/shared_attributes.rb
@@ -8,6 +8,7 @@ module SharedAttributes
       attribute :gitlab_address, '--address URL', 'Address of the instance to test'
       attribute :enable_feature, '--enable-feature FEATURE_FLAG', 'Enable a feature before running tests'
       attribute :parallel, '--parallel', 'Execute tests in parallel'
+      attribute :loop, '--loop', 'Execute test repeatedly'
     end
   end
 end
diff --git a/qa/qa/service/docker_run/node_js.rb b/qa/qa/service/docker_run/node_js.rb
new file mode 100644
index 0000000000000000000000000000000000000000..642f1d1a33a5d366ef74690b180b2b31b4626e7d
--- /dev/null
+++ b/qa/qa/service/docker_run/node_js.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module QA
+  module Service
+    module DockerRun
+      class NodeJs < Base
+        def initialize(volume_host_path)
+          @image = 'node:12.11.1-alpine'
+          @name = "qa-node-#{SecureRandom.hex(8)}"
+          @volume_host_path = volume_host_path
+
+          super()
+        end
+
+        def publish!
+          # When we run the tests via gitlab-qa, we use docker-in-docker
+          # which means that host of a volume mount would be the host that
+          # started the gitlab-qa QA container (e.g., the CI runner),
+          # not the gitlab-qa container itself. That means we can't
+          # mount a volume from the file system inside the gitlab-qa
+          # container.
+          #
+          # Instead, we copy the files into the container.
+          shell <<~CMD.tr("\n", ' ')
+            docker run -d --rm
+            --network #{network}
+            --hostname #{host_name}
+            --name #{@name}
+            --volume #{@volume_host_path}:/home/node
+            #{@image} sh -c "sleep 60"
+          CMD
+          shell "docker cp #{@volume_host_path}/. #{@name}:/home/node"
+          shell "docker exec -t #{@name} sh -c 'cd /home/node && npm publish'"
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/specs/features/ee/browser_ui/1_manage/group/group_file_template_spec.rb b/qa/qa/specs/features/ee/browser_ui/1_manage/group/group_file_template_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..44d9ea894a37dcb92d24b3be59df74895d529f1a
--- /dev/null
+++ b/qa/qa/specs/features/ee/browser_ui/1_manage/group/group_file_template_spec.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+module QA
+  context 'Manage' do
+    describe 'Group file templates' do
+      include Support::Api
+
+      templates = [
+        {
+          type: 'Dockerfile',
+          template: 'custom_dockerfile',
+          name: 'Dockerfile/custom_dockerfile.dockerfile',
+          content: 'dockerfile template test'
+        },
+        {
+          type: '.gitignore',
+          template: 'custom_gitignore',
+          name: 'gitignore/custom_gitignore.gitignore',
+          content: 'gitignore template test'
+        },
+        {
+          type: '.gitlab-ci.yml',
+          template: 'custom_gitlab-ci',
+          name: 'gitlab-ci/custom_gitlab-ci.yml',
+          content: 'gitlab-ci template test'
+        },
+        {
+          type: 'LICENSE',
+          template: 'custom_license',
+          name: 'LICENSE/custom_license.txt',
+          content: 'license template test'
+        }
+      ]
+
+      before(:all) do
+        login
+
+        @group = Resource::Group.fabricate_via_api! do |group|
+          group.path = 'template-group'
+        end
+
+        @file_template_project = Resource::Project.fabricate_via_api! do |project|
+          project.group = @group
+          project.name = 'group-file-template-project'
+          project.description = 'Add group file templates'
+          project.initialize_with_readme = true
+        end
+
+        templates.each do |template|
+          Resource::File.fabricate_via_api! do |file|
+            file.project = @file_template_project
+            file.name = template[:name]
+            file.content = template[:content]
+            file.commit_message = 'Add test file templates'
+          end
+        end
+
+        @project = Resource::Project.fabricate_via_api! do |project|
+          project.group = @group
+          project.name = 'group-file-template-project-2'
+          project.description = 'Add files for group file templates'
+          project.initialize_with_readme = true
+        end
+
+        Page::Main::Menu.perform(&:sign_out)
+      end
+
+      after(:all) do
+        login unless Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) }
+
+        remove_group_file_template_if_set
+      end
+
+      templates.each do |template|
+        it "creates file via custom #{template[:type]} file template" do
+          login
+          set_file_template_if_not_already_set
+
+          @project.visit!
+
+          Page::Project::Show.perform(&:create_new_file!)
+          Page::File::Form.perform do |form|
+            form.select_template template[:type], template[:template]
+          end
+
+          expect(page).to have_content(template[:content])
+
+          Page::File::Form.perform(&:commit_changes)
+
+          expect(page).to have_content('The file has been successfully created.')
+          expect(page).to have_content(template[:type])
+          expect(page).to have_content('Add new file')
+          expect(page).to have_content(template[:content])
+        end
+      end
+
+      def login
+        Runtime::Browser.visit(:gitlab, Page::Main::Login)
+        Page::Main::Login.perform(&:sign_in_using_admin_credentials)
+      end
+
+      def set_file_template_if_not_already_set
+        api_client = Runtime::API::Client.new(:gitlab)
+        response = get Runtime::API::Request.new(api_client, "/groups/#{@group.id}").url
+
+        if parse_body(response)[:file_template_project_id]
+          return
+        else
+          @group.visit!
+          Page::Group::Menu.perform(&:click_group_general_settings_item)
+
+          Page::Group::Settings::General.perform do |general|
+            general.choose_file_template_repository(@file_template_project.name)
+          end
+        end
+      end
+
+      def remove_group_file_template_if_set
+        api_client = Runtime::API::Client.new(:gitlab)
+        response = get Runtime::API::Request.new(api_client, "/groups/#{@group.id}").url
+
+        if parse_body(response)[:file_template_project_id]
+          put Runtime::API::Request.new(api_client, "/groups/#{@group.id}").url, { file_template_project_id: nil }
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/specs/features/ee/browser_ui/2_plan/epic/roadmap_spec.rb b/qa/qa/specs/features/ee/browser_ui/2_plan/epic/roadmap_spec.rb
index 737f2787acb41967113b40428eacee268fa8a2d7..ce6fcf569ac46b8b68a178dae1e6c92ecdd8dbae 100644
--- a/qa/qa/specs/features/ee/browser_ui/2_plan/epic/roadmap_spec.rb
+++ b/qa/qa/specs/features/ee/browser_ui/2_plan/epic/roadmap_spec.rb
@@ -1,8 +1,7 @@
 # frozen_string_literal: true
 
 module QA
-  # https://gitlab.com/gitlab-org/gitlab/issues/13360
-  context 'Plan', :skip do
+  context 'Plan' do
     describe 'Epics roadmap' do
       include Support::Dates
 
diff --git a/qa/qa/specs/features/ee/browser_ui/2_plan/scoped_labels/editing_scoped_labels_spec.rb b/qa/qa/specs/features/ee/browser_ui/2_plan/scoped_labels/editing_scoped_labels_spec.rb
index 3e9315d5c2a965a9c7f13ed0fd8cae54c18d9d02..c40694eb4bfd9dde94819d95c3f8a09871c5f85b 100644
--- a/qa/qa/specs/features/ee/browser_ui/2_plan/scoped_labels/editing_scoped_labels_spec.rb
+++ b/qa/qa/specs/features/ee/browser_ui/2_plan/scoped_labels/editing_scoped_labels_spec.rb
@@ -44,6 +44,8 @@ module QA
             new_label_different_scope_multi_colon
           ])
 
+          show.select_all_activities_filter
+
           initial_labels = "#{initial_label} #{initial_label_multi_colon}"
           new_labels = "#{new_label_same_scope} #{new_label_same_scope_multi_colon} #{new_label_different_scope_multi_colon} #{new_label_different_scope}"
 
diff --git a/qa/qa/specs/features/ee/browser_ui/3_create/web_ide/web_terminal_spec.rb b/qa/qa/specs/features/ee/browser_ui/3_create/web_ide/web_terminal_spec.rb
index 5a9093bc338f265094e1e7cedda27d5f684a701d..4271370e8d0996ea3c2a2e06a384c6e046490686 100644
--- a/qa/qa/specs/features/ee/browser_ui/3_create/web_ide/web_terminal_spec.rb
+++ b/qa/qa/specs/features/ee/browser_ui/3_create/web_ide/web_terminal_spec.rb
@@ -19,6 +19,7 @@ module QA
                 file_path: '.gitlab/.gitlab-webide.yml',
                 content: <<~YAML
                   terminal:
+                    tags: ["web-ide"]
                     script: sleep 60
                 YAML
               }
@@ -29,7 +30,7 @@ module QA
         @runner = Resource::Runner.fabricate_via_api! do |runner|
           runner.project = project
           runner.name = "qa-runner-#{Time.now.to_i}"
-          runner.tags = %w[qa docker web-ide]
+          runner.tags = %w[web-ide]
           runner.image = 'gitlab/gitlab-runner:latest'
           runner.config = <<~END
             concurrent = 1
diff --git a/qa/qa/specs/features/ee/browser_ui/5_package/npm_registry_spec.rb b/qa/qa/specs/features/ee/browser_ui/5_package/npm_registry_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d9c9421c3f75ebf3a835659de39d46528357c69a
--- /dev/null
+++ b/qa/qa/specs/features/ee/browser_ui/5_package/npm_registry_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+module QA
+  context 'Package', :docker, :orchestrated, :packages do
+    describe 'NPM registry' do
+      include Runtime::Fixtures
+
+      let(:registry_scope) { project.group.sandbox.path }
+      let(:package_name) { "@#{registry_scope}/#{project.name}" }
+      let(:auth_token) do
+        unless Page::Main::Menu.perform(&:signed_in?)
+          Runtime::Browser.visit(:gitlab, Page::Main::Login)
+          Page::Main::Login.perform(&:sign_in_using_credentials)
+        end
+
+        Resource::PersonalAccessToken.fabricate!.access_token
+      end
+      let(:project) do
+        Resource::Project.fabricate_via_api! do |resource|
+          resource.name = 'npm-registry-project'
+        end
+      end
+
+      it 'publishes an npm package and then deletes it' do
+        uri = URI.parse(Runtime::Scenario.gitlab_address)
+        gitlab_host_with_port = "#{uri.host}:#{uri.port}"
+        gitlab_address_with_port = "#{uri.scheme}://#{uri.host}:#{uri.port}"
+        package_json = {
+          file_path: 'package.json',
+          content: <<~JSON
+            {
+              "name": "#{package_name}",
+              "version": "1.0.0",
+              "description": "Example package for GitLab NPM registry",
+              "publishConfig": {
+                "@#{registry_scope}:registry": "#{gitlab_address_with_port}/api/v4/projects/#{project.id}/packages/npm/"
+              }
+            }
+          JSON
+        }
+        npmrc = {
+          file_path: '.npmrc',
+          content: <<~NPMRC
+            //#{gitlab_host_with_port}/api/v4/projects/#{project.id}/packages/npm/:_authToken=#{auth_token}
+            //#{gitlab_host_with_port}/api/v4/packages/npm/:_authToken=#{auth_token}
+            @#{registry_scope}:registry=#{gitlab_address_with_port}/api/v4/packages/npm/
+          NPMRC
+        }
+
+        # Use a node docker container to publish the package
+        with_fixtures([npmrc, package_json]) do |dir|
+          Service::DockerRun::NodeJs.new(dir).publish!
+        end
+
+        project.visit!
+        Page::Project::Menu.perform(&:click_packages_link)
+        EE::Page::Project::Packages::Index.perform do |index|
+          expect(index).to have_package(package_name)
+
+          index.click_package(package_name)
+        end
+
+        EE::Page::Project::Packages::Show.perform do |show|
+          expect(show).to have_package_info(package_name, "1.0.0")
+
+          show.click_delete
+        end
+
+        EE::Page::Project::Packages::Index.perform do |index|
+          expect(index).to have_content("Package was removed")
+          expect(index).to have_no_package(package_name)
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/specs/features/ee/browser_ui/6_release/multi-project_pipelines_spec.rb b/qa/qa/specs/features/ee/browser_ui/6_release/multi-project_pipelines_spec.rb
index aafa7e82bb7ec3b804640fd27391273e60d4e122..58c2cc90f83142e7b7d5f6dae6bdafefa330822f 100644
--- a/qa/qa/specs/features/ee/browser_ui/6_release/multi-project_pipelines_spec.rb
+++ b/qa/qa/specs/features/ee/browser_ui/6_release/multi-project_pipelines_spec.rb
@@ -22,7 +22,7 @@ module QA
           runner.project = upstream_project
           runner.token = upstream_project.group.sandbox.runners_token
           runner.name = upstream_project_name
-          runner.tags = %w[qa test]
+          runner.tags = [upstream_project_name]
         end
       end
 
@@ -38,6 +38,7 @@ module QA
 
             job1:
               stage: test
+              tags: ["#{upstream_project_name}"]
               script: echo "done"
 
             staging:
@@ -55,6 +56,7 @@ module QA
           project_push.file_content = <<~CI
             downstream_job:
               stage: test
+              tags: ["#{upstream_project_name}"]
               script: echo "done"
           CI
         end
@@ -74,7 +76,7 @@ module QA
 
       it 'creates a multi-project pipeline' do
         Page::MergeRequest::Show.perform do |show|
-          pipeline_passed = show.retry_until(reload: true) do
+          pipeline_passed = show.retry_until(reload: true, max_attempts: 20, sleep_interval: 6) do
             show.has_content?(/Pipeline #\d+ passed/)
           end
 
diff --git a/qa/qa/specs/features/ee/browser_ui/6_release/pipelines_for_merged_results_and_merge_trains_spec.rb b/qa/qa/specs/features/ee/browser_ui/6_release/pipelines_for_merged_results_and_merge_trains_spec.rb
index e26ba30efb9e19b233654200ae3562accbfe9f87..220236aa5e61b7f2c264334e01fb2055eed7f1bd 100644
--- a/qa/qa/specs/features/ee/browser_ui/6_release/pipelines_for_merged_results_and_merge_trains_spec.rb
+++ b/qa/qa/specs/features/ee/browser_ui/6_release/pipelines_for_merged_results_and_merge_trains_spec.rb
@@ -21,6 +21,7 @@ module QA
           project_push.commit_message = 'Add .gitlab-ci.yml'
           project_push.file_content = <<~EOF
             test:
+              tags: ["qa"]
               script: echo 'OK'
               only:
               - merge_requests
diff --git a/qa/qa/specs/features/ee/browser_ui/secure/security_reports_spec.rb b/qa/qa/specs/features/ee/browser_ui/secure/security_reports_spec.rb
index d756f4b117df4cc786b268703d48e80538a0b2ea..2b7cc3d836f5b1801ff914f4f1c4a5e09ef77c8a 100644
--- a/qa/qa/specs/features/ee/browser_ui/secure/security_reports_spec.rb
+++ b/qa/qa/specs/features/ee/browser_ui/secure/security_reports_spec.rb
@@ -5,14 +5,14 @@
 module QA
   context 'Secure', :docker do
     let(:number_of_dependencies_in_fixture) { 1309 }
-    let(:total_vuln_count) { 52 }
+    let(:total_vuln_count) { 54 }
     let(:dependency_scan_vuln_count) { 4 }
     let(:dependency_scan_example_vuln) { 'jQuery before 3.4.0' }
     let(:container_scan_vuln_count) { 8 }
     let(:container_scan_example_vuln) { 'CVE-2017-18269 in glibc' }
     let(:sast_scan_vuln_count) { 33 }
     let(:sast_scan_example_vuln) { 'Cipher with no integrity' }
-    let(:dast_scan_vuln_count) { 7 }
+    let(:dast_scan_vuln_count) { 9 }
     let(:dast_scan_example_vuln) { 'Cookie Without SameSite Attribute' }
 
     describe 'Security Reports' do
@@ -101,7 +101,7 @@ module QA
           end
 
           filter_report_and_perform(dashboard, "DAST") do
-            expect(dashboard).to have_low_vulnerability_count_of 6
+            expect(dashboard).to have_low_vulnerability_count_of 8
           end
         end
       end
diff --git a/qa/qa/specs/loop_runner.rb b/qa/qa/specs/loop_runner.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f97f5cbbd81a4737792a52a181dc5b4e7f0f5f6a
--- /dev/null
+++ b/qa/qa/specs/loop_runner.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module QA
+  module Specs
+    module LoopRunner
+      module_function
+
+      def run(args)
+        start = Time.now
+        loop_duration = 60 * QA::Runtime::Env.gitlab_qa_loop_runner_minutes
+
+        while Time.now - start < loop_duration
+          RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status|
+            abort if status.nonzero?
+          end
+          RSpec.clear_examples
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
index 6aa08cf77b416e69e485c66d28deb8a1953a6c46..ac73cc00dbf88323b2d2187f408737871b335866 100644
--- a/qa/qa/specs/runner.rb
+++ b/qa/qa/specs/runner.rb
@@ -63,6 +63,8 @@ def perform
 
         if Runtime::Scenario.attributes[:parallel]
           ParallelRunner.run(args.flatten)
+        elsif Runtime::Scenario.attributes[:loop]
+          LoopRunner.run(args.flatten)
         else
           RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status|
             abort if status.nonzero?
diff --git a/qa/spec/page/element_spec.rb b/qa/spec/page/element_spec.rb
index 20d4a00c0208b6fa9624a4d900a642796c8d467e..ff5e118cefa8010f00d0eba3f236d784be3a495b 100644
--- a/qa/spec/page/element_spec.rb
+++ b/qa/spec/page/element_spec.rb
@@ -113,6 +113,7 @@
 
   describe 'data-qa selectors' do
     subject { described_class.new(:my_element) }
+
     it 'properly translates to a data-qa-selector' do
       expect(subject.selector_css).to include(%q([data-qa-selector="my_element"]))
     end
diff --git a/rubocop/cop/gitlab/const_get_inherit_false.rb b/rubocop/cop/gitlab/const_get_inherit_false.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3d3bbc4c8d3e04ec98d062945b63a737ec7840c8
--- /dev/null
+++ b/rubocop/cop/gitlab/const_get_inherit_false.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module RuboCop
+  module Cop
+    module Gitlab
+      # Cop that encourages usage of inherit=false for 2nd argument when using const_get.
+      #
+      # See https://gitlab.com/gitlab-org/gitlab/issues/27678
+      class ConstGetInheritFalse < RuboCop::Cop::Cop
+        MSG = 'Use inherit=false when using const_get.'
+
+        def_node_matcher :const_get?, <<~PATTERN
+        (send _ :const_get ...)
+        PATTERN
+
+        def on_send(node)
+          return unless const_get?(node)
+          return if second_argument(node)&.false_type?
+
+          add_offense(node, location: :selector)
+        end
+
+        def autocorrect(node)
+          lambda do |corrector|
+            if arg = second_argument(node)
+              corrector.replace(arg.source_range, 'false')
+            else
+              first_argument = node.arguments[0]
+              corrector.insert_after(first_argument.source_range, ', false')
+            end
+          end
+        end
+
+        private
+
+        def second_argument(node)
+          node.arguments[1]
+        end
+      end
+    end
+  end
+end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index 8e7df62ea750c69961840239ebd2b8a29db45f19..70679aa1e780b93983314ef1845c92c6523feb39 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -1,3 +1,4 @@
+require_relative 'cop/gitlab/const_get_inherit_false'
 require_relative 'cop/gitlab/module_with_instance_variables'
 require_relative 'cop/gitlab/predicate_memoization'
 require_relative 'cop/gitlab/httparty'
diff --git a/scripts/review_apps/base-config.yaml b/scripts/review_apps/base-config.yaml
index de5e14af4d4b734c405b4050dfa8227ca032193c..ef5e2719fca9d6da8b9067994d98e4a3aba7194f 100644
--- a/scripts/review_apps/base-config.yaml
+++ b/scripts/review_apps/base-config.yaml
@@ -35,10 +35,10 @@ gitlab:
   gitlab-shell:
     resources:
       requests:
-        cpu: 70m
+        cpu: 125m
         memory: 20M
       limits:
-        cpu: 140m
+        cpu: 250m
         memory: 40M
     maxReplicas: 3
     hpa:
@@ -62,10 +62,10 @@ gitlab:
   unicorn:
     resources:
       requests:
-        cpu: 600m
+        cpu: 400m
         memory: 1.4G
       limits:
-        cpu: 1.2G
+        cpu: 800m
         memory: 2.8G
     deployment:
       readinessProbe:
@@ -75,10 +75,10 @@ gitlab:
     workhorse:
       resources:
         requests:
-          cpu: 100m
+          cpu: 300m
           memory: 100M
         limits:
-          cpu: 200m
+          cpu: 600m
           memory: 200M
       readinessProbe:
         initialDelaySeconds: 5  # Default is 0
@@ -87,18 +87,18 @@ gitlab:
 gitlab-runner:
   resources:
     requests:
-      cpu: 300m
+      cpu: 355m
       memory: 300M
     limits:
-      cpu: 600m
+      cpu: 710m
       memory: 600M
 minio:
   resources:
     requests:
-      cpu: 100m
+      cpu: 5m
       memory: 128M
     limits:
-      cpu: 200m
+      cpu: 10m
       memory: 280M
 nginx-ingress:
   controller:
@@ -107,10 +107,10 @@ nginx-ingress:
     replicaCount: 2
     resources:
       requests:
-        cpu: 150m
+        cpu: 100m
         memory: 250M
       limits:
-        cpu: 300m
+        cpu: 200m
         memory: 500M
     minAvailable: 1
     service:
@@ -153,7 +153,8 @@ redis:
 redis-ha:
   enabled: false
 registry:
-  minReplicas: 1
+  hpa:
+    minReplicas: 1
   resources:
     requests:
       cpu: 50m
diff --git a/scripts/update-feature-categories b/scripts/update-feature-categories
new file mode 100755
index 0000000000000000000000000000000000000000..ed5d8dccdd67dd1f792215f0216286de92e7cdd4
--- /dev/null
+++ b/scripts/update-feature-categories
@@ -0,0 +1,36 @@
+#!/usr/bin/env ruby
+
+require 'uri'
+require 'net/http'
+require 'yaml'
+
+url = URI("https://gitlab.com/gitlab-com/www-gitlab-com/raw/master/data/stages.yml")
+
+http = Net::HTTP.new(url.host, url.port)
+http.use_ssl = true
+
+request = Net::HTTP::Get.new(url)
+
+response = http.request(request)
+
+stages_doc = YAML.safe_load(response.read_body)
+feature_categories = stages_doc["stages"].values
+  .flat_map { |stage| stage["groups"].values }
+  .flat_map { |group| group["categories"] }
+  .select(&:itself)
+  .uniq
+  .sort
+
+File.open("#{__dir__}/../config/feature_categories.yml", 'w') do |file|
+  file.puts(<<~HEADER_COMMENT)
+    #
+    # This file contains a list of all feature categories in GitLab
+    # It is generated from the stages file at #{url}.
+    # If you would like to update it, please run
+    # `./scripts/update-feature-categories` to generate a new copy
+    #
+    # PLEASE DO NOT EDIT THIS FILE MANUALLY.
+    #
+  HEADER_COMMENT
+  file.write(feature_categories.to_yaml)
+end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 5e33421854b1c400b62f7ea3c4b7374d85cffc2f..ed91b5973b883aa65444541ecb997dc136939ffd 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -842,4 +842,48 @@ def index
       end
     end
   end
+
+  describe '#require_role' do
+    controller(described_class) do
+      def index; end
+    end
+
+    let(:user) { create(:user) }
+    let(:experiment_enabled) { true }
+
+    before do
+      stub_experiment(signup_flow: experiment_enabled)
+    end
+
+    context 'experiment enabled and user with required role' do
+      before do
+        user.set_role_required!
+        sign_in(user)
+        get :index
+      end
+
+      it { is_expected.to redirect_to users_sign_up_welcome_path }
+    end
+
+    context 'experiment enabled and user without a role' do
+      before do
+        sign_in(user)
+        get :index
+      end
+
+      it { is_expected.not_to redirect_to users_sign_up_welcome_path }
+    end
+
+    context 'experiment disabled and user with required role' do
+      let(:experiment_enabled) { false }
+
+      before do
+        user.set_role_required!
+        sign_in(user)
+        get :index
+      end
+
+      it { is_expected.not_to redirect_to users_sign_up_welcome_path }
+    end
+  end
 end
diff --git a/spec/controllers/concerns/redirects_for_missing_path_on_tree_spec.rb b/spec/controllers/concerns/redirects_for_missing_path_on_tree_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..903100ba93fd50a65b5ed46b5c80cc31bb930571
--- /dev/null
+++ b/spec/controllers/concerns/redirects_for_missing_path_on_tree_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe RedirectsForMissingPathOnTree, type: :controller do
+  controller(ActionController::Base) do
+    include Gitlab::Routing.url_helpers
+    include RedirectsForMissingPathOnTree
+
+    def fake
+      redirect_to_tree_root_for_missing_path(Project.find(params[:project_id]), params[:ref], params[:file_path])
+    end
+  end
+
+  let(:project) { create(:project) }
+
+  before do
+    routes.draw { get 'fake' => 'anonymous#fake' }
+  end
+
+  describe '#redirect_to_root_path' do
+    it 'redirects to the tree path with a notice' do
+      long_file_path = ('a/b/' * 30) + 'foo.txt'
+      truncated_file_path = '...b/' + ('a/b/' * 12) + 'foo.txt'
+      expected_message = "\"#{truncated_file_path}\" did not exist on \"theref\""
+
+      get :fake, params: { project_id: project.id, ref: 'theref', file_path: long_file_path }
+
+      expect(response).to redirect_to project_tree_path(project, 'theref')
+      expect(response.flash[:notice]).to eq(expected_message)
+    end
+  end
+end
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index 0c3dd9715828c00ea111de65b124b8315af4de8f..22f970133e3e81b5ad3eaf64566af73817a8864c 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -6,7 +6,7 @@
   include ExternalAuthorizationServiceHelpers
 
   let(:user)  { create(:user) }
-  let(:group) { create(:group, :public, :access_requestable) }
+  let(:group) { create(:group, :public) }
   let(:membership) { create(:group_member, group: group) }
 
   describe 'GET index' do
diff --git a/spec/controllers/groups/registry/repositories_controller_spec.rb b/spec/controllers/groups/registry/repositories_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4129891914dd6873b54767ca234e6618e129d8ec
--- /dev/null
+++ b/spec/controllers/groups/registry/repositories_controller_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Groups::Registry::RepositoriesController do
+  let_it_be(:user)  { create(:user) }
+  let_it_be(:guest) { create(:user) }
+  let_it_be(:group, reload: true) { create(:group) }
+
+  before do
+    stub_container_registry_config(enabled: true)
+    group.add_owner(user)
+    group.add_guest(guest)
+    sign_in(user)
+  end
+
+  context 'GET #index' do
+    context 'when container registry is enabled' do
+      it 'show index page' do
+        get :index, params: {
+            group_id: group
+        }
+
+        expect(response).to have_gitlab_http_status(:ok)
+      end
+
+      it 'has the correct response schema' do
+        get :index, params: {
+          group_id: group,
+          format: :json
+        }
+
+        expect(response).to match_response_schema('registry/repositories')
+      end
+
+      it 'returns a list of projects for json format' do
+        project = create(:project, group: group)
+        repo = create(:container_repository, project: project)
+
+        get :index, params: {
+          group_id: group,
+          format: :json
+        }
+
+        expect(response).to have_gitlab_http_status(:ok)
+        expect(json_response).to be_kind_of(Array)
+        expect(json_response.first).to include(
+          'id' => repo.id,
+          'name' => repo.name
+        )
+      end
+
+      it 'tracks the event' do
+        expect(Gitlab::Tracking).to receive(:event).with(anything, 'list_repositories', {})
+
+        get :index, params: {
+          group_id: group
+        }
+      end
+    end
+
+    context 'container registry is disabled' do
+      before do
+        stub_container_registry_config(enabled: false)
+      end
+
+      it 'renders not found' do
+        get :index, params: {
+            group_id: group
+        }
+
+        expect(response).to have_gitlab_http_status(:not_found)
+      end
+    end
+
+    context 'user do not have acces to container registry' do
+      before do
+        sign_out(user)
+        sign_in(guest)
+      end
+
+      it 'renders not found' do
+        get :index, params: {
+          group_id: group
+        }
+        expect(response).to have_gitlab_http_status(:not_found)
+      end
+    end
+  end
+end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index a35ef99ef12bdc2efaa4967d4f6f8d95326e2dd3..3c39a6468e518f0b376e98c83227130f1a9da562 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -385,6 +385,29 @@
       expect(response).to have_gitlab_http_status(302)
       expect(group.reload.project_creation_level).to eq(::Gitlab::Access::MAINTAINER_PROJECT_ACCESS)
     end
+
+    context 'when a project inside the group has container repositories' do
+      before do
+        stub_container_registry_config(enabled: true)
+        stub_container_registry_tags(repository: /image/, tags: %w[rc1])
+        create(:container_repository, project: project, name: :image)
+      end
+
+      it 'does allow the group to be renamed' do
+        post :update, params: { id: group.to_param, group: { name: 'new_name' } }
+
+        expect(controller).to set_flash[:notice]
+        expect(response).to have_gitlab_http_status(302)
+        expect(group.reload.name).to eq('new_name')
+      end
+
+      it 'does not allow to path of the group to be changed' do
+        post :update, params: { id: group.to_param, group: { path: 'new_path' } }
+
+        expect(assigns(:group).errors[:base].first).to match(/Docker images in their Container Registry/)
+        expect(response).to have_gitlab_http_status(200)
+      end
+    end
   end
 
   describe '#ensure_canonical_path' do
@@ -673,6 +696,28 @@ def group_moved_message(redirect_route, group)
         expect(response).to have_gitlab_http_status(404)
       end
     end
+
+    context 'transferring when a project has container images' do
+      let(:group) { create(:group, :public, :nested) }
+      let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
+
+      before do
+        stub_container_registry_config(enabled: true)
+        stub_container_registry_tags(repository: /image/, tags: %w[rc1])
+        create(:container_repository, project: project, name: :image)
+
+        put :transfer,
+          params: {
+            id: group.to_param,
+            new_parent_group_id: ''
+          }
+      end
+
+      it 'does not allow the group to be transferred' do
+        expect(controller).to set_flash[:alert].to match(/Docker images in their Container Registry/)
+        expect(response).to redirect_to(edit_group_path(group))
+      end
+    end
   end
 
   context 'token authentication' do
diff --git a/spec/controllers/profiles/notifications_controller_spec.rb b/spec/controllers/profiles/notifications_controller_spec.rb
index f69847119d48d43313b46889ce935ad0c9f8487d..dbc408bcdd94744bdd1fbf1a298526bff1bb8898 100644
--- a/spec/controllers/profiles/notifications_controller_spec.rb
+++ b/spec/controllers/profiles/notifications_controller_spec.rb
@@ -20,6 +20,38 @@
 
       expect(response).to render_template :show
     end
+
+    context 'with groups that do not have notification preferences' do
+      set(:group) { create(:group) }
+      set(:subgroup) { create(:group, parent: group) }
+
+      before do
+        group.add_developer(user)
+      end
+
+      it 'still shows up in the list' do
+        sign_in(user)
+
+        get :show
+
+        expect(assigns(:group_notifications).map(&:source_id)).to include(subgroup.id)
+      end
+
+      it 'has an N+1 (but should not)' do
+        sign_in(user)
+
+        control = ActiveRecord::QueryRecorder.new do
+          get :show
+        end
+
+        create_list(:group, 2, parent: group)
+
+        # We currently have an N + 1, switch to `not_to` once fixed
+        expect do
+          get :show
+        end.to exceed_query_limit(control)
+      end
+    end
   end
 
   describe 'POST update' do
diff --git a/spec/controllers/projects/blame_controller_spec.rb b/spec/controllers/projects/blame_controller_spec.rb
index f901fd45604902669f91bb1e0f4cdaeb2cc6edd7..dd7c0f45dc277b708c81cdcc8cc95eb81eda1bd9 100644
--- a/spec/controllers/projects/blame_controller_spec.rb
+++ b/spec/controllers/projects/blame_controller_spec.rb
@@ -25,14 +25,25 @@
           })
     end
 
-    context "valid file" do
+    context "valid branch, valid file" do
       let(:id) { 'master/files/ruby/popen.rb' }
+
       it { is_expected.to respond_with(:success) }
     end
 
-    context "invalid file" do
-      let(:id) { 'master/files/ruby/missing_file.rb'}
-      it { expect(response).to have_gitlab_http_status(404) }
+    context "valid branch, invalid file" do
+      let(:id) { 'master/files/ruby/invalid-path.rb' }
+
+      it 'redirects' do
+        expect(subject)
+            .to redirect_to("/#{project.full_path}/tree/master")
+      end
+    end
+
+    context "invalid branch, valid file" do
+      let(:id) { 'invalid-branch/files/ruby/missing_file.rb'}
+
+      it { is_expected.to respond_with(:not_found) }
     end
   end
 end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 17964c78e8d65fa4dba1a17872e02cc205bc02f4..086ec9dfbcfcd77f3000f0b5f71584cac433cc42 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -24,26 +24,34 @@
 
       context "valid branch, valid file" do
         let(:id) { 'master/README.md' }
+
         it { is_expected.to respond_with(:success) }
       end
 
       context "valid branch, invalid file" do
         let(:id) { 'master/invalid-path.rb' }
-        it { is_expected.to respond_with(:not_found) }
+
+        it 'redirects' do
+          expect(subject)
+              .to redirect_to("/#{project.full_path}/tree/master")
+        end
       end
 
       context "invalid branch, valid file" do
         let(:id) { 'invalid-branch/README.md' }
+
         it { is_expected.to respond_with(:not_found) }
       end
 
       context "binary file" do
         let(:id) { 'binary-encoding/encoding/binary-1.bin' }
+
         it { is_expected.to respond_with(:success) }
       end
 
       context "Markdown file" do
         let(:id) { 'master/README.md' }
+
         it { is_expected.to respond_with(:success) }
       end
     end
@@ -104,6 +112,7 @@
 
       context 'redirect to tree' do
         let(:id) { 'markdown/doc' }
+
         it 'redirects' do
           expect(subject)
             .to redirect_to("/#{project.full_path}/tree/markdown/doc")
diff --git a/spec/controllers/projects/deploy_keys_controller_spec.rb b/spec/controllers/projects/deploy_keys_controller_spec.rb
index ccad76eadddc156689b7acc2364eb5dad058ff91..8b1ca2efab2ae841296bd69a055e1b5ab871f2d7 100644
--- a/spec/controllers/projects/deploy_keys_controller_spec.rb
+++ b/spec/controllers/projects/deploy_keys_controller_spec.rb
@@ -5,6 +5,7 @@
 describe Projects::DeployKeysController do
   let(:project) { create(:project, :repository) }
   let(:user) { create(:user) }
+  let(:admin) { create(:admin) }
 
   before do
     project.add_maintainer(user)
@@ -37,7 +38,7 @@
         create(:deploy_keys_project, project: project2, deploy_key: deploy_key_internal)
       end
 
-      let!(:deploy_keys_actual_project) do
+      let!(:deploy_keys_project_actual) do
         create(:deploy_keys_project, project: project, deploy_key: deploy_key_actual)
       end
 
@@ -154,7 +155,7 @@ def create_params(title = 'my-key')
 
     context 'with admin' do
       before do
-        sign_in(create(:admin))
+        sign_in(admin)
       end
 
       it 'returns 302' do
@@ -219,7 +220,7 @@ def create_params(title = 'my-key')
 
     context 'with admin' do
       before do
-        sign_in(create(:admin))
+        sign_in(admin)
       end
 
       it 'returns 302' do
@@ -234,4 +235,80 @@ def create_params(title = 'my-key')
       end
     end
   end
+
+  describe 'PUT update' do
+    let(:extra_params) { {} }
+
+    subject do
+      put :update, params: extra_params.reverse_merge(id: deploy_key.id,
+                                                      namespace_id: project.namespace,
+                                                      project_id: project)
+    end
+
+    def deploy_key_params(title, can_push)
+      deploy_keys_projects_attributes = { '0' => { id: deploy_keys_project, can_push: can_push } }
+      { deploy_key: { title: title, deploy_keys_projects_attributes: deploy_keys_projects_attributes } }
+    end
+
+    let(:deploy_key) { create(:deploy_key, public: true) }
+    let(:project) { create(:project) }
+    let!(:deploy_keys_project) do
+      create(:deploy_keys_project, project: project, deploy_key: deploy_key)
+    end
+
+    context 'with project maintainer' do
+      before do
+        project.add_maintainer(user)
+      end
+
+      context 'public deploy key attached to project' do
+        let(:extra_params) { deploy_key_params('updated title', '1') }
+
+        it 'does not update the title of the deploy key' do
+          expect { subject }.not_to change { deploy_key.reload.title }
+        end
+
+        it 'updates can_push of deploy_keys_project' do
+          expect { subject }.to change { deploy_keys_project.reload.can_push }.from(false).to(true)
+        end
+      end
+    end
+
+    context 'with admin' do
+      before do
+        sign_in(admin)
+      end
+
+      context 'public deploy key attached to project' do
+        let(:extra_params) { deploy_key_params('updated title', '1') }
+
+        it 'updates the title of the deploy key' do
+          expect { subject }.to change { deploy_key.reload.title }.to('updated title')
+        end
+
+        it 'updates can_push of deploy_keys_project' do
+          expect { subject }.to change { deploy_keys_project.reload.can_push }.from(false).to(true)
+        end
+      end
+    end
+
+    context 'with admin as project maintainer' do
+      before do
+        sign_in(admin)
+        project.add_maintainer(admin)
+      end
+
+      context 'public deploy key attached to project' do
+        let(:extra_params) { deploy_key_params('updated title', '1') }
+
+        it 'updates the title of the deploy key' do
+          expect { subject }.to change { deploy_key.reload.title }.to('updated title')
+        end
+
+        it 'updates can_push of deploy_keys_project' do
+          expect { subject }.to change { deploy_keys_project.reload.can_push }.from(false).to(true)
+        end
+      end
+    end
+  end
 end
diff --git a/spec/controllers/projects/deployments_controller_spec.rb b/spec/controllers/projects/deployments_controller_spec.rb
index b9ee69a617b6c6919ef7c876ca05b89622d32963..66112c95742c3395d9fe730e51291cd45e8ae136 100644
--- a/spec/controllers/projects/deployments_controller_spec.rb
+++ b/spec/controllers/projects/deployments_controller_spec.rb
@@ -75,15 +75,13 @@
           }
         end
 
-        before do
+        it 'returns a metrics JSON document' do
           expect_next_instance_of(DeploymentMetrics) do |deployment_metrics|
             allow(deployment_metrics).to receive(:has_metrics?).and_return(true)
 
             expect(deployment_metrics).to receive(:metrics).and_return(empty_metrics)
           end
-        end
 
-        it 'returns a metrics JSON document' do
           get :metrics, params: deployment_params(id: deployment.to_param)
 
           expect(response).to be_ok
@@ -91,6 +89,19 @@
           expect(json_response['metrics']).to eq({})
           expect(json_response['last_update']).to eq(42)
         end
+
+        it 'returns a 404 if the deployment failed' do
+          failed_deployment = create(
+            :deployment,
+            :failed,
+            project: project,
+            environment: environment
+          )
+
+          get :metrics, params: deployment_params(id: failed_deployment.to_param)
+
+          expect(response).to have_gitlab_http_status(404)
+        end
       end
     end
   end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 2edc0aa55367e56072bf8f48173429e086055de5..c9558abab333b8828e77971218ec60bad1325688 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -1180,6 +1180,7 @@ def post_spam
         name: emoji_name
       })
     end
+
     let(:emoji_name) { 'thumbsup' }
 
     it "toggles the award emoji" do
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 53d32665b0ceedb3ffcbd468264b5866bf03f907..90ccb88492745966a8eeb72e57beb39b3a3b3972 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -527,6 +527,7 @@ def get_show(**extra_params)
 
   describe 'GET trace.json' do
     before do
+      stub_feature_flags(job_log_json: true)
       get_trace
     end
 
@@ -535,8 +536,119 @@ def get_show(**extra_params)
 
       it 'returns a trace' do
         expect(response).to have_gitlab_http_status(:ok)
+        expect(response).to match_response_schema('job/build_trace')
         expect(json_response['id']).to eq job.id
         expect(json_response['status']).to eq job.status
+        expect(json_response['state']).to be_present
+        expect(json_response['append']).not_to be_nil
+        expect(json_response['truncated']).not_to be_nil
+        expect(json_response['size']).to be_present
+        expect(json_response['total']).to be_present
+        expect(json_response['lines'].count).to be_positive
+      end
+    end
+
+    context 'when job has a trace' do
+      let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) }
+
+      it 'returns a trace' do
+        expect(response).to have_gitlab_http_status(:ok)
+        expect(response).to match_response_schema('job/build_trace')
+        expect(json_response['id']).to eq job.id
+        expect(json_response['status']).to eq job.status
+        expect(json_response['lines']).to eq [{ 'content' => [{ 'text' => 'BUILD TRACE' }], 'offset' => 0 }]
+      end
+    end
+
+    context 'when job has no traces' do
+      let(:job) { create(:ci_build, pipeline: pipeline) }
+
+      it 'returns no traces' do
+        expect(response).to have_gitlab_http_status(:ok)
+        expect(response).to match_response_schema('job/build_trace')
+        expect(json_response['id']).to eq job.id
+        expect(json_response['status']).to eq job.status
+        expect(json_response['lines']).to be_nil
+      end
+    end
+
+    context 'when job has a trace with ANSI sequence and Unicode' do
+      let(:job) { create(:ci_build, :unicode_trace_live, pipeline: pipeline) }
+
+      it 'returns a trace with Unicode' do
+        expect(response).to have_gitlab_http_status(:ok)
+        expect(response).to match_response_schema('job/build_trace')
+        expect(json_response['id']).to eq job.id
+        expect(json_response['status']).to eq job.status
+        expect(json_response['lines'].flat_map {|l| l['content'].map { |c| c['text'] } }).to include("ヾ(´༎ຶД༎ຶ`)ノ")
+      end
+    end
+
+    context 'when trace artifact is in ObjectStorage' do
+      let(:url) { 'http://object-storage/trace' }
+      let(:file_path) { expand_fixture_path('trace/sample_trace') }
+      let!(:job) { create(:ci_build, :success, :trace_artifact, pipeline: pipeline) }
+
+      before do
+        allow_any_instance_of(JobArtifactUploader).to receive(:file_storage?) { false }
+        allow_any_instance_of(JobArtifactUploader).to receive(:url) { url }
+        allow_any_instance_of(JobArtifactUploader).to receive(:size) { File.size(file_path) }
+      end
+
+      context 'when there are no network issues' do
+        before do
+          stub_remote_url_206(url, file_path)
+
+          get_trace
+        end
+
+        it 'returns a trace' do
+          expect(response).to have_gitlab_http_status(:ok)
+          expect(json_response['id']).to eq job.id
+          expect(json_response['status']).to eq job.status
+          expect(json_response['lines'].count).to be_positive
+        end
+      end
+
+      context 'when there is a network issue' do
+        before do
+          stub_remote_url_500(url)
+        end
+
+        it 'returns a trace' do
+          expect { get_trace }.to raise_error(Gitlab::HttpIO::FailedToGetChunkError)
+        end
+      end
+    end
+
+    def get_trace
+      get :trace,
+        params: {
+          namespace_id: project.namespace,
+          project_id: project,
+          id: job.id
+        },
+        format: :json
+    end
+  end
+
+  describe 'GET legacy trace.json' do
+    before do
+      get_trace
+    end
+
+    context 'when job has a trace artifact' do
+      let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
+
+      it 'returns a trace' do
+        expect(response).to have_gitlab_http_status(:ok)
+        expect(json_response['id']).to eq job.id
+        expect(json_response['status']).to eq job.status
+        expect(json_response['state']).to be_present
+        expect(json_response['append']).not_to be_nil
+        expect(json_response['truncated']).not_to be_nil
+        expect(json_response['size']).to be_present
+        expect(json_response['total']).to be_present
         expect(json_response['html']).to eq(job.trace.html)
       end
     end
@@ -612,12 +724,13 @@ def get_show(**extra_params)
     end
 
     def get_trace
-      get :trace, params: {
-                    namespace_id: project.namespace,
-                    project_id: project,
-                    id: job.id
-                  },
-                  format: :json
+      get :trace,
+        params: {
+          namespace_id: project.namespace,
+          project_id: project,
+          id: job.id
+        },
+        format: :json
     end
   end
 
diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index e677e836145fc26514273692d32d3eab9aac379d..5c02e8d6461b4a8e6430ddf12009a54733e1562e 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -82,9 +82,9 @@ def go(extra_params = {})
         end
       end
 
-      context 'when note has no position' do
+      context 'when note is a legacy diff note' do
         before do
-          create(:legacy_diff_note_on_merge_request, project: project, noteable: merge_request, position: nil)
+          create(:legacy_diff_note_on_merge_request, project: project, noteable: merge_request)
         end
 
         it 'serializes merge request diff collection' do
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index ea7027925578dddad2fa610d3c6a3f58f342452c..827b34b8850a8075aa5c6ed997ab1d1c17b9d00a 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -4,6 +4,7 @@
 
 describe Projects::MergeRequestsController do
   include ProjectForksHelper
+  include Gitlab::Routing
 
   let(:project) { create(:project, :repository) }
   let(:user)    { project.owner }
@@ -206,7 +207,7 @@ def get_merge_requests(page = nil)
       it 'redirects to last_page if page number is larger than number of pages' do
         get_merge_requests(last_page + 1)
 
-        expect(response).to redirect_to(namespace_project_merge_requests_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
+        expect(response).to redirect_to(project_merge_requests_path(project, page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
       end
 
       it 'redirects to specified page' do
@@ -227,7 +228,7 @@ def get_merge_requests(page = nil)
             host: external_host
           }
 
-        expect(response).to redirect_to(namespace_project_merge_requests_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
+        expect(response).to redirect_to(project_merge_requests_path(project, page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
       end
     end
 
@@ -770,6 +771,189 @@ def go(format: 'html')
     end
   end
 
+  describe 'GET exposed_artifacts' do
+    let(:merge_request) do
+      create(:merge_request,
+        :with_merge_request_pipeline,
+        target_project: project,
+        source_project: project)
+    end
+
+    let(:pipeline) do
+      create(:ci_pipeline,
+        :success,
+        project: merge_request.source_project,
+        ref: merge_request.source_branch,
+        sha: merge_request.diff_head_sha)
+    end
+
+    let!(:job) { create(:ci_build, pipeline: pipeline, options: job_options) }
+    let!(:job_metadata) { create(:ci_job_artifact, :metadata, job: job) }
+
+    before do
+      allow_any_instance_of(MergeRequest)
+        .to receive(:find_exposed_artifacts)
+        .and_return(report)
+
+      allow_any_instance_of(MergeRequest)
+        .to receive(:actual_head_pipeline)
+        .and_return(pipeline)
+    end
+
+    subject do
+      get :exposed_artifacts, params: {
+        namespace_id: project.namespace.to_param,
+        project_id: project,
+        id: merge_request.iid
+      },
+      format: :json
+    end
+
+    describe 'permissions on a public project with private CI/CD' do
+      let(:project) { create :project, :repository, :public, :builds_private }
+      let(:report) { { status: :parsed, data: [] } }
+      let(:job_options) { {} }
+
+      context 'while signed out' do
+        before do
+          sign_out(user)
+        end
+
+        it 'responds with a 404' do
+          subject
+
+          expect(response).to have_gitlab_http_status(404)
+          expect(response.body).to be_blank
+        end
+      end
+
+      context 'while signed in as an unrelated user' do
+        before do
+          sign_in(create(:user))
+        end
+
+        it 'responds with a 404' do
+          subject
+
+          expect(response).to have_gitlab_http_status(404)
+          expect(response.body).to be_blank
+        end
+      end
+    end
+
+    context 'when pipeline has jobs with exposed artifacts' do
+      let(:job_options) do
+        {
+          artifacts: {
+            paths: ['ci_artifacts.txt'],
+            expose_as: 'Exposed artifact'
+          }
+        }
+      end
+
+      context 'when fetching exposed artifacts is in progress' do
+        let(:report) { { status: :parsing } }
+
+        it 'sends polling interval' do
+          expect(Gitlab::PollingInterval).to receive(:set_header)
+
+          subject
+        end
+
+        it 'returns 204 HTTP status' do
+          subject
+
+          expect(response).to have_gitlab_http_status(:no_content)
+        end
+      end
+
+      context 'when fetching exposed artifacts is completed' do
+        let(:data) do
+          Ci::GenerateExposedArtifactsReportService.new(project, user)
+            .execute(nil, pipeline)
+        end
+
+        let(:report) { { status: :parsed, data: data } }
+
+        it 'returns exposed artifacts' do
+          subject
+
+          expect(response).to have_gitlab_http_status(200)
+          expect(json_response['status']).to eq('parsed')
+          expect(json_response['data']).to eq([{
+            'job_name' => 'test',
+            'job_path' => project_job_path(project, job),
+            'url' => file_project_job_artifacts_path(project, job, 'ci_artifacts.txt'),
+            'text' => 'Exposed artifact'
+          }])
+        end
+      end
+
+      context 'when something went wrong on our system' do
+        let(:report) { {} }
+
+        it 'does not send polling interval' do
+          expect(Gitlab::PollingInterval).not_to receive(:set_header)
+
+          subject
+        end
+
+        it 'returns 500 HTTP status' do
+          subject
+
+          expect(response).to have_gitlab_http_status(:internal_server_error)
+          expect(json_response).to eq({ 'status_reason' => 'Unknown error' })
+        end
+      end
+
+      context 'when feature flag :ci_expose_arbitrary_artifacts_in_mr is disabled' do
+        let(:job_options) do
+          {
+            artifacts: {
+              paths: ['ci_artifacts.txt'],
+              expose_as: 'Exposed artifact'
+            }
+          }
+        end
+        let(:report) { double }
+
+        before do
+          stub_feature_flags(ci_expose_arbitrary_artifacts_in_mr: false)
+        end
+
+        it 'does not send polling interval' do
+          expect(Gitlab::PollingInterval).not_to receive(:set_header)
+
+          subject
+        end
+
+        it 'returns 204 HTTP status' do
+          subject
+
+          expect(response).to have_gitlab_http_status(:no_content)
+        end
+      end
+    end
+
+    context 'when pipeline does not have jobs with exposed artifacts' do
+      let(:report) { double }
+      let(:job_options) do
+        {
+          artifacts: {
+            paths: ['ci_artifacts.txt']
+          }
+        }
+      end
+
+      it 'returns no content' do
+        subject
+
+        expect(response).to have_gitlab_http_status(204)
+        expect(response.body).to be_empty
+      end
+    end
+  end
+
   describe 'GET test_reports' do
     let(:merge_request) do
       create(:merge_request,
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 4db77921f243b066ddc1d6e2dc1ef07f710f49f5..3ab191c00322cef60f9eb17b386a3ad77ddb9518 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -713,6 +713,7 @@ def create!
     end
 
     subject { post(:toggle_award_emoji, params: request_params.merge(name: emoji_name)) }
+
     let(:emoji_name) { 'thumbsup' }
 
     it "toggles the award emoji" do
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index d964b0672a902a3ea96f575d2fd44f03238f67d3..e3ad36f8d2427ffeb808dd40be926f4d536114ba 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -217,6 +217,193 @@ def create_build(pipeline, stage, stage_idx, name)
       end
     end
 
+    context 'with triggered pipelines' do
+      let_it_be(:project) { create(:project, :repository) }
+      let_it_be(:source_project) { create(:project, :repository) }
+      let_it_be(:target_project) { create(:project, :repository) }
+      let_it_be(:root_pipeline) { create_pipeline(project) }
+      let_it_be(:source_pipeline) { create_pipeline(source_project) }
+      let_it_be(:source_of_source_pipeline) { create_pipeline(source_project) }
+      let_it_be(:target_pipeline) { create_pipeline(target_project) }
+      let_it_be(:target_of_target_pipeline) { create_pipeline(target_project) }
+
+      before do
+        create_link(source_of_source_pipeline, source_pipeline)
+        create_link(source_pipeline, root_pipeline)
+        create_link(root_pipeline, target_pipeline)
+        create_link(target_pipeline, target_of_target_pipeline)
+      end
+
+      shared_examples 'not expanded' do
+        let(:expected_stages) { be_nil }
+
+        it 'does return base details' do
+          get_pipeline_json(root_pipeline)
+
+          expect(json_response['triggered_by']).to include('id' => source_pipeline.id)
+          expect(json_response['triggered']).to contain_exactly(
+            include('id' => target_pipeline.id))
+        end
+
+        it 'does not expand triggered_by pipeline' do
+          get_pipeline_json(root_pipeline)
+
+          triggered_by = json_response['triggered_by']
+          expect(triggered_by['triggered_by']).to be_nil
+          expect(triggered_by['triggered']).to be_nil
+          expect(triggered_by['details']['stages']).to expected_stages
+        end
+
+        it 'does not expand triggered pipelines' do
+          get_pipeline_json(root_pipeline)
+
+          first_triggered = json_response['triggered'].first
+          expect(first_triggered['triggered_by']).to be_nil
+          expect(first_triggered['triggered']).to be_nil
+          expect(first_triggered['details']['stages']).to expected_stages
+        end
+      end
+
+      shared_examples 'expanded' do
+        it 'does return base details' do
+          get_pipeline_json(root_pipeline)
+
+          expect(json_response['triggered_by']).to include('id' => source_pipeline.id)
+          expect(json_response['triggered']).to contain_exactly(
+            include('id' => target_pipeline.id))
+        end
+
+        it 'does expand triggered_by pipeline' do
+          get_pipeline_json(root_pipeline)
+
+          triggered_by = json_response['triggered_by']
+          expect(triggered_by['triggered_by']).to include(
+            'id' => source_of_source_pipeline.id)
+          expect(triggered_by['details']['stages']).not_to be_nil
+        end
+
+        it 'does not recursively expand triggered_by' do
+          get_pipeline_json(root_pipeline)
+
+          triggered_by = json_response['triggered_by']
+          expect(triggered_by['triggered']).to be_nil
+        end
+
+        it 'does expand triggered pipelines' do
+          get_pipeline_json(root_pipeline)
+
+          first_triggered = json_response['triggered'].first
+          expect(first_triggered['triggered']).to contain_exactly(
+            include('id' => target_of_target_pipeline.id))
+          expect(first_triggered['details']['stages']).not_to be_nil
+        end
+
+        it 'does not recursively expand triggered' do
+          get_pipeline_json(root_pipeline)
+
+          first_triggered = json_response['triggered'].first
+          expect(first_triggered['triggered_by']).to be_nil
+        end
+      end
+
+      context 'when it does have permission to read other projects' do
+        before do
+          source_project.add_developer(user)
+          target_project.add_developer(user)
+        end
+
+        context 'when not-expanding any pipelines' do
+          let(:expanded) { nil }
+
+          it_behaves_like 'not expanded'
+        end
+
+        context 'when expanding non-existing pipeline' do
+          let(:expanded) { [-1] }
+
+          it_behaves_like 'not expanded'
+        end
+
+        context 'when expanding pipeline that is not directly expandable' do
+          let(:expanded) { [source_of_source_pipeline.id, target_of_target_pipeline.id] }
+
+          it_behaves_like 'not expanded'
+        end
+
+        context 'when expanding self' do
+          let(:expanded) { [root_pipeline.id] }
+
+          context 'it does not recursively expand pipelines' do
+            it_behaves_like 'not expanded'
+          end
+        end
+
+        context 'when expanding source and target pipeline' do
+          let(:expanded) { [source_pipeline.id, target_pipeline.id] }
+
+          it_behaves_like 'expanded'
+
+          context 'when expand depth is limited to 1' do
+            before do
+              stub_const('TriggeredPipelineEntity::MAX_EXPAND_DEPTH', 1)
+            end
+
+            it_behaves_like 'not expanded' do
+              # We expect that triggered/triggered_by is not expanded,
+              # but we still return details.stages for that pipeline
+              let(:expected_stages) { be_a(Array) }
+            end
+          end
+        end
+
+        context 'when expanding all' do
+          let(:expanded) do
+            [
+              source_of_source_pipeline.id,
+              source_pipeline.id,
+              root_pipeline.id,
+              target_pipeline.id,
+              target_of_target_pipeline.id
+            ]
+          end
+
+          it_behaves_like 'expanded'
+        end
+      end
+
+      context 'when does not have permission to read other projects' do
+        let(:expanded) { [source_pipeline.id, target_pipeline.id] }
+
+        it_behaves_like 'not expanded'
+      end
+
+      def create_pipeline(project)
+        create(:ci_empty_pipeline, project: project).tap do |pipeline|
+          create(:ci_build, pipeline: pipeline, stage: 'test', name: 'rspec')
+        end
+      end
+
+      def create_link(source_pipeline, pipeline)
+        source_pipeline.sourced_pipelines.create!(
+          source_job: source_pipeline.builds.all.sample,
+          source_project: source_pipeline.project,
+          project: pipeline.project,
+          pipeline: pipeline
+        )
+      end
+
+      def get_pipeline_json(pipeline)
+        params = {
+          namespace_id: pipeline.project.namespace,
+          project_id: pipeline.project,
+          id: pipeline,
+          expanded: expanded
+        }
+
+        get :show, params: params.compact, format: :json
+      end
+    end
+
     def get_pipeline_json
       get :show, params: { namespace_id: project.namespace, project_id: project, id: pipeline }, format: :json
     end
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index 5130e26c92806f8678ffc19f89bf0f3c651b6413..2f473d395add2d708b51aef49229c520905d395d 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -4,7 +4,7 @@
 
 describe Projects::ProjectMembersController do
   let(:user) { create(:user) }
-  let(:project) { create(:project, :public, :access_requestable) }
+  let(:project) { create(:project, :public) }
 
   describe 'GET index' do
     it 'has the project_members address with a 200 status code' do
diff --git a/spec/controllers/projects/serverless/functions_controller_spec.rb b/spec/controllers/projects/serverless/functions_controller_spec.rb
index 9f1ef3a4be8510f19be09e01bae5075ec7ee7631..eccc8e1d5de7ea9c9b617f34825686cc247c1fd3 100644
--- a/spec/controllers/projects/serverless/functions_controller_spec.rb
+++ b/spec/controllers/projects/serverless/functions_controller_spec.rb
@@ -107,26 +107,50 @@ def params(opts = {})
       end
     end
 
-    context 'valid data', :use_clean_rails_memory_store_caching do
-      before do
-        stub_kubeclient_service_pods
-        stub_reactive_cache(knative_services_finder,
-          {
-            services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
-            pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
-          },
-          *knative_services_finder.cache_args)
+    context 'with valid data', :use_clean_rails_memory_store_caching do
+      shared_examples 'GET #show with valid data' do
+        it 'has a valid function name' do
+          get :show, params: params({ format: :json, environment_id: "*", id: cluster.project.name })
+          expect(response).to have_gitlab_http_status(200)
+
+          expect(json_response).to include(
+            "name" => project.name,
+            "url" => "http://#{project.name}.#{namespace.namespace}.example.com",
+            "podcount" => 1
+          )
+        end
       end
 
-      it 'has a valid function name' do
-        get :show, params: params({ format: :json, environment_id: "*", id: cluster.project.name })
-        expect(response).to have_gitlab_http_status(200)
+      context 'on Knative 0.5' do
+        before do
+          stub_kubeclient_service_pods
+          stub_reactive_cache(knative_services_finder,
+                              {
+                                services: kube_knative_services_body(
+                                  legacy_knative: true,
+                                  namespace: namespace.namespace,
+                                  name: cluster.project.name
+                                )["items"],
+                                pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
+                              },
+                              *knative_services_finder.cache_args)
+        end
 
-        expect(json_response).to include(
-          "name" => project.name,
-          "url" => "http://#{project.name}.#{namespace.namespace}.example.com",
-          "podcount" => 1
-        )
+        include_examples 'GET #show with valid data'
+      end
+
+      context 'on Knative 0.6 or 0.7' do
+        before do
+          stub_kubeclient_service_pods
+          stub_reactive_cache(knative_services_finder,
+            {
+              services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
+              pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
+            },
+            *knative_services_finder.cache_args)
+        end
+
+        include_examples 'GET #show with valid data'
       end
     end
   end
@@ -141,38 +165,60 @@ def params(opts = {})
   end
 
   describe 'GET #index with data', :use_clean_rails_memory_store_caching do
-    before do
-      stub_kubeclient_service_pods
-      stub_reactive_cache(knative_services_finder,
-        {
-          services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
-          pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
-        },
-        *knative_services_finder.cache_args)
+    shared_examples 'GET #index with data' do
+      it 'has data' do
+        get :index, params: params({ format: :json })
+
+        expect(response).to have_gitlab_http_status(200)
+
+        expect(json_response).to match({
+                                         "knative_installed" => "checking",
+                                         "functions" => [
+                                           a_hash_including(
+                                             "name" => project.name,
+                                             "url" => "http://#{project.name}.#{namespace.namespace}.example.com"
+                                           )
+                                         ]
+                                       })
+      end
+
+      it 'has data in html' do
+        get :index, params: params
+
+        expect(response).to have_gitlab_http_status(200)
+      end
     end
 
-    it 'has data' do
-      get :index, params: params({ format: :json })
-
-      expect(response).to have_gitlab_http_status(200)
-
-      expect(json_response).to match(
-        {
-          "knative_installed" => "checking",
-          "functions" => [
-            a_hash_including(
-              "name" => project.name,
-              "url" => "http://#{project.name}.#{namespace.namespace}.example.com"
-            )
-          ]
-        }
-      )
+    context 'on Knative 0.5' do
+      before do
+        stub_kubeclient_service_pods
+        stub_reactive_cache(knative_services_finder,
+                            {
+                              services: kube_knative_services_body(
+                                legacy_knative: true,
+                                namespace: namespace.namespace,
+                                name: cluster.project.name
+                              )["items"],
+                              pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
+                            },
+                            *knative_services_finder.cache_args)
+      end
+
+      include_examples 'GET #index with data'
     end
 
-    it 'has data in html' do
-      get :index, params: params
+    context 'on Knative 0.6 or 0.7' do
+      before do
+        stub_kubeclient_service_pods
+        stub_reactive_cache(knative_services_finder,
+          {
+            services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
+            pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
+          },
+          *knative_services_finder.cache_args)
+      end
 
-      expect(response).to have_gitlab_http_status(200)
+      include_examples 'GET #index with data'
     end
   end
 end
diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
index 93507b5891049148aac67b2c63a9d789659fc08a..c67e7f7dadd7825bff23fa15c82f08f3de2aafdd 100644
--- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -78,6 +78,7 @@
 
   describe 'PUT #reset_registration_token' do
     subject { put :reset_registration_token, params: { namespace_id: project.namespace, project_id: project } }
+
     it 'resets runner registration token' do
       expect { subject }.to change { project.reload.runners_token }
     end
diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb
index 7f7cabe3b0cabfc0581f5189be87c33601e903df..c0c11db5dd632938a4799f6c1485c3df6b49a4de 100644
--- a/spec/controllers/projects/tree_controller_spec.rb
+++ b/spec/controllers/projects/tree_controller_spec.rb
@@ -30,46 +30,61 @@
 
     context "valid branch, no path" do
       let(:id) { 'master' }
+
       it { is_expected.to respond_with(:success) }
     end
 
     context "valid branch, valid path" do
       let(:id) { 'master/encoding/' }
+
       it { is_expected.to respond_with(:success) }
     end
 
     context "valid branch, invalid path" do
       let(:id) { 'master/invalid-path/' }
-      it { is_expected.to respond_with(:not_found) }
+
+      it 'redirects' do
+        expect(subject)
+            .to redirect_to("/#{project.full_path}/tree/master")
+      end
     end
 
     context "invalid branch, valid path" do
       let(:id) { 'invalid-branch/encoding/' }
+
       it { is_expected.to respond_with(:not_found) }
     end
 
     context "valid empty branch, invalid path" do
       let(:id) { 'empty-branch/invalid-path/' }
-      it { is_expected.to respond_with(:not_found) }
+
+      it 'redirects' do
+        expect(subject)
+            .to redirect_to("/#{project.full_path}/tree/empty-branch")
+      end
     end
 
     context "valid empty branch" do
       let(:id) { 'empty-branch' }
+
       it { is_expected.to respond_with(:success) }
     end
 
     context "invalid SHA commit ID" do
       let(:id) { 'ff39438/.gitignore' }
+
       it { is_expected.to respond_with(:not_found) }
     end
 
     context "valid SHA commit ID" do
       let(:id) { '6d39438' }
+
       it { is_expected.to respond_with(:success) }
     end
 
     context "valid SHA commit ID with path" do
       let(:id) { '6d39438/.gitignore' }
+
       it { expect(response).to have_gitlab_http_status(302) }
     end
   end
@@ -108,6 +123,7 @@
 
     context 'redirect to blob' do
       let(:id) { 'master/README.md' }
+
       it 'redirects' do
         redirect_url = "/#{project.full_path}/blob/master/README.md"
         expect(subject)
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 5d87dbdee8bbfccb9069aa1c5ea3466550ac21e9..ebeed94c274e4a1c99b14433c42eee0b13c59322 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -114,9 +114,14 @@
     context 'when invisible captcha is enabled' do
       before do
         stub_feature_flags(invisible_captcha: true)
+        InvisibleCaptcha.timestamp_enabled = true
         InvisibleCaptcha.timestamp_threshold = treshold
       end
 
+      after do
+        InvisibleCaptcha.timestamp_enabled = false
+      end
+
       let(:treshold) { 4 }
       let(:session_params) { { invisible_captcha_timestamp: form_rendered_time.iso8601 } }
       let(:form_rendered_time) { Time.current }
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 5216683bd36834cb9e4489fd250bb15e9cf83d98..53f4a2610927f880030f5a57b24a030a41097cb2 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -19,6 +19,7 @@
     approver_groups: %w[target_id],
     audit_events: %w[author_id entity_id],
     award_emoji: %w[awardable_id user_id],
+    aws_roles: %w[role_external_id],
     boards: %w[milestone_id],
     chat_names: %w[chat_id service_id team_id user_id],
     chat_teams: %w[team_id],
@@ -26,6 +27,7 @@
     ci_pipelines: %w[user_id],
     ci_runner_projects: %w[runner_id],
     ci_trigger_requests: %w[commit_id],
+    cluster_providers_aws: %w[security_group_id vpc_id access_key_id],
     cluster_providers_gcp: %w[gcp_project_id operation_id],
     deploy_keys_projects: %w[deploy_key_id],
     deployments: %w[deployable_id environment_id user_id],
diff --git a/spec/dependencies/omniauth_saml_spec.rb b/spec/dependencies/omniauth_saml_spec.rb
index ccc604dc23090a842d3e007b7e2c8977dc114f6b..8a685648c71596a25f5df679ad299eb65f96486d 100644
--- a/spec/dependencies/omniauth_saml_spec.rb
+++ b/spec/dependencies/omniauth_saml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 require 'omniauth/strategies/saml'
 
diff --git a/spec/factories/aws/roles.rb b/spec/factories/aws/roles.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c078033dfadd70c97e5fbfb2f7ee914ca507600d
--- /dev/null
+++ b/spec/factories/aws/roles.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+  factory :aws_role, class: Aws::Role do
+    user
+
+    role_arn { 'arn:aws:iam::123456789012:role/role-name' }
+    sequence(:role_external_id) { |n| "external-id-#{n}" }
+  end
+end
diff --git a/spec/factories/boards.rb b/spec/factories/boards.rb
index 29cfe8fb295766cfb925eafe7ee8635a465eb146..a201ca94380ce6df78bd987ed1f58405d7a62cae 100644
--- a/spec/factories/boards.rb
+++ b/spec/factories/boards.rb
@@ -7,7 +7,7 @@
       group { nil }
       project_id { nil }
       group_id { nil }
-      parent { nil }
+      resource_parent { nil }
     end
 
     after(:build, :stub) do |board, evaluator|
@@ -19,9 +19,9 @@
         board.project = evaluator.project
       elsif evaluator.project_id
         board.project_id = evaluator.project_id
-      elsif evaluator.parent
-        id = evaluator.parent.id
-        evaluator.parent.is_a?(Group) ? board.group_id = id : evaluator.project_id = id
+      elsif evaluator.resource_parent
+        id = evaluator.resource_parent.id
+        evaluator.resource_parent.is_a?(Group) ? board.group_id = id : evaluator.project_id = id
       else
         board.project = create(:project, :empty_repo)
       end
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index fefd89728e67d93d69b2083d2179708330cd7437..39ab574cc76619b6629836e12ccbb03c9aeb1e5d 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -95,6 +95,17 @@
         end
       end
 
+      trait :with_exposed_artifacts do
+        status { :success }
+
+        after(:build) do |pipeline, evaluator|
+          pipeline.builds << build(:ci_build, :artifacts,
+            pipeline: pipeline,
+            project: pipeline.project,
+            options: { artifacts: { expose_as: 'the artifact', paths: ['ci_artifacts.txt'] } })
+        end
+      end
+
       trait :with_job do
         after(:build) do |pipeline, evaluator|
           pipeline.builds << build(:ci_build, pipeline: pipeline, project: pipeline.project)
diff --git a/ee/spec/factories/ci/sources/pipelines.rb b/spec/factories/ci/sources/pipelines.rb
similarity index 100%
rename from ee/spec/factories/ci/sources/pipelines.rb
rename to spec/factories/ci/sources/pipelines.rb
diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb
index b43a88b39e35685b3b875033a3186b861ba481d2..63f33633a3ce5c68a1eac5a0a1736937518e7807 100644
--- a/spec/factories/clusters/clusters.rb
+++ b/spec/factories/clusters/clusters.rb
@@ -53,6 +53,14 @@
       platform_kubernetes factory: [:cluster_platform_kubernetes, :configured]
     end
 
+    trait :provided_by_aws do
+      provider_type { :aws }
+      platform_type { :kubernetes }
+
+      provider_aws factory: [:cluster_provider_aws, :created]
+      platform_kubernetes factory: [:cluster_platform_kubernetes, :configured]
+    end
+
     trait :providing_by_gcp do
       provider_type { :gcp }
       provider_gcp factory: [:cluster_provider_gcp, :creating]
diff --git a/spec/factories/clusters/providers/aws.rb b/spec/factories/clusters/providers/aws.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f4bc61455c501b1f33203960d1f367237de521c3
--- /dev/null
+++ b/spec/factories/clusters/providers/aws.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+  factory :cluster_provider_aws, class: Clusters::Providers::Aws do
+    cluster
+    created_by_user factory: :user
+
+    role_arn { 'arn:aws:iam::123456789012:role/role-name' }
+    vpc_id { 'vpc-00000000000000000' }
+    subnet_ids { %w(subnet-00000000000000000 subnet-11111111111111111) }
+    security_group_id { 'sg-00000000000000000' }
+    key_name { 'user' }
+
+    trait :scheduled do
+      access_key_id { 'access_key_id' }
+      secret_access_key { 'secret_access_key' }
+      session_token { 'session_token' }
+    end
+
+    trait :creating do
+      after(:build) do |provider|
+        provider.make_creating
+      end
+    end
+
+    trait :created do
+      after(:build) do |provider|
+        provider.make_created
+      end
+    end
+
+    trait :errored do
+      after(:build) do |provider|
+        provider.make_errored('An error occurred')
+      end
+    end
+  end
+end
diff --git a/spec/factories/container_repositories.rb b/spec/factories/container_repositories.rb
index b2862e41e65ba26fce54c291e84f0b61b5132dab..4cf1537f64b371ef70be622fd892974a8b84bf3d 100644
--- a/spec/factories/container_repositories.rb
+++ b/spec/factories/container_repositories.rb
@@ -16,19 +16,22 @@
     after(:build) do |repository, evaluator|
       next if evaluator.tags.to_a.none?
 
+      tags = evaluator.tags
+      # convert Array into Hash
+      tags = tags.product(['sha256:4c8e63ca4cb663ce6c688cb06f1c372b088dac5b6d7ad7d49cd620d85cf72a15']).to_h unless tags.is_a?(Hash)
+
       allow(repository.client)
         .to receive(:repository_tags)
         .and_return({
           'name' => repository.path,
-          'tags' => evaluator.tags
+          'tags' => tags.keys
         })
 
-      evaluator.tags.each do |tag|
+      tags.each_pair do |tag, digest|
         allow(repository.client)
           .to receive(:repository_tag_digest)
           .with(repository.path, tag)
-          .and_return('sha256:4c8e63ca4cb663ce6c688cb06f1c3' \
-                      '72b088dac5b6d7ad7d49cd620d85cf72a15')
+          .and_return(digest)
       end
     end
   end
diff --git a/spec/factories/evidences.rb b/spec/factories/evidences.rb
new file mode 100644
index 0000000000000000000000000000000000000000..964f232a1c9e034677def6dc97a0fa4d42d2536c
--- /dev/null
+++ b/spec/factories/evidences.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+  factory :evidence do
+    release
+  end
+end
diff --git a/spec/factories/gitaly/commit.rb b/spec/factories/gitaly/commit.rb
index 954b53388465c2f81b3d0d7eb3f250a5c5ba6ed4..ef5301db7704730e4f612cb568aab445d2dfd3fb 100644
--- a/spec/factories/gitaly/commit.rb
+++ b/spec/factories/gitaly/commit.rb
@@ -12,6 +12,7 @@
       Google::Protobuf::RepeatedField.new(:string, ids)
     end
     subject { "My commit" }
+
     body { subject + "\nMy body" }
     author { build(:gitaly_commit_author) }
     committer { build(:gitaly_commit_author) }
diff --git a/spec/factories/grafana_integrations.rb b/spec/factories/grafana_integrations.rb
index c19417f5a900b8ca2132af487d1d5bd9c3b77468..4eb3bee8b284809fe87d1bef87111e6c74f3f390 100644
--- a/spec/factories/grafana_integrations.rb
+++ b/spec/factories/grafana_integrations.rb
@@ -3,7 +3,7 @@
 FactoryBot.define do
   factory :grafana_integration, class: GrafanaIntegration do
     project
-    grafana_url { 'https://grafana.com' }
+    grafana_url { 'https://grafana.example.com' }
     token { SecureRandom.hex(10) }
   end
 end
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index ba1a4883f85a78ce1287f44d61b987fdb811051b..93c01f8034d3705bc49fb71397cf7ac98c906773 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -32,8 +32,8 @@
       avatar { fixture_file_upload('spec/fixtures/dk.png') }
     end
 
-    trait :access_requestable do
-      request_access_enabled { true }
+    trait :request_access_disabled do
+      request_access_enabled { false }
     end
 
     trait :nested do
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index 70f480a3bcb3fbe57676001def5c225230ef4865..46910078ee50a710a0d6c6b4824f180c36adbd77 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -12,7 +12,7 @@
     end
 
     trait :opened do
-      state { :opened }
+      state_id { Issue.available_states[:opened] }
     end
 
     trait :locked do
@@ -20,10 +20,14 @@
     end
 
     trait :closed do
-      state { :closed }
+      state_id { Issue.available_states[:closed] }
       closed_at { Time.now }
     end
 
+    after(:build) do |issue, evaluator|
+      issue.state_id = Issue.available_states[evaluator.state]
+    end
+
     factory :closed_issue, traits: [:closed]
     factory :reopened_issue, traits: [:opened]
 
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 28a3f76d485668ed74959cf5f93c99d6a8b34e91..13612214e722f06166a7ca4ded3c1f9638926226 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -40,7 +40,7 @@
     end
 
     trait :merged do
-      state { :merged }
+      state_id { MergeRequest.available_states[:merged] }
     end
 
     trait :merged_target do
@@ -57,7 +57,7 @@
     end
 
     trait :closed do
-      state { :closed }
+      state_id { MergeRequest.available_states[:closed] }
     end
 
     trait :closed_last_month do
@@ -69,7 +69,7 @@
     end
 
     trait :opened do
-      state { :opened }
+      state_id { MergeRequest.available_states[:opened] }
     end
 
     trait :invalid do
@@ -78,7 +78,7 @@
     end
 
     trait :locked do
-      state { :locked }
+      state_id { MergeRequest.available_states[:locked] }
     end
 
     trait :simple do
@@ -120,6 +120,18 @@
       end
     end
 
+    trait :with_exposed_artifacts do
+      after(:build) do |merge_request|
+        merge_request.head_pipeline = build(
+          :ci_pipeline,
+          :success,
+          :with_exposed_artifacts,
+          project: merge_request.source_project,
+          ref: merge_request.source_branch,
+          sha: merge_request.diff_head_sha)
+      end
+    end
+
     trait :with_legacy_detached_merge_request_pipeline do
       after(:create) do |merge_request|
         merge_request.pipelines_for_merge_request << create(:ci_pipeline,
@@ -186,6 +198,10 @@
       end
     end
 
+    after(:build) do |merge_request, evaluator|
+      merge_request.state_id = MergeRequest.available_states[evaluator.state]
+    end
+
     after(:create) do |merge_request, evaluator|
       merge_request.cache_merge_request_closes_issues!
     end
diff --git a/spec/factories/milestones.rb b/spec/factories/milestones.rb
index 75ff925774aa4cd79f7d9cbd403bf7757dbc7dd3..32eee645f6a031da2fffea73e534c11418555947 100644
--- a/spec/factories/milestones.rb
+++ b/spec/factories/milestones.rb
@@ -9,7 +9,7 @@
       group { nil }
       project_id { nil }
       group_id { nil }
-      parent { nil }
+      resource_parent { nil }
     end
 
     trait :active do
@@ -34,9 +34,9 @@
         milestone.project = evaluator.project
       elsif evaluator.project_id
         milestone.project_id = evaluator.project_id
-      elsif evaluator.parent
-        id = evaluator.parent.id
-        evaluator.parent.is_a?(Group) ? evaluator.group_id = id : evaluator.project_id = id
+      elsif evaluator.resource_parent
+        id = evaluator.resource_parent.id
+        evaluator.resource_parent.is_a?(Group) ? evaluator.group_id = id : evaluator.project_id = id
       else
         milestone.project = create(:project)
       end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index ae1feb73e4de0feaedb4b21c51824d3c1a4970bc..9477eeb18d4e697a21e393aba7c0b1c56b566bd8 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -117,8 +117,8 @@
       storage_version { nil }
     end
 
-    trait :access_requestable do
-      request_access_enabled { true }
+    trait :request_access_disabled do
+      request_access_enabled { false }
     end
 
     trait :with_avatar do
diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb
index ef464d3d6e08344207c0460aa97a975d61a1f564..a060cd7d6f8fbe4be734d942769c465db97b7268 100644
--- a/spec/factories/uploads.rb
+++ b/spec/factories/uploads.rb
@@ -15,7 +15,7 @@
     end
 
     path do
-      uploader_instance = Object.const_get(uploader.to_s).new(model, mount_point)
+      uploader_instance = Object.const_get(uploader.to_s, false).new(model, mount_point)
       File.join(uploader_instance.store_dir, filename)
     end
 
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index e3d20c1f46c06dc69860062365976634a3bb1cfa..f83c137b758605c28a15d8cb4a10288b805abc0c 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -6,6 +6,7 @@
     name { generate(:name) }
     username { generate(:username) }
     password { "12345678" }
+    role { 'software_developer' }
     confirmed_at { Time.now }
     confirmation_token { nil }
     can_create_group { true }
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index e1c9364067a280d9975b76e64eff45d312eba3db..99a6165cfc9b154260d1852aa7a957b4988afe37 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -5,6 +5,7 @@
 describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_mock_admin_mode do
   include StubENV
   include TermsHelper
+  include MobileHelpers
 
   let(:admin) { create(:admin) }
 
@@ -450,6 +451,32 @@
           expect(page).to have_link(text: 'Support', href: new_support_url)
         end
       end
+
+      it 'Shows admin dashboard links on bigger screen' do
+        visit root_dashboard_path
+
+        page.within '.navbar' do
+          expect(page).to have_link(text: 'Admin Area', href: admin_root_path, visible: true)
+          expect(page).to have_link(text: 'Leave Admin Mode', href: destroy_admin_session_path, visible: true)
+        end
+      end
+
+      it 'Relocates admin dashboard links to dropdown list on smaller screen', :js do
+        resize_screen_xs
+        visit root_dashboard_path
+
+        page.within '.navbar' do
+          expect(page).not_to have_link(text: 'Admin Area', href: admin_root_path, visible: true)
+          expect(page).not_to have_link(text: 'Leave Admin Mode', href: destroy_admin_session_path, visible: true)
+        end
+
+        find('.header-more').click
+
+        page.within '.navbar' do
+          expect(page).to have_link(text: 'Admin Area', href: admin_root_path, visible: true)
+          expect(page).to have_link(text: 'Leave Admin Mode', href: destroy_admin_session_path, visible: true)
+        end
+      end
     end
 
     context 'when in admin_mode' do
@@ -462,7 +489,7 @@
       it 'can leave admin mode' do
         page.within('.navbar-sub-nav') do
           # Select first, link is also included in mobile view list
-          click_on 'Leave admin mode', match: :first
+          click_on 'Leave Admin Mode', match: :first
 
           expect(page).to have_link(href: new_admin_session_path)
         end
@@ -481,7 +508,7 @@
       before do
         page.within('.navbar-sub-nav') do
           # Select first, link is also included in mobile view list
-          click_on 'Leave admin mode', match: :first
+          click_on 'Leave Admin Mode', match: :first
         end
       end
 
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index 0cb24ef856b520f005b11d656939df0fb661f187..5d87c9d7be87bf3a9f4c77e3546197b1702caa69 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -93,6 +93,7 @@
       end
 
       it 'shows projects only with issues feature enabled', :js do
+        find('.empty-state .js-lazy-loaded')
         find('.new-project-item-link').click
 
         page.within('.select2-results') do
diff --git a/spec/features/groups/members/master_manages_access_requests_spec.rb b/spec/features/groups/members/master_manages_access_requests_spec.rb
index 454da126c8115b96af80c51010560e83f8c5527a..1c13bd3d59e78bea4b46f1132ed38fce20e8eac7 100644
--- a/spec/features/groups/members/master_manages_access_requests_spec.rb
+++ b/spec/features/groups/members/master_manages_access_requests_spec.rb
@@ -4,7 +4,7 @@
 
 describe 'Groups > Members > Maintainer manages access requests' do
   it_behaves_like 'Maintainer manages access requests' do
-    let(:entity) { create(:group, :public, :access_requestable) }
+    let(:entity) { create(:group, :public) }
     let(:members_page_path) { group_group_members_path(entity) }
   end
 end
diff --git a/spec/features/groups/members/request_access_spec.rb b/spec/features/groups/members/request_access_spec.rb
index 0d5321709ae1dfad26f89a0f3ea7462bf04e22f9..5f22af3529c509209ffae74f53f172c16400e4a3 100644
--- a/spec/features/groups/members/request_access_spec.rb
+++ b/spec/features/groups/members/request_access_spec.rb
@@ -5,7 +5,7 @@
 describe 'Groups > Members > Request access' do
   let(:user) { create(:user) }
   let(:owner) { create(:user) }
-  let(:group) { create(:group, :public, :access_requestable) }
+  let(:group) { create(:group, :public) }
   let!(:project) { create(:project, :private, namespace: group) }
 
   before do
diff --git a/spec/features/groups/user_sees_package_sidebar_spec.rb b/spec/features/groups/user_sees_package_sidebar_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f85b68416369fb33d3b2a0b5af032af2c624f715
--- /dev/null
+++ b/spec/features/groups/user_sees_package_sidebar_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Groups > sidebar' do
+  let(:user) { create(:user) }
+  let(:group) { create(:group) }
+
+  before do
+    group.add_developer(user)
+    sign_in(user)
+  end
+
+  context 'Package menu' do
+    context 'when container registry is enabled' do
+      before do
+        stub_container_registry_config(enabled: true)
+        visit group_path(group)
+      end
+
+      it 'shows main menu' do
+        within '.nav-sidebar' do
+          expect(page).to have_link(_('Packages'))
+        end
+      end
+
+      it 'has container registry link' do
+        within '.nav-sidebar' do
+          expect(page).to have_link(_('Container Registry'))
+        end
+      end
+    end
+
+    context 'when container registry is disabled' do
+      before do
+        stub_container_registry_config(enabled: false)
+        visit group_path(group)
+      end
+
+      it 'does not have container registry link' do
+        within '.nav-sidebar' do
+          expect(page).not_to have_link(_('Container Registry'))
+        end
+      end
+    end
+  end
+end
diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb
index 1e054a7b3589a4b2feee83e4f8bb26cb297764f9..2a1980346e9528ec31e0836f7a4f5f649f440b16 100644
--- a/spec/features/invites_spec.rb
+++ b/spec/features/invites_spec.rb
@@ -10,7 +10,6 @@
   let(:group_invite) { group.group_members.invite.last }
 
   before do
-    stub_feature_flags(invisible_captcha: false)
     project.add_maintainer(owner)
     group.add_user(owner, Gitlab::Access::OWNER)
     group.add_developer('user@example.com', owner)
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index 6b6226ad1c5e78f04403ce6914bb8df38d0fb508..9fadd46ed44749e5c2769b6f032acca26875bec5 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -5,6 +5,7 @@
 describe 'Merge request > User sees merge widget', :js do
   include ProjectForksHelper
   include TestReportsHelper
+  include ReactiveCachingHelpers
 
   let(:project) { create(:project, :repository) }
   let(:project_only_mwps) { create(:project, :repository, only_allow_merge_if_pipeline_succeeds: true) }
@@ -435,6 +436,54 @@
     end
   end
 
+  context 'exposed artifacts' do
+    subject { visit project_merge_request_path(project, merge_request) }
+
+    context 'when merge request has exposed artifacts' do
+      let(:merge_request) { create(:merge_request, :with_exposed_artifacts, source_project: project) }
+      let(:job) { merge_request.head_pipeline.builds.last }
+      let!(:artifacts_metadata) { create(:ci_job_artifact, :metadata, job: job) }
+
+      context 'when result has not been parsed yet' do
+        it 'shows parsing status' do
+          subject
+
+          expect(page).to have_content('Loading artifacts')
+        end
+      end
+
+      context 'when result has been parsed' do
+        before do
+          allow_any_instance_of(MergeRequest).to receive(:find_exposed_artifacts).and_return(
+            status: :parsed, data: [
+              {
+                text: "the artifact",
+                url: "/namespace1/project1/-/jobs/1/artifacts/file/ci_artifacts.txt",
+                job_path: "/namespace1/project1/-/jobs/1",
+                job_name: "test"
+              }
+            ])
+        end
+
+        it 'shows the parsed results' do
+          subject
+
+          expect(page).to have_content('View exposed artifact')
+        end
+      end
+    end
+
+    context 'when merge request does not have exposed artifacts' do
+      let(:merge_request) { create(:merge_request, source_project: project) }
+
+      it 'does not show parsing status' do
+        subject
+
+        expect(page).not_to have_content('Loading artifacts')
+      end
+    end
+  end
+
   context 'when merge request has test reports' do
     let!(:head_pipeline) do
       create(:ci_pipeline,
diff --git a/spec/features/milestones/user_deletes_milestone_spec.rb b/spec/features/milestones/user_deletes_milestone_spec.rb
index 7c1d88f779814101ab6678752de3c414bed1f0b6..fd72f2dfefaee501a0597f0b0bf6846274a2b72b 100644
--- a/spec/features/milestones/user_deletes_milestone_spec.rb
+++ b/spec/features/milestones/user_deletes_milestone_spec.rb
@@ -12,7 +12,7 @@
   end
 
   context "when milestone belongs to project" do
-    let!(:milestone) { create(:milestone, parent: project, title: "project milestone") }
+    let!(:milestone) { create(:milestone, resource_parent: project, title: "project milestone") }
 
     it "deletes milestone" do
       project.add_developer(user)
@@ -30,8 +30,8 @@
   end
 
   context "when milestone belongs to group" do
-    let!(:milestone_to_be_deleted) { create(:milestone, parent: group, title: "group milestone 1") }
-    let!(:milestone) { create(:milestone, parent: group, title: "group milestone 2") }
+    let!(:milestone_to_be_deleted) { create(:milestone, resource_parent: group, title: "group milestone 1") }
+    let!(:milestone) { create(:milestone, resource_parent: group, title: "group milestone 2") }
 
     it "deletes milestone" do
       group.add_developer(user)
diff --git a/spec/features/projects/clusters/eks_spec.rb b/spec/features/projects/clusters/eks_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..758dccd6e493f1a1977b2ac1205c848f3a309efc
--- /dev/null
+++ b/spec/features/projects/clusters/eks_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'AWS EKS Cluster', :js do
+  let(:project) { create(:project) }
+  let(:user) { create(:user) }
+
+  before do
+    project.add_maintainer(user)
+    gitlab_sign_in(user)
+    allow(Projects::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
+  end
+
+  context 'when user does not have a cluster and visits cluster index page' do
+    let(:project_id) { 'test-project-1234' }
+
+    before do
+      visit project_clusters_path(project)
+
+      click_link 'Add Kubernetes cluster'
+    end
+
+    context 'when user creates a cluster on AWS EKS' do
+      before do
+        click_link 'Amazon EKS'
+      end
+
+      it 'user sees a form to create an EKS cluster' do
+        expect(page).to have_selector(:css, '.js-create-eks-cluster')
+      end
+    end
+  end
+end
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index a11237db50844f4b6c915fb17d32cea49524054e..b5ab9faa14b667f754ebf85adaac170216d1a4e2 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -177,6 +177,7 @@
 
   context 'when user has not dismissed GCP signup offer' do
     before do
+      stub_feature_flags(create_eks_clusters: false)
       visit project_clusters_path(project)
     end
 
@@ -200,6 +201,7 @@
 
   context 'when user has dismissed GCP signup offer' do
     before do
+      stub_feature_flags(create_eks_clusters: false)
       visit project_clusters_path(project)
     end
 
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index d1cd19dff2d434ded5a6a18babb5b4605a19f0c8..67d14d0a58a6e2ca240574432c4976615f593e23 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -74,7 +74,7 @@
         visit project_clusters_path(project)
 
         click_link 'Add Kubernetes cluster'
-        click_link 'Create new Cluster on GKE'
+        click_link 'Create new Cluster'
       end
 
       it 'user sees a link to create a GKE cluster' do
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index 25823b75d181b6a53299e7150695a82d88599348..dd690699ff63a1561875bf473d80b96302bb7580 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -66,8 +66,8 @@
           create(:deployment, :running, environment: environment, deployable: build)
         end
 
-        it 'does not show deployments' do
-          expect(page).to have_content('You don\'t have any deployments right now.')
+        it 'does show deployments' do
+          expect(page).to have_link("#{build.name} (##{build.id})")
         end
       end
 
@@ -79,8 +79,8 @@
           create(:deployment, :failed, environment: environment, deployable: build)
         end
 
-        it 'does not show deployments' do
-          expect(page).to have_content('You don\'t have any deployments right now.')
+        it 'does show deployments' do
+          expect(page).to have_link("#{build.name} (##{build.id})")
         end
       end
 
@@ -175,7 +175,7 @@
                     #
                     # In EE we have to stub EE::Environment since it overwrites
                     # the "terminals" method.
-                    allow_any_instance_of(defined?(EE) ? EE::Environment : Environment)
+                    allow_any_instance_of(Gitlab.ee? ? EE::Environment : Environment)
                       .to receive(:terminals) { nil }
 
                     visit terminal_project_environment_path(project, environment)
diff --git a/spec/features/projects/jobs/permissions_spec.rb b/spec/features/projects/jobs/permissions_spec.rb
index 44309a9c4bfa71e1dc5b463aa236fc45f23c51b4..ae506b66a862b56185c2521fb98e934494359ee0 100644
--- a/spec/features/projects/jobs/permissions_spec.rb
+++ b/spec/features/projects/jobs/permissions_spec.rb
@@ -10,6 +10,7 @@
   let!(:job) { create(:ci_build, :running, :coverage, :trace_artifact, pipeline: pipeline) }
 
   before do
+    stub_feature_flags(job_log_json: true)
     sign_in(user)
 
     project.enable_ci
@@ -69,7 +70,7 @@
         it_behaves_like 'recent job page details responds with status', 200 do
           it 'renders job details', :js do
             expect(page).to have_content "Job ##{job.id}"
-            expect(page).to have_css '.js-build-trace'
+            expect(page).to have_css '.log-line'
           end
         end
 
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index cebca338f3353ce70fb5a477296e8d61cd9e88c6..f5d5bc7f5b901f2f021c68034dd561e7f3bac7ca 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 require 'tempfile'
 
@@ -424,8 +426,8 @@
         it 'loads job trace' do
           expect(page).to have_content 'BUILD TRACE'
 
-          job.trace.write('a+b') do |stream|
-            stream.append(' and more trace', 11)
+          job.trace.write(+'a+b') do |stream|
+            stream.append(+' and more trace', 11)
           end
 
           expect(page).to have_content 'BUILD TRACE and more trace'
@@ -534,7 +536,7 @@
         end
 
         it 'shows deployment message' do
-          expect(page).to have_content 'This job is the most recent deployment to production'
+          expect(page).to have_content 'This job is deployed to production'
           expect(find('.js-environment-link')['href']).to match("environments/#{environment.id}")
         end
 
@@ -548,14 +550,14 @@
           end
 
           it 'shows the name of the cluster' do
-            expect(page).to have_content 'Cluster the-cluster was used'
+            expect(page).to have_content 'using cluster the-cluster'
           end
 
           context 'when the user is not able to view the cluster' do
             let(:user_access_level) { :developer }
 
             it 'includes only the name of the cluster without a link' do
-              expect(page).to have_content 'Cluster the-cluster was used'
+              expect(page).to have_content 'using cluster the-cluster'
               expect(page).not_to have_link 'the-cluster'
             end
           end
@@ -623,8 +625,7 @@
         let!(:second_deployment) { create(:deployment, :success, environment: environment, deployable: second_build) }
 
         it 'shows deployment message' do
-          expected_text = 'This job is an out-of-date deployment ' \
-            "to staging. View the most recent deployment ##{second_deployment.iid}."
+          expected_text = 'This job is an out-of-date deployment to staging. View the most recent deployment.'
 
           expect(page).to have_css('.environment-information', text: expected_text)
         end
diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb
index dd5fc82e058e05ad00ef87c0cded291b9d804485..4ecc3db78b36ce96ecb638ad1e0e3aa9e7673b56 100644
--- a/spec/features/projects/members/group_members_spec.rb
+++ b/spec/features/projects/members/group_members_spec.rb
@@ -5,8 +5,8 @@
 describe 'Projects members' do
   let(:user) { create(:user) }
   let(:developer) { create(:user) }
-  let(:group) { create(:group, :public, :access_requestable) }
-  let(:project) { create(:project, :public, :access_requestable, creator: user, group: group) }
+  let(:group) { create(:group, :public) }
+  let(:project) { create(:project, :public, creator: user, group: group) }
   let(:project_invitee) { create(:project_member, project: project, invite_token: '123', invite_email: 'test1@abc.com', user: nil) }
   let(:group_invitee) { create(:group_member, group: group, invite_token: '123', invite_email: 'test2@abc.com', user: nil) }
   let(:project_requester) { create(:user) }
diff --git a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
index fb4238f0a1fe5dd1c60f46045a511019d0bb23a4..ecd55f71c84f4fdbae79ce3a250e2b1f2c53216b 100644
--- a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
+++ b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
@@ -5,8 +5,8 @@
 describe 'Projects > Members > Group requester cannot request access to project', :js do
   let(:user) { create(:user) }
   let(:owner) { create(:user) }
-  let(:group) { create(:group, :public, :access_requestable) }
-  let(:project) { create(:project, :public, :access_requestable, namespace: group) }
+  let(:group) { create(:group, :public) }
+  let(:project) { create(:project, :public, namespace: group) }
 
   before do
     group.add_owner(owner)
diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb
index 17d6efbcaa5cc0eb22d5c3b25a42a52c5bb433cd..f113fb643f8cb56e524bbdfdac00ad944fff8f44 100644
--- a/spec/features/projects/members/master_manages_access_requests_spec.rb
+++ b/spec/features/projects/members/master_manages_access_requests_spec.rb
@@ -4,7 +4,7 @@
 
 describe 'Projects > Members > Maintainer manages access requests' do
   it_behaves_like 'Maintainer manages access requests' do
-    let(:entity) { create(:project, :public, :access_requestable) }
+    let(:entity) { create(:project, :public) }
     let(:members_page_path) { project_project_members_path(entity) }
   end
 end
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 9f7327cd6e43c020678406b3068eaa465f3d970b..a77f0bdcbe9a0458eae891ce766023d1ec734634 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -4,7 +4,7 @@
 
 describe 'Projects > Members > User requests access', :js do
   let(:user) { create(:user) }
-  let(:project) { create(:project, :public, :access_requestable, :repository) }
+  let(:project) { create(:project, :public, :repository) }
   let(:maintainer) { project.owner }
 
   before do
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 9759fd04ad27b71842aca7a95aac70510cffb6e8..66807eb1c171c634237df5ea2899efb57de1c4cd 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -98,6 +98,16 @@
       end
     end
 
+    it 'shows links to the related merge requests' do
+      visit_pipeline
+
+      within '.related-merge-request-info' do
+        pipeline.all_merge_requests.map do |merge_request|
+          expect(page).to have_link(project_merge_request_path(project, merge_request))
+        end
+      end
+    end
+
     it_behaves_like 'showing user status' do
       let(:user_with_status) { pipeline.user }
 
@@ -768,10 +778,10 @@
         expect(page).to have_content(failed_build.stage)
       end
 
-      it 'does not show trace' do
+      it 'does not show log' do
         subject
 
-        expect(page).to have_content('No job trace')
+        expect(page).to have_content('No job log')
       end
     end
 
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index 1294c8822b6def9ebe549e3b1dbb404c75fe7687..18031a40f152d586778ecb40238fbe39f986c8bd 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -66,6 +66,19 @@
         expect(page).to have_content('Write access allowed')
       end
 
+      it 'edit an existing public deploy key to be writable' do
+        project.deploy_keys << public_deploy_key
+        visit project_settings_repository_path(project)
+
+        find('.deploy-key', text: public_deploy_key.title).find('.ic-pencil').click
+
+        check 'deploy_key_deploy_keys_projects_attributes_0_can_push'
+        click_button 'Save changes'
+
+        expect(page).to have_content('public_deploy_key')
+        expect(page).to have_content('Write access allowed')
+      end
+
       it 'edit a deploy key from projects user has access to' do
         project2 = create(:project_empty_repo)
         project2.add_role(user, role)
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index 809372230163121cbc1a5b6c5854bd525f65023e..36c5a116b6643eb7034f898e46496be7682b7d22 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -92,7 +92,10 @@
         set_protected_branch_name('some-branch')
         click_on "Protect"
 
-        within(".protected-branches-list") { expect(page).to have_content(commit.id[0..7]) }
+        within(".protected-branches-list") do
+          expect(page).not_to have_content("matching")
+          expect(page).not_to have_content("was deleted")
+        end
       end
 
       it "displays an error message if the named branch does not exist" do
@@ -101,7 +104,7 @@
         set_protected_branch_name('some-branch')
         click_on "Protect"
 
-        within(".protected-branches-list") { expect(page).to have_content('branch was deleted') }
+        within(".protected-branches-list") { expect(page).to have_content('Branch was deleted') }
       end
     end
 
@@ -127,7 +130,6 @@
         click_on "Protect"
 
         within(".protected-branches-list") do
-          expect(page).to have_content("Protected branch (2)")
           expect(page).to have_content("2 matching branches")
         end
       end
diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb
index 9451ee6eb15a67e61a7eb70d13b10b22d89a7ea6..9949595fddf03d1b0c8cd839de11412f4662e099 100644
--- a/spec/features/search/user_searches_for_code_spec.rb
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -94,6 +94,13 @@
 
         expect(page).to have_selector('.results', text: 'path = gitlab-grack')
       end
+
+      it 'persist refs over browser tabs' do
+        ref = 'feature'
+        find('.js-project-refs-dropdown').click
+        link = find_link(ref)[:href]
+        expect(link.include?("repository_ref=" + ref)).to be(true)
+      end
     end
 
     it 'no ref switcher shown in issue result summary', :js do
diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb
index 7e7c09e4a1341cf87cb97f97fae4f7c9baace260..d386e489739b2a88bdfcdc7ba7147219e1a40e1f 100644
--- a/spec/features/search/user_uses_header_search_field_spec.rb
+++ b/spec/features/search/user_uses_header_search_field_spec.rb
@@ -28,8 +28,7 @@
 
     context 'when clicking the search field' do
       before do
-        page.find('#search').click
-        wait_for_all_requests
+        page.find('#search.js-autocomplete-disabled').click
       end
 
       it 'shows category search dropdown' do
diff --git a/spec/features/security/group/internal_access_spec.rb b/spec/features/security/group/internal_access_spec.rb
index d6575ec9de1cd9a67ff1bb231e1b4e9531242a46..a182b6b9d576f70c5239c4286aa1c752c7d4b4a1 100644
--- a/spec/features/security/group/internal_access_spec.rb
+++ b/spec/features/security/group/internal_access_spec.rb
@@ -16,6 +16,7 @@
   describe "Group should be internal" do
     describe '#internal?' do
       subject { group.internal? }
+
       it { is_expected.to be_truthy }
     end
   end
diff --git a/spec/features/security/group/private_access_spec.rb b/spec/features/security/group/private_access_spec.rb
index 2dc863a6e73ecec69c63f66e9995fff08b5e8542..5e3e9824aaabf9af0767b3c81031b9552d196016 100644
--- a/spec/features/security/group/private_access_spec.rb
+++ b/spec/features/security/group/private_access_spec.rb
@@ -16,6 +16,7 @@
   describe "Group should be private" do
     describe '#private?' do
       subject { group.private? }
+
       it { is_expected.to be_truthy }
     end
   end
diff --git a/spec/features/security/group/public_access_spec.rb b/spec/features/security/group/public_access_spec.rb
index 4066a19fce2344321857f63af4ba3abd41fbab23..efc84205980b92dc4a2c61ab89abc500d89d7a24 100644
--- a/spec/features/security/group/public_access_spec.rb
+++ b/spec/features/security/group/public_access_spec.rb
@@ -16,6 +16,7 @@
   describe "Group should be public" do
     describe '#public?' do
       subject { group.public? }
+
       it { is_expected.to be_truthy }
     end
   end
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index d089fa718d270204a1e31d2977bce988fd698e31..768b883a90e643e6d033918a764b671522ea6e03 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -14,6 +14,7 @@
   describe "Project should be internal" do
     describe '#internal?' do
       subject { project.internal? }
+
       it { is_expected.to be_truthy }
     end
   end
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index b868cd595cbd95f95c263d825509e07fb806fdd5..c2d44c05a229dfed9d2fe42dac321559c9edc8a2 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -14,6 +14,7 @@
   describe "Project should be private" do
     describe '#private?' do
       subject { project.private? }
+
       it { is_expected.to be_truthy }
     end
   end
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index 8db2f2d69e59e0ad2eb3a99b0000542dc24eda15..19f01257713eb007a1c26f19375dd7fc2fd45eb0 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -14,6 +14,7 @@
   describe "Project should be public" do
     describe '#public?' do
       subject { project.public? }
+
       it { is_expected.to be_truthy }
     end
   end
diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb
index 70e6978a7b66e434b13d40242786dcb25967bf9e..2615e8400a439698792517603c33e5c3b5fc9f39 100644
--- a/spec/features/signed_commits_spec.rb
+++ b/spec/features/signed_commits_spec.rb
@@ -152,4 +152,34 @@
       end
     end
   end
+
+  context 'view signed commit on the tree view', :js do
+    shared_examples 'a commit with a signature' do
+      before do
+        visit project_tree_path(project, 'signed-commits')
+      end
+
+      it 'displays commit signature' do
+        expect(page).to have_button 'Unverified'
+
+        click_on 'Unverified'
+
+        within '.popover' do
+          expect(page).to have_content 'This commit was signed with an unverified signature'
+        end
+      end
+    end
+
+    context 'with vue tree view enabled' do
+      it_behaves_like 'a commit with a signature'
+    end
+
+    context 'with vue tree view disabled' do
+      before do
+        stub_feature_flags(vue_file_list: false)
+      end
+
+      it_behaves_like 'a commit with a signature'
+    end
+  end
 end
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index 0846ec8dfb456b98b9b0345ee57cdbfb9de23774..562d6fcab1b5b85486270901e776a30362b6ad3a 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -5,10 +5,6 @@
 shared_examples 'Signup' do
   include TermsHelper
 
-  before do
-    stub_feature_flags(invisible_captcha: false)
-  end
-
   let(:new_user) { build_stubbed(:user) }
 
   describe 'username validation', :js do
@@ -129,35 +125,43 @@
 
   describe 'user\'s full name validation', :js do
     before do
-      visit new_user_registration_path
+      if Gitlab::Experimentation.enabled?(:signup_flow)
+        user = create(:user, role: nil)
+        sign_in(user)
+        visit users_sign_up_welcome_path
+        @user_name_field = 'user_name'
+      else
+        visit new_user_registration_path
+        @user_name_field = 'new_user_name'
+      end
     end
 
     it 'does not show an error border if the user\'s fullname length is not longer than 128 characters' do
-      fill_in 'new_user_name', with: 'u' * 128
+      fill_in @user_name_field, with: 'u' * 128
 
       expect(find('.name')).not_to have_css '.gl-field-error-outline'
     end
 
     it 'shows an error border if the user\'s fullname contains an emoji' do
-      simulate_input('#new_user_name', 'Ehsan 🦋')
+      simulate_input("##{@user_name_field}", 'Ehsan 🦋')
 
       expect(find('.name')).to have_css '.gl-field-error-outline'
     end
 
     it 'shows an error border if the user\'s fullname is longer than 128 characters' do
-      fill_in 'new_user_name', with: 'n' * 129
+      fill_in @user_name_field, with: 'n' * 129
 
       expect(find('.name')).to have_css '.gl-field-error-outline'
     end
 
     it 'shows an error message if the user\'s fullname is longer than 128 characters' do
-      fill_in 'new_user_name', with: 'n' * 129
+      fill_in @user_name_field, with: 'n' * 129
 
       expect(page).to have_content("Name is too long (maximum is 128 characters).")
     end
 
     it 'shows an error message if the username contains emojis' do
-      simulate_input('#new_user_name', 'Ehsan 🦋')
+      simulate_input("##{@user_name_field}", 'Ehsan 🦋')
 
       expect(page).to have_content("Invalid input, please avoid emojis")
     end
@@ -177,11 +181,11 @@
         it 'creates the user account and sends a confirmation email' do
           visit new_user_registration_path
 
-          fill_in 'new_user_name', with: new_user.name
           fill_in 'new_user_username', with: new_user.username
           fill_in 'new_user_email', with: new_user.email
 
-          unless Feature.enabled?(:experimental_separate_sign_up_flow)
+          unless Gitlab::Experimentation.enabled?(:signup_flow)
+            fill_in 'new_user_name', with: new_user.name
             fill_in 'new_user_email_confirmation', with: new_user.email
           end
 
@@ -202,11 +206,11 @@
         it 'creates the user account and sends a confirmation email' do
           visit new_user_registration_path
 
-          fill_in 'new_user_name', with: new_user.name
           fill_in 'new_user_username', with: new_user.username
           fill_in 'new_user_email', with: new_user.email
 
-          unless Feature.enabled?(:experimental_separate_sign_up_flow)
+          unless Gitlab::Experimentation.enabled?(:signup_flow)
+            fill_in 'new_user_name', with: new_user.name
             fill_in 'new_user_email_confirmation', with: new_user.email
           end
 
@@ -214,8 +218,12 @@
 
           expect { click_button 'Register' }.to change { User.count }.by(1)
 
-          expect(current_path).to eq dashboard_projects_path
-          expect(page).to have_content("Please check your email (#{new_user.email}) to verify that you own this address.")
+          if Gitlab::Experimentation.enabled?(:signup_flow)
+            expect(current_path).to eq users_sign_up_welcome_path
+          else
+            expect(current_path).to eq dashboard_projects_path
+            expect(page).to have_content("Please check your email (#{new_user.email}) to verify that you own this address.")
+          end
         end
       end
     end
@@ -224,19 +232,23 @@
       it "creates the user successfully" do
         visit new_user_registration_path
 
-        fill_in 'new_user_name', with: new_user.name
         fill_in 'new_user_username', with: new_user.username
         fill_in 'new_user_email', with: new_user.email
 
-        unless Feature.enabled?(:experimental_separate_sign_up_flow)
+        unless Gitlab::Experimentation.enabled?(:signup_flow)
+          fill_in 'new_user_name', with: new_user.name
           fill_in 'new_user_email_confirmation', with: new_user.email.capitalize
         end
 
         fill_in 'new_user_password', with: new_user.password
         click_button "Register"
 
-        expect(current_path).to eq dashboard_projects_path
-        expect(page).to have_content("Welcome! You have signed up successfully.")
+        if Gitlab::Experimentation.enabled?(:signup_flow)
+          expect(current_path).to eq users_sign_up_welcome_path
+        else
+          expect(current_path).to eq dashboard_projects_path
+          expect(page).to have_content("Welcome! You have signed up successfully.")
+        end
       end
     end
 
@@ -248,19 +260,23 @@
       it 'creates the user account and goes to dashboard' do
         visit new_user_registration_path
 
-        fill_in 'new_user_name', with: new_user.name
         fill_in 'new_user_username', with: new_user.username
         fill_in 'new_user_email', with: new_user.email
 
-        unless Feature.enabled?(:experimental_separate_sign_up_flow)
+        unless Gitlab::Experimentation.enabled?(:signup_flow)
+          fill_in 'new_user_name', with: new_user.name
           fill_in 'new_user_email_confirmation', with: new_user.email
         end
 
         fill_in 'new_user_password', with: new_user.password
         click_button "Register"
 
-        expect(current_path).to eq dashboard_projects_path
-        expect(page).to have_content("Welcome! You have signed up successfully.")
+        if Gitlab::Experimentation.enabled?(:signup_flow)
+          expect(current_path).to eq users_sign_up_welcome_path
+        else
+          expect(current_path).to eq dashboard_projects_path
+          expect(page).to have_content("Welcome! You have signed up successfully.")
+        end
       end
     end
   end
@@ -271,7 +287,10 @@
 
       visit new_user_registration_path
 
-      fill_in 'new_user_name', with: new_user.name
+      unless Gitlab::Experimentation.enabled?(:signup_flow)
+        fill_in 'new_user_name', with: new_user.name
+      end
+
       fill_in 'new_user_username', with: new_user.username
       fill_in 'new_user_email', with: existing_user.email
       fill_in 'new_user_password', with: new_user.password
@@ -279,14 +298,14 @@
 
       expect(current_path).to eq user_registration_path
 
-      if Feature.enabled?(:experimental_separate_sign_up_flow)
+      if Gitlab::Experimentation.enabled?(:signup_flow)
         expect(page).to have_content("error prohibited this user from being saved")
-        expect(page).to have_content("Email has already been taken")
       else
         expect(page).to have_content("errors prohibited this user from being saved")
-        expect(page).to have_content("Email has already been taken")
         expect(page).to have_content("Email confirmation doesn't match")
       end
+
+      expect(page).to have_content("Email has already been taken")
     end
 
     it 'does not redisplay the password' do
@@ -294,7 +313,10 @@
 
       visit new_user_registration_path
 
-      fill_in 'new_user_name', with: new_user.name
+      unless Gitlab::Experimentation.enabled?(:signup_flow)
+        fill_in 'new_user_name', with: new_user.name
+      end
+
       fill_in 'new_user_username', with: new_user.username
       fill_in 'new_user_email', with: existing_user.email
       fill_in 'new_user_password', with: new_user.password
@@ -313,11 +335,11 @@
     it 'requires the user to check the checkbox' do
       visit new_user_registration_path
 
-      fill_in 'new_user_name', with: new_user.name
       fill_in 'new_user_username', with: new_user.username
       fill_in 'new_user_email', with: new_user.email
 
-      unless Feature.enabled?(:experimental_separate_sign_up_flow)
+      unless Gitlab::Experimentation.enabled?(:signup_flow)
+        fill_in 'new_user_name', with: new_user.name
         fill_in 'new_user_email_confirmation', with: new_user.email
       end
 
@@ -332,11 +354,11 @@
     it 'asks the user to accept terms before going to the dashboard' do
       visit new_user_registration_path
 
-      fill_in 'new_user_name', with: new_user.name
       fill_in 'new_user_username', with: new_user.username
       fill_in 'new_user_email', with: new_user.email
 
-      unless Feature.enabled?(:experimental_separate_sign_up_flow)
+      unless Gitlab::Experimentation.enabled?(:signup_flow)
+        fill_in 'new_user_name', with: new_user.name
         fill_in 'new_user_email_confirmation', with: new_user.email
       end
 
@@ -345,24 +367,84 @@
 
       click_button "Register"
 
-      expect(current_path).to eq dashboard_projects_path
+      if Gitlab::Experimentation.enabled?(:signup_flow)
+        expect(current_path).to eq users_sign_up_welcome_path
+      else
+        expect(current_path).to eq dashboard_projects_path
+      end
     end
   end
-end
 
-describe 'With original flow' do
-  it_behaves_like 'Signup' do
+  context 'when reCAPTCHA and invisible captcha are enabled' do
     before do
-      stub_feature_flags(experimental_separate_sign_up_flow: false)
+      InvisibleCaptcha.timestamp_enabled = true
+      stub_application_setting(recaptcha_enabled: true)
+      allow_any_instance_of(RegistrationsController).to receive(:verify_recaptcha).and_return(false)
+    end
+
+    after do
+      InvisibleCaptcha.timestamp_enabled = false
+    end
+
+    it 'prevents from signing up' do
+      visit new_user_registration_path
+
+      fill_in 'new_user_username', with: new_user.username
+      fill_in 'new_user_email', with: new_user.email
+
+      unless Gitlab::Experimentation.enabled?(:signup_flow)
+        fill_in 'new_user_name', with: new_user.name
+        fill_in 'new_user_email_confirmation', with: new_user.email
+      end
+
+      fill_in 'new_user_password', with: new_user.password
+
+      expect { click_button 'Register' }.not_to change { User.count }
+
+      if Gitlab::Experimentation.enabled?(:signup_flow)
+        expect(page).to have_content('That was a bit too quick! Please resubmit.')
+      else
+        expect(page).to have_content('There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.')
+      end
     end
   end
 end
 
-describe 'With experimental flow on GitLab.com' do
-  it_behaves_like 'Signup' do
-    before do
-      expect(Gitlab).to receive(:com?).and_return(true).at_least(:once)
-      stub_feature_flags(experimental_separate_sign_up_flow: true)
+describe 'With original flow' do
+  before do
+    stub_experiment(signup_flow: false)
+  end
+
+  it_behaves_like 'Signup'
+end
+
+describe 'With experimental flow' do
+  before do
+    stub_experiment(signup_flow: true)
+  end
+
+  it_behaves_like 'Signup'
+
+  describe 'when role is required' do
+    it 'after registering, it redirects to step 2 of the signup process, sets the name and role and then redirects to the original requested url' do
+      new_user = build_stubbed(:user)
+      visit new_user_registration_path
+      fill_in 'new_user_username', with: new_user.username
+      fill_in 'new_user_email', with: new_user.email
+      fill_in 'new_user_password', with: new_user.password
+      click_button 'Register'
+      visit new_project_path
+
+      expect(page).to have_current_path(users_sign_up_welcome_path)
+
+      fill_in 'user_name', with: 'New name'
+      select 'Software Developer', from: 'user_role'
+      click_button 'Get started!'
+      new_user = User.find_by_username(new_user.username)
+
+      expect(new_user.name).to eq 'New name'
+      expect(new_user.software_developer_role?).to be_truthy
+      expect(page).to have_current_path(new_project_path)
     end
   end
 end
diff --git a/spec/finders/access_requests_finder_spec.rb b/spec/finders/access_requests_finder_spec.rb
index 605777462bba82e87e084a8c0266a3836fe52d61..fbfc8035bcc265eab157a2bcaeb52f08f599bc4d 100644
--- a/spec/finders/access_requests_finder_spec.rb
+++ b/spec/finders/access_requests_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe AccessRequestsFinder do
@@ -5,13 +7,13 @@
   let(:access_requester) { create(:user) }
 
   let(:project) do
-    create(:project, :public, :access_requestable) do |project|
+    create(:project, :public) do |project|
       project.request_access(access_requester)
     end
   end
 
   let(:group) do
-    create(:group, :public, :access_requestable) do |group|
+    create(:group, :public) do |group|
       group.request_access(access_requester)
     end
   end
diff --git a/spec/finders/admin/projects_finder_spec.rb b/spec/finders/admin/projects_finder_spec.rb
index 44cc8debd0487aefa0c347db61d63069f865d0f2..eb5d0bba183841563505956b7ab0f3dcf2015e40 100644
--- a/spec/finders/admin/projects_finder_spec.rb
+++ b/spec/finders/admin/projects_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Admin::ProjectsFinder do
diff --git a/spec/finders/autocomplete/move_to_project_finder_spec.rb b/spec/finders/autocomplete/move_to_project_finder_spec.rb
index 4a87b47bd08044e37ab54742249495d765aca61f..f997dd32c4002553753dd900cfe8ead9981ddedc 100644
--- a/spec/finders/autocomplete/move_to_project_finder_spec.rb
+++ b/spec/finders/autocomplete/move_to_project_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Autocomplete::MoveToProjectFinder do
diff --git a/spec/finders/autocomplete/users_finder_spec.rb b/spec/finders/autocomplete/users_finder_spec.rb
index f3b54ca0461dc4b61f42f41974f3ca85f0b14039..5d340c46114f82bc1b3026bc4e06b3b05419a953 100644
--- a/spec/finders/autocomplete/users_finder_spec.rb
+++ b/spec/finders/autocomplete/users_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Autocomplete::UsersFinder do
diff --git a/spec/finders/boards/visits_finder_spec.rb b/spec/finders/boards/visits_finder_spec.rb
index 4d40f4826f8759bf733ebb0d93583a1edaf8f5f5..7e3ad8aa9f0e664eac5073e171c7fc012f3b57ff 100644
--- a/spec/finders/boards/visits_finder_spec.rb
+++ b/spec/finders/boards/visits_finder_spec.rb
@@ -10,7 +10,7 @@
       let(:project)       { create(:project) }
       let(:project_board) { create(:board, project: project) }
 
-      subject(:finder) { described_class.new(project_board.parent, user) }
+      subject(:finder) { described_class.new(project_board.resource_parent, user) }
 
       it 'returns nil when there is no user' do
         finder.current_user = nil
@@ -27,7 +27,7 @@
       it 'queries for last N visits' do
         expect(BoardProjectRecentVisit).to receive(:latest).with(user, project, count: 5).once
 
-        described_class.new(project_board.parent, user).latest(5)
+        described_class.new(project_board.resource_parent, user).latest(5)
       end
     end
 
@@ -35,7 +35,7 @@
       let(:group)       { create(:group) }
       let(:group_board) { create(:board, group: group) }
 
-      subject(:finder) { described_class.new(group_board.parent, user) }
+      subject(:finder) { described_class.new(group_board.resource_parent, user) }
 
       it 'returns nil when there is no user' do
         finder.current_user = nil
@@ -52,7 +52,7 @@
       it 'queries for last N visits' do
         expect(BoardGroupRecentVisit).to receive(:latest).with(user, group, count: 5).once
 
-        described_class.new(group_board.parent, user).latest(5)
+        described_class.new(group_board.resource_parent, user).latest(5)
       end
     end
   end
diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb
index 3fc86f3e408b38e07a94e5cf169f572a69a2a5a3..1a33bdf11d7f28c9ca6fff7a22b3e6ca9862ae09 100644
--- a/spec/finders/branches_finder_spec.rb
+++ b/spec/finders/branches_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe BranchesFinder do
diff --git a/spec/finders/clusters/knative_services_finder_spec.rb b/spec/finders/clusters/knative_services_finder_spec.rb
index 159724b3c1f79c7d63435bf31e271b842f5e8a48..7ad64cc3bca0bacb033ae0d1f14ef7cb6e446f59 100644
--- a/spec/finders/clusters/knative_services_finder_spec.rb
+++ b/spec/finders/clusters/knative_services_finder_spec.rb
@@ -77,6 +77,7 @@
 
   describe '#knative_detected' do
     subject { finder.knative_detected }
+
     before do
       synchronous_reactive_cache(finder)
     end
diff --git a/spec/finders/clusters_finder_spec.rb b/spec/finders/clusters_finder_spec.rb
index da529e0670ff0dfc91c5e743380e4fd46193a0a4..f6ea8347f670329dff3c6fccf469e9361339bfaa 100644
--- a/spec/finders/clusters_finder_spec.rb
+++ b/spec/finders/clusters_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe ClustersFinder do
diff --git a/spec/finders/concerns/finder_methods_spec.rb b/spec/finders/concerns/finder_methods_spec.rb
index e074e53c2c5dcf95d50ef6bd041feefadd59f1b3..2e44df8b04483e49de5642914624683b32a14bfd 100644
--- a/spec/finders/concerns/finder_methods_spec.rb
+++ b/spec/finders/concerns/finder_methods_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe FinderMethods do
diff --git a/spec/finders/concerns/finder_with_cross_project_access_spec.rb b/spec/finders/concerns/finder_with_cross_project_access_spec.rb
index f29acb521a8dc4bdc71b5cb7d482cf71b21e8918..6ba98b7917642c752f8ec2f082979fe1b9ab4a00 100644
--- a/spec/finders/concerns/finder_with_cross_project_access_spec.rb
+++ b/spec/finders/concerns/finder_with_cross_project_access_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe FinderWithCrossProjectAccess do
@@ -22,6 +24,7 @@ def execute
 
   let(:user) { create(:user) }
   subject(:finder) { finder_class.new(user) }
+
   let!(:result) { create(:issue) }
 
   before do
diff --git a/spec/finders/contributed_projects_finder_spec.rb b/spec/finders/contributed_projects_finder_spec.rb
index ee84fd067d48276c4591b0d9162dda17646a5eda..1d907261fe9672acefe88b89eba2233fe49c5519 100644
--- a/spec/finders/contributed_projects_finder_spec.rb
+++ b/spec/finders/contributed_projects_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe ContributedProjectsFinder do
diff --git a/spec/finders/environments_finder_spec.rb b/spec/finders/environments_finder_spec.rb
index 25835bb4d94771e6093c63115f11b7e34aec54d4..69687eaa99fbce532188f21945cc7c3f8e809acd 100644
--- a/spec/finders/environments_finder_spec.rb
+++ b/spec/finders/environments_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe EnvironmentsFinder do
diff --git a/spec/finders/events_finder_spec.rb b/spec/finders/events_finder_spec.rb
index 3bce46cc4d1f461f7507b946b03944377d6572ff..848030262cd6cfa62861e33071ffdc319c723637 100644
--- a/spec/finders/events_finder_spec.rb
+++ b/spec/finders/events_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe EventsFinder do
diff --git a/spec/finders/fork_projects_finder_spec.rb b/spec/finders/fork_projects_finder_spec.rb
index 98cff37205ed78617825b6163a4275d376a38d84..2fba53a74a02c6ad2c0bd1b3cbd9cc803d05bb67 100644
--- a/spec/finders/fork_projects_finder_spec.rb
+++ b/spec/finders/fork_projects_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe ForkProjectsFinder do
diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb
index 5fb6739d6e25387bc71d2fc2013d32b63c61f134..17875a9b9ab83a045e0ff30b0c4c16cf6111a190 100644
--- a/spec/finders/group_descendants_finder_spec.rb
+++ b/spec/finders/group_descendants_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe GroupDescendantsFinder do
diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb
index 49b0e14241e9c95b256d4492b94cf35337d500b5..08f3b4024b3d87e1d2e945e5ccc6a7e950fca2da 100644
--- a/spec/finders/group_members_finder_spec.rb
+++ b/spec/finders/group_members_finder_spec.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe GroupMembersFinder, '#execute' do
   let(:group)        { create(:group) }
-  let(:nested_group) { create(:group, :access_requestable, parent: group) }
+  let(:nested_group) { create(:group, parent: group) }
   let(:user1)        { create(:user) }
   let(:user2)        { create(:user) }
   let(:user3)        { create(:user) }
diff --git a/spec/finders/group_projects_finder_spec.rb b/spec/finders/group_projects_finder_spec.rb
index f4bd8a3f6ba7a08f28cd7ab53ff3f9431a94f206..b291b5d4b90bffce6e5d26dbf6a9db05f2f2b9ea 100644
--- a/spec/finders/group_projects_finder_spec.rb
+++ b/spec/finders/group_projects_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe GroupProjectsFinder do
diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb
index c8875d1f92dbf6ee40031393da3e65e9253515a9..741a89a270b5695d992c6858936cfd4eca0ec7f0 100644
--- a/spec/finders/groups_finder_spec.rb
+++ b/spec/finders/groups_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe GroupsFinder do
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index a17ff1ad50d11cb251f82b8357b4b11ad45a6ba1..c27ce263bf0eee231ad501839d75700e05dda7e1 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe IssuesFinder do
diff --git a/spec/finders/joined_groups_finder_spec.rb b/spec/finders/joined_groups_finder_spec.rb
index ae3e55f90f16c4cd2d9b4a1e9be33436f3fe7a2e..b01bd44470a8d2d6017dd07acc3c7c0a5538ac88 100644
--- a/spec/finders/joined_groups_finder_spec.rb
+++ b/spec/finders/joined_groups_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe JoinedGroupsFinder do
diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb
index ba41ded112a38c67c107ab2ca375c2ad6527378f..2681f098fec2cce7dbe79a6090d14744844d2d47 100644
--- a/spec/finders/labels_finder_spec.rb
+++ b/spec/finders/labels_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe LabelsFinder do
diff --git a/spec/finders/license_template_finder_spec.rb b/spec/finders/license_template_finder_spec.rb
index f6f40bf33ccd28dcf79cd954f815dd3eefddbad5..183ee67d801e41b03a267383e22bedecfd8c7ea2 100644
--- a/spec/finders/license_template_finder_spec.rb
+++ b/spec/finders/license_template_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe LicenseTemplateFinder do
diff --git a/spec/finders/members_finder_spec.rb b/spec/finders/members_finder_spec.rb
index 5134db961336a2037531462eba280a418be0a8f3..f9b8fee6f2d0edabf2aa80926e6a763c99fa2299 100644
--- a/spec/finders/members_finder_spec.rb
+++ b/spec/finders/members_finder_spec.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe MembersFinder, '#execute' do
   set(:group)        { create(:group) }
-  set(:nested_group) { create(:group, :access_requestable, parent: group) }
+  set(:nested_group) { create(:group, parent: group) }
   set(:project)      { create(:project, namespace: nested_group) }
   set(:user1)        { create(:user) }
   set(:user2)        { create(:user) }
@@ -55,7 +57,7 @@
   context 'when include_invited_groups_members == true' do
     subject { described_class.new(project, user2).execute(include_invited_groups_members: true) }
 
-    set(:linked_group) { create(:group, :public, :access_requestable) }
+    set(:linked_group) { create(:group, :public) }
     set(:nested_linked_group) { create(:group, parent: linked_group) }
     set(:linked_group_member) { linked_group.add_guest(user1) }
     set(:nested_linked_group_member) { nested_linked_group.add_guest(user2) }
diff --git a/spec/finders/merge_request_target_project_finder_spec.rb b/spec/finders/merge_request_target_project_finder_spec.rb
index d26a75179de32ba29e2cfb065d4676b7743c8650..1d78b7ba4e32b2610b41cf046932b39e7de4fe18 100644
--- a/spec/finders/merge_request_target_project_finder_spec.rb
+++ b/spec/finders/merge_request_target_project_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe MergeRequestTargetProjectFinder do
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 6c0bbeff4f4ea7c27751d9e0a752859fe5a77bbc..a396284f1e940171fad5184ca415db3df386103c 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe MergeRequestsFinder do
diff --git a/spec/finders/milestones_finder_spec.rb b/spec/finders/milestones_finder_spec.rb
index 34c7b508c56c55679f285ad065bac2fd5a13a5b4..3545ff35ed8ca942995780d4e0d4942b4791684c 100644
--- a/spec/finders/milestones_finder_spec.rb
+++ b/spec/finders/milestones_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe MilestonesFinder do
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index 88906adfeeb17511766dc3c4ee466a8a170881ee..44636a22ef93887eb7caf26f67105d7e66ad4ecd 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe NotesFinder do
diff --git a/spec/finders/personal_access_tokens_finder_spec.rb b/spec/finders/personal_access_tokens_finder_spec.rb
index 3e849c9a644cb8f0133882efc444940c3026221a..a44daf585ba7dcfa42245425d873e3fa165a8781 100644
--- a/spec/finders/personal_access_tokens_finder_spec.rb
+++ b/spec/finders/personal_access_tokens_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe PersonalAccessTokensFinder do
diff --git a/spec/finders/personal_projects_finder_spec.rb b/spec/finders/personal_projects_finder_spec.rb
index ef7dd0cd4a82c988c28046fc78f570061c58b92d..7686dd3dc9d2efba6b83884ad30aa3c6e6c063fe 100644
--- a/spec/finders/personal_projects_finder_spec.rb
+++ b/spec/finders/personal_projects_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe PersonalProjectsFinder do
diff --git a/spec/finders/pipeline_schedules_finder_spec.rb b/spec/finders/pipeline_schedules_finder_spec.rb
index 2fefa0280d19490a8a22edd3426608e60ad3e9e9..8d0bde15e037d3e1b1c8a8d03723e95aaf04926b 100644
--- a/spec/finders/pipeline_schedules_finder_spec.rb
+++ b/spec/finders/pipeline_schedules_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe PipelineSchedulesFinder do
diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb
index b23fd8ccdc6a5cfc2baf5e81867b6653bbe2f792..05d13a76e0ea7827494da9dfc6d0e9cd459fa050 100644
--- a/spec/finders/pipelines_finder_spec.rb
+++ b/spec/finders/pipelines_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe PipelinesFinder do
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index ac866e49fcd4e446669ecffcdb0b38f1a216d796..4ec12b5a7f75fc73d3a34819bf30fda25883ef72 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe ProjectsFinder do
diff --git a/spec/finders/runner_jobs_finder_spec.rb b/spec/finders/runner_jobs_finder_spec.rb
index 01f45a37ba81ee1835044578c7539b49da7f1d28..c11f9182036166141ebb296077b08accf925b996 100644
--- a/spec/finders/runner_jobs_finder_spec.rb
+++ b/spec/finders/runner_jobs_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe RunnerJobsFinder do
diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb
index 72de05b513126e7b8e6766b44ca6211480bed5b7..bcb762664f7627ffb8f1950ca02ee9a3130077e0 100644
--- a/spec/finders/snippets_finder_spec.rb
+++ b/spec/finders/snippets_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe SnippetsFinder do
@@ -15,16 +17,27 @@
   end
 
   describe '#execute' do
-    set(:user) { create(:user) }
-    set(:private_personal_snippet) { create(:personal_snippet, :private, author: user) }
-    set(:internal_personal_snippet) { create(:personal_snippet, :internal, author: user) }
-    set(:public_personal_snippet) { create(:personal_snippet, :public, author: user) }
+    let_it_be(:user) { create(:user) }
+    let_it_be(:admin) { create(:admin) }
+    let_it_be(:group) { create(:group, :public) }
+    let_it_be(:project) { create(:project, :public, group: group) }
+
+    let_it_be(:private_personal_snippet) { create(:personal_snippet, :private, author: user) }
+    let_it_be(:internal_personal_snippet) { create(:personal_snippet, :internal, author: user) }
+    let_it_be(:public_personal_snippet) { create(:personal_snippet, :public, author: user) }
+
+    let_it_be(:private_project_snippet) { create(:project_snippet, :private, project: project) }
+    let_it_be(:internal_project_snippet) { create(:project_snippet, :internal, project: project) }
+    let_it_be(:public_project_snippet) { create(:project_snippet, :public, project: project) }
 
     context 'filter by scope' do
       it "returns all snippets for 'all' scope" do
         snippets = described_class.new(user, scope: :all).execute
 
-        expect(snippets).to contain_exactly(private_personal_snippet, internal_personal_snippet, public_personal_snippet)
+        expect(snippets).to contain_exactly(
+          private_personal_snippet, internal_personal_snippet, public_personal_snippet,
+          internal_project_snippet, public_project_snippet
+        )
       end
 
       it "returns all snippets for 'are_private' scope" do
@@ -36,13 +49,13 @@
       it "returns all snippets for 'are_internal' scope" do
         snippets = described_class.new(user, scope: :are_internal).execute
 
-        expect(snippets).to contain_exactly(internal_personal_snippet)
+        expect(snippets).to contain_exactly(internal_personal_snippet, internal_project_snippet)
       end
 
-      it "returns all snippets for 'are_private' scope" do
+      it "returns all snippets for 'are_public' scope" do
         snippets = described_class.new(user, scope: :are_public).execute
 
-        expect(snippets).to contain_exactly(public_personal_snippet)
+        expect(snippets).to contain_exactly(public_personal_snippet, public_project_snippet)
       end
     end
 
@@ -84,7 +97,6 @@
       end
 
       it 'returns all snippets for an admin' do
-        admin = create(:user, :admin)
         snippets = described_class.new(admin, author: user).execute
 
         expect(snippets).to contain_exactly(private_personal_snippet, internal_personal_snippet, public_personal_snippet)
@@ -92,12 +104,6 @@
     end
 
     context 'project snippets' do
-      let(:group) { create(:group, :public) }
-      let(:project) { create(:project, :public, group: group) }
-      let!(:private_project_snippet) { create(:project_snippet, :private, project: project) }
-      let!(:internal_project_snippet) { create(:project_snippet, :internal, project: project) }
-      let!(:public_project_snippet) { create(:project_snippet, :public, project: project) }
-
       it 'returns public personal and project snippets for unauthorized user' do
         snippets = described_class.new(nil, project: project).execute
 
@@ -145,7 +151,6 @@
       end
 
       it 'returns all snippets for an admin' do
-        admin = create(:user, :admin)
         snippets = described_class.new(admin, project: project).execute
 
         expect(snippets).to contain_exactly(private_project_snippet, internal_project_snippet, public_project_snippet)
@@ -172,6 +177,30 @@
       end
     end
 
+    context 'explore snippets' do
+      it 'returns only public personal snippets for unauthenticated users' do
+        snippets = described_class.new(nil, explore: true).execute
+
+        expect(snippets).to contain_exactly(public_personal_snippet)
+      end
+
+      it 'also returns internal personal snippets for authenticated users' do
+        snippets = described_class.new(user, explore: true).execute
+
+        expect(snippets).to contain_exactly(
+          internal_personal_snippet, public_personal_snippet
+        )
+      end
+
+      it 'returns all personal snippets for admins' do
+        snippets = described_class.new(admin, explore: true).execute
+
+        expect(snippets).to contain_exactly(
+          private_personal_snippet, internal_personal_snippet, public_personal_snippet
+        )
+      end
+    end
+
     context 'when the user cannot read cross project' do
       before do
         allow(Ability).to receive(:allowed?).and_call_original
diff --git a/spec/finders/tags_finder_spec.rb b/spec/finders/tags_finder_spec.rb
index 460e278e2d37ca193dd176cc3e9952d655277461..85f970b71c400bbef9e3611b6d9534bdda5df0f3 100644
--- a/spec/finders/tags_finder_spec.rb
+++ b/spec/finders/tags_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe TagsFinder do
diff --git a/spec/finders/template_finder_spec.rb b/spec/finders/template_finder_spec.rb
index 114af9461e0be2be61e501d89baf64a642fa15f4..ed47752cf6024c6054b24f051c30fa7a0abafa03 100644
--- a/spec/finders/template_finder_spec.rb
+++ b/spec/finders/template_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe TemplateFinder do
diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb
index 9a3ffffb3f265899b993f60b9e7db8f263932951..a4b076bc3671a14cf5f0fb2fdc760cce9a1b84e8 100644
--- a/spec/finders/todos_finder_spec.rb
+++ b/spec/finders/todos_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe TodosFinder do
@@ -14,6 +16,10 @@
     end
 
     describe '#execute' do
+      it 'returns no todos if user is nil' do
+        expect(described_class.new(nil, {}).execute).to be_empty
+      end
+
       context 'filtering' do
         let!(:todo1) { create(:todo, user: user, project: project, target: issue) }
         let!(:todo2) { create(:todo, user: user, group: group, target: merge_request) }
@@ -30,10 +36,18 @@
           expect(todos).to match_array([todo1, todo2])
         end
 
-        it 'returns correct todos when filtered by a type' do
-          todos = finder.new(user, { type: 'Issue' }).execute
+        context 'when filtering by type' do
+          it 'returns correct todos when filtered by a type' do
+            todos = finder.new(user, { type: 'Issue' }).execute
 
-          expect(todos).to match_array([todo1])
+            expect(todos).to match_array([todo1])
+          end
+
+          it 'returns the correct todos when filtering for multiple types' do
+            todos = finder.new(user, { type: %w[Issue MergeRequest] }).execute
+
+            expect(todos).to match_array([todo1, todo2])
+          end
         end
 
         context 'when filtering for actions' do
@@ -47,12 +61,10 @@
               expect(todos).to match_array([todo2])
             end
 
-            context 'multiple actions' do
-              it 'returns the expected todos' do
-                todos = finder.new(user, { action_id: [Todo::DIRECTLY_ADDRESSED, Todo::ASSIGNED] }).execute
+            it 'returns the expected todos when filtering for multiple action ids' do
+              todos = finder.new(user, { action_id: [Todo::DIRECTLY_ADDRESSED, Todo::ASSIGNED] }).execute
 
-                expect(todos).to match_array([todo2, todo1])
-              end
+              expect(todos).to match_array([todo2, todo1])
             end
           end
 
@@ -63,12 +75,10 @@
               expect(todos).to match_array([todo2])
             end
 
-            context 'multiple actions' do
-              it 'returns the expected todos' do
-                todos = finder.new(user, { action: [:directly_addressed, :assigned] }).execute
+            it 'returns the expected todos when filtering for multiple action names' do
+              todos = finder.new(user, { action: [:directly_addressed, :assigned] }).execute
 
-                expect(todos).to match_array([todo2, todo1])
-              end
+              expect(todos).to match_array([todo2, todo1])
             end
           end
         end
@@ -95,14 +105,39 @@
           end
         end
 
-        context 'with subgroups' do
-          let(:subgroup) { create(:group, parent: group) }
-          let!(:todo3) { create(:todo, user: user, group: subgroup, target: issue) }
+        context 'by groups' do
+          context 'with subgroups' do
+            let(:subgroup) { create(:group, parent: group) }
+            let!(:todo3) { create(:todo, user: user, group: subgroup, target: issue) }
 
-          it 'returns todos from subgroups when filtered by a group' do
-            todos = finder.new(user, { group_id: group.id }).execute
+            it 'returns todos from subgroups when filtered by a group' do
+              todos = finder.new(user, { group_id: group.id }).execute
 
-            expect(todos).to match_array([todo1, todo2, todo3])
+              expect(todos).to match_array([todo1, todo2, todo3])
+            end
+          end
+
+          context 'filtering for multiple groups' do
+            let_it_be(:group2) { create(:group) }
+            let_it_be(:group3) { create(:group) }
+
+            let!(:todo1) { create(:todo, user: user, project: project, target: issue) }
+            let!(:todo2) { create(:todo, user: user, group: group, target: merge_request) }
+            let!(:todo3) { create(:todo, user: user, group: group2, target: merge_request) }
+
+            let(:subgroup1) { create(:group, parent: group) }
+            let!(:todo4) { create(:todo, user: user, group: subgroup1, target: issue) }
+
+            let(:subgroup2) { create(:group, parent: group2) }
+            let!(:todo5) { create(:todo, user: user, group: subgroup2, target: issue) }
+
+            let!(:todo6) { create(:todo, user: user, group: group3, target: issue) }
+
+            it 'returns the expected groups' do
+              todos = finder.new(user, { group_id: [group.id, group2.id] }).execute
+
+              expect(todos).to match_array([todo1, todo2, todo3, todo4, todo5])
+            end
           end
         end
       end
diff --git a/spec/finders/user_recent_events_finder_spec.rb b/spec/finders/user_recent_events_finder_spec.rb
index 5ebceeb7586eed9a9d40064ea339a2a221d3ca8f..eef6448a4a27154410c44408819576a1ca7c57ac 100644
--- a/spec/finders/user_recent_events_finder_spec.rb
+++ b/spec/finders/user_recent_events_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe UserRecentEventsFinder do
diff --git a/spec/finders/users_finder_spec.rb b/spec/finders/users_finder_spec.rb
index d71d3c99272e68f3755482ff799d7403eddc2fa5..7f1fc1cc1c5cba32ffaf13a3ba46fd999bdcd2f1 100644
--- a/spec/finders/users_finder_spec.rb
+++ b/spec/finders/users_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe UsersFinder do
diff --git a/spec/fixtures/api/schemas/deployment.json b/spec/fixtures/api/schemas/deployment.json
index b1e3c000ddf663041997211934bd99659f5561f3..0cfeadfe548aea054509e6463666950050bd6880 100644
--- a/spec/fixtures/api/schemas/deployment.json
+++ b/spec/fixtures/api/schemas/deployment.json
@@ -61,7 +61,7 @@
       "type": "array",
       "items": { "$ref": "job/job.json" }
     },
-    "status": { "type": "string" }		
+    "status": { "type": "string" }
   },
   "additionalProperties": false
 }
diff --git a/spec/fixtures/api/schemas/evidences/evidence.json b/spec/fixtures/api/schemas/evidences/evidence.json
new file mode 100644
index 0000000000000000000000000000000000000000..ea3861258e1a64d37a9e478828fb05bf711f6d5e
--- /dev/null
+++ b/spec/fixtures/api/schemas/evidences/evidence.json
@@ -0,0 +1,11 @@
+{
+  "type": "object",
+  "required": [
+    "release"
+  ],
+  "properties": {
+    "release": { "$ref": "release.json" }
+  },
+  "additionalProperties": false
+}
+
diff --git a/spec/fixtures/api/schemas/evidences/issue.json b/spec/fixtures/api/schemas/evidences/issue.json
index 10e90dff455d79dc712594797c4c561e16bb9423..fd9daf17ab8231c6edbba54cb53e5a617e595683 100644
--- a/spec/fixtures/api/schemas/evidences/issue.json
+++ b/spec/fixtures/api/schemas/evidences/issue.json
@@ -14,13 +14,12 @@
   "properties": {
     "id": { "type": "integer" },
     "title": { "type": "string" },
-    "description": { "type": "string" },
-    "author": { "$ref": "author.json" },
+    "description": { "type": ["string", "null"] },
     "state": { "type": "string" },
     "iid": { "type": "integer" },
     "confidential": { "type": "boolean" },
     "created_at": { "type": "date" },
-    "due_date": { "type": "date" }
+    "due_date": { "type": ["date", "null"] }
   },
   "additionalProperties": false
 }
diff --git a/spec/fixtures/api/schemas/evidences/milestone.json b/spec/fixtures/api/schemas/evidences/milestone.json
index 91f0f48bd4c5841ab3e41a68b19f44eddd92dbaf..ab27fdecde23c52df970355753e5523724d3e76a 100644
--- a/spec/fixtures/api/schemas/evidences/milestone.json
+++ b/spec/fixtures/api/schemas/evidences/milestone.json
@@ -13,11 +13,11 @@
   "properties": {
     "id": { "type": "integer" },
     "title": { "type": "string" },
-    "description": { "type": "string" },
+    "description": { "type": ["string", "null"] },
     "state": { "type": "string" },
     "iid": { "type": "integer" },
     "created_at": { "type": "date" },
-    "due_date": { "type": "date" },
+    "due_date": { "type": ["date", "null"] },
     "issues": {
       "type": "array",
       "items": { "$ref": "issue.json" }
diff --git a/spec/fixtures/api/schemas/evidences/project.json b/spec/fixtures/api/schemas/evidences/project.json
index 542686542f84885b233206710e22d4061b8d1d6c..3a094bd276fa3ca3d481559eed9ed7c374ebd524 100644
--- a/spec/fixtures/api/schemas/evidences/project.json
+++ b/spec/fixtures/api/schemas/evidences/project.json
@@ -9,7 +9,7 @@
   "properties": {
     "id": { "type": "integer" },
     "name": { "type": "string" },
-    "description": { "type": "string" },
+    "description": { "type": ["string", "null"] },
     "created_at": { "type": "date" }
   },
   "additionalProperties": false
diff --git a/spec/fixtures/api/schemas/evidences/release.json b/spec/fixtures/api/schemas/evidences/release.json
index 68c872a9dc8a561c99c167c1739dd72caf095769..37eb9a9b5c0e42643ddfbff90a539e32aa3c6b72 100644
--- a/spec/fixtures/api/schemas/evidences/release.json
+++ b/spec/fixtures/api/schemas/evidences/release.json
@@ -2,7 +2,7 @@
   "type": "object",
   "required": [
     "id",
-    "tag",
+    "tag_name",
     "name",
     "description",
     "created_at",
@@ -11,8 +11,8 @@
   ],
   "properties": {
     "id": { "type": "integer" },
-    "tag": { "type": "string" },
-    "name": { "type": "string" },
+    "tag_name": { "type": "string" },
+    "name": { "type": ["string", "null"] },
     "description": { "type": "string" },
     "created_at": { "type": "date" },
     "project": { "$ref": "project.json" },
diff --git a/spec/fixtures/api/schemas/job/build_trace.json b/spec/fixtures/api/schemas/job/build_trace.json
new file mode 100644
index 0000000000000000000000000000000000000000..becd881ea572c31cb89e6c1e7e0501d8c0b15369
--- /dev/null
+++ b/spec/fixtures/api/schemas/job/build_trace.json
@@ -0,0 +1,31 @@
+{
+  "description": "Build trace",
+  "type": "object",
+  "required": [
+    "id",
+    "status",
+    "complete",
+    "state",
+    "append",
+    "truncated",
+    "offset",
+    "size",
+    "total"
+  ],
+  "properties": {
+    "id": { "type": "integer" },
+    "status": { "type": "string" },
+    "complete": { "type": "boolean" },
+    "state": { "type": ["string", "null"] },
+    "append": { "type": ["boolean", "null"] },
+    "truncated": { "type": ["boolean", "null"] },
+    "offset": { "type": ["integer", "null"] },
+    "size": { "type": ["integer", "null"] },
+    "total": { "type": ["integer", "null"] },
+    "html": { "type": ["string", "null"] },
+    "lines": {
+      "type": ["array", "null"],
+      "items": { "$ref": "./build_trace_line.json" }
+    }
+  }
+}
diff --git a/spec/fixtures/api/schemas/job/build_trace_line.json b/spec/fixtures/api/schemas/job/build_trace_line.json
new file mode 100644
index 0000000000000000000000000000000000000000..18726dff2bb0ba41a36ee7420d1762151e125feb
--- /dev/null
+++ b/spec/fixtures/api/schemas/job/build_trace_line.json
@@ -0,0 +1,18 @@
+{
+  "description": "Build trace line",
+  "type": "object",
+  "required": [
+    "offset",
+    "content"
+  ],
+  "properties": {
+    "offset": { "type": "integer" },
+    "content": {
+      "type": "array",
+      "items": { "$ref": "./build_trace_line_content.json" }
+    },
+    "section": "string",
+    "section_header": "boolean",
+    "section_duration": "string"
+  }
+}
diff --git a/spec/fixtures/api/schemas/job/build_trace_line_content.json b/spec/fixtures/api/schemas/job/build_trace_line_content.json
new file mode 100644
index 0000000000000000000000000000000000000000..41f8124c11392bb4ab245e2c75bf1ba6b1723a88
--- /dev/null
+++ b/spec/fixtures/api/schemas/job/build_trace_line_content.json
@@ -0,0 +1,11 @@
+{
+  "description": "Build trace line content",
+  "type": "object",
+  "required": [
+    "text"
+  ],
+  "properties": {
+    "text": { "type": "string" },
+    "style": { "type": "string" }
+  }
+}
diff --git a/spec/fixtures/grafana/dashboard_response.json b/spec/fixtures/grafana/dashboard_response.json
new file mode 100644
index 0000000000000000000000000000000000000000..4743ec39b445528ac4a9f63370d7bedb90348e51
--- /dev/null
+++ b/spec/fixtures/grafana/dashboard_response.json
@@ -0,0 +1,764 @@
+{
+  "meta": {
+    "type": "db",
+    "canSave": true,
+    "canEdit": true,
+    "canAdmin": true,
+    "canStar": true,
+    "slug": "gitlab-omnibus-redis",
+    "url": "/-/grafana/d/XDaNK6amz/gitlab-omnibus-redis",
+    "expires": "0001-01-01T00:00:00Z",
+    "created": "2019-10-04T13:43:20Z",
+    "updated": "2019-10-04T13:43:20Z",
+    "updatedBy": "Anonymous",
+    "createdBy": "Anonymous",
+    "version": 1,
+    "hasAcl": false,
+    "isFolder": false,
+    "folderId": 1,
+    "folderTitle": "GitLab Omnibus",
+    "folderUrl": "/-/grafana/dashboards/f/l2EpNh2Zk/gitlab-omnibus",
+    "provisioned": true,
+    "provisionedExternalId": "redis.json"
+  },
+  "dashboard": {
+    "annotations": {
+      "list": [
+        {
+          "builtIn": 1,
+          "datasource": "-- Grafana --",
+          "enable": true,
+          "hide": true,
+          "iconColor": "rgba(0, 211, 255, 1)",
+          "name": "Annotations \u0026 Alerts",
+          "type": "dashboard"
+        }
+      ]
+    },
+    "description": "GitLab Omnibus dashboard for Redis servers",
+    "editable": true,
+    "gnetId": 763,
+    "graphTooltip": 0,
+    "id": 3,
+    "iteration": 1556027798221,
+    "links": [],
+    "panels": [
+      {
+        "cacheTimeout": null,
+        "colorBackground": false,
+        "colorValue": false,
+        "colors": ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
+        "datasource": "GitLab Omnibus",
+        "decimals": 0,
+        "editable": true,
+        "error": false,
+        "format": "dtdurations",
+        "gauge": {
+          "maxValue": 100,
+          "minValue": 0,
+          "show": false,
+          "thresholdLabels": false,
+          "thresholdMarkers": true
+        },
+        "gridPos": { "h": 3, "w": 4, "x": 0, "y": 0 },
+        "id": 9,
+        "interval": null,
+        "isNew": true,
+        "links": [],
+        "mappingType": 1,
+        "mappingTypes": [
+          { "name": "value to text", "value": 1 },
+          { "name": "range to text", "value": 2 }
+        ],
+        "maxDataPoints": 100,
+        "nullPointMode": "connected",
+        "nullText": null,
+        "postfix": "",
+        "postfixFontSize": "50%",
+        "prefix": "",
+        "prefixFontSize": "50%",
+        "rangeMaps": [{ "from": "null", "text": "N/A", "to": "null" }],
+        "sparkline": {
+          "fillColor": "rgba(31, 118, 189, 0.18)",
+          "full": false,
+          "lineColor": "rgb(31, 120, 193)",
+          "show": false
+        },
+        "tableColumn": "addr",
+        "targets": [
+          {
+            "expr": "avg(time() - redis_start_time_seconds{instance=~\"$instance\"})",
+            "format": "time_series",
+            "instant": true,
+            "interval": "",
+            "intervalFactor": 2,
+            "legendFormat": "",
+            "metric": "",
+            "refId": "A",
+            "step": 1800
+          }
+        ],
+        "thresholds": "",
+        "title": "Uptime",
+        "type": "singlestat",
+        "valueFontSize": "70%",
+        "valueMaps": [{ "op": "=", "text": "N/A", "value": "null" }],
+        "valueName": "current"
+      },
+      {
+        "cacheTimeout": null,
+        "colorBackground": false,
+        "colorValue": false,
+        "colors": ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
+        "datasource": "GitLab Omnibus",
+        "decimals": 0,
+        "editable": true,
+        "error": false,
+        "format": "none",
+        "gauge": {
+          "maxValue": 100,
+          "minValue": 0,
+          "show": false,
+          "thresholdLabels": false,
+          "thresholdMarkers": true
+        },
+        "gridPos": { "h": 3, "w": 4, "x": 4, "y": 0 },
+        "hideTimeOverride": true,
+        "id": 12,
+        "interval": null,
+        "isNew": true,
+        "links": [],
+        "mappingType": 1,
+        "mappingTypes": [
+          { "name": "value to text", "value": 1 },
+          { "name": "range to text", "value": 2 }
+        ],
+        "maxDataPoints": 100,
+        "nullPointMode": "connected",
+        "nullText": null,
+        "postfix": "",
+        "postfixFontSize": "50%",
+        "prefix": "",
+        "prefixFontSize": "50%",
+        "rangeMaps": [{ "from": "null", "text": "N/A", "to": "null" }],
+        "sparkline": {
+          "fillColor": "rgba(31, 118, 189, 0.18)",
+          "full": false,
+          "lineColor": "rgb(31, 120, 193)",
+          "show": true
+        },
+        "tableColumn": "",
+        "targets": [
+          {
+            "expr": "sum(\n  avg_over_time(redis_connected_clients{instance=~\"$instance\"}[$__interval])\n)",
+            "format": "time_series",
+            "interval": "1m",
+            "intervalFactor": 2,
+            "legendFormat": "",
+            "metric": "",
+            "refId": "A",
+            "step": 2
+          }
+        ],
+        "thresholds": "",
+        "timeFrom": "1m",
+        "timeShift": null,
+        "title": "Clients",
+        "type": "singlestat",
+        "valueFontSize": "80%",
+        "valueMaps": [{ "op": "=", "text": "N/A", "value": "null" }],
+        "valueName": "avg"
+      },
+      {
+        "aliasColors": {},
+        "bars": false,
+        "dashLength": 10,
+        "dashes": false,
+        "datasource": "GitLab Omnibus",
+        "editable": true,
+        "error": false,
+        "fill": 1,
+        "grid": {},
+        "gridPos": { "h": 6, "w": 8, "x": 8, "y": 0 },
+        "id": 2,
+        "isNew": true,
+        "legend": {
+          "avg": false,
+          "current": false,
+          "max": false,
+          "min": false,
+          "show": false,
+          "total": false,
+          "values": false
+        },
+        "lines": true,
+        "linewidth": 2,
+        "links": [],
+        "nullPointMode": "connected",
+        "paceLength": 10,
+        "percentage": false,
+        "pointradius": 5,
+        "points": false,
+        "renderer": "flot",
+        "seriesOverrides": [],
+        "spaceLength": 10,
+        "stack": false,
+        "steppedLine": false,
+        "targets": [
+          {
+            "expr": "sum(\n  rate(redis_commands_processed_total{instance=~\"$instance\"}[$__interval])\n)",
+            "format": "time_series",
+            "interval": "1m",
+            "intervalFactor": 2,
+            "legendFormat": "",
+            "metric": "A",
+            "refId": "A",
+            "step": 240,
+            "target": ""
+          }
+        ],
+        "thresholds": [],
+        "timeFrom": null,
+        "timeRegions": [],
+        "timeShift": null,
+        "title": "Commands Executed",
+        "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" },
+        "type": "graph",
+        "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+        "yaxes": [
+          { "format": "reqps", "label": null, "logBase": 1, "max": null, "min": "0", "show": true },
+          { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+        ],
+        "yaxis": { "align": false, "alignLevel": null }
+      },
+      {
+        "aliasColors": {},
+        "bars": false,
+        "dashLength": 10,
+        "dashes": false,
+        "datasource": "GitLab Omnibus",
+        "decimals": 2,
+        "editable": true,
+        "error": false,
+        "fill": 1,
+        "grid": {},
+        "gridPos": { "h": 6, "w": 8, "x": 16, "y": 0 },
+        "id": 1,
+        "isNew": true,
+        "legend": {
+          "avg": false,
+          "current": false,
+          "max": false,
+          "min": false,
+          "show": false,
+          "total": false,
+          "values": false
+        },
+        "lines": true,
+        "linewidth": 2,
+        "links": [],
+        "nullPointMode": "connected",
+        "paceLength": 10,
+        "percentage": true,
+        "pointradius": 5,
+        "points": false,
+        "renderer": "flot",
+        "seriesOverrides": [],
+        "spaceLength": 10,
+        "stack": false,
+        "steppedLine": false,
+        "targets": [
+          {
+            "expr": "sum(\n  rate(redis_keyspace_hits_total{instance=~\"$instance\"}[$__interval])\n)",
+            "format": "time_series",
+            "hide": false,
+            "interval": "1m",
+            "intervalFactor": 1,
+            "legendFormat": "hits",
+            "metric": "",
+            "refId": "A",
+            "step": 240,
+            "target": ""
+          },
+          {
+            "expr": "sum(\n  rate(redis_keyspace_misses_total{instance=~\"$instance\"}[$__interval])\n)",
+            "format": "time_series",
+            "hide": false,
+            "interval": "1m",
+            "intervalFactor": 1,
+            "legendFormat": "misses",
+            "metric": "",
+            "refId": "B",
+            "step": 240,
+            "target": ""
+          }
+        ],
+        "thresholds": [],
+        "timeFrom": null,
+        "timeRegions": [],
+        "timeShift": null,
+        "title": "Hits, Misses per Second",
+        "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" },
+        "type": "graph",
+        "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+        "yaxes": [
+          { "format": "short", "label": "", "logBase": 1, "max": null, "min": 0, "show": true },
+          { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+        ],
+        "yaxis": { "align": false, "alignLevel": null }
+      },
+      {
+        "aliasColors": { "max": "#BF1B00" },
+        "bars": false,
+        "dashLength": 10,
+        "dashes": false,
+        "datasource": "GitLab Omnibus",
+        "editable": true,
+        "error": false,
+        "fill": 1,
+        "grid": {},
+        "gridPos": { "h": 10, "w": 8, "x": 0, "y": 3 },
+        "id": 7,
+        "isNew": true,
+        "legend": {
+          "avg": false,
+          "current": false,
+          "hideEmpty": false,
+          "hideZero": false,
+          "max": false,
+          "min": false,
+          "show": true,
+          "total": false,
+          "values": false
+        },
+        "lines": true,
+        "linewidth": 2,
+        "links": [],
+        "nullPointMode": "null as zero",
+        "paceLength": 10,
+        "percentage": false,
+        "pointradius": 5,
+        "points": false,
+        "renderer": "flot",
+        "seriesOverrides": [{ "alias": "/max - .*/", "dashes": true }],
+        "spaceLength": 10,
+        "stack": false,
+        "steppedLine": false,
+        "targets": [
+          {
+            "expr": "redis_memory_used_bytes{instance=~\"$instance\"}",
+            "format": "time_series",
+            "intervalFactor": 2,
+            "legendFormat": "used - {{instance}}",
+            "metric": "",
+            "refId": "A",
+            "step": 240,
+            "target": ""
+          },
+          {
+            "expr": "redis_config_maxmemory{instance=~\"$instance\"} \u003e 0",
+            "format": "time_series",
+            "hide": false,
+            "intervalFactor": 2,
+            "legendFormat": "max - {{instance}}",
+            "refId": "B",
+            "step": 240
+          }
+        ],
+        "thresholds": [],
+        "timeFrom": null,
+        "timeRegions": [],
+        "timeShift": null,
+        "title": "Memory Usage",
+        "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" },
+        "type": "graph",
+        "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+        "yaxes": [
+          { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": 0, "show": true },
+          { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+        ],
+        "yaxis": { "align": false, "alignLevel": null }
+      },
+      {
+        "aliasColors": {
+          "evicts": "#890F02",
+          "memcached_items_evicted_total{instance=\"172.17.0.1:9150\",job=\"prometheus\"}": "#890F02",
+          "reclaims": "#3F6833"
+        },
+        "bars": false,
+        "dashLength": 10,
+        "dashes": false,
+        "datasource": "GitLab Omnibus",
+        "editable": true,
+        "error": false,
+        "fill": 1,
+        "grid": {},
+        "gridPos": { "h": 7, "w": 8, "x": 8, "y": 6 },
+        "id": 8,
+        "isNew": true,
+        "legend": {
+          "avg": false,
+          "current": false,
+          "max": false,
+          "min": false,
+          "show": true,
+          "total": false,
+          "values": false
+        },
+        "lines": true,
+        "linewidth": 2,
+        "links": [],
+        "nullPointMode": "connected",
+        "paceLength": 10,
+        "percentage": false,
+        "pointradius": 5,
+        "points": false,
+        "renderer": "flot",
+        "seriesOverrides": [{ "alias": "reclaims", "yaxis": 2 }],
+        "spaceLength": 10,
+        "stack": false,
+        "steppedLine": false,
+        "targets": [
+          {
+            "expr": "sum(rate(redis_expired_keys_total{instance=~\"$instance\"}[$__interval]))",
+            "format": "time_series",
+            "interval": "1m",
+            "intervalFactor": 2,
+            "legendFormat": "expired",
+            "metric": "",
+            "refId": "A",
+            "step": 240,
+            "target": ""
+          },
+          {
+            "expr": "sum(rate(redis_evicted_keys_total{instance=~\"$instance\"}[$__interval]))",
+            "format": "time_series",
+            "interval": "1m",
+            "intervalFactor": 2,
+            "legendFormat": "evicted",
+            "refId": "B",
+            "step": 240
+          }
+        ],
+        "thresholds": [],
+        "timeFrom": null,
+        "timeRegions": [],
+        "timeShift": null,
+        "title": "Expired / Evicted",
+        "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" },
+        "type": "graph",
+        "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+        "yaxes": [
+          { "format": "short", "label": null, "logBase": 1, "max": null, "min": "0", "show": true },
+          { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+        ],
+        "yaxis": { "align": false, "alignLevel": null }
+      },
+      {
+        "aliasColors": {},
+        "bars": false,
+        "dashLength": 10,
+        "dashes": false,
+        "datasource": "GitLab Omnibus",
+        "editable": true,
+        "error": false,
+        "fill": 1,
+        "grid": {},
+        "gridPos": { "h": 7, "w": 8, "x": 16, "y": 6 },
+        "id": 10,
+        "isNew": true,
+        "legend": {
+          "avg": false,
+          "current": false,
+          "max": false,
+          "min": false,
+          "show": true,
+          "total": false,
+          "values": false
+        },
+        "lines": true,
+        "linewidth": 2,
+        "links": [],
+        "nullPointMode": "connected",
+        "paceLength": 10,
+        "percentage": false,
+        "pointradius": 5,
+        "points": false,
+        "renderer": "flot",
+        "seriesOverrides": [],
+        "spaceLength": 10,
+        "stack": false,
+        "steppedLine": false,
+        "targets": [
+          {
+            "expr": "sum(\n  rate(redis_net_input_bytes_total{instance=~\"$instance\"}[$__interval])\n)",
+            "format": "time_series",
+            "interval": "1m",
+            "intervalFactor": 2,
+            "legendFormat": "In",
+            "refId": "A",
+            "step": 240
+          },
+          {
+            "expr": "sum(\n  rate(redis_net_output_bytes_total{instance=~\"$instance\"}[$__interval])\n)",
+            "format": "time_series",
+            "interval": "1m",
+            "intervalFactor": 2,
+            "legendFormat": "Out",
+            "refId": "B",
+            "step": 240
+          }
+        ],
+        "thresholds": [],
+        "timeFrom": null,
+        "timeRegions": [],
+        "timeShift": null,
+        "title": "Network I/O",
+        "tooltip": { "msResolution": true, "shared": true, "sort": 0, "value_type": "cumulative" },
+        "type": "graph",
+        "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+        "yaxes": [
+          { "format": "Bps", "label": null, "logBase": 1, "max": null, "min": "0", "show": true },
+          { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+        ],
+        "yaxis": { "align": false, "alignLevel": null }
+      },
+      {
+        "aliasColors": {},
+        "bars": false,
+        "dashLength": 10,
+        "dashes": false,
+        "datasource": "GitLab Omnibus",
+        "editable": true,
+        "error": false,
+        "fill": 8,
+        "grid": {},
+        "gridPos": { "h": 7, "w": 16, "x": 0, "y": 13 },
+        "id": 14,
+        "isNew": true,
+        "legend": {
+          "alignAsTable": true,
+          "avg": true,
+          "current": true,
+          "max": true,
+          "min": false,
+          "rightSide": true,
+          "show": true,
+          "total": false,
+          "values": true
+        },
+        "lines": true,
+        "linewidth": 1,
+        "links": [],
+        "nullPointMode": "connected",
+        "paceLength": 10,
+        "percentage": false,
+        "pointradius": 5,
+        "points": false,
+        "renderer": "flot",
+        "seriesOverrides": [],
+        "spaceLength": 10,
+        "stack": true,
+        "steppedLine": false,
+        "targets": [
+          {
+            "expr": "sum without (instance) (\n  rate(redis_commands_total{instance=~\"$instance\"}[$__interval])\n) \u003e 0",
+            "format": "time_series",
+            "interval": "1m",
+            "intervalFactor": 2,
+            "legendFormat": "{{ cmd }}",
+            "metric": "redis_command_calls_total",
+            "refId": "A",
+            "step": 240
+          }
+        ],
+        "thresholds": [],
+        "timeFrom": null,
+        "timeRegions": [],
+        "timeShift": null,
+        "title": "Command Calls / sec",
+        "tooltip": { "msResolution": true, "shared": true, "sort": 2, "value_type": "individual" },
+        "type": "graph",
+        "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+        "yaxes": [
+          { "format": "short", "label": null, "logBase": 1, "max": null, "min": "0", "show": true },
+          { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+        ],
+        "yaxis": { "align": false, "alignLevel": null }
+      },
+      {
+        "aliasColors": {},
+        "bars": false,
+        "dashLength": 10,
+        "dashes": false,
+        "datasource": "GitLab Omnibus",
+        "editable": true,
+        "error": false,
+        "fill": 7,
+        "grid": {},
+        "gridPos": { "h": 7, "w": 8, "x": 16, "y": 13 },
+        "id": 13,
+        "isNew": true,
+        "legend": {
+          "avg": false,
+          "current": false,
+          "max": false,
+          "min": false,
+          "show": true,
+          "total": false,
+          "values": false
+        },
+        "lines": true,
+        "linewidth": 2,
+        "links": [],
+        "nullPointMode": "connected",
+        "paceLength": 10,
+        "percentage": false,
+        "pointradius": 5,
+        "points": false,
+        "renderer": "flot",
+        "seriesOverrides": [],
+        "spaceLength": 10,
+        "stack": true,
+        "steppedLine": false,
+        "targets": [
+          {
+            "expr": "sum(redis_db_keys{instance=~\"$instance\"} - redis_db_keys_expiring{instance=~\"$instance\"}) ",
+            "format": "time_series",
+            "interval": "",
+            "intervalFactor": 2,
+            "legendFormat": "not expiring",
+            "refId": "A",
+            "step": 240,
+            "target": ""
+          },
+          {
+            "expr": "sum(redis_db_keys_expiring{instance=~\"$instance\"})",
+            "format": "time_series",
+            "interval": "",
+            "intervalFactor": 2,
+            "legendFormat": "expiring",
+            "metric": "",
+            "refId": "B",
+            "step": 240
+          }
+        ],
+        "thresholds": [],
+        "timeFrom": null,
+        "timeRegions": [],
+        "timeShift": null,
+        "title": "Expiring vs Not-Expiring Keys",
+        "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" },
+        "type": "graph",
+        "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+        "yaxes": [
+          { "format": "short", "label": null, "logBase": 1, "max": null, "min": "0", "show": true },
+          { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+        ],
+        "yaxis": { "align": false, "alignLevel": null }
+      },
+      {
+        "aliasColors": {},
+        "bars": false,
+        "dashLength": 10,
+        "dashes": false,
+        "datasource": "GitLab Omnibus",
+        "editable": true,
+        "error": false,
+        "fill": 7,
+        "grid": {},
+        "gridPos": { "h": 7, "w": 16, "x": 0, "y": 20 },
+        "id": 5,
+        "isNew": true,
+        "legend": {
+          "alignAsTable": true,
+          "avg": false,
+          "current": true,
+          "max": false,
+          "min": false,
+          "rightSide": true,
+          "show": true,
+          "total": false,
+          "values": true
+        },
+        "lines": true,
+        "linewidth": 2,
+        "links": [],
+        "nullPointMode": "connected",
+        "paceLength": 10,
+        "percentage": false,
+        "pointradius": 5,
+        "points": false,
+        "renderer": "flot",
+        "seriesOverrides": [],
+        "spaceLength": 10,
+        "stack": true,
+        "steppedLine": false,
+        "targets": [
+          {
+            "expr": "sum by (db) (\n  redis_db_keys{instance=~\"$instance\"}\n)",
+            "format": "time_series",
+            "interval": "",
+            "intervalFactor": 2,
+            "legendFormat": "{{ db }} ",
+            "refId": "A",
+            "step": 240,
+            "target": ""
+          }
+        ],
+        "thresholds": [],
+        "timeFrom": null,
+        "timeRegions": [],
+        "timeShift": null,
+        "title": "Items per DB",
+        "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" },
+        "type": "graph",
+        "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+        "yaxes": [
+          { "format": "none", "label": null, "logBase": 1, "max": null, "min": "0", "show": true },
+          { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+        ],
+        "yaxis": { "align": false, "alignLevel": null }
+      }
+    ],
+    "refresh": "1m",
+    "schemaVersion": 18,
+    "style": "dark",
+    "tags": ["redis"],
+    "templating": {
+      "list": [
+        {
+          "allValue": null,
+          "current": { "tags": [], "text": "All", "value": "$__all" },
+          "datasource": "GitLab Omnibus",
+          "definition": "",
+          "hide": 0,
+          "includeAll": true,
+          "label": null,
+          "multi": false,
+          "name": "instance",
+          "options": [],
+          "query": "label_values(up{job=\"redis\"}, instance)",
+          "refresh": 1,
+          "regex": "",
+          "skipUrlSync": false,
+          "sort": 0,
+          "tagValuesQuery": "",
+          "tags": [],
+          "tagsQuery": "",
+          "type": "query",
+          "useTags": false
+        }
+      ]
+    },
+    "time": { "from": "now-24h", "to": "now" },
+    "timepicker": {
+      "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
+      "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
+    },
+    "timezone": "",
+    "title": "GitLab Omnibus - Redis",
+    "uid": "XDaNK6amz",
+    "version": 1
+  }
+}
diff --git a/spec/fixtures/grafana/datasource_response.json b/spec/fixtures/grafana/datasource_response.json
new file mode 100644
index 0000000000000000000000000000000000000000..07c075beb3565d8dab7b966681fb450323e94777
--- /dev/null
+++ b/spec/fixtures/grafana/datasource_response.json
@@ -0,0 +1,21 @@
+{
+  "id": 1,
+  "orgId": 1,
+  "name": "GitLab Omnibus",
+  "type": "prometheus",
+  "typeLogoUrl": "",
+  "access": "proxy",
+  "url": "http://localhost:9090",
+  "password": "",
+  "user": "",
+  "database": "",
+  "basicAuth": false,
+  "basicAuthUser": "",
+  "basicAuthPassword": "",
+  "withCredentials": false,
+  "isDefault": true,
+  "jsonData": {},
+  "secureJsonFields": {},
+  "version": 1,
+  "readOnly": true
+}
diff --git a/spec/fixtures/grafana/expected_grafana_embed.json b/spec/fixtures/grafana/expected_grafana_embed.json
new file mode 100644
index 0000000000000000000000000000000000000000..72fb5477b9e1bd41d0d58af4ce5476f1248c1a52
--- /dev/null
+++ b/spec/fixtures/grafana/expected_grafana_embed.json
@@ -0,0 +1,27 @@
+{
+  "panel_groups": [
+    {
+      "panels": [
+        {
+          "title": "Network I/O",
+          "type": "area-chart",
+          "y_label": "",
+          "metrics": [
+            {
+              "id": "In_0",
+              "query_range": "sum(  rate(redis_net_input_bytes_total{instance=~\"localhost:9121\"}[1m]))",
+              "label": "In",
+              "prometheus_endpoint_path": "/foo/bar/-/grafana/proxy/1/api/v1/query_range?query=sum%28++rate%28redis_net_input_bytes_total%7Binstance%3D~%22localhost%3A9121%22%7D%5B1m%5D%29%29"
+            },
+            {
+              "id": "Out_1",
+              "query_range": "sum(  rate(redis_net_output_bytes_total{instance=~\"localhost:9121\"}[1m]))",
+              "label": "Out",
+              "prometheus_endpoint_path": "/foo/bar/-/grafana/proxy/1/api/v1/query_range?query=sum%28++rate%28redis_net_output_bytes_total%7Binstance%3D~%22localhost%3A9121%22%7D%5B1m%5D%29%29"
+            }
+          ]
+        }
+      ]
+    }
+  ]
+}
diff --git a/spec/fixtures/grafana/simplified_dashboard_response.json b/spec/fixtures/grafana/simplified_dashboard_response.json
new file mode 100644
index 0000000000000000000000000000000000000000..b450fda082b257938ed46120b17f519141fe457e
--- /dev/null
+++ b/spec/fixtures/grafana/simplified_dashboard_response.json
@@ -0,0 +1,40 @@
+{
+  "dashboard": {
+    "panels": [
+      {
+        "datasource": "GitLab Omnibus",
+        "id": 8,
+        "lines": true,
+        "targets": [
+          {
+            "expr": "sum(\n  rate(redis_net_input_bytes_total{instance=~\"$instance\"}[$__interval])\n)",
+            "format": "time_series",
+            "interval": "1m",
+            "legendFormat": "In",
+            "refId": "A"
+          },
+          {
+            "expr": "sum(\n  rate(redis_net_output_bytes_total{instance=~\"[[instance]]\"}[$__interval])\n)",
+            "format": "time_series",
+            "interval": "1m",
+            "legendFormat": "Out",
+            "refId": "B"
+          }
+        ],
+        "title": "Network I/O",
+        "type": "graph",
+        "yaxes": [{ "format": "Bps" }, { "format": "short" }]
+      }
+    ],
+    "templating": {
+      "list": [
+        {
+          "current": {
+            "value": "localhost:9121"
+          },
+          "name": "instance"
+        }
+      ]
+    }
+  }
+}
diff --git a/spec/fixtures/lib/gitlab/import_export/project.group.json b/spec/fixtures/lib/gitlab/import_export/project.group.json
index 66f5bb4c87b9ce82134c929cfcb40179199b3b36..47faf271cca90613647900ca0a525de308837113 100644
--- a/spec/fixtures/lib/gitlab/import_export/project.group.json
+++ b/spec/fixtures/lib/gitlab/import_export/project.group.json
@@ -129,7 +129,7 @@
       "updated_at": "2017-08-15T18:37:40.807Z",
       "branch_name": null,
       "description": "Quam totam fuga numquam in eveniet.",
-      "state": "opened",
+      "state": "closed",
       "iid": 2,
       "updated_by_id": 1,
       "confidential": false,
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json
index 9c1be32645ab9969e8ac858fff29dbcbff692603..ac40f2dcd134f0c0652956bbc4555241c88aa045 100644
--- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json
@@ -1,7 +1,6 @@
 {
   "type": "object",
   "required": [
-    "unit",
     "label",
     "prometheus_endpoint_path"
   ],
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
index 1548daacd643d035bef78e3b1b8f48bf7f8fb3b6..a16f1ef592f611caa5989c8fbf67cb550c022305 100644
--- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
@@ -3,7 +3,6 @@
   "required": [
     "title",
     "y_label",
-    "weight",
     "metrics"
   ],
   "properties": {
diff --git a/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js b/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
index 321dc5bd6bad10017debf1c85453b876a2b37ce5..69290f6dfa96608908073c847c03c1cd8be5b0b7 100644
--- a/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
@@ -123,6 +123,7 @@ describe('EksClusterConfigurationForm', () => {
       store,
       propsData: {
         gitlabManagedClusterHelpPath: '',
+        kubernetesIntegrationHelpPath: '',
       },
     });
   });
diff --git a/spec/frontend/error_tracking/utils_spec.js b/spec/frontend/error_tracking/utils_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..0e9047cd375e6f1945ca91c6c1118a39bc14759c
--- /dev/null
+++ b/spec/frontend/error_tracking/utils_spec.js
@@ -0,0 +1,27 @@
+import * as errorTrackingUtils from '~/error_tracking/utils';
+
+const externalUrl = 'https://sentry.io/organizations/test-sentry-nk/issues/1/?project=1';
+
+describe('Error Tracking Events', () => {
+  describe('trackViewInSentryOptions', () => {
+    it('should return correct event options', () => {
+      expect(errorTrackingUtils.trackViewInSentryOptions(externalUrl)).toEqual({
+        category: 'Error Tracking',
+        action: 'click_view_in_sentry',
+        label: 'External Url',
+        property: externalUrl,
+      });
+    });
+  });
+
+  describe('trackClickErrorLinkToSentryOptions', () => {
+    it('should return correct event options', () => {
+      expect(errorTrackingUtils.trackClickErrorLinkToSentryOptions(externalUrl)).toEqual({
+        category: 'Error Tracking',
+        action: 'click_error_link_to_sentry',
+        label: 'Error Link',
+        property: externalUrl,
+      });
+    });
+  });
+});
diff --git a/spec/frontend/fixtures/abuse_reports.rb b/spec/frontend/fixtures/abuse_reports.rb
index 21356390cae25daa52971a679b1c6a78174b9f71..712ed2e8d7ed84aa656087b6e38a5c24e2f67af9 100644
--- a/spec/frontend/fixtures/abuse_reports.rb
+++ b/spec/frontend/fixtures/abuse_reports.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Admin::AbuseReportsController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/admin_users.rb b/spec/frontend/fixtures/admin_users.rb
index 0209594dadca93942ee502df2863c263ad9ccaf5..b0f7d69f09179e23eda31e41f2d544bd9f5646e6 100644
--- a/spec/frontend/fixtures/admin_users.rb
+++ b/spec/frontend/fixtures/admin_users.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Admin::UsersController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/application_settings.rb b/spec/frontend/fixtures/application_settings.rb
index afe5949ed3ba7db8f044e1c8cab689dce6e741f6..a16888d8f03610d3bff108bc4f6bd5c3d6f520bb 100644
--- a/spec/frontend/fixtures/application_settings.rb
+++ b/spec/frontend/fixtures/application_settings.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Admin::ApplicationSettingsController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/blob.rb b/spec/frontend/fixtures/blob.rb
index ce5030efbf81d657c03190283259a4402424ade0..28a3badaa17ef26c9540115c2944e78cd4fca01e 100644
--- a/spec/frontend/fixtures/blob.rb
+++ b/spec/frontend/fixtures/blob.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/boards.rb b/spec/frontend/fixtures/boards.rb
index f257d80390f4f648fa09e0442140d9d97f7f8e53..b3c7865a0881dfd6b3d2d94666f6316e21002e6c 100644
--- a/spec/frontend/fixtures/boards.rb
+++ b/spec/frontend/fixtures/boards.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Projects::BoardsController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/branches.rb b/spec/frontend/fixtures/branches.rb
index 197fe42c52a30031b6704345425e8af9cb2ef181..2dc8cde625aa2bfef7b28e005b9e6c2a1b8a7858 100644
--- a/spec/frontend/fixtures/branches.rb
+++ b/spec/frontend/fixtures/branches.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Projects::BranchesController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/clusters.rb b/spec/frontend/fixtures/clusters.rb
index f15ef010807a8aeafafcbfd6da1d2ee863cfa004..fd64d3c0e28a0890189016b72050176cbf745344 100644
--- a/spec/frontend/fixtures/clusters.rb
+++ b/spec/frontend/fixtures/clusters.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Projects::ClustersController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/commit.rb b/spec/frontend/fixtures/commit.rb
index a328c455356bbdaf2b7592c325eb8e5359de4f15..2c4bf6fbd3d599e7ec8b2b98973ab4e055dfabb1 100644
--- a/spec/frontend/fixtures/commit.rb
+++ b/spec/frontend/fixtures/commit.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Projects::CommitController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/deploy_keys.rb b/spec/frontend/fixtures/deploy_keys.rb
index fca233c6f5950b81245bea10624edf279cfea5b0..f491c424bcfb9a5d1a00773bf9efb1165dc64150 100644
--- a/spec/frontend/fixtures/deploy_keys.rb
+++ b/spec/frontend/fixtures/deploy_keys.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/groups.rb b/spec/frontend/fixtures/groups.rb
index c1bb2d433322c5fa7c3733438464dacdf1d3bff3..237fc7115944a4432efd7fad3b0cadb50f6587de 100644
--- a/spec/frontend/fixtures/groups.rb
+++ b/spec/frontend/fixtures/groups.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe 'Groups (JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/issues.rb b/spec/frontend/fixtures/issues.rb
index b5eb38e00236631f069a64edb61efd6bc46df830..7e52499086337b60a3a527d634b156d82a2d1e1f 100644
--- a/spec/frontend/fixtures/issues.rb
+++ b/spec/frontend/fixtures/issues.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/jobs.rb b/spec/frontend/fixtures/jobs.rb
index a3a7759c85b19490b79ce39e7d9ffc660a0f8002..787ab517f75dbfe66c5de7aba536af91ba91d88d 100644
--- a/spec/frontend/fixtures/jobs.rb
+++ b/spec/frontend/fixtures/jobs.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/labels.rb b/spec/frontend/fixtures/labels.rb
index a312287970fdd5b64dfca1a856bd6176c15bac69..e4d66dbcd0a7ff16f7c6622c45d22157bc4b61e4 100644
--- a/spec/frontend/fixtures/labels.rb
+++ b/spec/frontend/fixtures/labels.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe 'Labels (JavaScript fixtures)' do
diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb
index 88706e9667604ae5a82a43238987ac3c5131a2b2..8fbdb534b3d91e7c6ed5924c83bb9fd086c00ebe 100644
--- a/spec/frontend/fixtures/merge_requests.rb
+++ b/spec/frontend/fixtures/merge_requests.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/merge_requests_diffs.rb b/spec/frontend/fixtures/merge_requests_diffs.rb
index b633a0495a618c923b026d54557062fb3e9b091f..9493cba03bbc199f1303843914754b97893ac882 100644
--- a/spec/frontend/fixtures/merge_requests_diffs.rb
+++ b/spec/frontend/fixtures/merge_requests_diffs.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/pipeline_schedules.rb b/spec/frontend/fixtures/pipeline_schedules.rb
index a70091a3919022a0de7e88a184f63c932bed5b8b..e00a35d53622f9155b84bf59011bf3199c9412ae 100644
--- a/spec/frontend/fixtures/pipeline_schedules.rb
+++ b/spec/frontend/fixtures/pipeline_schedules.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Projects::PipelineSchedulesController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/pipelines.rb b/spec/frontend/fixtures/pipelines.rb
index ed57eb0aa80b755dcf1e787c7ce673140bec277d..83fc13af7d34ebe61b640071d7121e7a2ad15bc8 100644
--- a/spec/frontend/fixtures/pipelines.rb
+++ b/spec/frontend/fixtures/pipelines.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Projects::PipelinesController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/projects.rb b/spec/frontend/fixtures/projects.rb
index 91e3b65215a3d07e44a5215ffcd3e4f46b239b34..af5b70fbbeb79fb16b583ff72af3d255548dccab 100644
--- a/spec/frontend/fixtures/projects.rb
+++ b/spec/frontend/fixtures/projects.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe 'Projects (JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/prometheus_service.rb b/spec/frontend/fixtures/prometheus_service.rb
index 93ee81120d7c181422f2a99c8e038268026ac1dc..c404b8260d2a99a7de7058959d5d88887e24b54a 100644
--- a/spec/frontend/fixtures/prometheus_service.rb
+++ b/spec/frontend/fixtures/prometheus_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Projects::ServicesController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/raw.rb b/spec/frontend/fixtures/raw.rb
index 801c80a011236df0cc7b68c90456ea6a072d761f..9c9fa4ec40b00410ab7854437c6a6515bc790cd2 100644
--- a/spec/frontend/fixtures/raw.rb
+++ b/spec/frontend/fixtures/raw.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe 'Raw files', '(JavaScript fixtures)' do
diff --git a/spec/frontend/fixtures/search.rb b/spec/frontend/fixtures/search.rb
index c26c6998ae9b0d5d5b0272f42ec64ebd6162d7b4..025cc53c7450fc9aed2ef93637dc039a6b848cdb 100644
--- a/spec/frontend/fixtures/search.rb
+++ b/spec/frontend/fixtures/search.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe SearchController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/services.rb b/spec/frontend/fixtures/services.rb
index ee1e088f158bbbb56f147805535830fb482aa3b2..1b81a83ca49b07778e9dd07e60bd6ebbe3fdd18c 100644
--- a/spec/frontend/fixtures/services.rb
+++ b/spec/frontend/fixtures/services.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Projects::ServicesController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/sessions.rb b/spec/frontend/fixtures/sessions.rb
index 18574ea06b50a97de9bbdf22009fc85b0115fa44..a4dc0aef79ce1bcac492e9848eb8ca61c661ef43 100644
--- a/spec/frontend/fixtures/sessions.rb
+++ b/spec/frontend/fixtures/sessions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe 'Sessions (JavaScript fixtures)' do
diff --git a/spec/frontend/fixtures/snippet.rb b/spec/frontend/fixtures/snippet.rb
index 23bcdb47ac6b720341c3d60d200a0b188cf20121..34a6fade9c9373844afe785febad6cd6814513c9 100644
--- a/spec/frontend/fixtures/snippet.rb
+++ b/spec/frontend/fixtures/snippet.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe SnippetsController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/todos.rb b/spec/frontend/fixtures/todos.rb
index a7c183d24140ad98375ce380cc0c9df636898232..e5bdb4998eda8229258b966ee4398ea11ed68679 100644
--- a/spec/frontend/fixtures/todos.rb
+++ b/spec/frontend/fixtures/todos.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe 'Todos (JavaScript fixtures)' do
diff --git a/spec/frontend/fixtures/u2f.rb b/spec/frontend/fixtures/u2f.rb
index 8ecbc0390cd69fddeade570a4fa98e08f28c100a..dded6ce63804c62bf2d5dbb167450d8cc9b4dac7 100644
--- a/spec/frontend/fixtures/u2f.rb
+++ b/spec/frontend/fixtures/u2f.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 context 'U2F' do
diff --git a/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap b/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..5d6c31f01d9e5322e8d56bb6dbcd7b917ee00ff9
--- /dev/null
+++ b/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
@@ -0,0 +1,61 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`IDE pipeline stage renders stage details & icon 1`] = `
+<div
+  class="ide-stage card prepend-top-default"
+>
+  <div
+    class="card-header"
+  >
+    <ciicon-stub
+      cssclasses=""
+      size="24"
+      status="[object Object]"
+    />
+     
+    <strong
+      class="prepend-left-8 ide-stage-title"
+      data-container="body"
+      data-original-title=""
+      title=""
+    >
+      
+      build
+    
+    </strong>
+     
+    <div
+      class="append-right-8 prepend-left-4"
+    >
+      <span
+        class="badge badge-pill"
+      >
+         4 
+      </span>
+    </div>
+     
+    <icon-stub
+      class="ide-stage-collapse-icon"
+      name="angle-down"
+      size="16"
+    />
+  </div>
+   
+  <div
+    class="card-body"
+  >
+    <item-stub
+      job="[object Object]"
+    />
+    <item-stub
+      job="[object Object]"
+    />
+    <item-stub
+      job="[object Object]"
+    />
+    <item-stub
+      job="[object Object]"
+    />
+  </div>
+</div>
+`;
diff --git a/spec/frontend/ide/components/jobs/stage_spec.js b/spec/frontend/ide/components/jobs/stage_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..2e42ab26d27630d249a796d381ba2565ef89c6e8
--- /dev/null
+++ b/spec/frontend/ide/components/jobs/stage_spec.js
@@ -0,0 +1,86 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import Stage from '~/ide/components/jobs/stage.vue';
+import Item from '~/ide/components/jobs/item.vue';
+import { stages, jobs } from '../../mock_data';
+
+describe('IDE pipeline stage', () => {
+  let wrapper;
+  const defaultProps = {
+    stage: {
+      ...stages[0],
+      id: 0,
+      dropdownPath: stages[0].dropdown_path,
+      jobs: [...jobs],
+      isLoading: false,
+      isCollapsed: false,
+    },
+  };
+
+  const findHeader = () => wrapper.find({ ref: 'cardHeader' });
+  const findJobList = () => wrapper.find({ ref: 'jobList' });
+
+  const createComponent = props => {
+    wrapper = shallowMount(Stage, {
+      propsData: {
+        ...defaultProps,
+        ...props,
+      },
+      sync: false,
+    });
+  };
+
+  afterEach(() => {
+    wrapper.destroy();
+    wrapper = null;
+  });
+
+  it('emits fetch event when mounted', () => {
+    createComponent();
+    expect(wrapper.emitted().fetch).toBeDefined();
+  });
+
+  it('renders loading icon when no jobs and isLoading is true', () => {
+    createComponent({
+      stage: { ...defaultProps.stage, isLoading: true, jobs: [] },
+    });
+
+    expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+  });
+
+  it('emits toggleCollaped event with stage id when clicking header', () => {
+    const id = 5;
+    createComponent({ stage: { ...defaultProps.stage, id } });
+    findHeader().trigger('click');
+    expect(wrapper.emitted().toggleCollapsed[0][0]).toBe(id);
+  });
+
+  it('emits clickViewLog entity with job', () => {
+    const [job] = defaultProps.stage.jobs;
+    createComponent();
+    wrapper
+      .findAll(Item)
+      .at(0)
+      .vm.$emit('clickViewLog', job);
+    expect(wrapper.emitted().clickViewLog[0][0]).toBe(job);
+  });
+
+  it('renders stage details & icon', () => {
+    createComponent();
+    expect(wrapper.element).toMatchSnapshot();
+  });
+
+  describe('when collapsed', () => {
+    beforeEach(() => {
+      createComponent({ stage: { ...defaultProps.stage, isCollapsed: true } });
+    });
+
+    it('does not render job list', () => {
+      expect(findJobList().isVisible()).toBe(false);
+    });
+
+    it('sets border bottom class', () => {
+      expect(findHeader().classes('border-bottom-0')).toBe(true);
+    });
+  });
+});
diff --git a/spec/frontend/ide/stores/integration_spec.js b/spec/frontend/ide/stores/integration_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..443de18f2888d41022b57ecd0922b91d5f48ba36
--- /dev/null
+++ b/spec/frontend/ide/stores/integration_spec.js
@@ -0,0 +1,100 @@
+import { decorateFiles } from '~/ide/lib/files';
+import { createStore } from '~/ide/stores';
+
+const TEST_BRANCH = 'test_branch';
+const TEST_NAMESPACE = 'test_namespace';
+const TEST_PROJECT_ID = `${TEST_NAMESPACE}/test_project`;
+const TEST_PATH_DIR = 'src';
+const TEST_PATH = `${TEST_PATH_DIR}/foo.js`;
+const TEST_CONTENT = `Lorem ipsum dolar sit
+Lorem ipsum dolar
+Lorem ipsum
+Lorem
+`;
+
+jest.mock('~/ide/ide_router');
+
+describe('IDE store integration', () => {
+  let store;
+
+  beforeEach(() => {
+    store = createStore();
+    store.replaceState({
+      ...store.state,
+      projects: {
+        [TEST_PROJECT_ID]: {
+          web_url: 'test_web_url',
+          branches: [],
+        },
+      },
+      currentProjectId: TEST_PROJECT_ID,
+      currentBranchId: TEST_BRANCH,
+    });
+  });
+
+  describe('with project and files', () => {
+    beforeEach(() => {
+      const { entries, treeList } = decorateFiles({
+        data: [`${TEST_PATH_DIR}/`, TEST_PATH, 'README.md'],
+        projectId: TEST_PROJECT_ID,
+        branchId: TEST_BRANCH,
+      });
+
+      Object.assign(entries[TEST_PATH], {
+        raw: TEST_CONTENT,
+      });
+
+      store.replaceState({
+        ...store.state,
+        trees: {
+          [`${TEST_PROJECT_ID}/${TEST_BRANCH}`]: {
+            tree: treeList,
+          },
+        },
+        entries,
+      });
+    });
+
+    describe('when a file is deleted and readded', () => {
+      beforeEach(() => {
+        store.dispatch('deleteEntry', TEST_PATH);
+        store.dispatch('createTempEntry', { name: TEST_PATH, type: 'blob' });
+      });
+
+      it('has changed and staged', () => {
+        expect(store.state.changedFiles).toEqual([
+          expect.objectContaining({
+            path: TEST_PATH,
+            tempFile: true,
+            deleted: false,
+          }),
+        ]);
+
+        expect(store.state.stagedFiles).toEqual([
+          expect.objectContaining({
+            path: TEST_PATH,
+            deleted: true,
+          }),
+        ]);
+      });
+
+      it('cleans up after commit', () => {
+        const expected = expect.objectContaining({
+          path: TEST_PATH,
+          staged: false,
+          changed: false,
+          tempFile: false,
+          deleted: false,
+        });
+        store.dispatch('stageChange', TEST_PATH);
+
+        store.dispatch('commit/updateFilesAfterCommit', { data: {} });
+
+        expect(store.state.entries[TEST_PATH]).toEqual(expected);
+        expect(store.state.entries[TEST_PATH_DIR].tree.find(x => x.path === TEST_PATH)).toEqual(
+          expected,
+        );
+      });
+    });
+  });
+});
diff --git a/spec/frontend/jobs/store/mutations_spec.js b/spec/frontend/jobs/store/mutations_spec.js
index 6576f3d1ff299ce3f81a34e58d3157e3cbe7217b..d1ab152330e23643960d40f9ca6445aa526bc315 100644
--- a/spec/frontend/jobs/store/mutations_spec.js
+++ b/spec/frontend/jobs/store/mutations_spec.js
@@ -80,6 +80,81 @@ describe('Jobs Store Mutations', () => {
       expect(stateCopy.traceSize).toEqual(511846);
       expect(stateCopy.isTraceComplete).toEqual(true);
     });
+
+    describe('with new job log', () => {
+      let stateWithNewLog;
+      beforeEach(() => {
+        gon.features = gon.features || {};
+        gon.features.jobLogJson = true;
+
+        stateWithNewLog = state();
+      });
+
+      afterEach(() => {
+        gon.features.jobLogJson = false;
+      });
+
+      describe('log.lines', () => {
+        describe('when append is true', () => {
+          it('sets the parsed log ', () => {
+            mutations[types.RECEIVE_TRACE_SUCCESS](stateWithNewLog, {
+              append: true,
+              size: 511846,
+              complete: true,
+              lines: [
+                {
+                  offset: 1,
+                  content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }],
+                },
+              ],
+            });
+
+            expect(stateWithNewLog.trace).toEqual([
+              {
+                offset: 1,
+                content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }],
+                lineNumber: 0,
+              },
+            ]);
+          });
+        });
+
+        describe('when it is defined', () => {
+          it('sets the parsed log ', () => {
+            mutations[types.RECEIVE_TRACE_SUCCESS](stateWithNewLog, {
+              append: false,
+              size: 511846,
+              complete: true,
+              lines: [
+                { offset: 0, content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }] },
+              ],
+            });
+
+            expect(stateWithNewLog.trace).toEqual([
+              {
+                offset: 0,
+                content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }],
+                lineNumber: 0,
+              },
+            ]);
+          });
+        });
+
+        describe('when it is null', () => {
+          it('sets the default value', () => {
+            mutations[types.RECEIVE_TRACE_SUCCESS](stateWithNewLog, {
+              append: true,
+              html,
+              size: 511846,
+              complete: false,
+              lines: null,
+            });
+
+            expect(stateWithNewLog.trace).toEqual([]);
+          });
+        });
+      });
+    });
   });
 
   describe('STOP_POLLING_TRACE', () => {
diff --git a/spec/frontend/jobs/store/utils_spec.js b/spec/frontend/jobs/store/utils_spec.js
index 9890e01460e473fdc77eb85483c3e62c7fc938a9..43dacfe622cf2a695e1991b767bdce540dc4dfc2 100644
--- a/spec/frontend/jobs/store/utils_spec.js
+++ b/spec/frontend/jobs/store/utils_spec.js
@@ -291,6 +291,13 @@ describe('Jobs Store Utils', () => {
         });
       });
     });
+
+    describe('when no data is provided', () => {
+      it('returns an empty array', () => {
+        const result = findOffsetAndRemove();
+        expect(result).toEqual([]);
+      });
+    });
   });
 
   describe('getIncrementalLineNumber', () => {
diff --git a/spec/frontend/lib/utils/set_spec.js b/spec/frontend/lib/utils/set_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..7636a1c634cfd33fde050cf5ee36919f00e3c016
--- /dev/null
+++ b/spec/frontend/lib/utils/set_spec.js
@@ -0,0 +1,19 @@
+import { isSubset } from '~/lib/utils/set';
+
+describe('utils/set', () => {
+  describe('isSubset', () => {
+    it.each`
+      subset                   | superset              | expected
+      ${new Set()}             | ${new Set()}          | ${true}
+      ${new Set()}             | ${new Set([1])}       | ${true}
+      ${new Set([1])}          | ${new Set([1])}       | ${true}
+      ${new Set([1, 3])}       | ${new Set([1, 2, 3])} | ${true}
+      ${new Set([1])}          | ${new Set()}          | ${false}
+      ${new Set([1])}          | ${new Set([2])}       | ${false}
+      ${new Set([7, 8, 9])}    | ${new Set([1, 2, 3])} | ${false}
+      ${new Set([1, 2, 3, 4])} | ${new Set([1, 2, 3])} | ${false}
+    `('isSubset($subset, $superset) === $expected', ({ subset, superset, expected }) => {
+      expect(isSubset(subset, superset)).toBe(expected);
+    });
+  });
+});
diff --git a/spec/frontend/monitoring/components/date_time_picker/date_time_picker_input_spec.js b/spec/frontend/monitoring/components/date_time_picker/date_time_picker_input_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..1315e1226a40af6b762f78319cf639f1540ba9b4
--- /dev/null
+++ b/spec/frontend/monitoring/components/date_time_picker/date_time_picker_input_spec.js
@@ -0,0 +1,66 @@
+import { mount } from '@vue/test-utils';
+import DateTimePickerInput from '~/monitoring/components/date_time_picker/date_time_picker_input.vue';
+
+const inputLabel = 'This is a label';
+const inputValue = 'something';
+
+describe('DateTimePickerInput', () => {
+  let wrapper;
+
+  const createComponent = (propsData = {}) => {
+    wrapper = mount(DateTimePickerInput, {
+      propsData: {
+        state: null,
+        value: '',
+        label: '',
+        ...propsData,
+      },
+      sync: false,
+    });
+  };
+
+  afterEach(() => {
+    wrapper.destroy();
+  });
+
+  it('renders label above the input', () => {
+    createComponent({
+      label: inputLabel,
+    });
+
+    expect(wrapper.find('.gl-form-group label').text()).toBe(inputLabel);
+  });
+
+  it('renders the same `ID` for input and `for` for label', () => {
+    createComponent({ label: inputLabel });
+
+    expect(wrapper.find('.gl-form-group label').attributes('for')).toBe(
+      wrapper.find('input').attributes('id'),
+    );
+  });
+
+  it('renders valid input in gray color instead of green', () => {
+    createComponent({
+      state: true,
+    });
+
+    expect(wrapper.find('input').classes('is-valid')).toBe(false);
+  });
+
+  it('renders invalid input in red color', () => {
+    createComponent({
+      state: false,
+    });
+
+    expect(wrapper.find('input').classes('is-invalid')).toBe(true);
+  });
+
+  it('input event is emitted when focus is lost', () => {
+    createComponent();
+    jest.spyOn(wrapper.vm, '$emit');
+    wrapper.find('input').setValue(inputValue);
+    wrapper.find('input').trigger('blur');
+
+    expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', inputValue);
+  });
+});
diff --git a/spec/frontend/monitoring/components/date_time_picker/date_time_picker_spec.js b/spec/frontend/monitoring/components/date_time_picker/date_time_picker_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..be54443567163f64bac10ff7be3d0cb68d612ea3
--- /dev/null
+++ b/spec/frontend/monitoring/components/date_time_picker/date_time_picker_spec.js
@@ -0,0 +1,157 @@
+import { mount } from '@vue/test-utils';
+import DateTimePicker from '~/monitoring/components/date_time_picker/date_time_picker.vue';
+import { timeWindows } from '~/monitoring/constants';
+
+const timeWindowsCount = Object.keys(timeWindows).length;
+const selectedTimeWindow = {
+  start: '2019-10-10T07:00:00.000Z',
+  end: '2019-10-13T07:00:00.000Z',
+};
+const selectedTimeWindowText = `3 days`;
+
+describe('DateTimePicker', () => {
+  let dateTimePicker;
+
+  const dropdownToggle = () => dateTimePicker.find('.dropdown-toggle');
+  const dropdownMenu = () => dateTimePicker.find('.dropdown-menu');
+  const applyButtonElement = () => dateTimePicker.find('button[variant="success"]').element;
+  const cancelButtonElement = () => dateTimePicker.find('button.btn-secondary').element;
+  const fillInputAndBlur = (input, val) => {
+    dateTimePicker.find(input).setValue(val);
+    dateTimePicker.find(input).trigger('blur');
+  };
+
+  const createComponent = props => {
+    dateTimePicker = mount(DateTimePicker, {
+      propsData: {
+        timeWindows,
+        selectedTimeWindow,
+        ...props,
+      },
+      sync: false,
+    });
+  };
+
+  afterEach(() => {
+    dateTimePicker.destroy();
+  });
+
+  it('renders dropdown toggle button with selected text', done => {
+    createComponent();
+    dateTimePicker.vm.$nextTick(() => {
+      expect(dropdownToggle().text()).toBe(selectedTimeWindowText);
+      done();
+    });
+  });
+
+  it('renders dropdown with 2 custom time range inputs', () => {
+    createComponent();
+    dateTimePicker.vm.$nextTick(() => {
+      expect(dateTimePicker.findAll('input').length).toBe(2);
+    });
+  });
+
+  it('renders inputs with h/m/s truncated if its all 0s', done => {
+    createComponent({
+      selectedTimeWindow: {
+        start: '2019-10-10T00:00:00.000Z',
+        end: '2019-10-14T00:10:00.000Z',
+      },
+    });
+    dateTimePicker.vm.$nextTick(() => {
+      expect(dateTimePicker.find('#custom-time-from').element.value).toBe('2019-10-10');
+      expect(dateTimePicker.find('#custom-time-to').element.value).toBe('2019-10-14 00:10:00');
+      done();
+    });
+  });
+
+  it(`renders dropdown with ${timeWindowsCount} items in quick range`, done => {
+    createComponent();
+    dropdownToggle().trigger('click');
+    dateTimePicker.vm.$nextTick(() => {
+      expect(dateTimePicker.findAll('.dropdown-item').length).toBe(timeWindowsCount);
+      done();
+    });
+  });
+
+  it(`renders dropdown with correct quick range item selected`, done => {
+    createComponent();
+    dropdownToggle().trigger('click');
+    dateTimePicker.vm.$nextTick(() => {
+      expect(dateTimePicker.find('.dropdown-item.active').text()).toBe(selectedTimeWindowText);
+
+      expect(dateTimePicker.find('.dropdown-item.active svg').isVisible()).toBe(true);
+      done();
+    });
+  });
+
+  it('renders a disabled apply button on load', () => {
+    createComponent();
+
+    expect(applyButtonElement().getAttribute('disabled')).toBe('disabled');
+  });
+
+  it('displays inline error message if custom time range inputs are invalid', done => {
+    createComponent();
+    fillInputAndBlur('#custom-time-from', '2019-10-01abc');
+    fillInputAndBlur('#custom-time-to', '2019-10-10abc');
+
+    dateTimePicker.vm.$nextTick(() => {
+      expect(dateTimePicker.findAll('.invalid-feedback').length).toBe(2);
+      done();
+    });
+  });
+
+  it('keeps apply button disabled with invalid custom time range inputs', done => {
+    createComponent();
+    fillInputAndBlur('#custom-time-from', '2019-10-01abc');
+    fillInputAndBlur('#custom-time-to', '2019-09-19');
+
+    dateTimePicker.vm.$nextTick(() => {
+      expect(applyButtonElement().getAttribute('disabled')).toBe('disabled');
+      done();
+    });
+  });
+
+  it('enables apply button with valid custom time range inputs', done => {
+    createComponent();
+    fillInputAndBlur('#custom-time-from', '2019-10-01');
+    fillInputAndBlur('#custom-time-to', '2019-10-19');
+
+    dateTimePicker.vm.$nextTick(() => {
+      expect(applyButtonElement().getAttribute('disabled')).toBeNull();
+      done();
+    });
+  });
+
+  it('returns an object when apply is clicked', done => {
+    createComponent();
+    fillInputAndBlur('#custom-time-from', '2019-10-01');
+    fillInputAndBlur('#custom-time-to', '2019-10-19');
+
+    dateTimePicker.vm.$nextTick(() => {
+      jest.spyOn(dateTimePicker.vm, '$emit');
+      applyButtonElement().click();
+
+      expect(dateTimePicker.vm.$emit).toHaveBeenCalledWith('onApply', {
+        end: '2019-10-19T00:00:00Z',
+        start: '2019-10-01T00:00:00Z',
+      });
+      done();
+    });
+  });
+
+  it('hides the popover with cancel button', done => {
+    createComponent();
+    dropdownToggle().trigger('click');
+
+    dateTimePicker.vm.$nextTick(() => {
+      cancelButtonElement().click();
+
+      dateTimePicker.vm.$nextTick(() => {
+        expect(dropdownMenu().classes('show')).toBe(false);
+        done();
+      });
+    });
+  });
+});
diff --git a/spec/frontend/monitoring/embed/embed_spec.js b/spec/frontend/monitoring/embed/embed_spec.js
index 1ce14e2418aa610918320b51c79f61998ef7f879..5de1a7c4c3b93c94b8393d837a6a03b4e4956f84 100644
--- a/spec/frontend/monitoring/embed/embed_spec.js
+++ b/spec/frontend/monitoring/embed/embed_spec.js
@@ -74,5 +74,9 @@ describe('Embed', () => {
       expect(wrapper.find(MonitorTimeSeriesChart).exists()).toBe(true);
       expect(wrapper.findAll(MonitorTimeSeriesChart).length).toBe(2);
     });
+
+    it('includes groupId with dashboardUrl', () => {
+      expect(wrapper.find(MonitorTimeSeriesChart).props('groupId')).toBe(TEST_HOST);
+    });
   });
 });
diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..1e8d5753885b770206c0d801962dd39a48cf3624
--- /dev/null
+++ b/spec/frontend/monitoring/utils_spec.js
@@ -0,0 +1,54 @@
+import * as monitoringUtils from '~/monitoring/utils';
+
+describe('Snowplow Events', () => {
+  const generatedLink = 'http://chart.link.com';
+  const chartTitle = 'Some metric chart';
+
+  describe('trackGenerateLinkToChartEventOptions', () => {
+    it('should return Cluster Monitoring options if located on Cluster Health Dashboard', () => {
+      document.body.dataset.page = 'groups:clusters:show';
+
+      expect(monitoringUtils.generateLinkToChartOptions(generatedLink)).toEqual({
+        category: 'Cluster Monitoring',
+        action: 'generate_link_to_cluster_metric_chart',
+        label: 'Chart link',
+        property: generatedLink,
+      });
+    });
+
+    it('should return Incident Management event options if located on Metrics Dashboard', () => {
+      document.body.dataset.page = 'metrics:show';
+
+      expect(monitoringUtils.generateLinkToChartOptions(generatedLink)).toEqual({
+        category: 'Incident Management::Embedded metrics',
+        action: 'generate_link_to_metrics_chart',
+        label: 'Chart link',
+        property: generatedLink,
+      });
+    });
+  });
+
+  describe('trackDownloadCSVEvent', () => {
+    it('should return Cluster Monitoring options if located on Cluster Health Dashboard', () => {
+      document.body.dataset.page = 'groups:clusters:show';
+
+      expect(monitoringUtils.downloadCSVOptions(chartTitle)).toEqual({
+        category: 'Cluster Monitoring',
+        action: 'download_csv_of_cluster_metric_chart',
+        label: 'Chart title',
+        property: chartTitle,
+      });
+    });
+
+    it('should return Incident Management event options if located on Metrics Dashboard', () => {
+      document.body.dataset.page = 'metriss:show';
+
+      expect(monitoringUtils.downloadCSVOptions(chartTitle)).toEqual({
+        category: 'Incident Management::Embedded metrics',
+        action: 'download_csv_of_metrics_dashboard_chart',
+        label: 'Chart title',
+        property: chartTitle,
+      });
+    });
+  });
+});
diff --git a/spec/frontend/registry/components/__snapshots__/group_empty_state_spec.js.snap b/spec/frontend/registry/components/__snapshots__/group_empty_state_spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..3f13b7d4d763fd9bbc3e57f57dead5f401000c92
--- /dev/null
+++ b/spec/frontend/registry/components/__snapshots__/group_empty_state_spec.js.snap
@@ -0,0 +1,61 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Registry Group Empty state to match the default snapshot 1`] = `
+<div
+  class="row container-message empty-state"
+>
+  <div
+    class="col-12"
+  >
+    <div
+      class="svg-250 svg-content"
+    >
+      <img
+        alt="There are no container images available in this group"
+        class=""
+        src="imageUrl"
+      />
+    </div>
+  </div>
+   
+  <div
+    class="col-12"
+  >
+    <div
+      class="text-content"
+    >
+      <h4
+        class="center"
+        style=""
+      >
+        There are no container images available in this group
+      </h4>
+       
+      <p
+        class="center"
+        style=""
+      >
+        <p
+          class="js-no-container-images-text"
+        >
+          With the Container Registry, every project can have its own space to store its Docker images. Push at least one Docker image in one of this group's projects in order to show up here. 
+          <a
+            href="help"
+            target="_blank"
+          >
+            More Information
+          </a>
+        </p>
+      </p>
+       
+      <div
+        class="text-center"
+      >
+        <!---->
+         
+        <!---->
+      </div>
+    </div>
+  </div>
+</div>
+`;
diff --git a/spec/frontend/registry/components/__snapshots__/project_empty_state_spec.js.snap b/spec/frontend/registry/components/__snapshots__/project_empty_state_spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..3084462f5aea007e501bf4f7db8c130fc5811a5c
--- /dev/null
+++ b/spec/frontend/registry/components/__snapshots__/project_empty_state_spec.js.snap
@@ -0,0 +1,186 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Registry Project Empty state to match the default snapshot 1`] = `
+<div
+  class="row container-message empty-state"
+>
+  <div
+    class="col-12"
+  >
+    <div
+      class="svg-250 svg-content"
+    >
+      <img
+        alt="There are no container images stored for this project"
+        class=""
+        src="imageUrl"
+      />
+    </div>
+  </div>
+   
+  <div
+    class="col-12"
+  >
+    <div
+      class="text-content"
+    >
+      <h4
+        class="center"
+        style=""
+      >
+        There are no container images stored for this project
+      </h4>
+       
+      <p
+        class="center"
+        style=""
+      >
+        <p
+          class="js-no-container-images-text"
+        >
+          With the Container Registry, every project can have its own space to store its Docker images. 
+          <a
+            href="help"
+            target="_blank"
+          >
+            More Information
+          </a>
+        </p>
+         
+        <h5>
+          Quick Start
+        </h5>
+         
+        <p
+          class="js-not-logged-in-to-registry-text"
+        >
+          If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have 
+          <a
+            href="help_link"
+            target="_blank"
+          >
+            Two-Factor Authentication
+          </a>
+           enabled, use a 
+          <a
+            href="personal_token"
+            target="_blank"
+          >
+            Personal Access Token
+          </a>
+           instead of a password.
+        </p>
+         
+        <div
+          class="input-group append-bottom-10"
+        >
+          <input
+            class="form-control monospace"
+            readonly="readonly"
+            type="text"
+          />
+           
+          <span
+            class="input-group-append"
+          >
+            <button
+              class="btn input-group-text btn-secondary btn-default"
+              data-clipboard-text="docker login host"
+              data-original-title="Copy login command"
+              title=""
+              type="button"
+            >
+              <svg
+                aria-hidden="true"
+                class="s16 ic-duplicate"
+              >
+                <use
+                  xlink:href="#duplicate"
+                />
+              </svg>
+            </button>
+          </span>
+        </div>
+         
+        <p />
+         
+        <p>
+          
+      You can add an image to this registry with the following commands:
+    
+        </p>
+         
+        <div
+          class="input-group append-bottom-10"
+        >
+          <input
+            class="form-control monospace"
+            readonly="readonly"
+            type="text"
+          />
+           
+          <span
+            class="input-group-append"
+          >
+            <button
+              class="btn input-group-text btn-secondary btn-default"
+              data-clipboard-text="docker build -t url ."
+              data-original-title="Copy build command"
+              title=""
+              type="button"
+            >
+              <svg
+                aria-hidden="true"
+                class="s16 ic-duplicate"
+              >
+                <use
+                  xlink:href="#duplicate"
+                />
+              </svg>
+            </button>
+          </span>
+        </div>
+         
+        <div
+          class="input-group"
+        >
+          <input
+            class="form-control monospace"
+            readonly="readonly"
+            type="text"
+          />
+           
+          <span
+            class="input-group-append"
+          >
+            <button
+              class="btn input-group-text btn-secondary btn-default"
+              data-clipboard-text="docker push url"
+              data-original-title="Copy push command"
+              title=""
+              type="button"
+            >
+              <svg
+                aria-hidden="true"
+                class="s16 ic-duplicate"
+              >
+                <use
+                  xlink:href="#duplicate"
+                />
+              </svg>
+            </button>
+          </span>
+        </div>
+      </p>
+       
+      <div
+        class="text-center"
+      >
+        <!---->
+         
+        <!---->
+      </div>
+    </div>
+  </div>
+</div>
+`;
diff --git a/spec/frontend/registry/components/app_spec.js b/spec/frontend/registry/components/app_spec.js
index 190af5c11cd9d4bf31fa8b3c4dfd9fc53e1a05bd..a69c33c246dcb39c7506f6a6a4ed6eeeaf27b1f6 100644
--- a/spec/frontend/registry/components/app_spec.js
+++ b/spec/frontend/registry/components/app_spec.js
@@ -1,5 +1,6 @@
-import registry from '~/registry/components/app.vue';
+import Vue from 'vue';
 import { mount } from '@vue/test-utils';
+import registry from '~/registry/components/app.vue';
 import { TEST_HOST } from '../../helpers/test_constants';
 import { reposServerResponse, parsedReposServerResponse } from '../mock_data';
 
@@ -7,7 +8,8 @@ describe('Registry List', () => {
   let wrapper;
 
   const findCollapsibleContainer = w => w.findAll({ name: 'CollapsibeContainerRegisty' });
-  const findNoContainerImagesText = w => w.find('.js-no-container-images-text');
+  const findProjectEmptyState = w => w.find({ name: 'ProjectEmptyState' });
+  const findGroupEmptyState = w => w.find({ name: 'GroupEmptyState' });
   const findSpinner = w => w.find('.gl-spinner');
   const findCharacterErrorText = w => w.find('.js-character-error-text');
 
@@ -17,17 +19,25 @@ describe('Registry List', () => {
     noContainersImage: 'foo',
     containersErrorImage: 'foo',
     repositoryUrl: 'foo',
+    registryHostUrlWithPort: 'foo',
+    personalAccessTokensHelpLink: 'foo',
+    twoFactorAuthHelpLink: 'foo',
   };
 
   const setMainEndpoint = jest.fn();
   const fetchRepos = jest.fn();
+  const setIsDeleteDisabled = jest.fn();
 
   const methods = {
     setMainEndpoint,
     fetchRepos,
+    setIsDeleteDisabled,
   };
 
   beforeEach(() => {
+    // This is needed due to console.error called by vue to emit a warning that stop the tests.
+    // See https://github.com/vuejs/vue-test-utils/issues/532.
+    Vue.config.silent = true;
     wrapper = mount(registry, {
       propsData,
       computed: {
@@ -39,6 +49,12 @@ describe('Registry List', () => {
     });
   });
 
+  afterEach(() => {
+    jest.clearAllMocks();
+    Vue.config.silent = false;
+    wrapper.destroy();
+  });
+
   describe('with data', () => {
     it('should render a list of CollapsibeContainerRegisty', () => {
       const containers = findCollapsibleContainer(wrapper);
@@ -61,11 +77,9 @@ describe('Registry List', () => {
       });
     });
 
-    it('should render empty message', () => {
-      const noContainerImagesText = findNoContainerImagesText(localWrapper);
-      expect(noContainerImagesText.text()).toEqual(
-        'With the Container Registry, every project can have its own space to store its Docker images. More Information',
-      );
+    it('should render project empty message', () => {
+      const projectEmptyState = findProjectEmptyState(localWrapper);
+      expect(projectEmptyState.exists()).toBe(true);
     });
   });
 
@@ -118,4 +132,29 @@ describe('Registry List', () => {
       );
     });
   });
+
+  describe('with groupId set', () => {
+    const isGroupPage = true;
+
+    beforeEach(() => {
+      wrapper = mount(registry, {
+        propsData: {
+          ...propsData,
+          endpoint: null,
+          isGroupPage,
+        },
+        methods,
+      });
+    });
+
+    it('call the right vuex setters', () => {
+      expect(methods.setMainEndpoint).toHaveBeenLastCalledWith(null);
+      expect(methods.setIsDeleteDisabled).toHaveBeenLastCalledWith(true);
+    });
+
+    it('should render groups empty message', () => {
+      const groupEmptyState = findGroupEmptyState(wrapper);
+      expect(groupEmptyState.exists()).toBe(true);
+    });
+  });
 });
diff --git a/spec/frontend/registry/components/collapsible_container_spec.js b/spec/frontend/registry/components/collapsible_container_spec.js
index 0fe4338f1bae11b9d76232d169709b00aea601ba..f93ebab1a4d02ce2c47d11f9c30173ae97f1a4a4 100644
--- a/spec/frontend/registry/components/collapsible_container_spec.js
+++ b/spec/frontend/registry/components/collapsible_container_spec.js
@@ -1,24 +1,40 @@
 import Vue from 'vue';
-import { mount } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { mount, createLocalVue } from '@vue/test-utils';
 import collapsibleComponent from '~/registry/components/collapsible_container.vue';
 import { repoPropsData } from '../mock_data';
 import createFlash from '~/flash';
+import * as getters from '~/registry/stores/getters';
 
 jest.mock('~/flash.js');
 
+const localVue = createLocalVue();
+
+localVue.use(Vuex);
+
 describe('collapsible registry container', () => {
   let wrapper;
+  let store;
 
   const findDeleteBtn = w => w.find('.js-remove-repo');
   const findContainerImageTags = w => w.find('.container-image-tags');
   const findToggleRepos = w => w.findAll('.js-toggle-repo');
 
+  const mountWithStore = config => mount(collapsibleComponent, { ...config, store, localVue });
+
   beforeEach(() => {
     createFlash.mockClear();
     // This is needed due to  console.error called by vue to emit a warning that stop the tests
     // see  https://github.com/vuejs/vue-test-utils/issues/532
     Vue.config.silent = true;
-    wrapper = mount(collapsibleComponent, {
+    store = new Vuex.Store({
+      state: {
+        isDeleteDisabled: false,
+      },
+      getters,
+    });
+
+    wrapper = mountWithStore({
       propsData: {
         repo: repoPropsData,
       },
@@ -27,6 +43,7 @@ describe('collapsible registry container', () => {
 
   afterEach(() => {
     Vue.config.silent = false;
+    wrapper.destroy();
   });
 
   describe('toggle', () => {
@@ -86,4 +103,25 @@ describe('collapsible registry container', () => {
       });
     });
   });
+
+  describe('disabled delete', () => {
+    beforeEach(() => {
+      store = new Vuex.Store({
+        state: {
+          isDeleteDisabled: true,
+        },
+        getters,
+      });
+      wrapper = mountWithStore({
+        propsData: {
+          repo: repoPropsData,
+        },
+      });
+    });
+
+    it('should not render delete button', () => {
+      const deleteBtn = findDeleteBtn(wrapper);
+      expect(deleteBtn.exists()).toBe(false);
+    });
+  });
 });
diff --git a/spec/frontend/registry/components/group_empty_state_spec.js b/spec/frontend/registry/components/group_empty_state_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..f71074b515416978731e96fe5b27063c225eedef
--- /dev/null
+++ b/spec/frontend/registry/components/group_empty_state_spec.js
@@ -0,0 +1,23 @@
+import { mount } from '@vue/test-utils';
+import groupEmptyState from '~/registry/components/group_empty_state.vue';
+
+describe('Registry Group Empty state', () => {
+  let wrapper;
+
+  beforeEach(() => {
+    wrapper = mount(groupEmptyState, {
+      propsData: {
+        noContainersImage: 'imageUrl',
+        helpPagePath: 'help',
+      },
+    });
+  });
+
+  afterEach(() => {
+    wrapper.destroy();
+  });
+
+  it('to match the default snapshot', () => {
+    expect(wrapper.element).toMatchSnapshot();
+  });
+});
diff --git a/spec/frontend/registry/components/project_empty_state_spec.js b/spec/frontend/registry/components/project_empty_state_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..913524db3aa974d08f42a9bfcdc5b79ff3a02c8f
--- /dev/null
+++ b/spec/frontend/registry/components/project_empty_state_spec.js
@@ -0,0 +1,27 @@
+import { mount } from '@vue/test-utils';
+import projectEmptyState from '~/registry/components/project_empty_state.vue';
+
+describe('Registry Project Empty state', () => {
+  let wrapper;
+
+  beforeEach(() => {
+    wrapper = mount(projectEmptyState, {
+      propsData: {
+        noContainersImage: 'imageUrl',
+        helpPagePath: 'help',
+        repositoryUrl: 'url',
+        twoFactorAuthHelpLink: 'help_link',
+        personalAccessTokensHelpLink: 'personal_token',
+        registryHostUrlWithPort: 'host',
+      },
+    });
+  });
+
+  afterEach(() => {
+    wrapper.destroy();
+  });
+
+  it('to match the default snapshot', () => {
+    expect(wrapper.element).toMatchSnapshot();
+  });
+});
diff --git a/spec/frontend/registry/components/table_registry_spec.js b/spec/frontend/registry/components/table_registry_spec.js
index 021f13feeba4fa5eca3156b3346ec69907ad0e2d..7cb7c012d9d26dafdaf1abbf62b40d735e2c4eec 100644
--- a/spec/frontend/registry/components/table_registry_spec.js
+++ b/spec/frontend/registry/components/table_registry_spec.js
@@ -1,12 +1,19 @@
 import Vue from 'vue';
+import Vuex from 'vuex';
 import tableRegistry from '~/registry/components/table_registry.vue';
-import { mount } from '@vue/test-utils';
+import { mount, createLocalVue } from '@vue/test-utils';
 import { repoPropsData } from '../mock_data';
+import * as getters from '~/registry/stores/getters';
 
 const [firstImage, secondImage] = repoPropsData.list;
 
+const localVue = createLocalVue();
+
+localVue.use(Vuex);
+
 describe('table registry', () => {
   let wrapper;
+  let store;
 
   const findSelectAllCheckbox = w => w.find('.js-select-all-checkbox > input');
   const findSelectCheckboxes = w => w.findAll('.js-select-checkbox > input');
@@ -15,19 +22,31 @@ describe('table registry', () => {
   const findPagination = w => w.find('.js-registry-pagination');
   const bulkDeletePath = 'path';
 
+  const mountWithStore = config => mount(tableRegistry, { ...config, store, localVue });
+
   beforeEach(() => {
     // This is needed due to  console.error called by vue to emit a warning that stop the tests
     // see  https://github.com/vuejs/vue-test-utils/issues/532
     Vue.config.silent = true;
-    wrapper = mount(tableRegistry, {
+
+    store = new Vuex.Store({
+      state: {
+        isDeleteDisabled: false,
+      },
+      getters,
+    });
+
+    wrapper = mountWithStore({
       propsData: {
         repo: repoPropsData,
+        canDeleteRepo: true,
       },
     });
   });
 
   afterEach(() => {
     Vue.config.silent = false;
+    wrapper.destroy();
   });
 
   describe('rendering', () => {
@@ -93,11 +112,13 @@ describe('table registry', () => {
 
       Vue.nextTick(() => {
         const deleteBtn = findDeleteButton(wrapper);
-        expect(wrapper.vm.itemsToBeDeleted).toEqual([0, 1]);
+        expect(wrapper.vm.selectedItems).toEqual([0, 1]);
         expect(deleteBtn.attributes('disabled')).toEqual(undefined);
+        wrapper.setData({ itemsToBeDeleted: [...wrapper.vm.selectedItems] });
         wrapper.vm.handleMultipleDelete();
 
         Vue.nextTick(() => {
+          expect(wrapper.vm.selectedItems).toEqual([]);
           expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
           expect(wrapper.vm.multiDeleteItems).toHaveBeenCalledWith({
             path: bulkDeletePath,
@@ -124,13 +145,13 @@ describe('table registry', () => {
 
   describe('delete registry', () => {
     beforeEach(() => {
-      wrapper.setData({ itemsToBeDeleted: [0] });
+      wrapper.setData({ selectedItems: [0] });
     });
 
     it('should be possible to delete a registry', () => {
       const deleteBtn = findDeleteButton(wrapper);
       const deleteBtns = findDeleteButtonsRow(wrapper);
-      expect(wrapper.vm.itemsToBeDeleted).toEqual([0]);
+      expect(wrapper.vm.selectedItems).toEqual([0]);
       expect(deleteBtn).toBeDefined();
       expect(deleteBtn.attributes('disable')).toBe(undefined);
       expect(deleteBtns.is('button')).toBe(true);
@@ -149,7 +170,6 @@ describe('table registry', () => {
   });
 
   describe('pagination', () => {
-    let localWrapper = null;
     const repo = {
       repoPropsData,
       pagination: {
@@ -160,7 +180,7 @@ describe('table registry', () => {
     };
 
     beforeEach(() => {
-      localWrapper = mount(tableRegistry, {
+      wrapper = mount(tableRegistry, {
         propsData: {
           repo,
         },
@@ -168,13 +188,13 @@ describe('table registry', () => {
     });
 
     it('should exist', () => {
-      const pagination = findPagination(localWrapper);
+      const pagination = findPagination(wrapper);
       expect(pagination.exists()).toBe(true);
     });
     it('should be visible when pagination is needed', () => {
-      const pagination = findPagination(localWrapper);
+      const pagination = findPagination(wrapper);
       expect(pagination.isVisible()).toBe(true);
-      localWrapper.setProps({
+      wrapper.setProps({
         repo: {
           pagination: {
             total: 0,
@@ -182,30 +202,67 @@ describe('table registry', () => {
           },
         },
       });
-      expect(localWrapper.vm.shouldRenderPagination).toBe(false);
+      expect(wrapper.vm.shouldRenderPagination).toBe(false);
     });
     it('should have a change function that update the list when run', () => {
       const fetchList = jest.fn().mockResolvedValue();
-      localWrapper.setMethods({ fetchList });
-      localWrapper.vm.onPageChange(1);
-      expect(localWrapper.vm.fetchList).toHaveBeenCalledWith({ repo, page: 1 });
+      wrapper.setMethods({ fetchList });
+      wrapper.vm.onPageChange(1);
+      expect(wrapper.vm.fetchList).toHaveBeenCalledWith({ repo, page: 1 });
     });
   });
 
   describe('modal content', () => {
     it('should show the singular title and image name when deleting a single image', () => {
-      wrapper.setData({ itemsToBeDeleted: [1] });
-      wrapper.vm.setModalDescription(0);
+      wrapper.setData({ selectedItems: [1, 2, 3] });
+      wrapper.vm.deleteSingleItem(0);
       expect(wrapper.vm.modalAction).toBe('Remove tag');
       expect(wrapper.vm.modalDescription).toContain(firstImage.tag);
     });
 
     it('should show the plural title and image count when deleting more than one image', () => {
-      wrapper.setData({ itemsToBeDeleted: [1, 2] });
-      wrapper.vm.setModalDescription();
+      wrapper.setData({ selectedItems: [1, 2] });
+      wrapper.vm.deleteMultipleItems();
 
       expect(wrapper.vm.modalAction).toBe('Remove tags');
       expect(wrapper.vm.modalDescription).toContain('<b>2</b> tags');
     });
   });
+
+  describe('disabled delete', () => {
+    beforeEach(() => {
+      store = new Vuex.Store({
+        state: {
+          isDeleteDisabled: true,
+        },
+        getters,
+      });
+      wrapper = mountWithStore({
+        propsData: {
+          repo: repoPropsData,
+          canDeleteRepo: false,
+        },
+      });
+    });
+
+    it('should not render select all', () => {
+      const selectAll = findSelectAllCheckbox(wrapper);
+      expect(selectAll.exists()).toBe(false);
+    });
+
+    it('should not render any select checkbox', () => {
+      const selects = findSelectCheckboxes(wrapper);
+      expect(selects.length).toBe(0);
+    });
+
+    it('should not render delete registry button', () => {
+      const deleteBtn = findDeleteButton(wrapper);
+      expect(deleteBtn.exists()).toBe(false);
+    });
+
+    it('should not render delete row button', () => {
+      const deleteBtns = findDeleteButtonsRow(wrapper);
+      expect(deleteBtns.length).toBe(0);
+    });
+  });
 });
diff --git a/spec/frontend/registry/stores/actions_spec.js b/spec/frontend/registry/stores/actions_spec.js
index bf335904d23e49f6e380b595a9e14d35d87726af..7937fa82e80a8c250222d2f560a85f5a9a56a745 100644
--- a/spec/frontend/registry/stores/actions_spec.js
+++ b/spec/frontend/registry/stores/actions_spec.js
@@ -34,7 +34,7 @@ describe('Actions Registry Store', () => {
       mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, reposServerResponse, {});
     });
 
-    it('should set receveived repos', done => {
+    it('should set received repos', done => {
       testAction(
         actions.fetchRepos,
         null,
@@ -71,10 +71,10 @@ describe('Actions Registry Store', () => {
     beforeEach(() => {
       state.repos = parsedReposServerResponse;
       [, repo] = state.repos;
-      mock.onGet(repo.tagsPath).replyOnce(200, registryServerResponse, {});
     });
 
     it('should set received list', done => {
+      mock.onGet(repo.tagsPath).replyOnce(200, registryServerResponse, {});
       testAction(
         actions.fetchList,
         { repo },
@@ -97,6 +97,7 @@ describe('Actions Registry Store', () => {
     });
 
     it('should create flash on API error', done => {
+      mock.onGet(repo.tagsPath).replyOnce(400);
       const updatedRepo = {
         ...repo,
         tagsPath: null,
@@ -133,6 +134,19 @@ describe('Actions Registry Store', () => {
     });
   });
 
+  describe('setIsDeleteDisabled', () => {
+    it('should commit set is delete disabled', done => {
+      testAction(
+        actions.setIsDeleteDisabled,
+        true,
+        state,
+        [{ type: types.SET_IS_DELETE_DISABLED, payload: true }],
+        [],
+        done,
+      );
+    });
+  });
+
   describe('toggleLoading', () => {
     it('should commit toggle main loading', done => {
       testAction(
diff --git a/spec/frontend/registry/stores/getters_spec.js b/spec/frontend/registry/stores/getters_spec.js
index 839aa7189970032d6b2f7a7af3eb191d00ee9f8f..c16f520223bcca47e90ad2e62971ede9e1053696 100644
--- a/spec/frontend/registry/stores/getters_spec.js
+++ b/spec/frontend/registry/stores/getters_spec.js
@@ -7,6 +7,7 @@ describe('Getters Registry Store', () => {
     state = {
       isLoading: false,
       endpoint: '/root/empty-project/container_registry.json',
+      isDeleteDisabled: false,
       repos: [
         {
           canDelete: true,
@@ -43,4 +44,9 @@ describe('Getters Registry Store', () => {
       expect(getters.repos(state)).toEqual(state.repos);
     });
   });
+  describe('isDeleteDisabled', () => {
+    it('should return isDeleteDisabled', () => {
+      expect(getters.isDeleteDisabled(state)).toEqual(state.isDeleteDisabled);
+    });
+  });
 });
diff --git a/spec/frontend/registry/stores/mutations_spec.js b/spec/frontend/registry/stores/mutations_spec.js
index e19fe7a27cf683aec35fe4824f89cde2ce2f9246..1d583028ca69e347dd57af02a11929c5b89ab9e7 100644
--- a/spec/frontend/registry/stores/mutations_spec.js
+++ b/spec/frontend/registry/stores/mutations_spec.js
@@ -19,7 +19,16 @@ describe('Mutations Registry Store', () => {
       const expectedState = Object.assign({}, mockState, { endpoint: 'foo' });
       mutations[types.SET_MAIN_ENDPOINT](mockState, 'foo');
 
-      expect(mockState).toEqual(expectedState);
+      expect(mockState.endpoint).toEqual(expectedState.endpoint);
+    });
+  });
+
+  describe('SET_IS_DELETE_DISABLED', () => {
+    it('should set the is delete disabled', () => {
+      const expectedState = Object.assign({}, mockState, { isDeleteDisabled: true });
+      mutations[types.SET_IS_DELETE_DISABLED](mockState, true);
+
+      expect(mockState.isDeleteDisabled).toEqual(expectedState.isDeleteDisabled);
     });
   });
 
diff --git a/spec/frontend/releases/detail/components/app_spec.js b/spec/frontend/releases/detail/components/app_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..f8eb33a69a8822d1ec4f298f5357622389008a50
--- /dev/null
+++ b/spec/frontend/releases/detail/components/app_spec.js
@@ -0,0 +1,70 @@
+import Vuex from 'vuex';
+import { mount } from '@vue/test-utils';
+import ReleaseDetailApp from '~/releases/detail/components/app';
+import { release } from '../../mock_data';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+describe('Release detail component', () => {
+  let wrapper;
+  let releaseClone;
+  let actions;
+
+  beforeEach(() => {
+    gon.api_version = 'v4';
+
+    releaseClone = JSON.parse(JSON.stringify(convertObjectPropsToCamelCase(release)));
+
+    const state = {
+      release: releaseClone,
+      markdownDocsPath: 'path/to/markdown/docs',
+    };
+
+    actions = {
+      fetchRelease: jest.fn(),
+      updateRelease: jest.fn(),
+      navigateToReleasesPage: jest.fn(),
+    };
+
+    const store = new Vuex.Store({ actions, state });
+
+    wrapper = mount(ReleaseDetailApp, { store });
+
+    return wrapper.vm.$nextTick();
+  });
+
+  it('calls fetchRelease when the component is created', () => {
+    expect(actions.fetchRelease).toHaveBeenCalledTimes(1);
+  });
+
+  it('renders the description text at the top of the page', () => {
+    expect(wrapper.find('.js-subtitle-text').text()).toBe(
+      'Releases are based on Git tags. We recommend naming tags that fit within semantic versioning, for example v1.0, v2.0-pre.',
+    );
+  });
+
+  it('renders the correct tag name in the "Tag name" field', () => {
+    expect(wrapper.find('#git-ref').element.value).toBe(releaseClone.tagName);
+  });
+
+  it('renders the correct release title in the "Release title" field', () => {
+    expect(wrapper.find('#release-title').element.value).toBe(releaseClone.name);
+  });
+
+  it('renders the release notes in the "Release notes" textarea', () => {
+    expect(wrapper.find('#release-notes').element.value).toBe(releaseClone.description);
+  });
+
+  it('renders the "Save changes" button as type="submit"', () => {
+    expect(wrapper.find('.js-submit-button').attributes('type')).toBe('submit');
+  });
+
+  it('calls updateRelease when the form is submitted', () => {
+    wrapper.find('form').trigger('submit');
+    expect(actions.updateRelease).toHaveBeenCalledTimes(1);
+  });
+
+  it('calls navigateToReleasesPage when the "Cancel" button is clicked', () => {
+    wrapper.find('.js-cancel-button').vm.$emit('click');
+    expect(actions.navigateToReleasesPage).toHaveBeenCalledTimes(1);
+  });
+});
diff --git a/spec/frontend/releases/detail/store/actions_spec.js b/spec/frontend/releases/detail/store/actions_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..f1c7f3c10488061ca52c193bc87a78a452a2c535
--- /dev/null
+++ b/spec/frontend/releases/detail/store/actions_spec.js
@@ -0,0 +1,217 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import * as actions from '~/releases/detail/store/actions';
+import testAction from 'helpers/vuex_action_helper';
+import * as types from '~/releases/detail/store/mutation_types';
+import { release } from '../../mock_data';
+import state from '~/releases/detail/store/state';
+import createFlash from '~/flash';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+jest.mock('~/flash', () => jest.fn());
+
+jest.mock('~/lib/utils/url_utility', () => ({
+  redirectTo: jest.fn(),
+  joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths,
+}));
+
+describe('Release detail actions', () => {
+  let stateClone;
+  let releaseClone;
+  let mock;
+  let error;
+
+  beforeEach(() => {
+    stateClone = state();
+    releaseClone = JSON.parse(JSON.stringify(release));
+    mock = new MockAdapter(axios);
+    gon.api_version = 'v4';
+    error = { message: 'An error occurred' };
+    createFlash.mockClear();
+  });
+
+  afterEach(() => {
+    mock.restore();
+  });
+
+  describe('setInitialState', () => {
+    it(`commits ${types.SET_INITIAL_STATE} with the provided object`, () => {
+      const initialState = {};
+
+      return testAction(actions.setInitialState, initialState, stateClone, [
+        { type: types.SET_INITIAL_STATE, payload: initialState },
+      ]);
+    });
+  });
+
+  describe('requestRelease', () => {
+    it(`commits ${types.REQUEST_RELEASE}`, () =>
+      testAction(actions.requestRelease, undefined, stateClone, [{ type: types.REQUEST_RELEASE }]));
+  });
+
+  describe('receiveReleaseSuccess', () => {
+    it(`commits ${types.RECEIVE_RELEASE_SUCCESS}`, () =>
+      testAction(actions.receiveReleaseSuccess, releaseClone, stateClone, [
+        { type: types.RECEIVE_RELEASE_SUCCESS, payload: releaseClone },
+      ]));
+  });
+
+  describe('receiveReleaseError', () => {
+    it(`commits ${types.RECEIVE_RELEASE_ERROR}`, () =>
+      testAction(actions.receiveReleaseError, error, stateClone, [
+        { type: types.RECEIVE_RELEASE_ERROR, payload: error },
+      ]));
+
+    it('shows a flash with an error message', () => {
+      actions.receiveReleaseError({ commit: jest.fn() }, error);
+
+      expect(createFlash).toHaveBeenCalledTimes(1);
+      expect(createFlash).toHaveBeenCalledWith(
+        'Something went wrong while getting the release details',
+      );
+    });
+  });
+
+  describe('fetchRelease', () => {
+    let getReleaseUrl;
+
+    beforeEach(() => {
+      stateClone.projectId = '18';
+      stateClone.tagName = 'v1.3';
+      getReleaseUrl = `/api/v4/projects/${stateClone.projectId}/releases/${stateClone.tagName}`;
+    });
+
+    it(`dispatches requestRelease and receiveReleaseSuccess with the camel-case'd release object`, () => {
+      mock.onGet(getReleaseUrl).replyOnce(200, releaseClone);
+
+      return testAction(
+        actions.fetchRelease,
+        undefined,
+        stateClone,
+        [],
+        [
+          { type: 'requestRelease' },
+          {
+            type: 'receiveReleaseSuccess',
+            payload: convertObjectPropsToCamelCase(releaseClone, { deep: true }),
+          },
+        ],
+      );
+    });
+
+    it(`dispatches requestRelease and receiveReleaseError with an error object`, () => {
+      mock.onGet(getReleaseUrl).replyOnce(500);
+
+      return testAction(
+        actions.fetchRelease,
+        undefined,
+        stateClone,
+        [],
+        [{ type: 'requestRelease' }, { type: 'receiveReleaseError', payload: expect.anything() }],
+      );
+    });
+  });
+
+  describe('updateReleaseTitle', () => {
+    it(`commits ${types.UPDATE_RELEASE_TITLE} with the updated release title`, () => {
+      const newTitle = 'The new release title';
+      return testAction(actions.updateReleaseTitle, newTitle, stateClone, [
+        { type: types.UPDATE_RELEASE_TITLE, payload: newTitle },
+      ]);
+    });
+  });
+
+  describe('updateReleaseNotes', () => {
+    it(`commits ${types.UPDATE_RELEASE_NOTES} with the updated release notes`, () => {
+      const newReleaseNotes = 'The new release notes';
+      return testAction(actions.updateReleaseNotes, newReleaseNotes, stateClone, [
+        { type: types.UPDATE_RELEASE_NOTES, payload: newReleaseNotes },
+      ]);
+    });
+  });
+
+  describe('requestUpdateRelease', () => {
+    it(`commits ${types.REQUEST_UPDATE_RELEASE}`, () =>
+      testAction(actions.requestUpdateRelease, undefined, stateClone, [
+        { type: types.REQUEST_UPDATE_RELEASE },
+      ]));
+  });
+
+  describe('receiveUpdateReleaseSuccess', () => {
+    it(`commits ${types.RECEIVE_UPDATE_RELEASE_SUCCESS}`, () =>
+      testAction(
+        actions.receiveUpdateReleaseSuccess,
+        undefined,
+        stateClone,
+        [{ type: types.RECEIVE_UPDATE_RELEASE_SUCCESS }],
+        [{ type: 'navigateToReleasesPage' }],
+      ));
+  });
+
+  describe('receiveUpdateReleaseError', () => {
+    it(`commits ${types.RECEIVE_UPDATE_RELEASE_ERROR}`, () =>
+      testAction(actions.receiveUpdateReleaseError, error, stateClone, [
+        { type: types.RECEIVE_UPDATE_RELEASE_ERROR, payload: error },
+      ]));
+
+    it('shows a flash with an error message', () => {
+      actions.receiveUpdateReleaseError({ commit: jest.fn() }, error);
+
+      expect(createFlash).toHaveBeenCalledTimes(1);
+      expect(createFlash).toHaveBeenCalledWith(
+        'Something went wrong while saving the release details',
+      );
+    });
+  });
+
+  describe('updateRelease', () => {
+    let getReleaseUrl;
+
+    beforeEach(() => {
+      stateClone.release = releaseClone;
+      stateClone.projectId = '18';
+      stateClone.tagName = 'v1.3';
+      getReleaseUrl = `/api/v4/projects/${stateClone.projectId}/releases/${stateClone.tagName}`;
+    });
+
+    it(`dispatches requestUpdateRelease and receiveUpdateReleaseSuccess`, () => {
+      mock.onPut(getReleaseUrl).replyOnce(200);
+
+      return testAction(
+        actions.updateRelease,
+        undefined,
+        stateClone,
+        [],
+        [{ type: 'requestUpdateRelease' }, { type: 'receiveUpdateReleaseSuccess' }],
+      );
+    });
+
+    it(`dispatches requestUpdateRelease and receiveUpdateReleaseError with an error object`, () => {
+      mock.onPut(getReleaseUrl).replyOnce(500);
+
+      return testAction(
+        actions.updateRelease,
+        undefined,
+        stateClone,
+        [],
+        [
+          { type: 'requestUpdateRelease' },
+          { type: 'receiveUpdateReleaseError', payload: expect.anything() },
+        ],
+      );
+    });
+  });
+
+  describe('navigateToReleasesPage', () => {
+    it(`calls redirectTo() with the URL to the releases page`, () => {
+      const releasesPagePath = 'path/to/releases/page';
+      stateClone.releasesPagePath = releasesPagePath;
+
+      actions.navigateToReleasesPage({ state: stateClone });
+
+      expect(redirectTo).toHaveBeenCalledTimes(1);
+      expect(redirectTo).toHaveBeenCalledWith(releasesPagePath);
+    });
+  });
+});
diff --git a/spec/frontend/releases/detail/store/mutations_spec.js b/spec/frontend/releases/detail/store/mutations_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..106a40c812e520888885c4672d04f2704dd4132a
--- /dev/null
+++ b/spec/frontend/releases/detail/store/mutations_spec.js
@@ -0,0 +1,119 @@
+/* eslint-disable jest/valid-describe */
+/*
+ * ESLint disable directive ↑ can be removed once
+ * https://github.com/jest-community/eslint-plugin-jest/issues/203
+ * is resolved
+ */
+
+import state from '~/releases/detail/store/state';
+import mutations from '~/releases/detail/store/mutations';
+import * as types from '~/releases/detail/store/mutation_types';
+import { release } from '../../mock_data';
+
+describe('Release detail mutations', () => {
+  let stateClone;
+  let releaseClone;
+
+  beforeEach(() => {
+    stateClone = state();
+    releaseClone = JSON.parse(JSON.stringify(release));
+  });
+
+  describe(types.SET_INITIAL_STATE, () => {
+    it('populates the state with initial values', () => {
+      const initialState = {
+        projectId: '18',
+        tagName: 'v1.3',
+        releasesPagePath: 'path/to/releases/page',
+        markdownDocsPath: 'path/to/markdown/docs',
+        markdownPreviewPath: 'path/to/markdown/preview',
+      };
+
+      mutations[types.SET_INITIAL_STATE](stateClone, initialState);
+
+      expect(stateClone).toEqual(expect.objectContaining(initialState));
+    });
+  });
+
+  describe(types.REQUEST_RELEASE, () => {
+    it('set state.isFetchingRelease to true', () => {
+      mutations[types.REQUEST_RELEASE](stateClone);
+
+      expect(stateClone.isFetchingRelease).toEqual(true);
+    });
+  });
+
+  describe(types.RECEIVE_RELEASE_SUCCESS, () => {
+    it('handles a successful response from the server', () => {
+      mutations[types.RECEIVE_RELEASE_SUCCESS](stateClone, releaseClone);
+
+      expect(stateClone.fetchError).toEqual(undefined);
+
+      expect(stateClone.isFetchingRelease).toEqual(false);
+
+      expect(stateClone.release).toEqual(releaseClone);
+    });
+  });
+
+  describe(types.RECEIVE_RELEASE_ERROR, () => {
+    it('handles an unsuccessful response from the server', () => {
+      const error = { message: 'An error occurred!' };
+      mutations[types.RECEIVE_RELEASE_ERROR](stateClone, error);
+
+      expect(stateClone.isFetchingRelease).toEqual(false);
+
+      expect(stateClone.release).toBeUndefined();
+
+      expect(stateClone.fetchError).toEqual(error);
+    });
+  });
+
+  describe(types.UPDATE_RELEASE_TITLE, () => {
+    it("updates the release's title", () => {
+      stateClone.release = releaseClone;
+      const newTitle = 'The new release title';
+      mutations[types.UPDATE_RELEASE_TITLE](stateClone, newTitle);
+
+      expect(stateClone.release.name).toEqual(newTitle);
+    });
+  });
+
+  describe(types.UPDATE_RELEASE_NOTES, () => {
+    it("updates the release's notes", () => {
+      stateClone.release = releaseClone;
+      const newNotes = 'The new release notes';
+      mutations[types.UPDATE_RELEASE_NOTES](stateClone, newNotes);
+
+      expect(stateClone.release.description).toEqual(newNotes);
+    });
+  });
+
+  describe(types.REQUEST_UPDATE_RELEASE, () => {
+    it('set state.isUpdatingRelease to true', () => {
+      mutations[types.REQUEST_UPDATE_RELEASE](stateClone);
+
+      expect(stateClone.isUpdatingRelease).toEqual(true);
+    });
+  });
+
+  describe(types.RECEIVE_UPDATE_RELEASE_SUCCESS, () => {
+    it('handles a successful response from the server', () => {
+      mutations[types.RECEIVE_UPDATE_RELEASE_SUCCESS](stateClone, releaseClone);
+
+      expect(stateClone.updateError).toEqual(undefined);
+
+      expect(stateClone.isUpdatingRelease).toEqual(false);
+    });
+  });
+
+  describe(types.RECEIVE_UPDATE_RELEASE_ERROR, () => {
+    it('handles an unsuccessful response from the server', () => {
+      const error = { message: 'An error occurred!' };
+      mutations[types.RECEIVE_UPDATE_RELEASE_ERROR](stateClone, error);
+
+      expect(stateClone.isUpdatingRelease).toEqual(false);
+
+      expect(stateClone.updateError).toEqual(error);
+    });
+  });
+});
diff --git a/spec/frontend/releases/list/components/__snapshots__/release_block_spec.js.snap b/spec/frontend/releases/list/components/__snapshots__/release_block_spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..8f2c0427c8351efce3c0671c7de56dcd63687b41
--- /dev/null
+++ b/spec/frontend/releases/list/components/__snapshots__/release_block_spec.js.snap
@@ -0,0 +1,332 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Release block with default props matches the snapshot 1`] = `
+<div
+  class="card release-block"
+  id="v0.3"
+>
+  <div
+    class="card-body"
+  >
+    <div
+      class="d-flex align-items-start"
+    >
+      <h2
+        class="card-title mt-0 mr-auto"
+      >
+        
+        New release
+        
+        <!---->
+      </h2>
+       
+      <a
+        class="btn btn-default js-edit-button ml-2"
+        data-original-title="Edit this release"
+        href="http://0.0.0.0:3001/root/release-test/-/releases/v0.3/edit"
+        title=""
+      >
+        <svg
+          aria-hidden="true"
+          class="s16 ic-pencil"
+        >
+          <use
+            xlink:href="#pencil"
+          />
+        </svg>
+      </a>
+    </div>
+     
+    <div
+      class="card-subtitle d-flex flex-wrap text-secondary"
+    >
+      <div
+        class="append-right-8"
+      >
+        <svg
+          aria-hidden="true"
+          class="align-middle s16 ic-commit"
+        >
+          <use
+            xlink:href="#commit"
+          />
+        </svg>
+         
+        <span
+          data-original-title="Initial commit"
+          title=""
+        >
+          c22b0728
+        </span>
+      </div>
+       
+      <div
+        class="append-right-8"
+      >
+        <svg
+          aria-hidden="true"
+          class="align-middle s16 ic-tag"
+        >
+          <use
+            xlink:href="#tag"
+          />
+        </svg>
+         
+        <span
+          data-original-title="Tag"
+          title=""
+        >
+          v0.3
+        </span>
+      </div>
+       
+      <div
+        class="js-milestone-list-label"
+      >
+        <svg
+          aria-hidden="true"
+          class="align-middle s16 ic-flag"
+        >
+          <use
+            xlink:href="#flag"
+          />
+        </svg>
+         
+        <span
+          class="js-label-text"
+        >
+          Milestones
+        </span>
+      </div>
+       
+      <a
+        class="append-right-4 prepend-left-4 js-milestone-link"
+        data-original-title="The 13.6 milestone!"
+        href="http://0.0.0.0:3001/root/release-test/-/milestones/2"
+        title=""
+      >
+        
+            13.6
+          
+      </a>
+       
+            •
+          
+      <a
+        class="append-right-4 prepend-left-4 js-milestone-link"
+        data-original-title="The 13.5 milestone!"
+        href="http://0.0.0.0:3001/root/release-test/-/milestones/1"
+        title=""
+      >
+        
+            13.5
+          
+      </a>
+       
+      <!---->
+       
+      <div
+        class="append-right-4"
+      >
+        
+        •
+        
+        <span
+          data-original-title="Aug 26, 2019 5:54pm GMT+0000"
+          title=""
+        >
+          
+          released 1 month ago
+        
+        </span>
+      </div>
+       
+      <div
+        class="d-flex"
+      >
+        
+        by
+        
+        <a
+          class="user-avatar-link prepend-left-4"
+          href=""
+        >
+          <span>
+            <img
+              alt="root's avatar"
+              class="avatar s20 "
+              data-original-title=""
+              data-src="https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon"
+              height="20"
+              src="https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon"
+              title=""
+              width="20"
+            />
+             
+            <div
+              aria-hidden="true"
+              class="js-user-avatar-image-toolip d-none"
+              style="display: none;"
+            >
+              <div>
+                 root 
+              </div>
+            </div>
+          </span>
+          <!---->
+        </a>
+      </div>
+    </div>
+     
+    <div
+      class="card-text prepend-top-default"
+    >
+      <b>
+        
+        Assets
+        
+        <span
+          class="js-assets-count badge badge-pill"
+        >
+          5
+        </span>
+      </b>
+       
+      <ul
+        class="pl-0 mb-0 prepend-top-8 list-unstyled js-assets-list"
+      >
+        <li
+          class="append-bottom-8"
+        >
+          <a
+            class=""
+            data-original-title="Download asset"
+            href="https://google.com"
+            title=""
+          >
+            <svg
+              aria-hidden="true"
+              class="align-middle append-right-4 align-text-bottom s16 ic-package"
+            >
+              <use
+                xlink:href="#package"
+              />
+            </svg>
+            
+            my link
+            
+            <span>
+              (external source)
+            </span>
+          </a>
+        </li>
+        <li
+          class="append-bottom-8"
+        >
+          <a
+            class=""
+            data-original-title="Download asset"
+            href="https://gitlab.com/gitlab-org/gitlab-foss/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50"
+            title=""
+          >
+            <svg
+              aria-hidden="true"
+              class="align-middle append-right-4 align-text-bottom s16 ic-package"
+            >
+              <use
+                xlink:href="#package"
+              />
+            </svg>
+            
+            my second link
+            
+            <!---->
+          </a>
+        </li>
+      </ul>
+       
+      <div
+        class="dropdown"
+      >
+        <button
+          aria-expanded="false"
+          aria-haspopup="true"
+          class="btn btn-link"
+          data-toggle="dropdown"
+          type="button"
+        >
+          <svg
+            aria-hidden="true"
+            class="align-top append-right-4 s16 ic-doc-code"
+          >
+            <use
+              xlink:href="#doc-code"
+            />
+          </svg>
+          
+          Source code
+          
+          <svg
+            aria-hidden="true"
+            class="s16 ic-arrow-down"
+          >
+            <use
+              xlink:href="#arrow-down"
+            />
+          </svg>
+        </button>
+         
+        <div
+          class="js-sources-dropdown dropdown-menu"
+        >
+          <li>
+            <a
+              class=""
+              href="http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.zip"
+            >
+              Download zip
+            </a>
+          </li>
+          <li>
+            <a
+              class=""
+              href="http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar.gz"
+            >
+              Download tar.gz
+            </a>
+          </li>
+          <li>
+            <a
+              class=""
+              href="http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar.bz2"
+            >
+              Download tar.bz2
+            </a>
+          </li>
+          <li>
+            <a
+              class=""
+              href="http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar"
+            >
+              Download tar
+            </a>
+          </li>
+        </div>
+      </div>
+    </div>
+     
+    <div
+      class="card-text prepend-top-default"
+    >
+      <div>
+        <p
+          data-sourcepos="1:1-1:21"
+          dir="auto"
+        >
+          A super nice release!
+        </p>
+      </div>
+    </div>
+  </div>
+</div>
+`;
diff --git a/spec/frontend/releases/list/components/release_block_spec.js b/spec/frontend/releases/list/components/release_block_spec.js
index eae0025feac2f171b6ff4cded7e7c8500d48b0ba..0b908d7d6bc470197cf7abda185f66c073cf55e3 100644
--- a/spec/frontend/releases/list/components/release_block_spec.js
+++ b/spec/frontend/releases/list/components/release_block_spec.js
@@ -19,46 +19,53 @@ jest.mock('~/lib/utils/common_utils', () => ({
 
 describe('Release block', () => {
   let wrapper;
+  let releaseClone;
 
-  const factory = releaseProp => {
+  const factory = (releaseProp, releaseEditPageFeatureFlag = true) => {
     wrapper = mount(ReleaseBlock, {
       propsData: {
         release: releaseProp,
       },
+      provide: {
+        glFeatures: {
+          releaseEditPage: releaseEditPageFeatureFlag,
+        },
+      },
+      sync: false,
     });
+
+    return wrapper.vm.$nextTick();
   };
 
   const milestoneListLabel = () => wrapper.find('.js-milestone-list-label');
+  const editButton = () => wrapper.find('.js-edit-button');
+
+  beforeEach(() => {
+    releaseClone = JSON.parse(JSON.stringify(release));
+  });
 
   afterEach(() => {
     wrapper.destroy();
   });
 
   describe('with default props', () => {
-    beforeEach(() => {
-      factory(release);
+    beforeEach(() => factory(release));
+
+    it('matches the snapshot', () => {
+      expect(wrapper.element).toMatchSnapshot();
     });
 
     it("renders the block with an id equal to the release's tag name", () => {
       expect(wrapper.attributes().id).toBe('v0.3');
     });
 
-    it('renders release name', () => {
-      expect(wrapper.text()).toContain(release.name);
-    });
-
-    it('renders commit sha', () => {
-      expect(wrapper.text()).toContain(release.commit.short_id);
-
-      wrapper.setProps({ release: { ...release, commit_path: '/commit/example' } });
-      expect(wrapper.find('a[href="/commit/example"]').exists()).toBe(true);
+    it('renders an edit button that links to the "Edit release" page', () => {
+      expect(editButton().exists()).toBe(true);
+      expect(editButton().attributes('href')).toBe(release._links.edit);
     });
 
-    it('renders tag name', () => {
-      expect(wrapper.text()).toContain(release.tag_name);
-
-      wrapper.setProps({ release: { ...release, tag_path: '/tag/example' } });
-      expect(wrapper.find('a[href="/tag/example"]').exists()).toBe(true);
+    it('renders release name', () => {
+      expect(wrapper.text()).toContain(release.name);
     });
 
     it('renders release date', () => {
@@ -141,44 +148,73 @@ describe('Release block', () => {
     });
   });
 
+  it('renders commit sha', () => {
+    releaseClone.commit_path = '/commit/example';
+
+    return factory(releaseClone).then(() => {
+      expect(wrapper.text()).toContain(release.commit.short_id);
+
+      expect(wrapper.find('a[href="/commit/example"]').exists()).toBe(true);
+    });
+  });
+
+  it('renders tag name', () => {
+    releaseClone.tag_path = '/tag/example';
+
+    return factory(releaseClone).then(() => {
+      expect(wrapper.text()).toContain(release.tag_name);
+
+      expect(wrapper.find('a[href="/tag/example"]').exists()).toBe(true);
+    });
+  });
+
+  it("does not render an edit button if release._links.edit isn't a string", () => {
+    delete releaseClone._links;
+
+    return factory(releaseClone).then(() => {
+      expect(editButton().exists()).toBe(false);
+    });
+  });
+
+  it('does not render an edit button if the releaseEditPage feature flag is disabled', () =>
+    factory(releaseClone, false).then(() => {
+      expect(editButton().exists()).toBe(false);
+    }));
+
   it('does not render the milestone list if no milestones are associated to the release', () => {
-    const releaseClone = JSON.parse(JSON.stringify(release));
     delete releaseClone.milestones;
 
-    factory(releaseClone);
-
-    expect(milestoneListLabel().exists()).toBe(false);
+    return factory(releaseClone).then(() => {
+      expect(milestoneListLabel().exists()).toBe(false);
+    });
   });
 
   it('renders the label as "Milestone" if only a single milestone is passed in', () => {
-    const releaseClone = JSON.parse(JSON.stringify(release));
     releaseClone.milestones = releaseClone.milestones.slice(0, 1);
 
-    factory(releaseClone);
-
-    expect(
-      milestoneListLabel()
-        .find('.js-label-text')
-        .text(),
-    ).toEqual('Milestone');
+    return factory(releaseClone).then(() => {
+      expect(
+        milestoneListLabel()
+          .find('.js-label-text')
+          .text(),
+      ).toEqual('Milestone');
+    });
   });
 
   it('renders upcoming release badge', () => {
-    const releaseClone = JSON.parse(JSON.stringify(release));
     releaseClone.upcoming_release = true;
 
-    factory(releaseClone);
-
-    expect(wrapper.text()).toContain('Upcoming Release');
+    return factory(releaseClone).then(() => {
+      expect(wrapper.text()).toContain('Upcoming Release');
+    });
   });
 
   it('slugifies the tag_name before setting it as the elements ID', () => {
-    const releaseClone = JSON.parse(JSON.stringify(release));
     releaseClone.tag_name = 'a dangerous tag name <script>alert("hello")</script>';
 
-    factory(releaseClone);
-
-    expect(wrapper.attributes().id).toBe('a-dangerous-tag-name-script-alert-hello-script-');
+    return factory(releaseClone).then(() => {
+      expect(wrapper.attributes().id).toBe('a-dangerous-tag-name-script-alert-hello-script-');
+    });
   });
 
   describe('anchor scrolling', () => {
@@ -190,40 +226,39 @@ describe('Release block', () => {
 
     it('does not attempt to scroll the page if no anchor tag is included in the URL', () => {
       mockLocationHash = '';
-      factory(release);
-
-      expect(scrollToElement).not.toHaveBeenCalled();
+      return factory(release).then(() => {
+        expect(scrollToElement).not.toHaveBeenCalled();
+      });
     });
 
     it("does not attempt to scroll the page if the anchor tag doesn't match the release's tag name", () => {
       mockLocationHash = 'v0.4';
-      factory(release);
-
-      expect(scrollToElement).not.toHaveBeenCalled();
+      return factory(release).then(() => {
+        expect(scrollToElement).not.toHaveBeenCalled();
+      });
     });
 
     it("attempts to scroll itself into view if the anchor tag matches the release's tag name", () => {
       mockLocationHash = release.tag_name;
-      factory(release);
+      return factory(release).then(() => {
+        expect(scrollToElement).toHaveBeenCalledTimes(1);
 
-      expect(scrollToElement).toHaveBeenCalledTimes(1);
-      expect(scrollToElement).toHaveBeenCalledWith(wrapper.element);
+        expect(scrollToElement).toHaveBeenCalledWith(wrapper.element);
+      });
     });
 
     it('renders with a light blue background if it is the target of the anchor', () => {
       mockLocationHash = release.tag_name;
-      factory(release);
 
-      return wrapper.vm.$nextTick().then(() => {
+      return factory(release).then(() => {
         expect(hasTargetBlueBackground()).toBe(true);
       });
     });
 
     it('does not render with a light blue background if it is not the target of the anchor', () => {
       mockLocationHash = '';
-      factory(release);
 
-      return wrapper.vm.$nextTick().then(() => {
+      return factory(release).then(() => {
         expect(hasTargetBlueBackground()).toBe(false);
       });
     });
diff --git a/spec/frontend/releases/mock_data.js b/spec/frontend/releases/mock_data.js
index 328199343f58f854e5ef8f3d1c71b3079bda60c8..b2ebf1174d499d57b639b85049d4794d551eb40a 100644
--- a/spec/frontend/releases/mock_data.js
+++ b/spec/frontend/releases/mock_data.js
@@ -94,4 +94,7 @@ export const release = {
       },
     ],
   },
+  _links: {
+    edit: 'http://0.0.0.0:3001/root/release-test/-/releases/v0.3/edit',
+  },
 };
diff --git a/spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js b/spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js
index 49ed796d9a8b6db4432cb665ae34457ca4ebe9c2..7d593a77bf3499e33b657f947e0a38fb50cf2205 100644
--- a/spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js
+++ b/spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js
@@ -44,6 +44,7 @@ describe('Merge Requests Artifacts list app', () => {
 
   const findButtons = () => wrapper.findAll('button');
   const findTitle = () => wrapper.find('.js-title');
+  const findErrorMessage = () => wrapper.find('.js-error-state');
   const findTableRows = () => wrapper.findAll('tbody tr');
 
   describe('while loading', () => {
@@ -109,13 +110,12 @@ describe('Merge Requests Artifacts list app', () => {
     });
 
     it('renders the error state', () => {
-      expect(findTitle().text()).toBe('An error occurred while fetching the artifacts');
+      expect(findErrorMessage().text()).toBe('An error occurred while fetching the artifacts');
     });
 
-    it('renders disabled buttons', () => {
+    it('does not render buttons', () => {
       const buttons = findButtons();
-      expect(buttons.at(0).attributes('disabled')).toBe('disabled');
-      expect(buttons.at(1).attributes('disabled')).toBe('disabled');
+      expect(buttons.exists()).toBe(false);
     });
   });
 });
diff --git a/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js b/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js
index 4c9507223a184d14b54c8d16b3e966ea81217db4..ee107f297ef5c7edd74c4da6717b286f75212bf1 100644
--- a/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js
@@ -20,6 +20,7 @@ describe('Merge Request Collapsible Extension', () => {
   };
 
   const findTitle = () => wrapper.find('.js-title');
+  const findErrorMessage = () => wrapper.find('.js-error-state');
 
   afterEach(() => {
     wrapper.destroy();
@@ -87,19 +88,12 @@ describe('Merge Request Collapsible Extension', () => {
       mountComponent(Object.assign({}, data, { hasError: true }));
     });
 
-    it('renders the buttons disabled', () => {
-      expect(
-        wrapper
-          .findAll('button')
-          .at(0)
-          .attributes('disabled'),
-      ).toEqual('disabled');
-      expect(
-        wrapper
-          .findAll('button')
-          .at(1)
-          .attributes('disabled'),
-      ).toEqual('disabled');
+    it('does not render the buttons', () => {
+      expect(wrapper.findAll('button').exists()).toBe(false);
+    });
+
+    it('renders title message provided', () => {
+      expect(findErrorMessage().text()).toBe(data.title);
     });
   });
 });
diff --git a/spec/frontend/vue_shared/directives/track_event_spec.js b/spec/frontend/vue_shared/directives/track_event_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..d63f6ae05b430e28e3548c58e1d2e3cc8fe46b0c
--- /dev/null
+++ b/spec/frontend/vue_shared/directives/track_event_spec.js
@@ -0,0 +1,49 @@
+import Vue from 'vue';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Tracking from '~/tracking';
+import TrackEvent from '~/vue_shared/directives/track_event';
+
+jest.mock('~/tracking');
+
+const Component = Vue.component('dummy-element', {
+  directives: {
+    TrackEvent,
+  },
+  data() {
+    return {
+      trackingOptions: null,
+    };
+  },
+  template: '<button id="trackable" v-track-event="trackingOptions"></button>',
+});
+
+const localVue = createLocalVue();
+let wrapper;
+let button;
+
+describe('Error Tracking directive', () => {
+  beforeEach(() => {
+    wrapper = shallowMount(localVue.extend(Component), {
+      localVue,
+    });
+    button = wrapper.find('#trackable');
+  });
+
+  it('should not track the event if required arguments are not provided', () => {
+    button.trigger('click');
+    expect(Tracking.event).not.toHaveBeenCalled();
+  });
+
+  it('should track event on click if tracking info provided', () => {
+    const trackingOptions = {
+      category: 'Tracking',
+      action: 'click_trackable_btn',
+      label: 'Trackable Info',
+    };
+
+    wrapper.setData({ trackingOptions });
+    const { category, action, label, property, value } = trackingOptions;
+    button.trigger('click');
+    expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property, value });
+  });
+});
diff --git a/spec/graphql/mutations/concerns/mutations/resolves_group_spec.rb b/spec/graphql/mutations/concerns/mutations/resolves_group_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..897b8f4e9ef0832f8b2f456c675ee346edc9e6ba
--- /dev/null
+++ b/spec/graphql/mutations/concerns/mutations/resolves_group_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Mutations::ResolvesGroup do
+  let(:mutation_class) do
+    Class.new(Mutations::BaseMutation) do
+      include Mutations::ResolvesGroup
+    end
+  end
+
+  let(:context) { double }
+  subject(:mutation) { mutation_class.new(object: nil, context: context) }
+
+  it 'uses the GroupsResolver to resolve groups by path' do
+    group = create(:group)
+
+    expect(Resolvers::GroupResolver).to receive(:new).with(object: nil, context: context).and_call_original
+    expect(mutation.resolve_group(full_path: group.full_path).sync).to eq(group)
+  end
+end
diff --git a/spec/graphql/mutations/concerns/mutations/resolves_project_spec.rb b/spec/graphql/mutations/concerns/mutations/resolves_project_spec.rb
index aa0f5c55902b3205d06a05de9ea207f7842ffc4a..09d1f66a2c783b70186bfd6ddc951c6928e1fd64 100644
--- a/spec/graphql/mutations/concerns/mutations/resolves_project_spec.rb
+++ b/spec/graphql/mutations/concerns/mutations/resolves_project_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Mutations::ResolvesProject do
diff --git a/spec/graphql/mutations/merge_requests/set_wip_spec.rb b/spec/graphql/mutations/merge_requests/set_wip_spec.rb
index e600abf3941560276d963a87af4e20f195fab488..c4accab9e46815411de573aff5bd9328bce28dae 100644
--- a/spec/graphql/mutations/merge_requests/set_wip_spec.rb
+++ b/spec/graphql/mutations/merge_requests/set_wip_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Mutations::MergeRequests::SetWip do
diff --git a/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb b/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb
index 3140af27af51e6b71117a2e1883cfae1a5f82afa..fa031af4013bf3b79db2295306525f8e30e48de1 100644
--- a/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb
+++ b/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe ResolvesPipelines do
diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb
index d122c081069929467c44397391d4a1becc88fc58..2232c9b7d7bc750272855e64fafb01842bdd96e7 100644
--- a/spec/graphql/resolvers/issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/issues_resolver_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Resolvers::IssuesResolver do
diff --git a/spec/graphql/resolvers/merge_request_pipelines_resolver_spec.rb b/spec/graphql/resolvers/merge_request_pipelines_resolver_spec.rb
index 09b17bf6fc9d0bb6acd87a683ee30e4dd6bd2bee..b8bdfc36ba7a8db17f7e000a44eed305be8d86b7 100644
--- a/spec/graphql/resolvers/merge_request_pipelines_resolver_spec.rb
+++ b/spec/graphql/resolvers/merge_request_pipelines_resolver_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Resolvers::MergeRequestPipelinesResolver do
diff --git a/spec/graphql/resolvers/merge_requests_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
index 97b8e5ed41ccb924c51041c674085a97df60d020..fe167a6ae3e173a0b1209612cdb56f58f1a2acd6 100644
--- a/spec/graphql/resolvers/merge_requests_resolver_spec.rb
+++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Resolvers::MergeRequestsResolver do
diff --git a/spec/graphql/resolvers/metadata_resolver_spec.rb b/spec/graphql/resolvers/metadata_resolver_spec.rb
index e662ed127a5c1d8be1d8a4d1d43858f911b80e57..afff9eabb973d100584a2cd44a112dede4fb6a19 100644
--- a/spec/graphql/resolvers/metadata_resolver_spec.rb
+++ b/spec/graphql/resolvers/metadata_resolver_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Resolvers::MetadataResolver do
diff --git a/spec/graphql/resolvers/project_pipelines_resolver_spec.rb b/spec/graphql/resolvers/project_pipelines_resolver_spec.rb
index 6862ae8a5ed0df2132602ccbd41349f501ff9290..f312a118c960b590299c17f0245d45cebf1dd9bf 100644
--- a/spec/graphql/resolvers/project_pipelines_resolver_spec.rb
+++ b/spec/graphql/resolvers/project_pipelines_resolver_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Resolvers::ProjectPipelinesResolver do
diff --git a/spec/graphql/resolvers/project_resolver_spec.rb b/spec/graphql/resolvers/project_resolver_spec.rb
index d0fc295790913873b8e747b4333162622512ab76..860f8b4abb8d912ea8d2be26b94ed5e1ecd90c94 100644
--- a/spec/graphql/resolvers/project_resolver_spec.rb
+++ b/spec/graphql/resolvers/project_resolver_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Resolvers::ProjectResolver do
diff --git a/spec/graphql/resolvers/todo_resolver_spec.rb b/spec/graphql/resolvers/todo_resolver_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fef761d7243af115315db7a2f8e7e9fca4fa7826
--- /dev/null
+++ b/spec/graphql/resolvers/todo_resolver_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::TodoResolver do
+  include GraphqlHelpers
+
+  describe '#resolve' do
+    let_it_be(:current_user) { create(:user) }
+    let_it_be(:user) { create(:user) }
+    let_it_be(:author1) { create(:user) }
+    let_it_be(:author2) { create(:user) }
+
+    let_it_be(:todo1) { create(:todo, user: user, target_type: 'MergeRequest', state: :pending, action: Todo::MENTIONED, author: author1) }
+    let_it_be(:todo2) { create(:todo, user: user, state: :done, action: Todo::ASSIGNED, author: author2) }
+    let_it_be(:todo3) { create(:todo, user: user, state: :pending, action: Todo::ASSIGNED, author: author1) }
+
+    it 'calls TodosFinder' do
+      expect_next_instance_of(TodosFinder) do |finder|
+        expect(finder).to receive(:execute)
+      end
+
+      resolve_todos
+    end
+
+    context 'when using no filter' do
+      it 'returns expected todos' do
+        todos = resolve(described_class, obj: user, args: {}, ctx: { current_user: user })
+
+        expect(todos).to contain_exactly(todo1, todo3)
+      end
+    end
+
+    context 'when using filters' do
+      # TODO These can be removed as soon as we support filtering for multiple field contents for todos
+
+      it 'just uses the first state' do
+        todos = resolve(described_class, obj: user, args: { state: [:done, :pending] }, ctx: { current_user: user })
+
+        expect(todos).to contain_exactly(todo2)
+      end
+
+      it 'just uses the first action' do
+        todos = resolve(described_class, obj: user, args: { action: [Todo::MENTIONED, Todo::ASSIGNED] }, ctx: { current_user: user })
+
+        expect(todos).to contain_exactly(todo1)
+      end
+
+      it 'just uses the first author id' do
+        # We need a pending todo for now because of TodosFinder's state query
+        todo4 = create(:todo, user: user, state: :pending, action: Todo::ASSIGNED, author: author2)
+
+        todos = resolve(described_class, obj: user, args: { author_id: [author2.id, author1.id] }, ctx: { current_user: user })
+
+        expect(todos).to contain_exactly(todo4)
+      end
+
+      it 'just uses the first project id' do
+        project1 = create(:project)
+        project2 = create(:project)
+
+        create(:todo, project: project1, user: user, state: :pending, action: Todo::ASSIGNED, author: author1)
+        todo5 = create(:todo, project: project2, user: user, state: :pending, action: Todo::ASSIGNED, author: author1)
+
+        todos = resolve(described_class, obj: user, args: { project_id: [project2.id, project1.id] }, ctx: { current_user: user })
+
+        expect(todos).to contain_exactly(todo5)
+      end
+
+      it 'just uses the first group id' do
+        group1 = create(:group)
+        group2 = create(:group)
+
+        group1.add_developer(user)
+        group2.add_developer(user)
+
+        create(:todo, group: group1, user: user, state: :pending, action: Todo::ASSIGNED, author: author1)
+        todo5 = create(:todo, group: group2, user: user, state: :pending, action: Todo::ASSIGNED, author: author1)
+
+        todos = resolve(described_class, obj: user, args: { group_id: [group2.id, group1.id] }, ctx: { current_user: user })
+
+        expect(todos).to contain_exactly(todo5)
+      end
+
+      it 'just uses the first target' do
+        todos = resolve(described_class, obj: user, args: { type: %w[Issue MergeRequest] }, ctx: { current_user: user })
+
+        # Just todo3 because todo2 is in state "done"
+        expect(todos).to contain_exactly(todo3)
+      end
+    end
+
+    context 'when no user is provided' do
+      it 'returns no todos' do
+        todos = resolve(described_class, obj: nil, args: {}, ctx: { current_user: current_user })
+
+        expect(todos).to be_empty
+      end
+    end
+
+    context 'when provided user is not current user' do
+      it 'returns no todos' do
+        todos = resolve(described_class, obj: user, args: {}, ctx: { current_user: current_user })
+
+        expect(todos).to be_empty
+      end
+    end
+  end
+
+  def resolve_todos(args = {}, context = { current_user: current_user })
+    resolve(described_class, obj: current_user, args: args, ctx: context)
+  end
+end
diff --git a/spec/graphql/resolvers/tree_resolver_spec.rb b/spec/graphql/resolvers/tree_resolver_spec.rb
index 9f95b740ab196c98a724c6de0aad0a5177bede3b..0ea4e6eeaadc9a6200e795881952a7deb8e6d3e0 100644
--- a/spec/graphql/resolvers/tree_resolver_spec.rb
+++ b/spec/graphql/resolvers/tree_resolver_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Resolvers::TreeResolver do
diff --git a/spec/graphql/types/ci/detailed_status_type_spec.rb b/spec/graphql/types/ci/detailed_status_type_spec.rb
index a21162adb42d8f77c8f142048e33e00a6932d42e..169a03c770b6a2b09e390daac04d88b5f40b6310 100644
--- a/spec/graphql/types/ci/detailed_status_type_spec.rb
+++ b/spec/graphql/types/ci/detailed_status_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Types::Ci::DetailedStatusType do
diff --git a/spec/graphql/types/ci/pipeline_type_spec.rb b/spec/graphql/types/ci/pipeline_type_spec.rb
index ec1c689a4be94f25383b53c363578161639a8837..2fafc1bc13f4efba439f9664fbf1eaa5a8f50f12 100644
--- a/spec/graphql/types/ci/pipeline_type_spec.rb
+++ b/spec/graphql/types/ci/pipeline_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Types::Ci::PipelineType do
diff --git a/spec/graphql/types/extended_issue_type_spec.rb b/spec/graphql/types/extended_issue_type_spec.rb
index 047f4a90d54e98a8e741bc141e515417469e3444..72ce53ae1be944cc978defd28e91eb44029f7c10 100644
--- a/spec/graphql/types/extended_issue_type_spec.rb
+++ b/spec/graphql/types/extended_issue_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe GitlabSchema.types['ExtendedIssue'] do
diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb
index 59c42aa446de57e35a7eb28b3ac34dfb6a3a500c..8aa2385ddaa17e4dc2a1aeec709d9f954709dc70 100644
--- a/spec/graphql/types/issue_type_spec.rb
+++ b/spec/graphql/types/issue_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe GitlabSchema.types['Issue'] do
diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb
index 59bd0123d8870404bc7b80f3eb50fc96adcb44f6..04e9bb6861f4586bbe16034fdfd3866e571ee13d 100644
--- a/spec/graphql/types/merge_request_type_spec.rb
+++ b/spec/graphql/types/merge_request_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe GitlabSchema.types['MergeRequest'] do
@@ -18,7 +20,9 @@
       merge_error allow_collaboration should_be_rebased rebase_commit_sha
       rebase_in_progress merge_commit_message default_merge_commit_message
       merge_ongoing source_branch_exists mergeable_discussions_state web_url
-      upvotes downvotes subscribed head_pipeline pipelines task_completion_status
+      upvotes downvotes head_pipeline pipelines task_completion_status
+      milestone assignees participants subscribed labels discussion_locked time_estimate
+      total_time_spent reference
     ]
 
     is_expected.to have_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/metadata_type_spec.rb b/spec/graphql/types/metadata_type_spec.rb
index 5236380e4778ac50e16489f24004bfdda5a35a2d..2988f3c6ba59fe8f519478add1462a345ffea427 100644
--- a/spec/graphql/types/metadata_type_spec.rb
+++ b/spec/graphql/types/metadata_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe GitlabSchema.types['Metadata'] do
diff --git a/spec/graphql/types/notes/diff_position_type_spec.rb b/spec/graphql/types/notes/diff_position_type_spec.rb
index 345bca8f7021ddf5e0704a0778b82d9bcf9c6bbd..aa08daaacd4542eff1b210b32a81081237fe63a5 100644
--- a/spec/graphql/types/notes/diff_position_type_spec.rb
+++ b/spec/graphql/types/notes/diff_position_type_spec.rb
@@ -7,6 +7,6 @@
                        :new_path, :position_type, :old_line, :new_line, :x, :y,
                        :width, :height]
 
-    is_expected.to have_graphql_field(*expected_fields)
+    is_expected.to have_graphql_fields(*expected_fields)
   end
 end
diff --git a/spec/graphql/types/permission_types/base_permission_type_spec.rb b/spec/graphql/types/permission_types/base_permission_type_spec.rb
index 0ee8b883d519b24761a425d8fa5d564631594c31..a45102e5b50cf4d51081179ab9ccf88bf16c41a1 100644
--- a/spec/graphql/types/permission_types/base_permission_type_spec.rb
+++ b/spec/graphql/types/permission_types/base_permission_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Types::PermissionTypes::BasePermissionType do
diff --git a/spec/graphql/types/permission_types/issue_spec.rb b/spec/graphql/types/permission_types/issue_spec.rb
index f0fbeda202fe2eb38edea20d10d33904df67c860..a94bc6b780e1d9ad72f5254c73394d65371468e4 100644
--- a/spec/graphql/types/permission_types/issue_spec.rb
+++ b/spec/graphql/types/permission_types/issue_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Types::PermissionTypes::Issue do
diff --git a/spec/graphql/types/permission_types/merge_request_spec.rb b/spec/graphql/types/permission_types/merge_request_spec.rb
index e1026b01a74611c4fdae401cd088367c6c213dd8..e0f8bdd4712196c4dc106e63daeca25ba9826e47 100644
--- a/spec/graphql/types/permission_types/merge_request_spec.rb
+++ b/spec/graphql/types/permission_types/merge_request_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Types::PermissionTypes::MergeRequest do
diff --git a/spec/graphql/types/permission_types/merge_request_type_spec.rb b/spec/graphql/types/permission_types/merge_request_type_spec.rb
index 6e57122867ac21d57f2dd6d24d563fc4fbca6617..572b4ac42d09600ffaf6bac7d59f9a71c6355330 100644
--- a/spec/graphql/types/permission_types/merge_request_type_spec.rb
+++ b/spec/graphql/types/permission_types/merge_request_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Types::MergeRequestType do
diff --git a/spec/graphql/types/permission_types/note_spec.rb b/spec/graphql/types/permission_types/note_spec.rb
index 32d56eb1f7a786894c8ba0800e1fdc548fa373bc..a7811fc20e50d4479c2959f34fec15520b54c412 100644
--- a/spec/graphql/types/permission_types/note_spec.rb
+++ b/spec/graphql/types/permission_types/note_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe GitlabSchema.types['NotePermissions'] do
diff --git a/spec/graphql/types/permission_types/project_spec.rb b/spec/graphql/types/permission_types/project_spec.rb
index 4974995b587e2ed923ab5023a9781a982ebe7ed5..6d5a905c128feb8eceab2e972562fa8aa67c2858 100644
--- a/spec/graphql/types/permission_types/project_spec.rb
+++ b/spec/graphql/types/permission_types/project_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe Types::PermissionTypes::Project do
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 739e48044d1dd3cb451e9c665b17818692ca0efb..cfd0f8ec7a7df278b1e16f3732aead962c4a6a45 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe GitlabSchema.types['Project'] do
@@ -43,4 +45,22 @@
       is_expected.to have_graphql_resolver(Resolvers::IssuesResolver)
     end
   end
+
+  describe 'merge_requests field' do
+    subject { described_class.fields['mergeRequest'] }
+
+    it 'returns merge requests' do
+      is_expected.to have_graphql_type(Types::MergeRequestType)
+      is_expected.to have_graphql_resolver(Resolvers::MergeRequestsResolver.single)
+    end
+  end
+
+  describe 'merge_request field' do
+    subject { described_class.fields['mergeRequests'] }
+
+    it 'returns merge request' do
+      is_expected.to have_graphql_type(Types::MergeRequestType.connection_type)
+      is_expected.to have_graphql_resolver(Resolvers::MergeRequestsResolver)
+    end
+  end
 end
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index bc3b8a4239265bb18f9a41979fcb35141eec9d78..1365bc0dc149329000a4bb4d2ef864040d6e9f27 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe GitlabSchema.types['Query'] do
@@ -5,7 +7,7 @@
     expect(described_class.graphql_name).to eq('Query')
   end
 
-  it { is_expected.to have_graphql_fields(:project, :namespace, :group, :echo, :metadata) }
+  it { is_expected.to have_graphql_fields(:project, :namespace, :group, :echo, :metadata, :current_user) }
 
   describe 'namespace field' do
     subject { described_class.fields['namespace'] }
diff --git a/spec/graphql/types/repository_type_spec.rb b/spec/graphql/types/repository_type_spec.rb
index 8a8238f2a2ae7879731927007814bd23b0df6256..236f9bb94593910eee39a4e898f1a7d9c2577340 100644
--- a/spec/graphql/types/repository_type_spec.rb
+++ b/spec/graphql/types/repository_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe GitlabSchema.types['Repository'] do
diff --git a/spec/graphql/types/time_type_spec.rb b/spec/graphql/types/time_type_spec.rb
index 4196d9d27d465d3205fbea25aa9cdb3df17bcad6..88a535ed3bb1a50270ba53745e0058a48b46d20e 100644
--- a/spec/graphql/types/time_type_spec.rb
+++ b/spec/graphql/types/time_type_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe GitlabSchema.types['Time'] do
diff --git a/spec/graphql/types/todo_type_spec.rb b/spec/graphql/types/todo_type_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a5ea5bcffb073ad885909bb4b01334a9885f850a
--- /dev/null
+++ b/spec/graphql/types/todo_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['Todo'] do
+  it 'has the correct fields' do
+    expected_fields = [:id, :project, :group, :author, :action, :target_type, :body, :state, :created_at]
+
+    is_expected.to have_graphql_fields(*expected_fields)
+  end
+
+  it { expect(described_class).to require_graphql_authorizations(:read_todo) }
+end
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index e5e16e69833239bb15d003ef46b0128781632c18..4996e27c2e6146fc46cbef8e4140d130a7f83846 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -270,4 +270,32 @@
       end
     end
   end
+
+  describe '#ide_fork_and_edit_path' do
+    let(:project) { create(:project) }
+    let(:current_user) { create(:user) }
+    let(:can_push_code) { true }
+
+    before do
+      allow(helper).to receive(:current_user).and_return(current_user)
+      allow(helper).to receive(:can?).and_return(can_push_code)
+    end
+
+    it 'returns path to fork the repo with a redirect param to the full IDE path' do
+      uri = URI(helper.ide_fork_and_edit_path(project, "master", ""))
+      params = CGI.unescape(uri.query)
+
+      expect(uri.path).to eq("/#{project.namespace.path}/#{project.path}/-/forks")
+      expect(params).to include("continue[to]=/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master")
+      expect(params).to include("namespace_key=#{current_user.namespace.id}")
+    end
+
+    context 'when user is not logged in' do
+      let(:current_user) { nil }
+
+      it 'returns nil' do
+        expect(helper.ide_fork_and_edit_path(project, "master", "")).to be_nil
+      end
+    end
+  end
 end
diff --git a/spec/helpers/environment_helper_spec.rb b/spec/helpers/environment_helper_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..53953d72b061346f33b0ee65e12d5b8a3e5610e1
--- /dev/null
+++ b/spec/helpers/environment_helper_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe EnvironmentHelper do
+  describe '#render_deployment_status' do
+    context 'when using a manual deployment' do
+      it 'renders a span tag' do
+        deploy = build(:deployment, deployable: nil, status: :success)
+        html = helper.render_deployment_status(deploy)
+
+        expect(html).to have_css('span.ci-status.ci-success')
+      end
+    end
+
+    context 'when using a deployment from a build' do
+      it 'renders a link tag' do
+        deploy = build(:deployment, status: :success)
+        html = helper.render_deployment_status(deploy)
+
+        expect(html).to have_css('a.ci-status.ci-success')
+      end
+    end
+  end
+end
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 98719697cea4a4feb1a451aa2da34ea88d6fb420..8b33277ea18590361205a444ee430aec052d18ac 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -191,6 +191,41 @@
     end
   end
 
+  describe '#group_container_registry_nav' do
+    let(:group) { create(:group, :public) }
+    let(:user) { create(:user) }
+    before do
+      stub_container_registry_config(enabled: true)
+      allow(helper).to receive(:current_user) { user }
+      allow(helper).to receive(:can?).with(user, :read_container_image, group) { true }
+      helper.instance_variable_set(:@group, group)
+    end
+
+    subject { helper.group_container_registry_nav? }
+
+    context 'when container registry is enabled' do
+      it { is_expected.to be_truthy }
+
+      it 'is disabled for guest' do
+        allow(helper).to receive(:can?).with(user, :read_container_image, group) { false }
+        expect(subject).to be false
+      end
+    end
+
+    context 'when container registry is not enabled' do
+      before do
+        stub_container_registry_config(enabled: false)
+      end
+
+      it { is_expected.to be_falsy }
+
+      it 'is disabled for guests' do
+        allow(helper).to receive(:can?).with(user, :read_container_image, group) { false }
+        expect(subject).to be false
+      end
+    end
+  end
+
   describe '#group_sidebar_links' do
     let(:group) { create(:group, :public) }
     let(:user) { create(:user) }
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index e52d2166a69806a86d7cfaac10b18b9e7f2c26b4..169c8707bf4526d203115159fb0c400f67a86c33 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -5,11 +5,11 @@
 describe MembersHelper do
   describe '#remove_member_message' do
     let(:requester) { create(:user) }
-    let(:project) { create(:project, :public, :access_requestable) }
+    let(:project) { create(:project, :public) }
     let(:project_member) { build(:project_member, project: project) }
     let(:project_member_invite) { build(:project_member, project: project).tap { |m| m.generate_invite_token! } }
     let(:project_member_request) { project.request_access(requester) }
-    let(:group) { create(:group, :access_requestable) }
+    let(:group) { create(:group) }
     let(:group_member) { build(:group_member, group: group) }
     let(:group_member_invite) { build(:group_member, group: group).tap { |m| m.generate_invite_token! } }
     let(:group_member_request) { group.request_access(requester) }
@@ -26,10 +26,10 @@
 
   describe '#remove_member_title' do
     let(:requester) { create(:user) }
-    let(:project) { create(:project, :public, :access_requestable) }
+    let(:project) { create(:project, :public) }
     let(:project_member) { build(:project_member, project: project) }
     let(:project_member_request) { project.request_access(requester) }
-    let(:group) { create(:group, :access_requestable) }
+    let(:group) { create(:group) }
     let(:group_member) { build(:group_member, group: group) }
     let(:group_member_request) { group.request_access(requester) }
 
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index 193390d2f2c38300d383a7438c217ce6d7cd13f2..695d1520897a22d4f6723af8ec5f13f7de1816bd 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -39,6 +39,7 @@
       let(:forked_project) { fork_project(project) }
       let(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project) }
       subject { format_mr_branch_names(merge_request) }
+
       let(:source_title) { "#{forked_project.full_path}:#{merge_request.source_branch}" }
       let(:target_title) { "#{project.full_path}:#{merge_request.target_branch}" }
 
diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb
index 73fbd4c7a441854b35b703ec20c1bec037f6d442..248f967311b8ed88e7c20ce5874ca4f0faaeb711 100644
--- a/spec/initializers/6_validations_spec.rb
+++ b/spec/initializers/6_validations_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 require_relative '../../config/initializers/6_validations.rb'
 
diff --git a/spec/initializers/action_mailer_hooks_spec.rb b/spec/initializers/action_mailer_hooks_spec.rb
index 3826ed9b00ac3af7b7c91b9fc51ac53ed2ee0212..ce6e1ed0fa22e8ab63011a8646a6f50e01fc440a 100644
--- a/spec/initializers/action_mailer_hooks_spec.rb
+++ b/spec/initializers/action_mailer_hooks_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe 'ActionMailer hooks' do
diff --git a/spec/initializers/asset_proxy_setting_spec.rb b/spec/initializers/asset_proxy_setting_spec.rb
index 42e4d4aa5943faf62f8c68b98375e29fd4a07d3a..7eab5de155b43cf65670fd5ee5651089aa12afec 100644
--- a/spec/initializers/asset_proxy_setting_spec.rb
+++ b/spec/initializers/asset_proxy_setting_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe 'Asset proxy settings initialization' do
diff --git a/spec/initializers/attr_encrypted_no_db_connection_spec.rb b/spec/initializers/attr_encrypted_no_db_connection_spec.rb
index 2da9f1cbd9686e65bc5fd77119878ff594a9f19d..14e0e1f21678f9a7a718a03c26ada6719e4fa216 100644
--- a/spec/initializers/attr_encrypted_no_db_connection_spec.rb
+++ b/spec/initializers/attr_encrypted_no_db_connection_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe 'GitLab monkey-patches to AttrEncrypted' do
diff --git a/spec/initializers/direct_upload_support_spec.rb b/spec/initializers/direct_upload_support_spec.rb
index e51d404e030b630ee034dc8cdc1f5d2f227bde2c..4b3fe871cefaad504ddd4e1e0da2fc4e108e1b83 100644
--- a/spec/initializers/direct_upload_support_spec.rb
+++ b/spec/initializers/direct_upload_support_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe 'Direct upload support' do
diff --git a/spec/initializers/doorkeeper_spec.rb b/spec/initializers/doorkeeper_spec.rb
index 1a78196e33dd18ab326460f0458eaa1c100d25e2..47c196cb3a3ce0cf2930910d9e45ebaf6f191428 100644
--- a/spec/initializers/doorkeeper_spec.rb
+++ b/spec/initializers/doorkeeper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 require_relative '../../config/initializers/doorkeeper'
 
diff --git a/spec/initializers/fog_google_https_private_urls_spec.rb b/spec/initializers/fog_google_https_private_urls_spec.rb
index 08346b71feecc9915c241ce9ac119e8a4bd56950..8a0d7ad8f1571c7946e307db2409339a5bf0aecc 100644
--- a/spec/initializers/fog_google_https_private_urls_spec.rb
+++ b/spec/initializers/fog_google_https_private_urls_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe 'Fog::Storage::GoogleXML::File', :fog_requests do
diff --git a/spec/initializers/lograge_spec.rb b/spec/initializers/lograge_spec.rb
index 24d366731a2668d8193840b1b569880194027b49..c2c1960eeabe00e2715185238293c4ce6f2cd951 100644
--- a/spec/initializers/lograge_spec.rb
+++ b/spec/initializers/lograge_spec.rb
@@ -34,5 +34,38 @@
 
       subject
     end
+
+    it 'logs cpu_s on supported platform' do
+      allow(Gitlab::Metrics::System).to receive(:thread_cpu_time)
+        .and_return(
+          0.111222333,
+          0.222333833
+        )
+
+      expect(Lograge.formatter).to receive(:call)
+        .with(a_hash_including(cpu_s: 0.1111115))
+        .and_call_original
+
+      expect(Lograge.logger).to receive(:send)
+        .with(anything, include('"cpu_s":0.1111115'))
+        .and_call_original
+
+      subject
+    end
+
+    it 'does not log cpu_s on unsupported platform' do
+      allow(Gitlab::Metrics::System).to receive(:thread_cpu_time)
+        .and_return(nil)
+
+      expect(Lograge.formatter).to receive(:call)
+        .with(hash_not_including(:cpu_s))
+        .and_call_original
+
+      expect(Lograge.logger).not_to receive(:send)
+        .with(anything, include('"cpu_s":'))
+        .and_call_original
+
+      subject
+    end
   end
 end
diff --git a/spec/initializers/rest-client-hostname_override_spec.rb b/spec/initializers/rest-client-hostname_override_spec.rb
index 3707e001d41f5f4931df0ea477d58a9c25d2beaa..90a0305c9a93fed409ad6bc97c6a6cd2cd51b1a4 100644
--- a/spec/initializers/rest-client-hostname_override_spec.rb
+++ b/spec/initializers/rest-client-hostname_override_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe 'rest-client dns rebinding protection' do
diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb
index 726ce07a2d1563288b33cf6c611aee8617830464..c29f46e7779f34163427805d203c1d1504584fa3 100644
--- a/spec/initializers/secret_token_spec.rb
+++ b/spec/initializers/secret_token_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 require_relative '../../config/initializers/01_secret_token'
 
diff --git a/spec/initializers/settings_spec.rb b/spec/initializers/settings_spec.rb
index 57f5adbbc40d2a68b584d43e218396dbc431bb9d..6cb45b4c86bd515ba4fc7cf8f9fc759ae483604b 100644
--- a/spec/initializers/settings_spec.rb
+++ b/spec/initializers/settings_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 require_relative '../../config/initializers/1_settings' unless defined?(Settings)
 
diff --git a/spec/initializers/trusted_proxies_spec.rb b/spec/initializers/trusted_proxies_spec.rb
index 02a9446ad7bb1c880496e48611505e883a2d0ce8..a2bd0ff9f1cf2eae2e24fc6738ca1dfec5e1dfce 100644
--- a/spec/initializers/trusted_proxies_spec.rb
+++ b/spec/initializers/trusted_proxies_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe 'trusted_proxies' do
diff --git a/spec/initializers/zz_metrics_spec.rb b/spec/initializers/zz_metrics_spec.rb
index 3eaccfe8d8b9cc6ff9e0863ff5a43de5037810af..b9a1919ceae46347a94f9e6f6cc316ca3a7ee118 100644
--- a/spec/initializers/zz_metrics_spec.rb
+++ b/spec/initializers/zz_metrics_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe 'instrument_classes' do
diff --git a/spec/javascripts/ide/components/jobs/stage_spec.js b/spec/javascripts/ide/components/jobs/stage_spec.js
deleted file mode 100644
index fc3831f2d0572cdf187679e8403f9caf5f96244e..0000000000000000000000000000000000000000
--- a/spec/javascripts/ide/components/jobs/stage_spec.js
+++ /dev/null
@@ -1,95 +0,0 @@
-import Vue from 'vue';
-import Stage from '~/ide/components/jobs/stage.vue';
-import { stages, jobs } from '../../mock_data';
-
-describe('IDE pipeline stage', () => {
-  const Component = Vue.extend(Stage);
-  let vm;
-  let stage;
-
-  beforeEach(() => {
-    stage = {
-      ...stages[0],
-      id: 0,
-      dropdownPath: stages[0].dropdown_path,
-      jobs: [...jobs],
-      isLoading: false,
-      isCollapsed: false,
-    };
-
-    vm = new Component({
-      propsData: { stage },
-    });
-
-    spyOn(vm, '$emit');
-
-    vm.$mount();
-  });
-
-  afterEach(() => {
-    vm.$destroy();
-  });
-
-  it('emits fetch event when mounted', () => {
-    expect(vm.$emit).toHaveBeenCalledWith('fetch', vm.stage);
-  });
-
-  it('renders stages details', () => {
-    expect(vm.$el.textContent).toContain(vm.stage.name);
-  });
-
-  it('renders CI icon', () => {
-    expect(vm.$el.querySelector('.ic-status_failed')).not.toBe(null);
-  });
-
-  describe('collapsed', () => {
-    it('emits event when clicking header', done => {
-      vm.$el.querySelector('.card-header').click();
-
-      vm.$nextTick(() => {
-        expect(vm.$emit).toHaveBeenCalledWith('toggleCollapsed', vm.stage.id);
-
-        done();
-      });
-    });
-
-    it('toggles collapse status when collapsed', done => {
-      vm.stage.isCollapsed = true;
-
-      vm.$nextTick(() => {
-        expect(vm.$el.querySelector('.card-body').style.display).toBe('none');
-
-        done();
-      });
-    });
-
-    it('sets border bottom class when collapsed', done => {
-      vm.stage.isCollapsed = true;
-
-      vm.$nextTick(() => {
-        expect(vm.$el.querySelector('.card-header').classList).toContain('border-bottom-0');
-
-        done();
-      });
-    });
-  });
-
-  it('renders jobs count', () => {
-    expect(vm.$el.querySelector('.badge').textContent).toContain('4');
-  });
-
-  it('renders loading icon when no jobs and isLoading is true', done => {
-    vm.stage.isLoading = true;
-    vm.stage.jobs = [];
-
-    vm.$nextTick(() => {
-      expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
-
-      done();
-    });
-  });
-
-  it('renders list of jobs', () => {
-    expect(vm.$el.querySelectorAll('.ide-job-item').length).toBe(4);
-  });
-});
diff --git a/spec/javascripts/jobs/components/environments_block_spec.js b/spec/javascripts/jobs/components/environments_block_spec.js
index 4bbc5f5a348ec8638a5582d13aeab1fa90525ee4..64a59d659a739e934e1c5e732b9709bd9c676a9f 100644
--- a/spec/javascripts/jobs/components/environments_block_spec.js
+++ b/spec/javascripts/jobs/components/environments_block_spec.js
@@ -2,6 +2,9 @@ import Vue from 'vue';
 import component from '~/jobs/components/environments_block.vue';
 import mountComponent from '../../helpers/vue_mount_component_helper';
 
+const TEST_CLUSTER_NAME = 'test_cluster';
+const TEST_CLUSTER_PATH = 'path/to/test_cluster';
+
 describe('Environments block', () => {
   const Component = Vue.extend(component);
   let vm;
@@ -20,22 +23,53 @@ describe('Environments block', () => {
 
   const lastDeployment = { iid: 'deployment', deployable: { build_path: 'bar' } };
 
+  const createEnvironmentWithLastDeployment = () => ({
+    ...environment,
+    last_deployment: { ...lastDeployment },
+  });
+
+  const createEnvironmentWithCluster = () => ({
+    ...environment,
+    last_deployment: {
+      ...lastDeployment,
+      cluster: { name: TEST_CLUSTER_NAME, path: TEST_CLUSTER_PATH },
+    },
+  });
+
+  const createComponent = (deploymentStatus = {}) => {
+    vm = mountComponent(Component, {
+      deploymentStatus,
+      iconStatus: status,
+    });
+  };
+
+  const findText = () => vm.$el.textContent.trim();
+  const findJobDeploymentLink = () => vm.$el.querySelector('.js-job-deployment-link');
+  const findEnvironmentLink = () => vm.$el.querySelector('.js-environment-link');
+  const findClusterLink = () => vm.$el.querySelector('.js-job-cluster-link');
+
   afterEach(() => {
     vm.$destroy();
   });
 
   describe('with last deployment', () => {
     it('renders info for most recent deployment', () => {
-      vm = mountComponent(Component, {
-        deploymentStatus: {
-          status: 'last',
-          environment,
-        },
-        iconStatus: status,
+      createComponent({
+        status: 'last',
+        environment,
       });
 
-      expect(vm.$el.textContent.trim()).toEqual(
-        'This job is the most recent deployment to environment.',
+      expect(findText()).toEqual('This job is deployed to environment.');
+    });
+
+    it('renders info with cluster', () => {
+      createComponent({
+        status: 'last',
+        environment: createEnvironmentWithCluster(),
+      });
+
+      expect(findText()).toEqual(
+        `This job is deployed to environment using cluster ${TEST_CLUSTER_NAME}.`,
       );
     });
   });
@@ -43,133 +77,106 @@ describe('Environments block', () => {
   describe('with out of date deployment', () => {
     describe('with last deployment', () => {
       it('renders info for out date and most recent', () => {
-        vm = mountComponent(Component, {
-          deploymentStatus: {
-            status: 'out_of_date',
-            environment: Object.assign({}, environment, {
-              last_deployment: lastDeployment,
-            }),
-          },
-          iconStatus: status,
+        createComponent({
+          status: 'out_of_date',
+          environment: createEnvironmentWithLastDeployment(),
         });
 
-        expect(vm.$el.textContent.trim()).toEqual(
-          'This job is an out-of-date deployment to environment. View the most recent deployment #deployment.',
+        expect(findText()).toEqual(
+          'This job is an out-of-date deployment to environment. View the most recent deployment.',
         );
 
-        expect(vm.$el.querySelector('.js-job-deployment-link').getAttribute('href')).toEqual('bar');
+        expect(findJobDeploymentLink().getAttribute('href')).toEqual('bar');
+      });
+
+      it('renders info with cluster', () => {
+        createComponent({
+          status: 'out_of_date',
+          environment: createEnvironmentWithCluster(),
+        });
+
+        expect(findText()).toEqual(
+          `This job is an out-of-date deployment to environment using cluster ${TEST_CLUSTER_NAME}. View the most recent deployment.`,
+        );
       });
     });
 
     describe('without last deployment', () => {
       it('renders info about out of date deployment', () => {
-        vm = mountComponent(Component, {
-          deploymentStatus: {
-            status: 'out_of_date',
-            environment,
-          },
-          iconStatus: status,
+        createComponent({
+          status: 'out_of_date',
+          environment,
         });
 
-        expect(vm.$el.textContent.trim()).toEqual(
-          'This job is an out-of-date deployment to environment.',
-        );
+        expect(findText()).toEqual('This job is an out-of-date deployment to environment.');
       });
     });
   });
 
   describe('with failed deployment', () => {
     it('renders info about failed deployment', () => {
-      vm = mountComponent(Component, {
-        deploymentStatus: {
-          status: 'failed',
-          environment,
-        },
-        iconStatus: status,
+      createComponent({
+        status: 'failed',
+        environment,
       });
 
-      expect(vm.$el.textContent.trim()).toEqual(
-        'The deployment of this job to environment did not succeed.',
-      );
+      expect(findText()).toEqual('The deployment of this job to environment did not succeed.');
     });
   });
 
   describe('creating deployment', () => {
     describe('with last deployment', () => {
       it('renders info about creating deployment and overriding latest deployment', () => {
-        vm = mountComponent(Component, {
-          deploymentStatus: {
-            status: 'creating',
-            environment: Object.assign({}, environment, {
-              last_deployment: lastDeployment,
-            }),
-          },
-          iconStatus: status,
+        createComponent({
+          status: 'creating',
+          environment: createEnvironmentWithLastDeployment(),
         });
 
-        expect(vm.$el.textContent.trim()).toEqual(
-          'This job is creating a deployment to environment and will overwrite the latest deployment.',
+        expect(findText()).toEqual(
+          'This job is creating a deployment to environment. This will overwrite the latest deployment.',
         );
 
-        expect(vm.$el.querySelector('.js-job-deployment-link').getAttribute('href')).toEqual('bar');
+        expect(findJobDeploymentLink().getAttribute('href')).toEqual('bar');
+        expect(findEnvironmentLink().getAttribute('href')).toEqual(environment.environment_path);
+        expect(findClusterLink()).toBeNull();
       });
     });
 
     describe('without last deployment', () => {
       it('renders info about failed deployment', () => {
-        vm = mountComponent(Component, {
-          deploymentStatus: {
-            status: 'creating',
-            environment,
-          },
-          iconStatus: status,
+        createComponent({
+          status: 'creating',
+          environment,
         });
 
-        expect(vm.$el.textContent.trim()).toEqual(
-          'This job is creating a deployment to environment.',
-        );
+        expect(findText()).toEqual('This job is creating a deployment to environment.');
       });
     });
 
     describe('without environment', () => {
       it('does not render environment link', () => {
-        vm = mountComponent(Component, {
-          deploymentStatus: {
-            status: 'creating',
-            environment: null,
-          },
-          iconStatus: status,
+        createComponent({
+          status: 'creating',
+          environment: null,
         });
 
-        expect(vm.$el.querySelector('.js-environment-link')).toBeNull();
+        expect(findEnvironmentLink()).toBeNull();
       });
     });
   });
 
   describe('with a cluster', () => {
     it('renders the cluster link', () => {
-      const cluster = {
-        name: 'the-cluster',
-        path: '/the-cluster-path',
-      };
-      vm = mountComponent(Component, {
-        deploymentStatus: {
-          status: 'last',
-          environment: Object.assign({}, environment, {
-            last_deployment: {
-              ...lastDeployment,
-              cluster,
-            },
-          }),
-        },
-        iconStatus: status,
+      createComponent({
+        status: 'last',
+        environment: createEnvironmentWithCluster(),
       });
 
-      expect(vm.$el.textContent.trim()).toContain('Cluster the-cluster was used.');
-
-      expect(vm.$el.querySelector('.js-job-cluster-link').getAttribute('href')).toEqual(
-        '/the-cluster-path',
+      expect(findText()).toEqual(
+        `This job is deployed to environment using cluster ${TEST_CLUSTER_NAME}.`,
       );
+
+      expect(findClusterLink().getAttribute('href')).toEqual(TEST_CLUSTER_PATH);
     });
 
     describe('when the cluster is missing the path', () => {
@@ -177,39 +184,20 @@ describe('Environments block', () => {
         const cluster = {
           name: 'the-cluster',
         };
-        vm = mountComponent(Component, {
-          deploymentStatus: {
-            status: 'last',
-            environment: Object.assign({}, environment, {
-              last_deployment: {
-                ...lastDeployment,
-                cluster,
-              },
-            }),
-          },
-          iconStatus: status,
-        });
-
-        expect(vm.$el.textContent.trim()).toContain('Cluster the-cluster was used.');
-
-        expect(vm.$el.querySelector('.js-job-cluster-link')).toBeNull();
-      });
-    });
-  });
-
-  describe('without a cluster', () => {
-    it('does not render a cluster link', () => {
-      vm = mountComponent(Component, {
-        deploymentStatus: {
+        createComponent({
           status: 'last',
           environment: Object.assign({}, environment, {
-            last_deployment: lastDeployment,
+            last_deployment: {
+              ...lastDeployment,
+              cluster,
+            },
           }),
-        },
-        iconStatus: status,
-      });
+        });
+
+        expect(findText()).toContain('using cluster the-cluster.');
 
-      expect(vm.$el.querySelector('.js-job-cluster-link')).toBeNull();
+        expect(findClusterLink()).toBeNull();
+      });
     });
   });
 });
diff --git a/spec/javascripts/monitoring/components/dashboard_spec.js b/spec/javascripts/monitoring/components/dashboard_spec.js
index 1b2b01d1c8cd56994501e41e99c0c9bd40d4949d..75df2ce3103afe2839c00525061a4c6414c70dc2 100644
--- a/spec/javascripts/monitoring/components/dashboard_spec.js
+++ b/spec/javascripts/monitoring/components/dashboard_spec.js
@@ -4,7 +4,6 @@ import { GlToast } from '@gitlab/ui';
 import VueDraggable from 'vuedraggable';
 import MockAdapter from 'axios-mock-adapter';
 import Dashboard from '~/monitoring/components/dashboard.vue';
-import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants';
 import * as types from '~/monitoring/stores/mutation_types';
 import { createStore } from '~/monitoring/stores';
 import axios from '~/lib/utils/axios_utils';
@@ -37,6 +36,12 @@ const propsData = {
   validateQueryPath: '',
 };
 
+const resetSpy = spy => {
+  if (spy) {
+    spy.calls.reset();
+  }
+};
+
 export default propsData;
 
 describe('Dashboard', () => {
@@ -96,10 +101,15 @@ describe('Dashboard', () => {
   });
 
   describe('requests information to the server', () => {
+    let spy;
     beforeEach(() => {
       mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
     });
 
+    afterEach(() => {
+      resetSpy(spy);
+    });
+
     it('shows up a loading state', done => {
       component = new DashboardComponent({
         el: document.querySelector('.prometheus-graphs'),
@@ -272,7 +282,7 @@ describe('Dashboard', () => {
       });
     });
 
-    it('renders the time window dropdown with a set of options', done => {
+    it('renders the datetimepicker dropdown', done => {
       component = new DashboardComponent({
         el: document.querySelector('.prometheus-graphs'),
         propsData: {
@@ -282,17 +292,9 @@ describe('Dashboard', () => {
         },
         store,
       });
-      const numberOfTimeWindows = Object.keys(timeWindows).length;
 
       setTimeout(() => {
-        const timeWindowDropdown = component.$el.querySelector('.js-time-window-dropdown');
-        const timeWindowDropdownEls = component.$el.querySelectorAll(
-          '.js-time-window-dropdown .dropdown-item',
-        );
-
-        expect(timeWindowDropdown).not.toBeNull();
-        expect(timeWindowDropdownEls.length).toEqual(numberOfTimeWindows);
-
+        expect(component.$el.querySelector('.js-time-window-dropdown')).not.toBeNull();
         done();
       });
     });
@@ -355,7 +357,7 @@ describe('Dashboard', () => {
       });
     });
 
-    it('defaults to the eight hours time window for non valid url parameters', done => {
+    it('shows an error message if invalid url parameters are passed', done => {
       spyOnDependency(Dashboard, 'getParameterValues').and.returnValue([
         '<script>alert("XSS")</script>',
       ]);
@@ -366,9 +368,11 @@ describe('Dashboard', () => {
         store,
       });
 
-      Vue.nextTick(() => {
-        expect(component.selectedTimeWindowKey).toEqual(timeWindowsKeyNames.eightHours);
+      spy = spyOn(component, 'showInvalidDateError');
+      component.$mount();
 
+      component.$nextTick(() => {
+        expect(component.showInvalidDateError).toHaveBeenCalled();
         done();
       });
     });
diff --git a/spec/javascripts/monitoring/utils_spec.js b/spec/javascripts/monitoring/utils_spec.js
index 7030156931f09c49a09ce0fbd44aa956210ce36f..512dd2a0eb3e88017e8da1f7909882e1ef5ac7a3 100644
--- a/spec/javascripts/monitoring/utils_spec.js
+++ b/spec/javascripts/monitoring/utils_spec.js
@@ -1,4 +1,13 @@
-import { getTimeDiff, getTimeWindow, graphDataValidatorForValues } from '~/monitoring/utils';
+import {
+  getTimeDiff,
+  getTimeWindow,
+  graphDataValidatorForValues,
+  isDateTimePickerInputValid,
+  truncateZerosInDateTime,
+  stringToISODate,
+  ISODateToString,
+  isValidDate,
+} from '~/monitoring/utils';
 import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants';
 import { graphDataPrometheusQuery, graphDataPrometheusQueryRange } from './mock_data';
 
@@ -57,7 +66,7 @@ describe('getTimeWindow', () => {
           end: '2019-10-01T21:27:47.000Z',
         },
       ],
-      expected: timeWindowsKeyNames.eightHours,
+      expected: null,
     },
     {
       args: [
@@ -66,7 +75,7 @@ describe('getTimeWindow', () => {
           end: '',
         },
       ],
-      expected: timeWindowsKeyNames.eightHours,
+      expected: null,
     },
     {
       args: [
@@ -75,11 +84,11 @@ describe('getTimeWindow', () => {
           end: null,
         },
       ],
-      expected: timeWindowsKeyNames.eightHours,
+      expected: null,
     },
     {
       args: [{}],
-      expected: timeWindowsKeyNames.eightHours,
+      expected: null,
     },
   ].forEach(({ args, expected }) => {
     it(`returns "${expected}" with args=${JSON.stringify(args)}`, () => {
@@ -111,3 +120,190 @@ describe('graphDataValidatorForValues', () => {
     expect(validGraphData).toBe(true);
   });
 });
+
+describe('stringToISODate', () => {
+  ['', 'null', undefined, 'abc'].forEach(input => {
+    it(`throws error for invalid input like ${input}`, done => {
+      try {
+        stringToISODate(input);
+      } catch (e) {
+        expect(e).toBeDefined();
+        done();
+      }
+    });
+  });
+  [
+    {
+      input: '2019-09-09 01:01:01',
+      output: '2019-09-09T01:01:01Z',
+    },
+    {
+      input: '2019-09-09 00:00:00',
+      output: '2019-09-09T00:00:00Z',
+    },
+    {
+      input: '2019-09-09 23:59:59',
+      output: '2019-09-09T23:59:59Z',
+    },
+    {
+      input: '2019-09-09',
+      output: '2019-09-09T00:00:00Z',
+    },
+  ].forEach(({ input, output }) => {
+    it(`returns ${output} from ${input}`, () => {
+      expect(stringToISODate(input)).toBe(output);
+    });
+  });
+});
+
+describe('ISODateToString', () => {
+  [
+    {
+      input: new Date('2019-09-09T00:00:00.000Z'),
+      output: '2019-09-09 00:00:00',
+    },
+    {
+      input: new Date('2019-09-09T07:00:00.000Z'),
+      output: '2019-09-09 07:00:00',
+    },
+  ].forEach(({ input, output }) => {
+    it(`ISODateToString return ${output} for ${input}`, () => {
+      expect(ISODateToString(input)).toBe(output);
+    });
+  });
+});
+
+describe('truncateZerosInDateTime', () => {
+  [
+    {
+      input: '',
+      output: '',
+    },
+    {
+      input: '2019-10-10',
+      output: '2019-10-10',
+    },
+    {
+      input: '2019-10-10 00:00:01',
+      output: '2019-10-10 00:00:01',
+    },
+    {
+      input: '2019-10-10 00:00:00',
+      output: '2019-10-10',
+    },
+  ].forEach(({ input, output }) => {
+    it(`truncateZerosInDateTime return ${output} for ${input}`, () => {
+      expect(truncateZerosInDateTime(input)).toBe(output);
+    });
+  });
+});
+
+describe('isValidDate', () => {
+  [
+    {
+      input: '2019-09-09T00:00:00.000Z',
+      output: true,
+    },
+    {
+      input: '2019-09-09T000:00.000Z',
+      output: false,
+    },
+    {
+      input: 'a2019-09-09T000:00.000Z',
+      output: false,
+    },
+    {
+      input: '2019-09-09T',
+      output: false,
+    },
+    {
+      input: '2019-09-09',
+      output: true,
+    },
+    {
+      input: '2019-9-9',
+      output: true,
+    },
+    {
+      input: '2019-9-',
+      output: true,
+    },
+    {
+      input: '2019--',
+      output: false,
+    },
+    {
+      input: '2019',
+      output: true,
+    },
+    {
+      input: '',
+      output: false,
+    },
+    {
+      input: null,
+      output: false,
+    },
+  ].forEach(({ input, output }) => {
+    it(`isValidDate return ${output} for ${input}`, () => {
+      expect(isValidDate(input)).toBe(output);
+    });
+  });
+});
+
+describe('isDateTimePickerInputValid', () => {
+  [
+    {
+      input: null,
+      output: false,
+    },
+    {
+      input: '',
+      output: false,
+    },
+    {
+      input: 'xxxx-xx-xx',
+      output: false,
+    },
+    {
+      input: '9999-99-19',
+      output: false,
+    },
+    {
+      input: '2019-19-23',
+      output: false,
+    },
+    {
+      input: '2019-09-23',
+      output: true,
+    },
+    {
+      input: '2019-09-23 x',
+      output: false,
+    },
+    {
+      input: '2019-09-29 0:0:0',
+      output: false,
+    },
+    {
+      input: '2019-09-29 00:00:00',
+      output: true,
+    },
+    {
+      input: '2019-09-29 24:24:24',
+      output: false,
+    },
+    {
+      input: '2019-09-29 23:24:24',
+      output: true,
+    },
+    {
+      input: '2019-09-29 23:24:24 ',
+      output: false,
+    },
+  ].forEach(({ input, output }) => {
+    it(`returns ${output} for ${input}`, () => {
+      expect(isDateTimePickerInputValid(input)).toBe(output);
+    });
+  });
+});
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index 191df3cc709fef5edafa781709a9fc081b3572d0..cb6b158f01c259a12765b6cdfa55cd14bd1b3bdf 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -70,7 +70,7 @@ window.gl = window.gl || {};
 window.gl.TEST_HOST = TEST_HOST;
 window.gon = window.gon || {};
 window.gon.test_env = true;
-window.gon.ee = process.env.IS_GITLAB_EE;
+window.gon.ee = process.env.IS_EE;
 gon.relative_url_root = '';
 
 let hasUnhandledPromiseRejections = false;
@@ -118,7 +118,7 @@ const axiosDefaultAdapter = getDefaultAdapter();
 // render all of our tests
 const testContexts = [require.context('spec', true, /_spec$/)];
 
-if (process.env.IS_GITLAB_EE) {
+if (process.env.IS_EE) {
   testContexts.push(require.context('ee_spec', true, /_spec$/));
 }
 
@@ -207,7 +207,7 @@ if (process.env.BABEL_ENV === 'coverage') {
   describe('Uncovered files', function() {
     const sourceFilesContexts = [require.context('~', true, /\.(js|vue)$/)];
 
-    if (process.env.IS_GITLAB_EE) {
+    if (process.env.IS_EE) {
       sourceFilesContexts.push(require.context('ee', true, /\.(js|vue)$/));
     }
 
diff --git a/spec/lib/backup/files_spec.rb b/spec/lib/backup/files_spec.rb
index e903eada62d7e945bc9a91cfd2f4154e45e48fd6..b75f3bafeefd63c10df3fac8ab67322e33f296a0 100644
--- a/spec/lib/backup/files_spec.rb
+++ b/spec/lib/backup/files_spec.rb
@@ -24,6 +24,7 @@
 
   describe '#restore' do
     subject { described_class.new('registry', '/var/gitlab-registry') }
+
     let(:timestamp) { Time.utc(2017, 3, 22) }
 
     around do |example|
diff --git a/spec/lib/banzai/filter/project_reference_filter_spec.rb b/spec/lib/banzai/filter/project_reference_filter_spec.rb
index 927d226c400219477a2b92d52e1566375d0e40b1..d0b4542d5030418900f0afc7450ef2f198cf1ecf 100644
--- a/spec/lib/banzai/filter/project_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/project_reference_filter_spec.rb
@@ -15,6 +15,7 @@ def get_reference(project)
 
   let(:project) { create(:project, :public) }
   subject { project }
+
   let(:subject_name) { "project" }
   let(:reference) { get_reference(project) }
 
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index 6bc87d245f55855ac56b1f2299e568674dd17625..a09aeb7d7f62d5c7ca1a576e7a00d015a852435c 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -12,6 +12,7 @@ def get_reference(user)
   let(:project)   { create(:project, :public) }
   let(:user)      { create(:user) }
   subject { user }
+
   let(:subject_name) { "user" }
   let(:reference) { get_reference(user) }
 
diff --git a/spec/lib/banzai/filter/video_link_filter_spec.rb b/spec/lib/banzai/filter/video_link_filter_spec.rb
index 332817d6585b2e47f08ecfeaacebb725a6011534..a395b021f32dc6f6f0b57aadd57aa80f5dcc3aa4 100644
--- a/spec/lib/banzai/filter/video_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/video_link_filter_spec.rb
@@ -32,6 +32,7 @@ def link_to_image(path)
 
       expect(video.name).to eq 'video'
       expect(video['src']).to eq src
+      expect(video['width']).to eq "100%"
 
       expect(paragraph.name).to eq 'p'
 
diff --git a/spec/lib/banzai/reference_parser/commit_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_parser_spec.rb
index b44ae67e430d4a6b5f5ce7b459778515db81657f..eac1cf16a8f88484ef56f3dd7d212c217592dc26 100644
--- a/spec/lib/banzai/reference_parser/commit_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/commit_parser_spec.rb
@@ -8,6 +8,7 @@
   let(:project) { create(:project, :public) }
   let(:user) { create(:user) }
   subject { described_class.new(Banzai::RenderContext.new(project, user)) }
+
   let(:link) { empty_html_link }
 
   describe '#nodes_visible_to_user' do
diff --git a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
index da853233018c26f86401b8f93484441b0ea2ec59..78b337466aaa8e18b2baff1767c093ba586c0bef 100644
--- a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
@@ -8,6 +8,7 @@
   let(:project) { create(:project, :public) }
   let(:user) { create(:user) }
   subject { described_class.new(Banzai::RenderContext.new(project, user)) }
+
   let(:link) { empty_html_link }
 
   describe '#nodes_visible_to_user' do
diff --git a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
index 0f29a95bdccf0e562457c95412848a610c2c4834..9343d52e44b81abf370e3584439dcf1808cef605 100644
--- a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
@@ -8,6 +8,7 @@
   let(:project) { create(:project, :public) }
   let(:user) { create(:user) }
   subject { described_class.new(Banzai::RenderContext.new(project, user)) }
+
   let(:link) { empty_html_link }
 
   describe '#nodes_visible_to_user' do
diff --git a/spec/lib/banzai/reference_parser/label_parser_spec.rb b/spec/lib/banzai/reference_parser/label_parser_spec.rb
index cf8adb57ffc714a4863ce73004e6f73deff625e1..8b66a891e69eb916c7db9c75b688075fd67ce27f 100644
--- a/spec/lib/banzai/reference_parser/label_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/label_parser_spec.rb
@@ -9,6 +9,7 @@
   let(:user) { create(:user) }
   let(:label) { create(:label, project: project) }
   subject { described_class.new(Banzai::RenderContext.new(project, user)) }
+
   let(:link) { empty_html_link }
 
   describe '#nodes_visible_to_user' do
diff --git a/spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb b/spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1be279375bd371ca7e05c93d5e348b829468bb28
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::MentionedUserParser do
+  include ReferenceParserHelpers
+
+  let(:group) { create(:group, :private) }
+  let(:user) { create(:user) }
+  let(:new_user) { create(:user) }
+  let(:project) { create(:project, group: group, creator: user) }
+  let(:link) { empty_html_link }
+
+  subject { described_class.new(Banzai::RenderContext.new(project, new_user)) }
+
+  describe '#gather_references' do
+    context 'when the link has a data-group attribute' do
+      context 'using an existing group ID' do
+        before do
+          link['data-group'] = project.group.id.to_s
+          group.add_developer(new_user)
+        end
+
+        it 'returns empty list of users' do
+          expect(subject.gather_references([link])).to eq([])
+        end
+      end
+    end
+
+    context 'when the link has a data-project attribute' do
+      context 'using an existing project ID' do
+        before do
+          link['data-project'] = project.id.to_s
+          project.add_developer(new_user)
+        end
+
+        it 'returns empty list of users' do
+          expect(subject.gather_references([link])).to eq([])
+        end
+      end
+    end
+
+    context 'when the link has a data-user attribute' do
+      it 'returns an Array of users' do
+        link['data-user'] = user.id.to_s
+
+        expect(subject.referenced_by([link])).to eq([user])
+      end
+    end
+  end
+end
diff --git a/spec/lib/banzai/reference_parser/mentioned_users_by_group_parser_spec.rb b/spec/lib/banzai/reference_parser/mentioned_users_by_group_parser_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..99d607629ebdea8969a9ddbe908483a5c00603e0
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/mentioned_users_by_group_parser_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::MentionedUsersByGroupParser do
+  include ReferenceParserHelpers
+
+  let(:group) { create(:group, :private) }
+  let(:user) { create(:user) }
+  let(:new_user) { create(:user) }
+  let(:project) { create(:project, group: group, creator: user) }
+  let(:link) { empty_html_link }
+
+  subject { described_class.new(Banzai::RenderContext.new(project, new_user)) }
+
+  describe '#gather_references' do
+    context 'when the link has a data-group attribute' do
+      context 'using an existing group ID where user does not have access' do
+        it 'returns empty array' do
+          link['data-group'] = project.group.id.to_s
+
+          expect(subject.gather_references([link])).to eq([])
+        end
+      end
+
+      context 'using an existing group ID' do
+        before do
+          link['data-group'] = project.group.id.to_s
+          group.add_developer(new_user)
+        end
+
+        it 'returns groups' do
+          expect(subject.gather_references([link])).to eq([group])
+        end
+      end
+
+      context 'using a non-existing group ID' do
+        it 'returns an empty Array' do
+          link['data-group'] = 'test-non-existing'
+
+          expect(subject.gather_references([link])).to eq([])
+        end
+      end
+    end
+  end
+end
diff --git a/spec/lib/banzai/reference_parser/mentioned_users_by_project_parser_spec.rb b/spec/lib/banzai/reference_parser/mentioned_users_by_project_parser_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..155f2189d9ea8f33a2bcb9e4426795f437a0483b
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/mentioned_users_by_project_parser_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::MentionedUsersByProjectParser do
+  include ReferenceParserHelpers
+
+  let(:group) { create(:group, :private) }
+  let(:user) { create(:user) }
+  let(:new_user) { create(:user) }
+  let(:project) { create(:project, group: group, creator: user) }
+  let(:link) { empty_html_link }
+
+  subject { described_class.new(Banzai::RenderContext.new(project, new_user)) }
+
+  describe '#gather_references' do
+    context 'when the link has a data-project attribute' do
+      context 'using an existing project ID where user does not have access' do
+        it 'returns empty Array' do
+          link['data-project'] = project.id.to_s
+
+          expect(subject.gather_references([link])).to eq([])
+        end
+      end
+
+      context 'using an existing project ID' do
+        before do
+          link['data-project'] = project.id.to_s
+          project.add_developer(new_user)
+        end
+
+        it 'returns an Array of referenced projects' do
+          expect(subject.gather_references([link])).to eq([project])
+        end
+      end
+
+      context 'using a non-existing project ID' do
+        it 'returns an empty Array' do
+          link['data-project'] = 'inexisting-project-id'
+
+          expect(subject.gather_references([link])).to eq([])
+        end
+      end
+    end
+  end
+end
diff --git a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
index 1561dabcdbf43a633c6ddb25c67ac5a899d7f044..cb65893aea0ae3ec9b94a6798cb8f21b187e7d58 100644
--- a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
@@ -9,6 +9,7 @@
   let(:project) { create(:project, :public) }
   let(:merge_request) { create(:merge_request, source_project: project) }
   subject { described_class.new(Banzai::RenderContext.new(merge_request.target_project, user)) }
+
   let(:link) { empty_html_link }
 
   describe '#nodes_visible_to_user' do
diff --git a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
index 006f8e3769000f756e7920f3eea045a17cb222fa..25ba41dd8a0384deac6556d032b3f42f9eeb137b 100644
--- a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
@@ -9,6 +9,7 @@
   let(:user) { create(:user) }
   let(:milestone) { create(:milestone, project: project) }
   subject { described_class.new(Banzai::RenderContext.new(project, user)) }
+
   let(:link) { empty_html_link }
 
   describe '#nodes_visible_to_user' do
diff --git a/spec/lib/banzai/reference_parser/project_parser_spec.rb b/spec/lib/banzai/reference_parser/project_parser_spec.rb
index e4936aa9e5752db8d6c957722c4eb9c55acb9f29..356dde1e9c209b9c3c9331fa294be98d6c12f555 100644
--- a/spec/lib/banzai/reference_parser/project_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/project_parser_spec.rb
@@ -8,6 +8,7 @@
   let(:project) { create(:project, :public) }
   let(:user) { create(:user) }
   subject { described_class.new(Banzai::RenderContext.new(project, user)) }
+
   let(:link) { empty_html_link }
 
   describe '#referenced_by' do
diff --git a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
index 528f79ed020ebd8597b907c5ae4130b3268a609c..05dc1cb4d2d062513a870237f897c79285f31f89 100644
--- a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
@@ -12,6 +12,7 @@
   let(:project_member) { create(:user) }
 
   subject { described_class.new(Banzai::RenderContext.new(project, user)) }
+
   let(:link) { empty_html_link }
 
   def visible_references(snippet_visibility, user = nil)
diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb
index a5b4e59a3a1da2c4177abf0be0f46e7ea179ba7d..931fb1e3953fb0971d63c28dfa258113443c4bdd 100644
--- a/spec/lib/banzai/reference_parser/user_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb
@@ -9,6 +9,7 @@
   let(:user) { create(:user) }
   let(:project) { create(:project, :public, group: group, creator: user) }
   subject { described_class.new(Banzai::RenderContext.new(project, user)) }
+
   let(:link) { empty_html_link }
 
   describe '#referenced_by' do
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index 3d0d3f91859153b1762a8fd5e718ea4cccce3cca..7f7a285c45305cd671df943547afec85491b8944 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -308,8 +308,8 @@
 
       importer.execute
 
-      expect(project.issues.where(state: "closed").size).to eq(5)
-      expect(project.issues.where(state: "opened").size).to eq(2)
+      expect(project.issues.where(state_id: Issue.available_states[:closed]).size).to eq(5)
+      expect(project.issues.where(state_id: Issue.available_states[:opened]).size).to eq(2)
     end
 
     describe 'wiki import' do
diff --git a/spec/lib/gitlab/ci/ansi2json/line_spec.rb b/spec/lib/gitlab/ci/ansi2json/line_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4b5c3f9489e04d03c7e97605eb29f461ac830fb7
--- /dev/null
+++ b/spec/lib/gitlab/ci/ansi2json/line_spec.rb
@@ -0,0 +1,168 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Ansi2json::Line do
+  let(:offset) { 0 }
+  let(:style) { Gitlab::Ci::Ansi2json::Style.new }
+
+  subject { described_class.new(offset: offset, style: style) }
+
+  describe '#<<' do
+    it 'appends new data to the current segment' do
+      expect { subject << 'test 1' }.to change { subject.current_segment.text }
+      expect(subject.current_segment.text).to eq('test 1')
+
+      expect { subject << ', test 2' }.to change { subject.current_segment.text }
+      expect(subject.current_segment.text).to eq('test 1, test 2')
+    end
+  end
+
+  describe '#style' do
+    context 'when style is passed to the initializer' do
+      let(:style) { double }
+
+      it 'returns the same style' do
+        expect(subject.style).to eq(style)
+      end
+    end
+
+    context 'when style is not passed to the initializer' do
+      it 'returns the default style' do
+        expect(subject.style.set?).to be_falsey
+      end
+    end
+  end
+
+  describe '#update_style' do
+    let(:expected_style) do
+      Gitlab::Ci::Ansi2json::Style.new(
+        fg: 'term-fg-l-yellow',
+        bg: 'term-bg-blue',
+        mask: 1)
+    end
+
+    it 'sets the style' do
+      subject.update_style(%w[1 33 44])
+
+      expect(subject.style).to eq(expected_style)
+    end
+  end
+
+  describe '#add_section' do
+    it 'appends a new section to the list' do
+      subject.add_section('section_1')
+      subject.add_section('section_2')
+
+      expect(subject.sections).to eq(%w[section_1 section_2])
+    end
+  end
+
+  describe '#set_as_section_header' do
+    it 'change the section_header to true' do
+      expect { subject.set_as_section_header }
+        .to change { subject.section_header }
+        .to be_truthy
+    end
+  end
+
+  describe '#set_section_duration' do
+    it 'sets and formats the section_duration' do
+      subject.set_section_duration(75)
+
+      expect(subject.section_duration).to eq('01:15')
+    end
+  end
+
+  describe '#flush_current_segment!' do
+    context 'when current segment is not empty' do
+      before do
+        subject << 'some data'
+      end
+
+      it 'adds the segment to the list' do
+        expect { subject.flush_current_segment! }.to change { subject.segments.count }.by(1)
+
+        expect(subject.segments.map { |s| s[:text] }).to eq(['some data'])
+      end
+
+      it 'updates the current segment pointer propagating the style' do
+        previous_segment = subject.current_segment
+
+        subject.flush_current_segment!
+
+        expect(subject.current_segment).not_to eq(previous_segment)
+        expect(subject.current_segment.style).to eq(previous_segment.style)
+      end
+    end
+
+    context 'when current segment is empty' do
+      it 'does not add any segments to the list' do
+        expect { subject.flush_current_segment! }.not_to change { subject.segments.count }
+      end
+
+      it 'does not change the current segment' do
+        expect { subject.flush_current_segment! }.not_to change { subject.current_segment }
+      end
+    end
+  end
+
+  describe '#to_h' do
+    before do
+      subject << 'some data'
+      subject.update_style(['1'])
+    end
+
+    context 'when sections are present' do
+      before do
+        subject.add_section('section_1')
+        subject.add_section('section_2')
+      end
+
+      context 'when section header is set' do
+        before do
+          subject.set_as_section_header
+        end
+
+        it 'serializes the attributes set' do
+          result = {
+            offset: 0,
+            content: [{ text: 'some data', style: 'term-bold' }],
+            section: 'section_2',
+            section_header: true
+          }
+
+          expect(subject.to_h).to eq(result)
+        end
+      end
+
+      context 'when section duration is set' do
+        before do
+          subject.set_section_duration(75)
+        end
+
+        it 'serializes the attributes set' do
+          result = {
+            offset: 0,
+            content: [{ text: 'some data', style: 'term-bold' }],
+            section: 'section_2',
+            section_duration: '01:15'
+          }
+
+          expect(subject.to_h).to eq(result)
+        end
+      end
+    end
+
+    context 'when there are no sections' do
+      it 'serializes the attributes set' do
+        result = {
+          offset: 0,
+          content: [{ text: 'some data', style: 'term-bold' }]
+        }
+
+        expect(subject.to_h).to eq(result)
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/ci/ansi2json/parser_spec.rb b/spec/lib/gitlab/ci/ansi2json/parser_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e161e74c1ff2a4047f3162d7d1abc0d1b4ea1ba8
--- /dev/null
+++ b/spec/lib/gitlab/ci/ansi2json/parser_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# The rest of the specs for this class are covered in style_spec.rb
+describe Gitlab::Ci::Ansi2json::Parser do
+  subject { described_class }
+
+  describe 'bold?' do
+    it 'returns true if style mask matches bold format' do
+      expect(subject.bold?(0x01)).to be_truthy
+    end
+
+    it 'returns false if style mask does not match bold format' do
+      expect(subject.bold?(0x02)).to be_falsey
+    end
+  end
+
+  describe 'matching_formats' do
+    it 'returns matching formats given a style mask' do
+      expect(subject.matching_formats(0x01)).to eq(%w[term-bold])
+      expect(subject.matching_formats(0x03)).to eq(%w[term-bold term-italic])
+      expect(subject.matching_formats(0x07)).to eq(%w[term-bold term-italic term-underline])
+    end
+
+    it 'returns an empty array if no formats match the style mask' do
+      expect(subject.matching_formats(0)).to eq([])
+    end
+  end
+end
diff --git a/spec/lib/gitlab/ci/ansi2json/style_spec.rb b/spec/lib/gitlab/ci/ansi2json/style_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..88a0ca3585933c92831fa82ab9e59fc865beca42
--- /dev/null
+++ b/spec/lib/gitlab/ci/ansi2json/style_spec.rb
@@ -0,0 +1,166 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Ansi2json::Style do
+  describe '#set?' do
+    subject { described_class.new(params).set? }
+
+    context 'when fg color is set' do
+      let(:params) { { fg: 'term-fg-black' } }
+
+      it { is_expected.to be_truthy }
+    end
+
+    context 'when bg color is set' do
+      let(:params) { { bg: 'term-bg-black' } }
+
+      it { is_expected.to be_truthy }
+    end
+
+    context 'when mask is set' do
+      let(:params) { { mask: 0x01 } }
+
+      it { is_expected.to be_truthy }
+    end
+
+    context 'nothing is set' do
+      let(:params) { {} }
+
+      it { is_expected.to be_falsey }
+    end
+  end
+
+  describe '#reset!' do
+    let(:style) { described_class.new(fg: 'term-fg-black', bg: 'term-bg-yellow', mask: 0x01) }
+
+    it 'set the style params to default' do
+      style.reset!
+
+      expect(style.fg).to be_nil
+      expect(style.bg).to be_nil
+      expect(style.mask).to be_zero
+    end
+  end
+
+  describe 'update formats to mimic terminals' do
+    subject { described_class.new(params) }
+
+    context 'when fg color present' do
+      let(:params) { { fg: 'term-fg-black', mask: mask } }
+
+      context 'when mask is set to bold' do
+        let(:mask) { 0x01 }
+
+        it 'changes the fg color to a lighter version' do
+          expect(subject.fg).to eq('term-fg-l-black')
+        end
+      end
+
+      context 'when mask set to another format' do
+        let(:mask) { 0x02 }
+
+        it 'does not change the fg color' do
+          expect(subject.fg).to eq('term-fg-black')
+        end
+      end
+
+      context 'when mask is not set' do
+        let(:mask) { 0 }
+
+        it 'does not change the fg color' do
+          expect(subject.fg).to eq('term-fg-black')
+        end
+      end
+    end
+  end
+
+  describe '#update' do
+    where(:initial_state, :ansi_commands, :result, :description) do
+      [
+        # add format
+        [[], %w[0], '', 'does not set any style'],
+        [[], %w[1], 'term-bold', 'enables format bold'],
+        [[], %w[3], 'term-italic', 'enables format italic'],
+        [[], %w[4], 'term-underline', 'enables format underline'],
+        [[], %w[8], 'term-conceal', 'enables format conceal'],
+        [[], %w[9], 'term-cross', 'enables format cross'],
+        # remove format
+        [%w[1], %w[21], '', 'disables format bold'],
+        [%w[1 3], %w[21], 'term-italic', 'disables format bold and leaves italic'],
+        [%w[1], %w[22], '', 'disables format bold using command 22'],
+        [%w[1 3], %w[22], 'term-italic', 'disables format bold and leaves italic using command 22'],
+        [%w[3], %w[23], '', 'disables format italic'],
+        [%w[1 3], %w[23], 'term-bold', 'disables format italic and leaves bold'],
+        [%w[4], %w[24], '', 'disables format underline'],
+        [%w[1 4], %w[24], 'term-bold', 'disables format underline and leaves bold'],
+        [%w[8], %w[28], '', 'disables format conceal'],
+        [%w[1 8], %w[28], 'term-bold', 'disables format conceal and leaves bold'],
+        [%w[9], %w[29], '', 'disables format cross'],
+        [%w[1 9], %w[29], 'term-bold', 'disables format cross and leaves bold'],
+        # set fg color
+        [[], %w[30], 'term-fg-black', 'sets fg color black'],
+        [[], %w[31], 'term-fg-red', 'sets fg color red'],
+        [[], %w[32], 'term-fg-green', 'sets fg color green'],
+        [[], %w[33], 'term-fg-yellow', 'sets fg color yellow'],
+        [[], %w[34], 'term-fg-blue', 'sets fg color blue'],
+        [[], %w[35], 'term-fg-magenta', 'sets fg color magenta'],
+        [[], %w[36], 'term-fg-cyan', 'sets fg color cyan'],
+        [[], %w[37], 'term-fg-white', 'sets fg color white'],
+        # sets xterm fg color
+        [[], %w[38 5 1], 'xterm-fg-1', 'sets xterm fg color 1'],
+        [[], %w[38 5 2], 'xterm-fg-2', 'sets xterm fg color 2'],
+        [[], %w[38 1], 'term-bold', 'ignores 38 command if not followed by 5 and sets format bold'],
+        # set bg color
+        [[], %w[40], 'term-bg-black', 'sets bg color black'],
+        [[], %w[41], 'term-bg-red', 'sets bg color red'],
+        [[], %w[42], 'term-bg-green', 'sets bg color green'],
+        [[], %w[43], 'term-bg-yellow', 'sets bg color yellow'],
+        [[], %w[44], 'term-bg-blue', 'sets bg color blue'],
+        [[], %w[45], 'term-bg-magenta', 'sets bg color magenta'],
+        [[], %w[46], 'term-bg-cyan', 'sets bg color cyan'],
+        [[], %w[47], 'term-bg-white', 'sets bg color white'],
+        # set xterm bg color
+        [[], %w[48 5 1], 'xterm-bg-1', 'sets xterm bg color 1'],
+        [[], %w[48 5 2], 'xterm-bg-2', 'sets xterm bg color 2'],
+        [[], %w[48 1], 'term-bold', 'ignores 48 command if not followed by 5 and sets format bold'],
+        # set light fg color
+        [[], %w[90], 'term-fg-l-black', 'sets fg color light black'],
+        [[], %w[91], 'term-fg-l-red', 'sets fg color light red'],
+        [[], %w[92], 'term-fg-l-green', 'sets fg color light green'],
+        [[], %w[93], 'term-fg-l-yellow', 'sets fg color light yellow'],
+        [[], %w[94], 'term-fg-l-blue', 'sets fg color light blue'],
+        [[], %w[95], 'term-fg-l-magenta', 'sets fg color light magenta'],
+        [[], %w[96], 'term-fg-l-cyan', 'sets fg color light cyan'],
+        [[], %w[97], 'term-fg-l-white', 'sets fg color light white'],
+        # set light bg color
+        [[], %w[100], 'term-bg-l-black', 'sets bg color light black'],
+        [[], %w[101], 'term-bg-l-red', 'sets bg color light red'],
+        [[], %w[102], 'term-bg-l-green', 'sets bg color light green'],
+        [[], %w[103], 'term-bg-l-yellow', 'sets bg color light yellow'],
+        [[], %w[104], 'term-bg-l-blue', 'sets bg color light blue'],
+        [[], %w[105], 'term-bg-l-magenta', 'sets bg color light magenta'],
+        [[], %w[106], 'term-bg-l-cyan', 'sets bg color light cyan'],
+        [[], %w[107], 'term-bg-l-white', 'sets bg color light white'],
+        # reset
+        [%w[1], %w[0], '', 'resets style from format bold'],
+        [%w[1 3], %w[0], '', 'resets style from format bold and italic'],
+        [%w[1 3 term-fg-l-red term-bg-yellow], %w[0], '', 'resets all formats and colors'],
+        # misc
+        [[], %w[1 30 42 3], 'term-fg-l-black term-bg-green term-bold term-italic', 'adds fg color, bg color and formats from no style'],
+        [%w[3 31], %w[23 1 43], 'term-fg-l-red term-bg-yellow term-bold', 'replaces format italic with bold and adds a yellow background']
+      ]
+    end
+
+    with_them do
+      it 'change the style' do
+        style = described_class.new
+        style.update(initial_state)
+
+        style.update(ansi_commands)
+
+        expect(style.to_s).to eq(result)
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/ci/ansi2json_spec.rb b/spec/lib/gitlab/ci/ansi2json_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3c6bc46436bd638348b0ae21436d2f2209f450a0
--- /dev/null
+++ b/spec/lib/gitlab/ci/ansi2json_spec.rb
@@ -0,0 +1,544 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Ansi2json do
+  subject { described_class }
+
+  describe 'lines' do
+    it 'prints non-ansi as-is' do
+      expect(convert_json('Hello')).to eq([
+        { offset: 0, content: [{ text: 'Hello' }] }
+      ])
+    end
+
+    it 'adds new line in a separate element' do
+      expect(convert_json("Hello\nworld")).to eq([
+        { offset: 0, content: [{ text: 'Hello' }] },
+        { offset: 6, content: [{ text: 'world' }] }
+      ])
+    end
+
+    it 'recognizes color changing ANSI sequences' do
+      expect(convert_json("\e[31mHello\e[0m")).to eq([
+        { offset: 0, content: [{ text: 'Hello', style: 'term-fg-red' }] }
+      ])
+    end
+
+    it 'recognizes color changing ANSI sequences across multiple lines' do
+      expect(convert_json("\e[31mHello\nWorld\e[0m")).to eq([
+        { offset: 0, content: [{ text: 'Hello', style: 'term-fg-red' }] },
+        { offset: 11, content: [{ text: 'World', style: 'term-fg-red' }] }
+      ])
+    end
+
+    it 'recognizes background and foreground colors' do
+      expect(convert_json("\e[31;44mHello")).to eq([
+        { offset: 0, content: [{ text: 'Hello', style: 'term-fg-red term-bg-blue' }] }
+      ])
+    end
+
+    it 'recognizes style changes within the same line' do
+      expect(convert_json("\e[31;44mHello\e[0m world")).to eq([
+        { offset: 0, content: [
+          { text: 'Hello', style: 'term-fg-red term-bg-blue' },
+          { text: ' world' }
+        ] }
+      ])
+    end
+
+    context 'with section markers' do
+      let(:section_name) { 'prepare-script' }
+      let(:section_duration) { 63.seconds }
+      let(:section_start_time) { Time.new(2019, 9, 17).utc }
+      let(:section_end_time) { section_start_time + section_duration }
+      let(:section_start) { "section_start:#{section_start_time.to_i}:#{section_name}\r\033[0K"}
+      let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K"}
+
+      it 'marks the first line of the section as header' do
+        expect(convert_json("Hello#{section_start}world!")).to eq([
+          {
+            offset: 0,
+            content: [{ text: 'Hello' }]
+          },
+          {
+            offset: 5,
+            content: [{ text: 'world!' }],
+            section: 'prepare-script',
+            section_header: true
+          }
+        ])
+      end
+
+      it 'does not marks the other lines of the section as header' do
+        expect(convert_json("outside section#{section_start}Hello\nworld!")).to eq([
+          {
+            offset: 0,
+            content: [{ text: 'outside section' }]
+          },
+          {
+            offset: 15,
+            content: [{ text: 'Hello' }],
+            section: 'prepare-script',
+            section_header: true
+          },
+          {
+            offset: 65,
+            content: [{ text: 'world!' }],
+            section: 'prepare-script'
+          }
+        ])
+      end
+
+      it 'marks the last line of the section as footer' do
+        expect(convert_json("#{section_start}Good\nmorning\nworld!#{section_end}")).to eq([
+          {
+            offset: 0,
+            content: [{ text: 'Good' }],
+            section: 'prepare-script',
+            section_header: true
+          },
+          {
+            offset: 49,
+            content: [{ text: 'morning' }],
+            section: 'prepare-script'
+          },
+          {
+            offset: 57,
+            content: [{ text: 'world!' }],
+            section: 'prepare-script'
+          },
+          {
+            offset: 63,
+            content: [],
+            section_duration: '01:03',
+            section: 'prepare-script'
+          },
+          {
+            offset: 63,
+            content: []
+          }
+        ])
+      end
+
+      it 'marks the first line as header and footer if is the only line in the section' do
+        expect(convert_json("#{section_start}Hello world!#{section_end}")).to eq([
+          {
+            offset: 0,
+            content: [{ text: 'Hello world!' }],
+            section: 'prepare-script',
+            section_header: true
+          },
+          {
+            offset: 56,
+            content: [],
+            section: 'prepare-script',
+            section_duration: '01:03'
+          },
+          {
+            offset: 56,
+            content: []
+          }
+        ])
+      end
+
+      it 'does not add sections attribute to lines after the section is closed' do
+        expect(convert_json("#{section_start}Hello#{section_end}world")).to eq([
+          {
+            offset: 0,
+            content: [{ text: 'Hello' }],
+            section: 'prepare-script',
+            section_header: true
+          },
+          {
+            offset: 49,
+            content: [],
+            section: 'prepare-script',
+            section_duration: '01:03'
+          },
+          {
+            offset: 49,
+            content: [{ text: 'world' }]
+          }
+        ])
+      end
+
+      it 'ignores section_end marker if no section_start exists' do
+        expect(convert_json("Hello #{section_end}world")).to eq([
+          {
+            offset: 0,
+            content: [{ text: 'Hello world' }]
+          }
+        ])
+      end
+
+      context 'when section name contains .-_ and capital letters' do
+        let(:section_name) { 'a.Legit-SeCtIoN_namE' }
+
+        it 'sanitizes the section name' do
+          expect(convert_json("Hello#{section_start}world!")).to eq([
+            {
+              offset: 0,
+              content: [{ text: 'Hello' }]
+            },
+            {
+              offset: 5,
+              content: [{ text: 'world!' }],
+              section: 'a-legit-section-name',
+              section_header: true
+            }
+          ])
+        end
+      end
+
+      context 'when section name includes $' do
+        let(:section_name) { 'my_$ection' }
+
+        it 'ignores the section' do
+          expect(convert_json("#{section_start}hello")).to eq([
+            {
+              offset: 0,
+              content: [{ text: "#{section_start.gsub("\033[0K", '')}hello" }]
+            }
+          ])
+        end
+      end
+
+      context 'when section name includes <' do
+        let(:section_name) { '<a_tag>' }
+
+        it 'ignores the section' do
+          expect(convert_json("#{section_start}hello")).to eq([
+            {
+              offset: 0,
+              content: [{ text: "#{section_start.gsub("\033[0K", '').gsub('<', '&lt;')}hello" }]
+            }
+          ])
+        end
+      end
+
+      it 'prevents XSS injection' do
+        trace = "#{section_start}section_end:1:2<script>alert('XSS Hack!');</script>#{section_end}"
+        expect(convert_json(trace)).to eq([
+          {
+            offset: 0,
+            content: [{ text: "section_end:1:2&lt;script>alert('XSS Hack!');&lt;/script>" }],
+            section: 'prepare-script',
+            section_header: true
+          },
+          {
+            offset: 95,
+            content: [],
+            section: 'prepare-script',
+            section_duration: '01:03'
+          },
+          {
+            offset: 95,
+            content: []
+          }
+        ])
+      end
+
+      context 'with nested section' do
+        let(:nested_section_name) { 'prepare-script-nested' }
+        let(:nested_section_duration) { 2.seconds }
+        let(:nested_section_start_time) { Time.new(2019, 9, 17).utc }
+        let(:nested_section_end_time) { nested_section_start_time + nested_section_duration }
+        let(:nested_section_start) { "section_start:#{nested_section_start_time.to_i}:#{nested_section_name}\r\033[0K"}
+        let(:nested_section_end) { "section_end:#{nested_section_end_time.to_i}:#{nested_section_name}\r\033[0K"}
+
+        it 'adds multiple sections to the lines inside the nested section' do
+          trace = "Hello#{section_start}foo#{nested_section_start}bar#{nested_section_end}baz#{section_end}world"
+
+          expect(convert_json(trace)).to eq([
+              {
+                offset: 0,
+                content: [{ text: 'Hello' }]
+              },
+              {
+                offset: 5,
+                content: [{ text: 'foo' }],
+                section: 'prepare-script',
+                section_header: true
+              },
+              {
+                offset: 52,
+                content: [{ text: 'bar' }],
+                section: 'prepare-script-nested',
+                section_header: true
+              },
+              {
+                offset: 106,
+                content: [],
+                section: 'prepare-script-nested',
+                section_duration: '00:02'
+              },
+              {
+                offset: 106,
+                content: [{ text: 'baz' }],
+                section: 'prepare-script'
+              },
+              {
+                offset: 158,
+                content: [],
+                section: 'prepare-script',
+                section_duration: '01:03'
+              },
+              {
+                offset: 158,
+                content: [{ text: 'world' }]
+              }
+            ])
+        end
+
+        it 'adds multiple sections to the lines inside the nested section and closes all sections together' do
+          trace = "Hello#{section_start}\e[91mfoo\e[0m#{nested_section_start}bar#{nested_section_end}#{section_end}"
+
+          expect(convert_json(trace)).to eq([
+              {
+                offset: 0,
+                content: [{ text: 'Hello' }]
+              },
+              {
+                offset: 5,
+                content: [{ text: 'foo', style: 'term-fg-l-red' }],
+                section: 'prepare-script',
+                section_header: true
+              },
+              {
+                offset: 61,
+                content: [{ text: 'bar' }],
+                section: 'prepare-script-nested',
+                section_header: true
+              },
+              {
+                offset: 115,
+                content: [],
+                section: 'prepare-script-nested',
+                section_duration: '00:02'
+              },
+              {
+                offset: 115,
+                content: [],
+                section: 'prepare-script',
+                section_duration: '01:03'
+              },
+              {
+                offset: 164,
+                content: []
+              }
+            ])
+        end
+      end
+    end
+
+    describe 'incremental updates' do
+      let(:pass1_stream) { StringIO.new(pre_text) }
+      let(:pass2_stream) { StringIO.new(pre_text + text) }
+      let(:pass1) { subject.convert(pass1_stream) }
+      let(:pass2) { subject.convert(pass2_stream, pass1.state) }
+
+      context 'with split word' do
+        let(:pre_text) { "\e[1mHello " }
+        let(:text) { "World" }
+
+        let(:lines) do
+          [
+            { offset: 0, content: [{ text: 'Hello World', style: 'term-bold' }] }
+          ]
+        end
+
+        it 'returns the full line' do
+          expect(pass2.lines).to eq(lines)
+          expect(pass2.append).to be_falsey
+        end
+      end
+
+      context 'with split word on second line' do
+        let(:pre_text) { "Good\nmorning " }
+        let(:text) { "World" }
+
+        let(:lines) do
+          [
+            { offset: 5, content: [{ text: 'morning World' }] }
+          ]
+        end
+
+        it 'returns all lines since last partially processed line' do
+          expect(pass2.lines).to eq(lines)
+          expect(pass2.append).to be_truthy
+        end
+      end
+
+      context 'with split sequence across multiple lines' do
+        let(:pre_text) { "\e[1mgood\nmorning\n" }
+        let(:text) { "\e[3mworld" }
+
+        let(:lines) do
+          [
+            { offset: 17, content: [{ text: 'world', style: 'term-bold term-italic' }] }
+          ]
+        end
+
+        it 'returns the full line' do
+          expect(pass2.lines).to eq(lines)
+          expect(pass2.append).to be_truthy
+        end
+      end
+
+      context 'with split partial sequence' do
+        let(:pre_text) { "hello\e" }
+        let(:text) { "[1m world" }
+
+        let(:lines) do
+          [
+            { offset: 0, content: [
+              { text: 'hello' },
+              { text: ' world', style: 'term-bold' }
+            ] }
+          ]
+        end
+
+        it 'returns the full line' do
+          expect(pass2.lines).to eq(lines)
+          expect(pass2.append).to be_falsey
+        end
+      end
+
+      context 'with split new line' do
+        let(:pre_text) { "hello\r" }
+        let(:text) { "\nworld" }
+
+        let(:lines) do
+          [
+            { offset: 0, content: [{ text: 'hello' }] },
+            { offset: 7, content: [{ text: 'world' }] }
+          ]
+        end
+
+        it 'returns the full line' do
+          expect(pass2.lines).to eq(lines)
+          expect(pass2.append).to be_falsey
+        end
+      end
+
+      context 'with split section' do
+        let(:section_name) { 'prepare-script' }
+        let(:section_duration) { 63.seconds }
+        let(:section_start_time) { Time.new(2019, 9, 17).utc }
+        let(:section_end_time) { section_start_time + section_duration }
+        let(:section_start) { "section_start:#{section_start_time.to_i}:#{section_name}\r\033[0K"}
+        let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K"}
+
+        context 'with split section body' do
+          let(:pre_text) { "#{section_start}this is a header\nand " }
+          let(:text) { "this\n is a body" }
+
+          let(:lines) do
+            [
+              {
+                offset: 61,
+                content: [{ text: 'and this' }],
+                section: 'prepare-script'
+              },
+              {
+                offset: 70,
+                content: [{ text: ' is a body' }],
+                section: 'prepare-script'
+              }
+            ]
+          end
+
+          it 'returns the full line' do
+            expect(pass2.lines).to eq(lines)
+            expect(pass2.append).to be_truthy
+          end
+        end
+
+        context 'with split section where header is also split' do
+          let(:pre_text) { "#{section_start}this is " }
+          let(:text) { "a header\nand body" }
+
+          let(:lines) do
+            [
+              {
+                offset: 0,
+                content: [{ text: 'this is a header' }],
+                section: 'prepare-script',
+                section_header: true
+              },
+              {
+                offset: 61,
+                content: [{ text: 'and body' }],
+                section: 'prepare-script'
+              }
+            ]
+          end
+
+          it 'returns the full line' do
+            expect(pass2.lines).to eq(lines)
+            expect(pass2.append).to be_falsey
+          end
+        end
+
+        context 'with split section end' do
+          let(:pre_text) { "#{section_start}this is a header\nthe" }
+          let(:text) { " body\nthe end#{section_end}" }
+
+          let(:lines) do
+            [
+              {
+                offset: 61,
+                content: [{ text: 'the body' }],
+                section: 'prepare-script'
+              },
+              {
+                offset: 70,
+                content: [{ text: 'the end' }],
+                section: 'prepare-script'
+              },
+              {
+                offset: 77,
+                content: [],
+                section: 'prepare-script',
+                section_duration: '01:03'
+              },
+              {
+                offset: 77,
+                content: []
+              }
+            ]
+          end
+
+          it 'returns the full line' do
+            expect(pass2.lines).to eq(lines)
+            expect(pass2.append).to be_truthy
+          end
+        end
+      end
+    end
+
+    describe 'trucates' do
+      let(:text) { "Hello World" }
+      let(:stream) { StringIO.new(text) }
+      let(:subject) { described_class.convert(stream) }
+
+      before do
+        stream.seek(3, IO::SEEK_SET)
+      end
+
+      it "returns truncated output" do
+        expect(subject.truncated).to be_truthy
+      end
+
+      it "does not append output" do
+        expect(subject.append).to be_falsey
+      end
+    end
+
+    def convert_json(data)
+      stream = StringIO.new(data)
+      subject.convert(stream).lines
+    end
+  end
+end
diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
index 24d17eb0fb390964fcf05d521144dd1878811485..73c3cad88bcc48cdaebe762415bf090628bad670 100644
--- a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
+++ b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
@@ -34,27 +34,32 @@ def entry(path)
 
     describe '#basename' do
       subject { |example| path(example).basename }
+
       it { is_expected.to eq 'absolute_path' }
     end
   end
 
   describe 'path/dir_1/', path: 'path/dir_1/' do
     subject { |example| path(example) }
+
     it { is_expected.to have_parent }
     it { is_expected.to be_directory }
 
     describe '#basename' do
       subject { |example| path(example).basename }
+
       it { is_expected.to eq 'dir_1/' }
     end
 
     describe '#name' do
       subject { |example| path(example).name }
+
       it { is_expected.to eq 'dir_1' }
     end
 
     describe '#parent' do
       subject { |example| path(example).parent }
+
       it { is_expected.to eq entry('path/') }
     end
 
@@ -102,21 +107,25 @@ def entry(path)
 
       describe '#nodes' do
         subject { |example| path(example).nodes }
+
         it { is_expected.to eq 2 }
       end
 
       describe '#exists?' do
         subject { |example| path(example).exists? }
+
         it { is_expected.to be true }
       end
 
       describe '#empty?' do
         subject { |example| path(example).empty? }
+
         it { is_expected.to be false }
       end
 
       describe '#total_size' do
         subject { |example| path(example).total_size }
+
         it { is_expected.to eq(30) }
       end
     end
@@ -124,10 +133,12 @@ def entry(path)
 
   describe 'empty path', path: '' do
     subject { |example| path(example) }
+
     it { is_expected.not_to have_parent }
 
     describe '#children' do
       subject { |example| path(example).children }
+
       it { expect(subject.count).to eq 3 }
     end
   end
@@ -135,6 +146,7 @@ def entry(path)
   describe 'path/dir_1/subdir/subfile', path: 'path/dir_1/subdir/subfile' do
     describe '#nodes' do
       subject { |example| path(example).nodes }
+
       it { is_expected.to eq 4 }
     end
 
@@ -153,11 +165,13 @@ def entry(path)
   describe 'non-existent/', path: 'non-existent/' do
     describe '#empty?' do
       subject { |example| path(example).empty? }
+
       it { is_expected.to be true }
     end
 
     describe '#exists?' do
       subject { |example| path(example).exists? }
+
       it { is_expected.to be false }
     end
   end
@@ -165,6 +179,7 @@ def entry(path)
   describe 'another_directory/', path: 'another_directory/' do
     describe '#empty?' do
       subject { |example| path(example).empty? }
+
       it { is_expected.to be true }
     end
   end
diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
index ff189c4701e90043b077d817d59abba44ea72891..bfa65c66b3322d52a9b1ddb888b962d674024a44 100644
--- a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
+++ b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
@@ -76,21 +76,25 @@ def metadata(path = '', **opts)
 
     describe '#to_entry' do
       subject { metadata('').to_entry }
+
       it { is_expected.to be_an_instance_of(Gitlab::Ci::Build::Artifacts::Metadata::Entry) }
     end
 
     describe '#full_version' do
       subject { metadata('').full_version }
+
       it { is_expected.to eq 'GitLab Build Artifacts Metadata 0.0.1' }
     end
 
     describe '#version' do
       subject { metadata('').version }
+
       it { is_expected.to eq '0.0.1' }
     end
 
     describe '#errors' do
       subject { metadata('').errors }
+
       it { is_expected.to eq({}) }
     end
   end
diff --git a/spec/lib/gitlab/ci/config/edge_stages_injector_spec.rb b/spec/lib/gitlab/ci/config/edge_stages_injector_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..042f9b591b62a8816b444920d3daead0a43e81e7
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/edge_stages_injector_spec.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::Ci::Config::EdgeStagesInjector do
+  describe '#call' do
+    subject { described_class.new(config).to_hash }
+
+    context 'without stages' do
+      let(:config) do
+        {
+          test: { script: 'test' }
+        }
+      end
+
+      it { is_expected.to match config }
+    end
+
+    context 'with values' do
+      let(:config) do
+        {
+          stages: %w[stage1 stage2],
+          test: { script: 'test' }
+        }
+      end
+
+      let(:expected_stages) do
+        %w[.pre stage1 stage2 .post]
+      end
+
+      it { is_expected.to match(config.merge(stages: expected_stages)) }
+    end
+
+    context 'with bad values' do
+      let(:config) do
+        {
+          stages: 'stage1',
+          test: { script: 'test' }
+        }
+      end
+
+      it { is_expected.to match(config) }
+    end
+
+    context 'with collision values' do
+      let(:config) do
+        {
+          stages: %w[.post stage1 .pre .post stage2],
+          test: { script: 'test' }
+        }
+      end
+
+      let(:expected_stages) do
+        %w[.pre stage1 stage2 .post]
+      end
+
+      it { is_expected.to match(config.merge(stages: expected_stages)) }
+    end
+
+    context 'with types' do
+      let(:config) do
+        {
+          types: %w[stage1 stage2],
+          test: { script: 'test' }
+        }
+      end
+
+      let(:expected_config) do
+        {
+          types: %w[.pre stage1 stage2 .post],
+          test: { script: 'test' }
+        }
+      end
+
+      it { is_expected.to match expected_config }
+    end
+
+    context 'with types' do
+      let(:config) do
+        {
+          types: %w[.post stage1 .pre .post stage2],
+          test: { script: 'test' }
+        }
+      end
+
+      let(:expected_config) do
+        {
+          types: %w[.pre stage1 stage2 .post],
+          test: { script: 'test' }
+        }
+      end
+
+      it { is_expected.to match expected_config }
+    end
+  end
+
+  describe '.wrap_stages' do
+    subject { described_class.wrap_stages(stages) }
+
+    context 'with empty value' do
+      let(:stages) {}
+
+      it { is_expected.to eq %w[.pre .post] }
+    end
+
+    context 'with values' do
+      let(:stages) { %w[s1 .pre] }
+
+      it { is_expected.to eq %w[.pre s1 .post] }
+    end
+  end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb
index a7f457e0f5e7f920f2169abee4dce0720b6c8b53..513a9b8f2b4cf05916cc88e19bbff3028c85cf6f 100644
--- a/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb
@@ -28,6 +28,14 @@
           expect(entry.value).to eq config
         end
       end
+
+      context "when value includes 'expose_as' keyword" do
+        let(:config) { { paths: %w[results.txt], expose_as: "Test results" } }
+
+        it 'returns general artifact and report-type artifacts configuration' do
+          expect(entry.value).to eq config
+        end
+      end
     end
 
     context 'when entry value is not correct' do
@@ -58,6 +66,84 @@
               .to include 'artifacts reports should be a hash'
           end
         end
+
+        context "when 'expose_as' is not a string" do
+          let(:config) { { paths: %w[results.txt], expose_as: 1 } }
+
+          it 'reports error' do
+            expect(entry.errors)
+              .to include 'artifacts expose as should be a string'
+          end
+        end
+
+        context "when 'expose_as' is too long" do
+          let(:config) { { paths: %w[results.txt], expose_as: 'A' * 101 } }
+
+          it 'reports error' do
+            expect(entry.errors)
+              .to include 'artifacts expose as is too long (maximum is 100 characters)'
+          end
+        end
+
+        context "when 'expose_as' is an empty string" do
+          let(:config) { { paths: %w[results.txt], expose_as: '' } }
+
+          it 'reports error' do
+            expect(entry.errors)
+              .to include 'artifacts expose as ' + Gitlab::Ci::Config::Entry::Artifacts::EXPOSE_AS_ERROR_MESSAGE
+          end
+        end
+
+        context "when 'expose_as' contains invalid characters" do
+          let(:config) do
+            { paths: %w[results.txt], expose_as: '<script>alert("xss");</script>' }
+          end
+
+          it 'reports error' do
+            expect(entry.errors)
+              .to include 'artifacts expose as ' + Gitlab::Ci::Config::Entry::Artifacts::EXPOSE_AS_ERROR_MESSAGE
+          end
+        end
+
+        context "when 'expose_as' is used without 'paths'" do
+          let(:config) { { expose_as: 'Test results' } }
+
+          it 'reports error' do
+            expect(entry.errors)
+              .to include "artifacts paths can't be blank"
+          end
+        end
+
+        context "when 'paths' includes '*' and 'expose_as' is defined" do
+          let(:config) { { expose_as: 'Test results', paths: ['test.txt', 'test*.txt'] } }
+
+          it 'reports error' do
+            expect(entry.errors)
+              .to include "artifacts paths can't contain '*' when used with 'expose_as'"
+          end
+        end
+      end
+
+      context 'when feature flag :ci_expose_arbitrary_artifacts_in_mr is disabled' do
+        before do
+          stub_feature_flags(ci_expose_arbitrary_artifacts_in_mr: false)
+        end
+
+        context 'when syntax is correct' do
+          let(:config) { { expose_as: 'Test results', paths: ['test.txt'] } }
+
+          it 'is valid' do
+            expect(entry.errors).to be_empty
+          end
+        end
+
+        context 'when syntax for :expose_as is incorrect' do
+          let(:config) { { paths: %w[results.txt], expose_as: '' } }
+
+          it 'is valid' do
+            expect(entry.errors).to be_empty
+          end
+        end
       end
     end
   end
diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
index 4cb63168ec7f9e6141fcae9344d5e8351f01ff02..9aab3664e1c3ad59ecdf3771128d01eae5a40c3a 100644
--- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
@@ -69,6 +69,7 @@
     context 'when entry value is not correct' do
       describe '#errors' do
         subject { entry.errors }
+
         context 'when is not a hash' do
           let(:config) { 'ls' }
 
diff --git a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb
index 48d0864cfca4724acb5e5555502b861e90c7ccf8..877e3ec621687847e11393de95af0679b1465bf0 100644
--- a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb
@@ -11,11 +11,13 @@
 
       describe '#errors' do
         subject { entry.errors }
+
         it { is_expected.to include(/coverage config must be a regular expression/) }
       end
 
       describe '#valid?' do
         subject { entry }
+
         it { is_expected.not_to be_valid }
       end
     end
@@ -25,16 +27,19 @@
 
       describe '#value' do
         subject { entry.value }
+
         it { is_expected.to eq(config[1...-1]) }
       end
 
       describe '#errors' do
         subject { entry.errors }
+
         it { is_expected.to be_empty }
       end
 
       describe '#valid?' do
         subject { entry }
+
         it { is_expected.to be_valid }
       end
     end
@@ -44,11 +49,13 @@
 
       describe '#errors' do
         subject { entry.errors }
+
         it { is_expected.to include(/coverage config must be a regular expression/) }
       end
 
       describe '#valid?' do
         subject { entry }
+
         it { is_expected.not_to be_valid }
       end
     end
diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb
index 968dbb9c7f2cb61a60104b512382b261867259ad..7e1a80414d42e8de435c5d51a0d43fb13ad03169 100644
--- a/spec/lib/gitlab/ci/config/entry/root_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb
@@ -215,7 +215,7 @@
 
       describe '#stages_value' do
         it 'returns an array of root stages' do
-          expect(root.stages_value).to eq %w[build test deploy]
+          expect(root.stages_value).to eq %w[.pre build test deploy .post]
         end
       end
 
diff --git a/spec/lib/gitlab/ci/config/entry/stages_spec.rb b/spec/lib/gitlab/ci/config/entry/stages_spec.rb
index 97970522104342e5c04c204ed19f4019baf43204..3e6ff8eca28a43f02b06924372a57344c26e8449 100644
--- a/spec/lib/gitlab/ci/config/entry/stages_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/stages_spec.rb
@@ -42,7 +42,7 @@
 
   describe '.default' do
     it 'returns default stages' do
-      expect(described_class.default).to eq %w[build test deploy]
+      expect(described_class.default).to eq %w[.pre build test deploy .post]
     end
   end
 end
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
index 68c38644b5c76904ef6a20852658ffb2a8028e7e..b254f9af2f125d88c6e487af78a122e8c42fe51d 100644
--- a/spec/lib/gitlab/ci/config_spec.rb
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -51,6 +51,54 @@
         end
       end
     end
+
+    describe '#stages' do
+      subject(:subject) { config.stages }
+
+      context 'with default stages' do
+        let(:default_stages) do
+          %w[.pre build test deploy .post]
+        end
+
+        it { is_expected.to eq default_stages }
+      end
+
+      context 'with custom stages' do
+        let(:yml) do
+          <<-EOS
+            stages:
+              - stage1
+              - stage2
+            job1:
+              stage: stage1
+              script:
+                - ls
+          EOS
+        end
+
+        it { is_expected.to eq %w[.pre stage1 stage2 .post] }
+      end
+
+      context 'with feature disabled' do
+        before do
+          stub_feature_flags(ci_pre_post_pipeline_stages: false)
+        end
+
+        let(:yml) do
+          <<-EOS
+            stages:
+              - stage1
+              - stage2
+            job1:
+              stage: stage1
+              script:
+                - ls
+          EOS
+        end
+
+        it { is_expected.to eq %w[stage1 stage2] }
+      end
+    end
   end
 
   context 'when using extendable hash' do
diff --git a/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb
index 92ad20f30a05e74521957f6b46a0984671d8f56e..71389999c6e4e8ee8937921d656c5d23d1a544ca 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb
@@ -23,9 +23,9 @@
         }
       end
 
-      it 'returns an environment object' do
+      it 'returns a persisted environment object' do
         expect(subject).to be_a(Environment)
-        expect(subject).not_to be_persisted
+        expect(subject).to be_persisted
         expect(subject.project).to eq(project)
         expect(subject.name).to eq('production')
       end
diff --git a/spec/lib/gitlab/ci/status/external/factory_spec.rb b/spec/lib/gitlab/ci/status/external/factory_spec.rb
index 3b90fb60cca459771f00975955512e64a9a41ab1..9d7dfc4284889be438dd1db4041f395f9118825b 100644
--- a/spec/lib/gitlab/ci/status/external/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/external/factory_spec.rb
@@ -22,7 +22,7 @@
         end
 
         let(:expected_status) do
-          Gitlab::Ci::Status.const_get(simple_status.capitalize)
+          Gitlab::Ci::Status.const_get(simple_status.capitalize, false)
         end
 
         it "fabricates a core status #{simple_status}" do
diff --git a/spec/lib/gitlab/ci/status/factory_spec.rb b/spec/lib/gitlab/ci/status/factory_spec.rb
index b51c0bec47ec8a753537bb58f1cb2b0930821f84..c6d7a1ec5d91e9640286c887ce316f6784aaf1be 100644
--- a/spec/lib/gitlab/ci/status/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/factory_spec.rb
@@ -13,7 +13,7 @@
         let(:resource) { double('resource', status: simple_status) }
 
         let(:expected_status) do
-          Gitlab::Ci::Status.const_get(simple_status.capitalize)
+          Gitlab::Ci::Status.const_get(simple_status.capitalize, false)
         end
 
         it "fabricates a core status #{simple_status}" do
diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
index 8a36cd1b65826b5e8eace36a1c70d9f39c244dd6..3acc767ab7a883999be8cf004b9296761b7a63bd 100644
--- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
@@ -18,7 +18,7 @@
         let(:pipeline) { create(:ci_pipeline, status: simple_status) }
 
         let(:expected_status) do
-          Gitlab::Ci::Status.const_get(simple_status.capitalize)
+          Gitlab::Ci::Status.const_get(simple_status.capitalize, false)
         end
 
         it "matches correct core status for #{simple_status}" do
diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
index 8f5b1ff62a56c945d649a170408ed07dadbc356b..dcb537121576c4d32fd8c1ebe2221cab95a69596 100644
--- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
@@ -34,7 +34,7 @@
 
         it "fabricates a core status #{core_status}" do
           expect(status).to be_a(
-            Gitlab::Ci::Status.const_get(core_status.capitalize))
+            Gitlab::Ci::Status.const_get(core_status.capitalize, false))
         end
 
         it 'extends core status with common stage methods' do
diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb
index dd5f2f97ac9f4db5fb0f77f701d4c5d4be03cfa0..1baea13299b4d26c8a790a4507bfdfd16a306bf2 100644
--- a/spec/lib/gitlab/ci/trace/stream_spec.rb
+++ b/spec/lib/gitlab/ci/trace/stream_spec.rb
@@ -248,60 +248,6 @@
     end
   end
 
-  describe '#html_with_state' do
-    shared_examples_for 'html_with_states' do
-      it 'returns html content with state' do
-        result = stream.html_with_state
-
-        expect(result.html).to eq("<span>1234</span>")
-      end
-
-      context 'follow-up state' do
-        let!(:last_result) { stream.html_with_state }
-
-        before do
-          data_stream.seek(4, IO::SEEK_SET)
-          data_stream.write("5678")
-          stream.seek(0)
-        end
-
-        it "returns appended trace" do
-          result = stream.html_with_state(last_result.state)
-
-          expect(result.append).to be_truthy
-          expect(result.html).to eq("<span>5678</span>")
-        end
-      end
-    end
-
-    context 'when stream is StringIO' do
-      let(:data_stream) do
-        StringIO.new("1234")
-      end
-
-      let(:stream) do
-        described_class.new { data_stream }
-      end
-
-      it_behaves_like 'html_with_states'
-    end
-
-    context 'when stream is ChunkedIO' do
-      let(:data_stream) do
-        Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
-          chunked_io.write("1234")
-          chunked_io.seek(0, IO::SEEK_SET)
-        end
-      end
-
-      let(:stream) do
-        described_class.new { data_stream }
-      end
-
-      it_behaves_like 'html_with_states'
-    end
-  end
-
   describe '#html' do
     shared_examples_for 'htmls' do
       it "returns html" do
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index d43eb4e4b4a4da72e2d007a22152772990708573..c7a90d2a2542e1552ff276606766a892d33b5da9 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -26,7 +26,7 @@ module Ci
           it 'returns valid build attributes' do
             expect(subject).to eq({
               stage: "test",
-              stage_idx: 1,
+              stage_idx: 2,
               name: "rspec",
               options: {
                 before_script: ["pwd"],
@@ -56,7 +56,7 @@ module Ci
           it 'returns valid build attributes' do
             expect(subject).to eq({
               stage: 'test',
-              stage_idx: 1,
+              stage_idx: 2,
               name: 'rspec',
               options: { script: ['rspec'] },
               rules: [
@@ -209,13 +209,16 @@ module Ci
         end
 
         let(:attributes) do
-          [{ name: "build",
+          [{ name: ".pre",
              index: 0,
              builds: [] },
-           { name: "test",
+           { name: "build",
              index: 1,
+             builds: [] },
+           { name: "test",
+             index: 2,
              builds:
-               [{ stage_idx: 1,
+               [{ stage_idx: 2,
                   stage: "test",
                   name: "rspec",
                   allow_failure: false,
@@ -225,9 +228,9 @@ module Ci
                   only: { refs: ["branches"] },
                   except: {} }] },
            { name: "deploy",
-             index: 2,
+             index: 3,
              builds:
-               [{ stage_idx: 2,
+               [{ stage_idx: 3,
                   stage: "deploy",
                   name: "prod",
                   allow_failure: false,
@@ -235,7 +238,10 @@ module Ci
                   yaml_variables: [],
                   options: { script: ["cap prod"] },
                   only: { refs: ["tags"] },
-                  except: {} }] }]
+                  except: {} }] },
+           { name: ".post",
+             index: 4,
+             builds: [] }]
         end
 
         it 'returns stages seed attributes' do
@@ -425,7 +431,7 @@ module Ci
             expect(config_processor.stage_builds_attributes("test").size).to eq(1)
             expect(config_processor.stage_builds_attributes("test").first).to eq({
               stage: "test",
-              stage_idx: 1,
+              stage_idx: 2,
               name: "rspec",
               options: {
                 before_script: ["pwd"],
@@ -456,7 +462,7 @@ module Ci
             expect(config_processor.stage_builds_attributes("test").size).to eq(1)
             expect(config_processor.stage_builds_attributes("test").first).to eq({
               stage: "test",
-              stage_idx: 1,
+              stage_idx: 2,
               name: "rspec",
               options: {
                 before_script: ["pwd"],
@@ -485,7 +491,7 @@ module Ci
             expect(config_processor.stage_builds_attributes("test").size).to eq(1)
             expect(config_processor.stage_builds_attributes("test").first).to eq({
               stage: "test",
-              stage_idx: 1,
+              stage_idx: 2,
               name: "rspec",
               options: {
                 before_script: ["pwd"],
@@ -510,7 +516,7 @@ module Ci
             expect(config_processor.stage_builds_attributes("test").size).to eq(1)
             expect(config_processor.stage_builds_attributes("test").first).to eq({
               stage: "test",
-              stage_idx: 1,
+              stage_idx: 2,
               name: "rspec",
               options: {
                 before_script: ["pwd"],
@@ -964,6 +970,7 @@ module Ci
                                rspec:         {
                                  artifacts: {
                                    paths: ["logs/", "binaries/"],
+                                   expose_as: "Exposed artifacts",
                                    untracked: true,
                                    name: "custom_name",
                                    expire_in: "7d"
@@ -977,7 +984,7 @@ module Ci
           expect(config_processor.stage_builds_attributes("test").size).to eq(1)
           expect(config_processor.stage_builds_attributes("test").first).to eq({
             stage: "test",
-            stage_idx: 1,
+            stage_idx: 2,
             name: "rspec",
             options: {
               before_script: ["pwd"],
@@ -987,6 +994,7 @@ module Ci
               artifacts: {
                 name: "custom_name",
                 paths: ["logs/", "binaries/"],
+                expose_as: "Exposed artifacts",
                 untracked: true,
                 expire_in: "7d"
               }
@@ -1272,7 +1280,7 @@ module Ci
             expect(subject.builds.size).to eq(5)
             expect(subject.builds[0]).to eq(
               stage: "build",
-              stage_idx: 0,
+              stage_idx: 1,
               name: "build1",
               options: {
                 script: ["test"]
@@ -1283,7 +1291,7 @@ module Ci
             )
             expect(subject.builds[2]).to eq(
               stage: "test",
-              stage_idx: 1,
+              stage_idx: 2,
               name: "test1",
               options: {
                 script: ["test"],
@@ -1398,7 +1406,7 @@ module Ci
             expect(subject.size).to eq(1)
             expect(subject.first).to eq({
               stage: "test",
-              stage_idx: 1,
+              stage_idx: 2,
               name: "normal_job",
               options: {
                 script: ["test"]
@@ -1442,7 +1450,7 @@ module Ci
             expect(subject.size).to eq(2)
             expect(subject.first).to eq({
               stage: "build",
-              stage_idx: 0,
+              stage_idx: 1,
               name: "job1",
               options: {
                 script: ["execute-script-for-job"]
@@ -1453,7 +1461,7 @@ module Ci
             })
             expect(subject.second).to eq({
               stage: "build",
-              stage_idx: 0,
+              stage_idx: 1,
               name: "job2",
               options: {
                 script: ["execute-script-for-job"]
@@ -1665,14 +1673,14 @@ module Ci
           config = YAML.dump({ rspec: { script: "test", type: "acceptance" } })
           expect do
             Gitlab::Ci::YamlProcessor.new(config)
-          end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy")
+          end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be .pre, build, test, deploy, .post")
         end
 
         it "returns errors if job stage is not a defined stage" do
           config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } })
           expect do
             Gitlab::Ci::YamlProcessor.new(config)
-          end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test")
+          end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be .pre, build, test, .post")
         end
 
         it "returns errors if stages is not an array" do
diff --git a/spec/lib/gitlab/cleanup/project_uploads_spec.rb b/spec/lib/gitlab/cleanup/project_uploads_spec.rb
index 7bad788e44e71437990c680a321b29c179b826c4..5787cce7d20647e37e63639147da5400928c4fa6 100644
--- a/spec/lib/gitlab/cleanup/project_uploads_spec.rb
+++ b/spec/lib/gitlab/cleanup/project_uploads_spec.rb
@@ -4,6 +4,7 @@
 
 describe Gitlab::Cleanup::ProjectUploads do
   subject { described_class.new(logger: logger) }
+
   let(:logger) { double(:logger) }
 
   before do
diff --git a/spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb b/spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1eddf488c5dd8bee2b2677d6fbf1d2c49ba3126a
--- /dev/null
+++ b/spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# For easier debugging set `PUMA_DEBUG=1`
+
+describe Gitlab::Cluster::Mixins::PumaCluster do
+  PUMA_STARTUP_TIMEOUT = 30
+
+  context 'when running Puma in Cluster-mode' do
+    %i[USR1 USR2 INT HUP].each do |signal|
+      it "for #{signal} does execute phased restart block" do
+        with_puma(workers: 1) do |pid|
+          Process.kill(signal, pid)
+
+          child_pid, child_status = Process.wait2(pid)
+          expect(child_pid).to eq(pid)
+          expect(child_status).to be_exited
+          expect(child_status.exitstatus).to eq(140)
+        end
+      end
+    end
+  end
+
+  private
+
+  def with_puma(workers:, timeout: PUMA_STARTUP_TIMEOUT)
+    with_puma_config(workers: workers) do |puma_rb|
+      cmdline = [
+        "bundle", "exec", "puma",
+        "-C", puma_rb,
+        "-I", Rails.root.to_s
+      ]
+
+      IO.popen(cmdline) do |process|
+        # wait for process to start:
+        # [2123] * Listening on tcp://127.0.0.1:0
+        wait_for_output(process, /Listening on/, timeout: timeout)
+        consume_output(process)
+
+        yield(process.pid)
+      ensure
+        begin
+          Process.kill(:KILL, process.pid)
+        rescue Errno::ESRCH
+        end
+      end
+    end
+  end
+
+  def with_puma_config(workers:)
+    Dir.mktmpdir do |dir|
+      File.write "#{dir}/puma.rb", <<-EOF
+        require './lib/gitlab/cluster/lifecycle_events'
+        require './lib/gitlab/cluster/mixins/puma_cluster'
+
+        workers #{workers}
+        bind "tcp://127.0.0.1:0"
+        preload_app!
+
+        app -> (env) { [404, {}, ['']] }
+
+        Puma::Cluster.prepend(#{described_class})
+
+        Gitlab::Cluster::LifecycleEvents.on_before_phased_restart do
+          exit(140)
+        end
+
+        # redirect stderr to stdout
+        $stderr.reopen($stdout)
+      EOF
+
+      yield("#{dir}/puma.rb")
+    end
+  end
+
+  def wait_for_output(process, output, timeout:)
+    Timeout.timeout(timeout) do
+      loop do
+        line = process.readline
+        puts "PUMA_DEBUG: #{line}" if ENV['PUMA_DEBUG']
+        break if line =~ output
+      end
+    end
+  end
+
+  def consume_output(process)
+    Thread.new do
+      loop do
+        line = process.readline
+        puts "PUMA_DEBUG: #{line}" if ENV['PUMA_DEBUG']
+      end
+    rescue
+    end
+  end
+end
diff --git a/spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb b/spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2b3a267991cb44c5edf26930d0ff579d4b0044a0
--- /dev/null
+++ b/spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# For easier debugging set `UNICORN_DEBUG=1`
+
+describe Gitlab::Cluster::Mixins::UnicornHttpServer do
+  UNICORN_STARTUP_TIMEOUT = 10
+
+  context 'when running Unicorn' do
+    %i[USR2].each do |signal|
+      it "for #{signal} does execute phased restart block" do
+        with_unicorn(workers: 1) do |pid|
+          Process.kill(signal, pid)
+
+          child_pid, child_status = Process.wait2(pid)
+          expect(child_pid).to eq(pid)
+          expect(child_status).to be_exited
+          expect(child_status.exitstatus).to eq(140)
+        end
+      end
+    end
+
+    %i[QUIT TERM INT].each do |signal|
+      it "for #{signal} does not execute phased restart block" do
+        with_unicorn(workers: 1) do |pid|
+          Process.kill(signal, pid)
+
+          child_pid, child_status = Process.wait2(pid)
+          expect(child_pid).to eq(pid)
+          expect(child_status).to be_exited
+          expect(child_status.exitstatus).to eq(0)
+        end
+      end
+    end
+  end
+
+  private
+
+  def with_unicorn(workers:, timeout: UNICORN_STARTUP_TIMEOUT)
+    with_unicorn_configs(workers: workers) do |unicorn_rb, config_ru|
+      cmdline = [
+        "bundle", "exec", "unicorn",
+        "-I", Rails.root.to_s,
+        "-c", unicorn_rb,
+        config_ru
+      ]
+
+      IO.popen(cmdline) do |process|
+        # wait for process to start:
+        # I, [2019-10-15T13:21:27.565225 #3089]  INFO -- : master process ready
+        wait_for_output(process, /master process ready/, timeout: timeout)
+        consume_output(process)
+
+        yield(process.pid)
+      ensure
+        begin
+          Process.kill(:KILL, process.pid)
+        rescue Errno::ESRCH
+        end
+      end
+    end
+  end
+
+  def with_unicorn_configs(workers:)
+    Dir.mktmpdir do |dir|
+      File.write "#{dir}/unicorn.rb", <<-EOF
+        require './lib/gitlab/cluster/lifecycle_events'
+        require './lib/gitlab/cluster/mixins/unicorn_http_server'
+
+        worker_processes #{workers}
+        listen "127.0.0.1:0"
+        preload_app true
+
+        Unicorn::HttpServer.prepend(#{described_class})
+
+        Gitlab::Cluster::LifecycleEvents.on_before_phased_restart do
+          exit(140)
+        end
+
+        # redirect stderr to stdout
+        $stderr.reopen($stdout)
+      EOF
+
+      File.write "#{dir}/config.ru", <<-EOF
+        run -> (env) { [404, {}, ['']] }
+      EOF
+
+      yield("#{dir}/unicorn.rb", "#{dir}/config.ru")
+    end
+  end
+
+  def wait_for_output(process, output, timeout:)
+    Timeout.timeout(timeout) do
+      loop do
+        line = process.readline
+        puts "UNICORN_DEBUG: #{line}" if ENV['UNICORN_DEBUG']
+        break if line =~ output
+      end
+    end
+  end
+
+  def consume_output(process)
+    Thread.new do
+      loop do
+        line = process.readline
+        puts "UNICORN_DEBUG: #{line}" if ENV['UNICORN_DEBUG']
+      end
+    rescue
+    end
+  end
+end
diff --git a/spec/lib/gitlab/config/entry/simplifiable_spec.rb b/spec/lib/gitlab/config/entry/simplifiable_spec.rb
index 65e18fe3f1054d7f09b8d7791e2cc461e79bf939..5c208cab4497632caf3063e8fffa1057dac3a362 100644
--- a/spec/lib/gitlab/config/entry/simplifiable_spec.rb
+++ b/spec/lib/gitlab/config/entry/simplifiable_spec.rb
@@ -24,9 +24,9 @@
     let(:unknown) { double('unknown strategy') }
 
     before do
-      stub_const("#{described_class.name}::Something", first)
-      stub_const("#{described_class.name}::DifferentOne", second)
-      stub_const("#{described_class.name}::UnknownStrategy", unknown)
+      entry::Something = first
+      entry::DifferentOne = second
+      entry::UnknownStrategy = unknown
     end
 
     context 'when first strategy should be used' do
diff --git a/spec/lib/gitlab/daemon_spec.rb b/spec/lib/gitlab/daemon_spec.rb
index 0372b770844947001435f6995fe75e8e579addf8..cf1f089c577f56a5d955e7721a66f3cba6bc0672 100644
--- a/spec/lib/gitlab/daemon_spec.rb
+++ b/spec/lib/gitlab/daemon_spec.rb
@@ -6,7 +6,7 @@
   subject { described_class.new }
 
   before do
-    allow(subject).to receive(:start_working)
+    allow(subject).to receive(:run_thread)
     allow(subject).to receive(:stop_working)
   end
 
@@ -44,7 +44,7 @@
         it 'starts the Daemon' do
           expect { subject.start.join }.to change { subject.thread? }.from(false).to(true)
 
-          expect(subject).to have_received(:start_working)
+          expect(subject).to have_received(:run_thread)
         end
       end
 
@@ -52,7 +52,21 @@
         it "doesn't shutdown stopped Daemon" do
           expect { subject.stop }.not_to change { subject.thread? }
 
-          expect(subject).not_to have_received(:start_working)
+          expect(subject).not_to have_received(:run_thread)
+        end
+      end
+    end
+
+    describe '#start_working' do
+      context 'when start_working fails' do
+        before do
+          expect(subject).to receive(:start_working) { false }
+        end
+
+        it 'does not start thread' do
+          expect(subject).not_to receive(:run_thread)
+
+          expect(subject.start).to eq(nil)
         end
       end
     end
@@ -66,7 +80,7 @@
         it "doesn't start running Daemon" do
           expect { subject.start.join }.not_to change { subject.thread }
 
-          expect(subject).to have_received(:start_working).once
+          expect(subject).to have_received(:run_thread).once
         end
       end
 
@@ -79,7 +93,7 @@
 
         context 'when stop_working raises exception' do
           before do
-            allow(subject).to receive(:start_working) do
+            allow(subject).to receive(:run_thread) do
               sleep(1000)
             end
           end
@@ -108,7 +122,7 @@
         expect(subject.start).to be_nil
         expect { subject.start }.not_to change { subject.thread? }
 
-        expect(subject).not_to have_received(:start_working)
+        expect(subject).not_to have_received(:run_thread)
       end
     end
 
diff --git a/spec/lib/gitlab/danger/roulette_spec.rb b/spec/lib/gitlab/danger/roulette_spec.rb
index 121c5d8ecd9ae572bc81d9fa8d9d09be4322a3fa..4d41e2c45aa1588981983293886bd7fcda61e16d 100644
--- a/spec/lib/gitlab/danger/roulette_spec.rb
+++ b/spec/lib/gitlab/danger/roulette_spec.rb
@@ -104,11 +104,13 @@
     let(:person2) { Gitlab::Danger::Teammate.new('username' => 'godfat') }
     let(:author) { Gitlab::Danger::Teammate.new('username' => 'filipa') }
     let(:ooo) { Gitlab::Danger::Teammate.new('username' => 'jacopo-beschi') }
+    let(:no_capacity) { Gitlab::Danger::Teammate.new('username' => 'uncharged') }
 
     before do
-      stub_person_message(person1, 'making GitLab magic')
-      stub_person_message(person2, 'making GitLab magic')
-      stub_person_message(ooo, 'OOO till 15th')
+      stub_person_status(person1, message: 'making GitLab magic')
+      stub_person_status(person2, message: 'making GitLab magic')
+      stub_person_status(ooo, message: 'OOO till 15th')
+      stub_person_status(no_capacity, message: 'At capacity for the next few days', emoji: 'red_circle')
       # we don't stub Filipa, as she is the author and
       # we should not fire request checking for her
 
@@ -131,10 +133,14 @@
       expect(subject.spin_for_person([author], random: Random.new)).to be_nil
     end
 
+    it 'excludes person with no capacity' do
+      expect(subject.spin_for_person([no_capacity], random: Random.new)).to be_nil
+    end
+
     private
 
-    def stub_person_message(person, message)
-      body = { message: message }.to_json
+    def stub_person_status(person, message: 'dummy message', emoji: 'unicorn')
+      body = { message: message, emoji: emoji }.to_json
 
       WebMock
         .stub_request(:get, "https://gitlab.com/api/v4/users/#{person.username}/status")
diff --git a/spec/lib/gitlab/danger/teammate_spec.rb b/spec/lib/gitlab/danger/teammate_spec.rb
index ca036390bde09d0144d4a425d64b706860d374df..bd1c2b10dc82790b246fa52dfda304f702ffb474 100644
--- a/spec/lib/gitlab/danger/teammate_spec.rb
+++ b/spec/lib/gitlab/danger/teammate_spec.rb
@@ -2,11 +2,14 @@
 
 require 'fast_spec_helper'
 
+require 'rspec-parameterized'
+
 require 'gitlab/danger/teammate'
 
 describe Gitlab::Danger::Teammate do
-  subject { described_class.new(options) }
-  let(:options) { { 'projects' => projects, 'role' => role } }
+  subject { described_class.new(options.stringify_keys) }
+
+  let(:options) { { username: 'luigi', projects: projects, role: role } }
   let(:projects) { { project => capabilities } }
   let(:role) { 'Engineer, Manage' }
   let(:labels) { [] }
@@ -95,4 +98,72 @@
       expect(subject.maintainer?(project, :frontend, labels)).to be_falsey
     end
   end
+
+  describe '#status' do
+    let(:capabilities) { ['dish washing'] }
+
+    context 'with empty cache' do
+      context 'for successful request' do
+        it 'returns the response' do
+          mock_status = double(does_not: 'matter')
+          expect(Gitlab::Danger::RequestHelper).to receive(:http_get_json)
+                                                       .and_return(mock_status)
+
+          expect(subject.status).to be mock_status
+        end
+      end
+
+      context 'for failing request' do
+        it 'returns nil' do
+          expect(Gitlab::Danger::RequestHelper).to receive(:http_get_json)
+                                                       .and_raise(Gitlab::Danger::RequestHelper::HTTPError.new)
+
+          expect(subject.status).to be nil
+        end
+      end
+    end
+
+    context 'with filled cache' do
+      it 'returns the cached response' do
+        mock_status = double(does_not: 'matter')
+        expect(Gitlab::Danger::RequestHelper).to receive(:http_get_json)
+                                                     .and_return(mock_status)
+        subject.status
+
+        expect(Gitlab::Danger::RequestHelper).not_to receive(:http_get_json)
+        expect(subject.status).to be mock_status
+      end
+    end
+  end
+
+  describe '#available?' do
+    using RSpec::Parameterized::TableSyntax
+
+    let(:capabilities) { ['dry head'] }
+
+    where(:status, :result) do
+      {}                               | true
+      { message: 'dear reader' }       | true
+      { message: 'OOO: massage' }      | false
+      { message: 'love it SOOO much' } | false
+      { emoji: 'red_circle' }          | false
+    end
+
+    with_them do
+      before do
+        expect(Gitlab::Danger::RequestHelper).to receive(:http_get_json)
+                                                     .and_return(status&.stringify_keys)
+      end
+
+      it { expect(subject.available?).to be result }
+    end
+
+    it 'returns true if request fails' do
+      expect(Gitlab::Danger::RequestHelper).to receive(:http_get_json)
+                                                   .exactly(2).times
+                                                   .and_raise(Gitlab::Danger::RequestHelper::HTTPError.new)
+
+      expect(subject.available?).to be true
+    end
+  end
 end
diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb
index e8a9f0b06a8a9509016273bdde299d417a5e3be9..58509b6946380cbfda41a085a2c65c02e5aa5820 100644
--- a/spec/lib/gitlab/data_builder/push_spec.rb
+++ b/spec/lib/gitlab/data_builder/push_spec.rb
@@ -90,4 +90,12 @@
         .not_to raise_error
     end
   end
+
+  describe '.build_bulk' do
+    subject do
+      described_class.build_bulk(action: :created, ref_type: :branch, changes: [double, double])
+    end
+
+    it { is_expected.to eq(action: :created, ref_count: 2, ref_type: :branch) }
+  end
 end
diff --git a/spec/lib/gitlab/diff/position_collection_spec.rb b/spec/lib/gitlab/diff/position_collection_spec.rb
index de0e631ab0318f281ceb40ac2410921cc91527ad..f2a8312587cbc39ed66d19cb50b7adff59147028 100644
--- a/spec/lib/gitlab/diff/position_collection_spec.rb
+++ b/spec/lib/gitlab/diff/position_collection_spec.rb
@@ -35,14 +35,15 @@ def build_image_position(attrs = {})
   let(:text_position) { build_text_position }
   let(:folded_text_position) { build_text_position(old_line: 1, new_line: 1) }
   let(:image_position) { build_image_position }
+  let(:invalid_position) { 'a position' }
   let(:head_sha) { merge_request.diff_head_sha }
 
   let(:collection) do
-    described_class.new([text_position, folded_text_position, image_position], head_sha)
+    described_class.new([text_position, folded_text_position, image_position, invalid_position], head_sha)
   end
 
   describe '#to_a' do
-    it 'returns all positions' do
+    it 'returns all positions that are Gitlab::Diff::Position' do
       expect(collection.to_a).to eq([text_position, folded_text_position, image_position])
     end
   end
@@ -59,6 +60,14 @@ def build_image_position(attrs = {})
         expect(collection.unfoldable).to be_empty
       end
     end
+
+    context 'when given head_sha is nil' do
+      let(:head_sha) { nil }
+
+      it 'returns unfoldable diff positions unfiltered by head_sha' do
+        expect(collection.unfoldable).to eq([folded_text_position])
+      end
+    end
   end
 
   describe '#concat' do
diff --git a/spec/lib/gitlab/discussions_diff/file_collection_spec.rb b/spec/lib/gitlab/discussions_diff/file_collection_spec.rb
index 6ef1e41450fb9fd300a33229296fc3662b7f89a8..a13727b62eab203c85fe5cda92abd6bd7f20362f 100644
--- a/spec/lib/gitlab/discussions_diff/file_collection_spec.rb
+++ b/spec/lib/gitlab/discussions_diff/file_collection_spec.rb
@@ -40,6 +40,14 @@
       subject.load_highlight
     end
 
+    it 'does not write cache for empty mapping' do
+      allow(subject).to receive(:highlighted_lines_by_ids).and_return([])
+
+      expect(Gitlab::DiscussionsDiff::HighlightCache).not_to receive(:write_multiple)
+
+      subject.load_highlight
+    end
+
     it 'does not write cache for resolved notes' do
       diff_note_a.update_column(:resolved_at, Time.now)
 
diff --git a/spec/lib/gitlab/downtime_check_spec.rb b/spec/lib/gitlab/downtime_check_spec.rb
index 56ad49d528fcad21b4cffc29c69ff4e2a6921285..5a5e34961a448618cb6aa2de3c367f7b673d1816 100644
--- a/spec/lib/gitlab/downtime_check_spec.rb
+++ b/spec/lib/gitlab/downtime_check_spec.rb
@@ -4,6 +4,7 @@
 
 describe Gitlab::DowntimeCheck do
   subject { described_class.new }
+
   let(:path) { 'foo.rb' }
 
   describe '#check' do
diff --git a/spec/lib/gitlab/email/message/repository_push_spec.rb b/spec/lib/gitlab/email/message/repository_push_spec.rb
index 84c5b38127e377daebe10f3392429fcfd5cf9b3a..b57764bceefe15476c7ace5466e8ed60b7450dd2 100644
--- a/spec/lib/gitlab/email/message/repository_push_spec.rb
+++ b/spec/lib/gitlab/email/message/repository_push_spec.rb
@@ -28,90 +28,107 @@
 
     describe '#project' do
       subject { message.project }
+
       it { is_expected.to eq project }
       it { is_expected.to be_an_instance_of Project }
     end
 
     describe '#project_namespace' do
       subject { message.project_namespace }
+
       it { is_expected.to eq group }
       it { is_expected.to be_kind_of Namespace }
     end
 
     describe '#project_name_with_namespace' do
       subject { message.project_name_with_namespace }
+
       it { is_expected.to eq "#{group.name} / #{project.path}" }
     end
 
     describe '#author' do
       subject { message.author }
+
       it { is_expected.to eq author }
       it { is_expected.to be_an_instance_of User }
     end
 
     describe '#author_name' do
       subject { message.author_name }
+
       it { is_expected.to eq 'Author' }
     end
 
     describe '#commits' do
       subject { message.commits }
+
       it { is_expected.to be_kind_of Array }
       it { is_expected.to all(be_instance_of Commit) }
     end
 
     describe '#diffs' do
       subject { message.diffs }
+
       it { is_expected.to all(be_an_instance_of Gitlab::Diff::File) }
     end
 
     describe '#diffs_count' do
       subject { message.diffs_count }
+
       it { is_expected.to eq raw_compare.diffs.size }
     end
 
     describe '#compare' do
       subject { message.compare }
+
       it { is_expected.to be_an_instance_of Compare }
     end
 
     describe '#compare_timeout' do
       subject { message.compare_timeout }
+
       it { is_expected.to eq raw_compare.diffs.overflow? }
     end
 
     describe '#reverse_compare?' do
       subject { message.reverse_compare? }
+
       it { is_expected.to eq false }
     end
 
     describe '#disable_diffs?' do
       subject { message.disable_diffs? }
+
       it { is_expected.to eq false }
     end
 
     describe '#send_from_committer_email?' do
       subject { message.send_from_committer_email? }
+
       it { is_expected.to eq true }
     end
 
     describe '#action_name' do
       subject { message.action_name }
+
       it { is_expected.to eq 'pushed to' }
     end
 
     describe '#ref_name' do
       subject { message.ref_name }
+
       it { is_expected.to eq 'master' }
     end
 
     describe '#ref_type' do
       subject { message.ref_type }
+
       it { is_expected.to eq 'branch' }
     end
 
     describe '#target_url' do
       subject { message.target_url }
+
       it { is_expected.to include 'compare' }
       it { is_expected.to include compare.commits.first.parents.first.id }
       it { is_expected.to include compare.commits.last.id }
@@ -119,6 +136,7 @@
 
     describe '#subject' do
       subject { message.subject }
+
       it { is_expected.to include "[Git][#{project.full_path}]" }
       it { is_expected.to include "#{compare.commits.length} commits" }
       it { is_expected.to include compare.commits.first.message.split("\n").first }
diff --git a/spec/lib/gitlab/experimentation_spec.rb b/spec/lib/gitlab/experimentation_spec.rb
index 4d473731f390586a243e500c0373a8eb762f52e8..2e5fd16d3707d1d33b567f5867eea3c3a939b927 100644
--- a/spec/lib/gitlab/experimentation_spec.rb
+++ b/spec/lib/gitlab/experimentation_spec.rb
@@ -75,6 +75,7 @@ def index
 
   describe '.enabled?' do
     subject { described_class.enabled?(:test_experiment, experimentation_subject_index) }
+
     let(:experimentation_subject_index) { 9 }
 
     context 'feature toggle is enabled, we are on the right environment and we are selected' do
diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
index d24f5c45107bb297885218e4ad05bb39befdcca9..eef3b9de476ef46b8bcecfd891cb126a37f787d9 100644
--- a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
@@ -84,11 +84,13 @@
 
     describe '#needs_rewrite?' do
       subject { rewriter.needs_rewrite? }
+
       it { is_expected.to eq true }
     end
 
     describe '#files' do
       subject { rewriter.files }
+
       it { is_expected.to be_an(Array) }
     end
   end
diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb
index 0764e525ede9c1676fcfdf878a3bfedac1538369..02ef7b925388c782ed21656c3510e687f94c8b01 100644
--- a/spec/lib/gitlab/git/branch_spec.rb
+++ b/spec/lib/gitlab/git/branch_spec.rb
@@ -44,6 +44,7 @@
 
   describe '#size' do
     subject { super().size }
+
     it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) }
   end
 
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 3f0e6b34291265429f6804f94b1760c48b946eea..cdab712774882249a5d99b7af7eca3f5777538c1 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -174,6 +174,7 @@
 
         describe '#id' do
           subject { super().id }
+
           it { is_expected.to eq(SeedRepo::LastCommit::ID) }
         end
       end
@@ -183,6 +184,7 @@
 
         describe '#id' do
           subject { super().id }
+
           it { is_expected.to eq(SeedRepo::Commit::ID) }
         end
       end
@@ -192,6 +194,7 @@
 
         describe '#id' do
           subject { super().id }
+
           it { is_expected.to eq(SeedRepo::BigCommit::ID) }
         end
       end
@@ -425,7 +428,9 @@
       end
     end
 
-    shared_examples 'extracting commit signature' do
+    describe '.extract_signature_lazily' do
+      subject { described_class.extract_signature_lazily(repository, commit_id).itself }
+
       context 'when the commit is signed' do
         let(:commit_id) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
 
@@ -489,10 +494,8 @@
           expect { subject }.to raise_error(ArgumentError)
         end
       end
-    end
 
-    describe '.extract_signature_lazily' do
-      describe 'loading signatures in batch once' do
+      context 'when loading signatures in batch once' do
         it 'fetches signatures in batch once' do
           commit_ids = %w[0b4bc9a49b562e85de7cc9e834518ea6828729b9 4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6]
           signatures = commit_ids.map do |commit_id|
@@ -513,16 +516,6 @@
           2.times { signatures.each(&:itself) }
         end
       end
-
-      subject { described_class.extract_signature_lazily(repository, commit_id).itself }
-
-      it_behaves_like 'extracting commit signature'
-    end
-
-    describe '.extract_signature' do
-      subject { described_class.extract_signature(repository, commit_id) }
-
-      it_behaves_like 'extracting commit signature'
     end
   end
 
@@ -544,11 +537,13 @@
 
     describe '#id' do
       subject { super().id }
+
       it { is_expected.to eq(sample_commit_hash[:id])}
     end
 
     describe '#message' do
       subject { super().message }
+
       it { is_expected.to eq(sample_commit_hash[:message])}
     end
   end
@@ -558,16 +553,19 @@
 
     describe '#additions' do
       subject { super().additions }
+
       it { is_expected.to eq(11) }
     end
 
     describe '#deletions' do
       subject { super().deletions }
+
       it { is_expected.to eq(6) }
     end
 
     describe '#total' do
       subject { super().total }
+
       it { is_expected.to eq(17) }
     end
   end
@@ -596,6 +594,7 @@
 
     describe '#keys' do
       subject { super().keys.sort }
+
       it { is_expected.to match(sample_commit_hash.keys.sort) }
     end
   end
diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb
index ded173c49ef6b94282f0a39fc7baecf1a5bc05d3..ce45d6e24ba01919f286cffd328d4953bb0abe25 100644
--- a/spec/lib/gitlab/git/diff_collection_spec.rb
+++ b/spec/lib/gitlab/git/diff_collection_spec.rb
@@ -10,6 +10,7 @@
       expanded: expanded
     )
   end
+
   let(:iterator) { MutatingConstantIterator.new(file_count, fake_diff(line_length, line_count)) }
   let(:file_count) { 0 }
   let(:line_length) { 1 }
@@ -21,6 +22,7 @@
 
   describe '#to_a' do
     subject { super().to_a }
+
     it { is_expected.to be_kind_of ::Array }
   end
 
@@ -52,16 +54,19 @@
 
         describe '#overflow?' do
           subject { super().overflow? }
+
           it { is_expected.to be_falsey }
         end
 
         describe '#empty?' do
           subject { super().empty? }
+
           it { is_expected.to be_falsey }
         end
 
         describe '#real_size' do
           subject { super().real_size }
+
           it { is_expected.to eq('3') }
         end
 
@@ -76,6 +81,7 @@
 
         describe '#line_count' do
           subject { super().line_count }
+
           it { is_expected.to eq file_count * line_count }
         end
 
@@ -84,16 +90,19 @@
 
           describe '#overflow?' do
             subject { super().overflow? }
+
             it { is_expected.to be_falsey }
           end
 
           describe '#empty?' do
             subject { super().empty? }
+
             it { is_expected.to be_falsey }
           end
 
           describe '#real_size' do
             subject { super().real_size }
+
             it { is_expected.to eq('3') }
           end
 
@@ -108,6 +117,7 @@
 
           describe '#line_count' do
             subject { super().line_count }
+
             it { is_expected.to eq file_count * line_count }
           end
         end
@@ -118,21 +128,25 @@
 
         describe '#overflow?' do
           subject { super().overflow? }
+
           it { is_expected.to be_truthy }
         end
 
         describe '#empty?' do
           subject { super().empty? }
+
           it { is_expected.to be_falsey }
         end
 
         describe '#real_size' do
           subject { super().real_size }
+
           it { is_expected.to eq('0+') }
         end
 
         describe '#line_count' do
           subject { super().line_count }
+
           it { is_expected.to eq 1000 }
         end
 
@@ -143,21 +157,25 @@
 
           describe '#overflow?' do
             subject { super().overflow? }
+
             it { is_expected.to be_falsey }
           end
 
           describe '#empty?' do
             subject { super().empty? }
+
             it { is_expected.to be_falsey }
           end
 
           describe '#real_size' do
             subject { super().real_size }
+
             it { is_expected.to eq('3') }
           end
 
           describe '#line_count' do
             subject { super().line_count }
+
             it { is_expected.to eq file_count * line_count }
           end
 
@@ -174,21 +192,25 @@
 
         describe '#overflow?' do
           subject { super().overflow? }
+
           it { is_expected.to be_truthy }
         end
 
         describe '#empty?' do
           subject { super().empty? }
+
           it { is_expected.to be_falsey }
         end
 
         describe '#real_size' do
           subject { super().real_size }
+
           it { is_expected.to eq('10+') }
         end
 
         describe '#line_count' do
           subject { super().line_count }
+
           it { is_expected.to eq 10 }
         end
 
@@ -199,21 +221,25 @@
 
           describe '#overflow?' do
             subject { super().overflow? }
+
             it { is_expected.to be_falsey }
           end
 
           describe '#empty?' do
             subject { super().empty? }
+
             it { is_expected.to be_falsey }
           end
 
           describe '#real_size' do
             subject { super().real_size }
+
             it { is_expected.to eq('11') }
           end
 
           describe '#line_count' do
             subject { super().line_count }
+
             it { is_expected.to eq file_count * line_count }
           end
 
@@ -226,21 +252,25 @@
 
         describe '#overflow?' do
           subject { super().overflow? }
+
           it { is_expected.to be_truthy }
         end
 
         describe '#empty?' do
           subject { super().empty? }
+
           it { is_expected.to be_falsey }
         end
 
         describe '#real_size' do
           subject { super().real_size }
+
           it { is_expected.to eq('3+') }
         end
 
         describe '#line_count' do
           subject { super().line_count }
+
           it { is_expected.to eq 120 }
         end
 
@@ -251,21 +281,25 @@
 
           describe '#overflow?' do
             subject { super().overflow? }
+
             it { is_expected.to be_falsey }
           end
 
           describe '#empty?' do
             subject { super().empty? }
+
             it { is_expected.to be_falsey }
           end
 
           describe '#real_size' do
             subject { super().real_size }
+
             it { is_expected.to eq('11') }
           end
 
           describe '#line_count' do
             subject { super().line_count }
+
             it { is_expected.to eq file_count * line_count }
           end
 
@@ -282,21 +316,25 @@
 
         describe '#overflow?' do
           subject { super().overflow? }
+
           it { is_expected.to be_falsey }
         end
 
         describe '#empty?' do
           subject { super().empty? }
+
           it { is_expected.to be_falsey }
         end
 
         describe '#real_size' do
           subject { super().real_size }
+
           it { is_expected.to eq('10') }
         end
 
         describe '#line_count' do
           subject { super().line_count }
+
           it { is_expected.to eq file_count * line_count }
         end
 
@@ -310,21 +348,25 @@
 
       describe '#overflow?' do
         subject { super().overflow? }
+
         it { is_expected.to be_truthy }
       end
 
       describe '#empty?' do
         subject { super().empty? }
+
         it { is_expected.to be_falsey }
       end
 
       describe '#real_size' do
         subject { super().real_size }
+
         it { is_expected.to eq('9+') }
       end
 
       describe '#line_count' do
         subject { super().line_count }
+
         it { is_expected.to eq file_count * line_count }
       end
 
@@ -335,21 +377,25 @@
 
         describe '#overflow?' do
           subject { super().overflow? }
+
           it { is_expected.to be_falsey }
         end
 
         describe '#empty?' do
           subject { super().empty? }
+
           it { is_expected.to be_falsey }
         end
 
         describe '#real_size' do
           subject { super().real_size }
+
           it { is_expected.to eq('10') }
         end
 
         describe '#line_count' do
           subject { super().line_count }
+
           it { is_expected.to eq file_count * line_count }
         end
 
@@ -363,26 +409,31 @@
 
     describe '#overflow?' do
       subject { super().overflow? }
+
       it { is_expected.to be_falsey }
     end
 
     describe '#empty?' do
       subject { super().empty? }
+
       it { is_expected.to be_truthy }
     end
 
     describe '#size' do
       subject { super().size }
+
       it { is_expected.to eq(0) }
     end
 
     describe '#real_size' do
       subject { super().real_size }
+
       it { is_expected.to eq('0')}
     end
 
     describe '#line_count' do
       subject { super().line_count }
+
       it { is_expected.to eq 0 }
     end
   end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 04a648a0da0705dbd0244fff17c7e87cb036dfd3..44c41da756075fd7ac7ec4abe431c2adc1bdb448 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -93,6 +93,7 @@
 
     describe '#last' do
       subject { super().last }
+
       it { is_expected.to eq("v1.2.1") }
     end
     it { is_expected.to include("v1.0.0") }
@@ -215,11 +216,13 @@
 
     describe '#first' do
       subject { super().first }
+
       it { is_expected.to eq('feature') }
     end
 
     describe '#last' do
       subject { super().last }
+
       it { is_expected.to eq('v1.2.1') }
     end
   end
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index ba6abba4e6155b9246fdeb3268a1500fca24a63c..71489adb3730125c355f0ecc09efd1d71ebd19d5 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -252,31 +252,6 @@
     end
   end
 
-  describe '#patch' do
-    let(:request) do
-      Gitaly::CommitPatchRequest.new(
-        repository: repository_message, revision: revision
-      )
-    end
-    let(:response) { [double(data: "my "), double(data: "diff")] }
-
-    subject { described_class.new(repository).patch(revision) }
-
-    it 'sends an RPC request' do
-      expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_patch)
-        .with(request, kind_of(Hash)).and_return([])
-
-      subject
-    end
-
-    it 'concatenates the responses data' do
-      allow_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_patch)
-        .with(request, kind_of(Hash)).and_return(response)
-
-      expect(subject).to eq("my diff")
-    end
-  end
-
   describe '#commit_stats' do
     let(:request) do
       Gitaly::CommitStatsRequest.new(
diff --git a/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb b/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb
index 1c933410bd57ca23418a570b23db78b7f53711bb..a36024637563012e8c3a6984a5763369fca427cd 100644
--- a/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb
@@ -32,7 +32,7 @@
         double(files: [double(header: nil, content: content_2[11..-1])])
       ]
 
-      conflict_files = described_class.new(messages).to_a
+      conflict_files = described_class.new(messages, target_repository.gitaly_repository).to_a
 
       expect(conflict_files.size).to be(2)
 
diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
index 6d614c6527a867aa4c9929f0992b862ace562a24..8331f0b6bc7e51aa33e705f0a22c02e35ac5498a 100644
--- a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
@@ -311,10 +311,11 @@
       end
     end
 
-    it 'creates the merge request diffs' do
+    it 'creates a merge request diff and sets it as the latest' do
       mr = insert_git_data
 
       expect(mr.merge_request_diffs.exists?).to eq(true)
+      expect(mr.reload.latest_merge_request_diff_id).to eq(mr.merge_request_diffs.first.id)
     end
 
     it 'creates the merge request diff commits' do
diff --git a/spec/lib/gitlab/gitlab_import/client_spec.rb b/spec/lib/gitlab/gitlab_import/client_spec.rb
index 22ad88e28cbbf50a5e7c1fb2ffc5a5c333793ed8..0f1745fcc02f78a369e9ec2633e1d2412aab9ab9 100644
--- a/spec/lib/gitlab/gitlab_import/client_spec.rb
+++ b/spec/lib/gitlab/gitlab_import/client_spec.rb
@@ -52,6 +52,7 @@
 
   describe '#projects' do
     subject(:method) { :projects }
+
     let(:args) { [] }
     let(:element_list) { build_list(:project, 2) }
 
@@ -67,6 +68,7 @@
 
   describe '#issues' do
     subject(:method) { :issues }
+
     let(:args) { [1] }
     let(:element_list) { build_list(:issue, 2) }
 
@@ -82,6 +84,7 @@
 
   describe '#issue_comments' do
     subject(:method) { :issue_comments }
+
     let(:args) { [1, 1] }
     let(:element_list) { build_list(:note_on_issue, 2) }
 
diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb
index fa47cfd519b0c0495d05860cd1045b06229fa3ef..8401b683fd5ee16baa3fa601659dedfa497d3962 100644
--- a/spec/lib/gitlab/gpg/commit_spec.rb
+++ b/spec/lib/gitlab/gpg/commit_spec.rb
@@ -370,5 +370,33 @@
 
       it_behaves_like 'returns the cached signature on second call'
     end
+
+    context 'multiple commits with signatures' do
+      let(:first_signature) { create(:gpg_signature) }
+
+      let(:gpg_key) { create(:gpg_key, key: GpgHelpers::User2.public_key) }
+      let(:second_signature) { create(:gpg_signature, gpg_key: gpg_key) }
+
+      let!(:first_commit) { create(:commit, project: project, sha: first_signature.commit_sha) }
+      let!(:second_commit) { create(:commit, project: project, sha: second_signature.commit_sha) }
+
+      let(:commits) do
+        [first_commit, second_commit].map do |commit|
+          gpg_commit = described_class.new(commit)
+
+          allow(gpg_commit).to receive(:has_signature?).and_return(true)
+
+          gpg_commit
+        end
+      end
+
+      it 'does an aggregated sql request instead of 2 separate ones' do
+        recorder = ActiveRecord::QueryRecorder.new do
+          commits.each(&:signature)
+        end
+
+        expect(recorder.count).to eq(1)
+      end
+    end
   end
 end
diff --git a/spec/lib/gitlab/graphs/commits_spec.rb b/spec/lib/gitlab/graphs/commits_spec.rb
index 530d4a981bfee9de48ee7c0f30b92c4d1d56f6a0..09654e0439e9333f1461e19be83fa37f513ccad2 100644
--- a/spec/lib/gitlab/graphs/commits_spec.rb
+++ b/spec/lib/gitlab/graphs/commits_spec.rb
@@ -11,12 +11,14 @@
   describe '#commit_per_day' do
     context 'when range is only commits from today' do
       subject { described_class.new([commit2, commit1]).commit_per_day }
+
       it { is_expected.to eq 2 }
     end
   end
 
   context 'when range is only commits from today' do
     subject { described_class.new([commit2, commit1]) }
+
     describe '#commit_per_day' do
       it { expect(subject.commit_per_day).to eq 2 }
     end
@@ -28,6 +30,7 @@
 
   context 'with commits from yesterday and today' do
     subject { described_class.new([commit2, commit1_yesterday]) }
+
     describe '#commit_per_day' do
       it { expect(subject.commit_per_day).to eq 1.0 }
     end
diff --git a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
index 99d10312c1574ee98a79d9d84a48d5d5c804eb2a..36e2fd04aeb434cd34a3c02231bdf109cf1dab5b 100644
--- a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
@@ -30,6 +30,7 @@
 
   describe '#metrics' do
     subject { described_class.metrics }
+
     let(:server) { double(storage: 'default', read_writeable?: up) }
 
     before do
diff --git a/spec/lib/gitlab/health_checks/probes/collection_spec.rb b/spec/lib/gitlab/health_checks/probes/collection_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..33efc640257361e80337fe6eff90ffc69258acf1
--- /dev/null
+++ b/spec/lib/gitlab/health_checks/probes/collection_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::HealthChecks::Probes::Collection do
+  let(:readiness) { described_class.new(*checks) }
+
+  describe '#call' do
+    subject { readiness.execute }
+
+    context 'with all checks' do
+      let(:checks) do
+        [
+          Gitlab::HealthChecks::DbCheck,
+          Gitlab::HealthChecks::Redis::RedisCheck,
+          Gitlab::HealthChecks::Redis::CacheCheck,
+          Gitlab::HealthChecks::Redis::QueuesCheck,
+          Gitlab::HealthChecks::Redis::SharedStateCheck,
+          Gitlab::HealthChecks::GitalyCheck
+        ]
+      end
+
+      it 'responds with readiness checks data' do
+        expect(subject.http_status).to eq(200)
+
+        expect(subject.json[:status]).to eq('ok')
+        expect(subject.json['db_check']).to contain_exactly(status: 'ok')
+        expect(subject.json['cache_check']).to contain_exactly(status: 'ok')
+        expect(subject.json['queues_check']).to contain_exactly(status: 'ok')
+        expect(subject.json['shared_state_check']).to contain_exactly(status: 'ok')
+        expect(subject.json['gitaly_check']).to contain_exactly(
+          status: 'ok', labels: { shard: 'default' })
+      end
+
+      context 'when Redis fails' do
+        before do
+          allow(Gitlab::HealthChecks::Redis::RedisCheck).to receive(:readiness).and_return(
+            Gitlab::HealthChecks::Result.new('redis_check', false, "check error"))
+        end
+
+        it 'responds with failure' do
+          expect(subject.http_status).to eq(503)
+
+          expect(subject.json[:status]).to eq('failed')
+          expect(subject.json['cache_check']).to contain_exactly(status: 'ok')
+          expect(subject.json['redis_check']).to contain_exactly(
+            status: 'failed', message: 'check error')
+        end
+      end
+    end
+
+    context 'without checks' do
+      let(:checks) { [] }
+
+      it 'responds with success' do
+        expect(subject.http_status).to eq(200)
+
+        expect(subject.json).to eq(status: 'ok')
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/health_checks/probes/liveness_spec.rb b/spec/lib/gitlab/health_checks/probes/liveness_spec.rb
deleted file mode 100644
index 91066cb8ba0667eeb45b77084960d8f3e3d738a8..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/health_checks/probes/liveness_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::HealthChecks::Probes::Liveness do
-  let(:liveness) { described_class.new }
-
-  describe '#call' do
-    subject { liveness.execute }
-
-    it 'responds with liveness checks data' do
-      expect(subject.http_status).to eq(200)
-
-      expect(subject.json[:status]).to eq('ok')
-    end
-  end
-end
diff --git a/spec/lib/gitlab/health_checks/probes/readiness_spec.rb b/spec/lib/gitlab/health_checks/probes/readiness_spec.rb
deleted file mode 100644
index d88ffd984c22a44ae0fa217771425c0b75fb0150..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/health_checks/probes/readiness_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::HealthChecks::Probes::Readiness do
-  let(:readiness) { described_class.new }
-
-  describe '#call' do
-    subject { readiness.execute }
-
-    it 'responds with readiness checks data' do
-      expect(subject.http_status).to eq(200)
-
-      expect(subject.json[:status]).to eq('ok')
-      expect(subject.json['db_check']).to contain_exactly(status: 'ok')
-      expect(subject.json['cache_check']).to contain_exactly(status: 'ok')
-      expect(subject.json['queues_check']).to contain_exactly(status: 'ok')
-      expect(subject.json['shared_state_check']).to contain_exactly(status: 'ok')
-      expect(subject.json['gitaly_check']).to contain_exactly(
-        status: 'ok', labels: { shard: 'default' })
-    end
-
-    context 'when Redis fails' do
-      before do
-        allow(Gitlab::HealthChecks::Redis::RedisCheck).to receive(:readiness).and_return(
-          Gitlab::HealthChecks::Result.new('redis_check', false, "check error"))
-      end
-
-      it 'responds with failure' do
-        expect(subject.http_status).to eq(503)
-
-        expect(subject.json[:status]).to eq('failed')
-        expect(subject.json['cache_check']).to contain_exactly(status: 'ok')
-        expect(subject.json['redis_check']).to contain_exactly(
-          status: 'failed', message: 'check error')
-      end
-    end
-  end
-end
diff --git a/spec/lib/gitlab/health_checks/puma_check_spec.rb b/spec/lib/gitlab/health_checks/puma_check_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..71b6386b17460b15f877fbfccef3cb53d171b9da
--- /dev/null
+++ b/spec/lib/gitlab/health_checks/puma_check_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe Gitlab::HealthChecks::PumaCheck do
+  let(:result_class) { Gitlab::HealthChecks::Result }
+  let(:readiness) { described_class.readiness }
+  let(:metrics) { described_class.metrics }
+
+  shared_examples 'with state' do |(state, message)|
+    it "does provide readiness" do
+      expect(readiness).to eq(result_class.new('puma_check', state, message))
+    end
+
+    it "does provide metrics" do
+      expect(metrics).to include(
+        an_object_having_attributes(name: 'puma_check_success', value: state ? 1 : 0))
+      expect(metrics).to include(
+        an_object_having_attributes(name: 'puma_check_latency_seconds', value: be >= 0))
+    end
+  end
+
+  context 'when Puma is not loaded' do
+    before do
+      hide_const('Puma')
+    end
+
+    it "does not provide readiness and metrics" do
+      expect(readiness).to be_nil
+      expect(metrics).to be_nil
+    end
+  end
+
+  context 'when Puma is loaded' do
+    before do
+      stub_const('Puma', Module.new)
+    end
+
+    context 'when stats are missing' do
+      before do
+        expect(Puma).to receive(:stats).and_raise(NoMethodError)
+      end
+
+      it_behaves_like 'with state', [false, 'unexpected Puma check result: 0']
+    end
+
+    context 'for Single mode' do
+      before do
+        expect(Puma).to receive(:stats) do
+          '{}'
+        end
+      end
+
+      it_behaves_like 'with state', true
+    end
+
+    context 'for Cluster mode' do
+      before do
+        expect(Puma).to receive(:stats) do
+          '{"workers":2}'
+        end
+      end
+
+      it_behaves_like 'with state', true
+    end
+  end
+end
diff --git a/spec/lib/gitlab/health_checks/simple_check_shared.rb b/spec/lib/gitlab/health_checks/simple_check_shared.rb
index c3d55a119096097cad55d56ea4609824a9bba9cc..03a7cf249cf3627551cc56652edf80bcffc3c736 100644
--- a/spec/lib/gitlab/health_checks/simple_check_shared.rb
+++ b/spec/lib/gitlab/health_checks/simple_check_shared.rb
@@ -1,6 +1,7 @@
 shared_context 'simple_check' do |metrics_prefix, check_name, success_result|
   describe '#metrics' do
     subject { described_class.metrics }
+
     context 'Check is passing' do
       before do
         allow(described_class).to receive(:check).and_return success_result
@@ -34,6 +35,7 @@
 
   describe '#readiness' do
     subject { described_class.readiness }
+
     context 'Check returns ok' do
       before do
         allow(described_class).to receive(:check).and_return success_result
diff --git a/spec/lib/gitlab/health_checks/unicorn_check_spec.rb b/spec/lib/gitlab/health_checks/unicorn_check_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c02d0c377383950973341292c1a7fbb74895aaff
--- /dev/null
+++ b/spec/lib/gitlab/health_checks/unicorn_check_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper'
+
+describe Gitlab::HealthChecks::UnicornCheck do
+  let(:result_class) { Gitlab::HealthChecks::Result }
+  let(:readiness) { described_class.readiness }
+  let(:metrics) { described_class.metrics }
+
+  before do
+    described_class.clear_memoization(:http_servers)
+  end
+
+  shared_examples 'with state' do |(state, message)|
+    it "does provide readiness" do
+      expect(readiness).to eq(result_class.new('unicorn_check', state, message))
+    end
+
+    it "does provide metrics" do
+      expect(metrics).to include(
+        an_object_having_attributes(name: 'unicorn_check_success', value: state ? 1 : 0))
+      expect(metrics).to include(
+        an_object_having_attributes(name: 'unicorn_check_latency_seconds', value: be >= 0))
+    end
+  end
+
+  context 'when Unicorn is not loaded' do
+    before do
+      hide_const('Unicorn')
+    end
+
+    it "does not provide readiness and metrics" do
+      expect(readiness).to be_nil
+      expect(metrics).to be_nil
+    end
+  end
+
+  context 'when Unicorn is loaded' do
+    let(:http_server_class) { Struct.new(:worker_processes) }
+
+    before do
+      stub_const('Unicorn::HttpServer', http_server_class)
+    end
+
+    context 'when no servers are running' do
+      it_behaves_like 'with state', [false, 'unexpected Unicorn check result: 0']
+    end
+
+    context 'when servers without workers are running' do
+      before do
+        http_server_class.new(0)
+      end
+
+      it_behaves_like 'with state', [false, 'unexpected Unicorn check result: 0']
+    end
+
+    context 'when servers with workers are running' do
+      before do
+        http_server_class.new(1)
+      end
+
+      it_behaves_like 'with state', true
+    end
+  end
+end
diff --git a/spec/lib/gitlab/hook_data/issue_builder_spec.rb b/spec/lib/gitlab/hook_data/issue_builder_spec.rb
index 6013fb78bc780f294cdffd825797eeec5ff4357c..ebd7feb0055abb9d28ba8792a1f39c30637a57f1 100644
--- a/spec/lib/gitlab/hook_data/issue_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/issue_builder_spec.rb
@@ -26,7 +26,7 @@
         duplicated_to_id
         project_id
         relative_position
-        state
+        state_id
         time_estimate
         title
         updated_at
@@ -41,6 +41,7 @@
       expect(data).to include(:human_time_estimate)
       expect(data).to include(:human_total_time_spent)
       expect(data).to include(:assignee_ids)
+      expect(data).to include(:state)
       expect(data).to include('labels' => [label.hook_attrs])
     end
 
diff --git a/spec/lib/gitlab/import/merge_request_creator_spec.rb b/spec/lib/gitlab/import/merge_request_creator_spec.rb
index 7c73e9b39f738d01f4c0ee7a151711a5f0e6d860..ff2c3032dbf4ed75d6e8f0c89425dbcadbe19737 100644
--- a/spec/lib/gitlab/import/merge_request_creator_spec.rb
+++ b/spec/lib/gitlab/import/merge_request_creator_spec.rb
@@ -21,8 +21,11 @@
 
         subject.execute(attributes)
 
-        expect(merge_request.reload.merge_request_diffs.count).to eq(1)
-        expect(merge_request.reload.merge_request_diffs.first.commits.count).to eq(commits_count)
+        merge_request.reload
+
+        expect(merge_request.merge_request_diffs.count).to eq(1)
+        expect(merge_request.merge_request_diffs.first.commits.count).to eq(commits_count)
+        expect(merge_request.latest_merge_request_diff_id).to eq(merge_request.merge_request_diffs.first.id)
       end
     end
 
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 187a8a371792564e455dda91aa5266577f2d94b8..4fd61383c6b204933602c96219b079ae5b805b4d 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -25,8 +25,10 @@ issues:
 - epic
 - designs
 - design_versions
+- description_versions
 - prometheus_alerts
 - prometheus_alert_events
+- self_managed_prometheus_alert_events
 events:
 - author
 - project
@@ -81,6 +83,7 @@ releases:
 - links
 - milestone_releases
 - milestones
+- evidence
 links:
 - release
 project_members:
@@ -130,6 +133,7 @@ merge_requests:
 - blocks_as_blockee
 - blocking_merge_requests
 - blocked_merge_requests
+- description_versions
 external_pull_requests:
 - project
 merge_request_diff:
@@ -400,6 +404,7 @@ project:
 - operations_feature_flags_client
 - prometheus_alerts
 - prometheus_alert_events
+- self_managed_prometheus_alert_events
 - software_license_policies
 - project_registry
 - packages
@@ -473,6 +478,8 @@ prometheus_alerts:
 - prometheus_alert_events
 prometheus_alert_events:
 - project
+self_managed_prometheus_alert_events:
+- project
 epic_issues:
 - issue
 - epic
@@ -506,6 +513,8 @@ lists:
 milestone_releases:
 - milestone
 - release
+evidences:
+- release
 design: &design
 - issue
 - actions
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index 218031784cbaac5ba43bb54473e439dff514f23b..676973ff5e7d2ea0a5b7fc170c1ecb7435bac8a2 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -434,6 +434,11 @@
                       labels: 0,
                       milestones: 0,
                       first_issue_labels: 1
+
+      it 'restores issue states' do
+        expect(project.issues.with_state(:closed).count).to eq(1)
+        expect(project.issues.with_state(:opened).count).to eq(1)
+      end
     end
 
     context 'with existing group models' do
diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb
index 51b2fd06b46225c82050c9239709b5062a49506e..a23e68a8f0071ea98a806d5d21f467482ab8b673 100644
--- a/spec/lib/gitlab/import_export/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb
@@ -85,7 +85,7 @@
   class FooModel
     include ActiveModel::Model
 
-    def initialize(params)
+    def initialize(params = {})
       params.each { |key, value| send("#{key}=", value) }
     end
 
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index d3b51a53ede6b701a79bf5d7dfe8ba5a2651fea9..8ae571a69efbad956d09d31627cac2fac0925f24 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -47,6 +47,7 @@ PushEventPayload:
 - commit_to
 - ref
 - commit_title
+- ref_count
 Note:
 - id
 - note
@@ -126,6 +127,12 @@ Release:
 - created_at
 - updated_at
 - released_at
+Evidence:
+- id
+- release_id
+- summary
+- created_at
+- updated_at
 Releases::Link:
 - id
 - release_id
diff --git a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
index e2ce1869810c1b414f8196a68a4908f14853f55e..4fa136bc405af3e9887b922ba485c0490c350e32 100644
--- a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
@@ -25,6 +25,14 @@
       end
     end
 
+    context 'when the dashboard is not present' do
+      let(:dashboard_yml) { nil }
+
+      it 'returns nil' do
+        expect(dashboard).to be_nil
+      end
+    end
+
     context 'when dashboard config corresponds to common metrics' do
       let!(:common_metric) { create(:prometheus_metric, :common, identifier: 'metric_a1') }
 
diff --git a/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb b/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5c2ec6dae6b079174c5ca6dceb61c87da7f1dd33
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Dashboard::Stages::GrafanaFormatter do
+  include GrafanaApiHelpers
+
+  let_it_be(:namespace) { create(:namespace, name: 'foo') }
+  let_it_be(:project) { create(:project, namespace: namespace, name: 'bar') }
+
+  describe '#transform!' do
+    let(:grafana_dashboard) { JSON.parse(fixture_file('grafana/simplified_dashboard_response.json'), symbolize_names: true) }
+    let(:datasource) { JSON.parse(fixture_file('grafana/datasource_response.json'), symbolize_names: true) }
+
+    let(:dashboard) { described_class.new(project, {}, params).transform! }
+
+    let(:params) do
+      {
+        grafana_dashboard: grafana_dashboard,
+        datasource: datasource,
+        grafana_url: valid_grafana_dashboard_link('https://grafana.example.com')
+      }
+    end
+
+    context 'when the query and resources are configured correctly' do
+      let(:expected_dashboard) { JSON.parse(fixture_file('grafana/expected_grafana_embed.json'), symbolize_names: true) }
+
+      it 'generates a gitlab-yml formatted dashboard' do
+        expect(dashboard).to eq(expected_dashboard)
+      end
+    end
+
+    context 'when the inputs are invalid' do
+      shared_examples_for 'processing error' do
+        it 'raises a processing error' do
+          expect { dashboard }
+            .to raise_error(Gitlab::Metrics::Dashboard::Stages::InputFormatValidator::DashboardProcessingError)
+        end
+      end
+
+      context 'when the datasource is not proxyable' do
+        before do
+          params[:datasource][:access] = 'not-proxy'
+        end
+
+        it_behaves_like 'processing error'
+      end
+
+      context 'when query param "panelId" is not specified' do
+        before do
+          params[:grafana_url].gsub!('panelId=8', '')
+        end
+
+        it_behaves_like 'processing error'
+      end
+
+      context 'when query param "from" is not specified' do
+        before do
+          params[:grafana_url].gsub!('from=1570397739557', '')
+        end
+
+        it_behaves_like 'processing error'
+      end
+
+      context 'when query param "to" is not specified' do
+        before do
+          params[:grafana_url].gsub!('to=1570484139557', '')
+        end
+
+        it_behaves_like 'processing error'
+      end
+
+      context 'when the panel is not a graph' do
+        before do
+          params[:grafana_dashboard][:dashboard][:panels][0][:type] = 'singlestat'
+        end
+
+        it_behaves_like 'processing error'
+      end
+
+      context 'when the panel is not a line graph' do
+        before do
+          params[:grafana_dashboard][:dashboard][:panels][0][:lines] = false
+        end
+
+        it_behaves_like 'processing error'
+      end
+
+      context 'when the query dashboard includes undefined variables' do
+        before do
+          params[:grafana_url].gsub!('&var-instance=localhost:9121', '')
+        end
+
+        it_behaves_like 'processing error'
+      end
+
+      context 'when the expression contains unsupported global variables' do
+        before do
+          params[:grafana_dashboard][:dashboard][:panels][0][:targets][0][:expr] = 'sum(important_metric[$__interval_ms])'
+        end
+
+        it_behaves_like 'processing error'
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb b/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb
index bedf4fedcfa57f4aa2b5c2346ed8363aa1dc12d5..47ec69e2f45952f79d544ed665e2f5f1585d9866 100644
--- a/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb
+++ b/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb
@@ -19,15 +19,11 @@
         BindAddress: anything,
         Logger: anything,
         AccessLog: anything
-      ).and_wrap_original do |m, *args|
-        m.call(DoNotListen: true, Logger: args.first[:Logger])
-      end
-
-      allow_any_instance_of(::WEBrick::HTTPServer).to receive(:start)
+      ).and_call_original
 
       allow(settings).to receive(:enabled).and_return(true)
-      allow(settings).to receive(:port).and_return(8082)
-      allow(settings).to receive(:address).and_return('localhost')
+      allow(settings).to receive(:port).and_return(0)
+      allow(settings).to receive(:address).and_return('127.0.0.1')
     end
 
     after do
@@ -61,9 +57,23 @@
               m.call(DoNotListen: true, Logger: args.first[:Logger])
             end
 
+            allow_any_instance_of(::WEBrick::HTTPServer).to receive(:start)
+
             exporter.start.join
           end
         end
+
+        describe 'when thread is not alive' do
+          it 'does close listeners' do
+            expect_any_instance_of(::WEBrick::HTTPServer).to receive(:start)
+            expect_any_instance_of(::WEBrick::HTTPServer).to receive(:listeners)
+              .and_call_original
+
+            expect { exporter.start.join }.to change { exporter.thread? }.from(false).to(true)
+
+            exporter.stop
+          end
+        end
       end
 
       describe '#stop' do
@@ -77,14 +87,14 @@
 
     describe 'when exporter is running' do
       before do
-        exporter.start.join
+        exporter.start
       end
 
       describe '#start' do
         it "doesn't start running server" do
           expect_any_instance_of(::WEBrick::HTTPServer).not_to receive(:start)
 
-          expect { exporter.start.join }.not_to change { exporter.thread? }
+          expect { exporter.start }.not_to change { exporter.thread? }
         end
       end
 
diff --git a/spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb b/spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a415b6407d538b478bcf1d23861d4eb6420c98b9
--- /dev/null
+++ b/spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Exporter::SidekiqExporter do
+  let(:exporter) { described_class.new }
+
+  after do
+    exporter.stop
+  end
+
+  context 'with valid config' do
+    before do
+      stub_config(
+        monitoring: {
+          sidekiq_exporter: {
+            enabled: true,
+            port: 0,
+            address: '127.0.0.1'
+          }
+        }
+      )
+    end
+
+    it 'does start thread' do
+      expect(exporter.start).not_to be_nil
+    end
+  end
+
+  context 'when port is already taken' do
+    let(:first_exporter) { described_class.new }
+
+    before do
+      stub_config(
+        monitoring: {
+          sidekiq_exporter: {
+            enabled: true,
+            port: 9992,
+            address: '127.0.0.1'
+          }
+        }
+      )
+
+      first_exporter.start
+    end
+
+    after do
+      first_exporter.stop
+    end
+
+    it 'does print error message' do
+      expect(Sidekiq.logger).to receive(:error)
+        .with(
+          class: described_class.to_s,
+          message: 'Cannot start sidekiq_exporter',
+          exception: anything)
+
+      exporter.start
+    end
+
+    it 'does not start thread' do
+      expect(exporter.start).to be_nil
+    end
+  end
+end
diff --git a/spec/lib/gitlab/metrics/exporter/web_exporter_spec.rb b/spec/lib/gitlab/metrics/exporter/web_exporter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..99349934e63d750ed013eaff5d3f0d74aef298c8
--- /dev/null
+++ b/spec/lib/gitlab/metrics/exporter/web_exporter_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Exporter::WebExporter do
+  let(:exporter) { described_class.new }
+
+  context 'when blackout seconds is used' do
+    let(:blackout_seconds) { 0 }
+    let(:readiness_probe) { exporter.send(:readiness_probe).execute }
+
+    before do
+      stub_config(
+        monitoring: {
+          web_exporter: {
+            enabled: true,
+            port: 0,
+            address: '127.0.0.1',
+            blackout_seconds: blackout_seconds
+          }
+        }
+      )
+
+      exporter.start
+    end
+
+    after do
+      exporter.stop
+    end
+
+    context 'when running server' do
+      it 'readiness probe returns succesful status' do
+        expect(readiness_probe.http_status).to eq(200)
+        expect(readiness_probe.json).to include(status: 'ok')
+        expect(readiness_probe.json).to include('web_exporter' => [{ 'status': 'ok' }])
+      end
+    end
+
+    context 'when blackout seconds is 10s' do
+      let(:blackout_seconds) { 10 }
+
+      it 'readiness probe returns a failure status' do
+        # during sleep we check the status of readiness probe
+        expect(exporter).to receive(:sleep).with(10) do
+          expect(readiness_probe.http_status).to eq(503)
+          expect(readiness_probe.json).to include(status: 'failed')
+          expect(readiness_probe.json).to include('web_exporter' => [{ 'status': 'failed' }])
+        end
+
+        exporter.stop
+      end
+    end
+
+    context 'when blackout is disabled' do
+      let(:blackout_seconds) { 0 }
+
+      it 'readiness probe returns a failure status' do
+        expect(exporter).not_to receive(:sleep)
+
+        exporter.stop
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb
index b8add3c1324d67c9fc6d2e4ae3dffd6cfb94b47e..1097d26c3206e9c60c571ba98d49f8da978f291e 100644
--- a/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb
@@ -4,6 +4,7 @@
 
 describe Gitlab::Metrics::Samplers::PumaSampler do
   subject { described_class.new(5) }
+
   let(:null_metric) { double('null_metric', set: nil, observe: nil) }
 
   before do
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
index 6d2764a06f2135f21078748ec6603005f01f4763..a5aa80686fdf9dd2a88e4f6bad8012e10288449e 100644
--- a/spec/lib/gitlab/metrics/system_spec.rb
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -58,4 +58,44 @@
       expect(described_class.monotonic_time).to be_an(Float)
     end
   end
+
+  describe '.thread_cpu_time' do
+    it 'returns cpu_time on supported platform' do
+      stub_const("Process::CLOCK_THREAD_CPUTIME_ID", 16)
+
+      expect(Process).to receive(:clock_gettime)
+        .with(16, kind_of(Symbol)) { 0.111222333 }
+
+      expect(described_class.thread_cpu_time).to eq(0.111222333)
+    end
+
+    it 'returns nil on unsupported platform' do
+      hide_const("Process::CLOCK_THREAD_CPUTIME_ID")
+
+      expect(described_class.thread_cpu_time).to be_nil
+    end
+  end
+
+  describe '.thread_cpu_duration' do
+    let(:start_time) { described_class.thread_cpu_time }
+
+    it 'returns difference between start and current time' do
+      stub_const("Process::CLOCK_THREAD_CPUTIME_ID", 16)
+
+      expect(Process).to receive(:clock_gettime)
+        .with(16, kind_of(Symbol))
+        .and_return(
+          0.111222333,
+          0.222333833
+        )
+
+      expect(described_class.thread_cpu_duration(start_time)).to eq(0.1111115)
+    end
+
+    it 'returns nil on unsupported platform' do
+      hide_const("Process::CLOCK_THREAD_CPUTIME_ID")
+
+      expect(described_class.thread_cpu_duration(start_time)).to be_nil
+    end
+  end
 end
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
index 45e74597a2e851929db9d227e83f4fc170fbfc2a..08de2426c5aca633f7ac28d8866171a696900414 100644
--- a/spec/lib/gitlab/metrics/transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -27,6 +27,14 @@
     end
   end
 
+  describe '#thread_cpu_duration' do
+    it 'returns the duration of a transaction in seconds' do
+      transaction.run { }
+
+      expect(transaction.thread_cpu_duration).to be > 0
+    end
+  end
+
   describe '#allocated_memory' do
     it 'returns the allocated memory in bytes' do
       transaction.run { 'a' * 32 }
diff --git a/spec/lib/gitlab/patch/prependable_spec.rb b/spec/lib/gitlab/patch/prependable_spec.rb
index 725d733d1769afcb197b523acf2ff3b919730824..255324f89d52e9aa12f52ab49901287eb63bd46f 100644
--- a/spec/lib/gitlab/patch/prependable_spec.rb
+++ b/spec/lib/gitlab/patch/prependable_spec.rb
@@ -72,8 +72,8 @@ def name
       expect(subject.ancestors.take(3)).to eq([subject, ee, ce])
       expect(subject.singleton_class.ancestors.take(3))
         .to eq([subject.singleton_class,
-                ee.const_get(:ClassMethods),
-                ce.const_get(:ClassMethods)])
+                ee.const_get(:ClassMethods, false),
+                ce.const_get(:ClassMethods, false)])
     end
 
     it 'prepends only once even if called twice' do
@@ -115,8 +115,8 @@ def name
     it 'has the expected ancestors' do
       expect(subject.ancestors.take(3)).to eq([ee, ce, subject])
       expect(subject.singleton_class.ancestors.take(3))
-        .to eq([ee.const_get(:ClassMethods),
-                ce.const_get(:ClassMethods),
+        .to eq([ee.const_get(:ClassMethods, false),
+                ce.const_get(:ClassMethods, false),
                 subject.singleton_class])
     end
 
@@ -152,7 +152,7 @@ def name
     it 'has the expected ancestors' do
       expect(subject.ancestors.take(2)).to eq([ee, subject])
       expect(subject.singleton_class.ancestors.take(2))
-        .to eq([ee.const_get(:ClassMethods),
+        .to eq([ee.const_get(:ClassMethods, false),
                 subject.singleton_class])
     end
 
diff --git a/spec/lib/gitlab/phabricator_import/worker_state_spec.rb b/spec/lib/gitlab/phabricator_import/worker_state_spec.rb
index b6f2524a9d06865327e8d5047861292ff04bbbd8..51514dd0ffd83663a653ddcec09b0bff6da338ea 100644
--- a/spec/lib/gitlab/phabricator_import/worker_state_spec.rb
+++ b/spec/lib/gitlab/phabricator_import/worker_state_spec.rb
@@ -4,6 +4,7 @@
 
 describe Gitlab::PhabricatorImport::WorkerState, :clean_gitlab_redis_shared_state do
   subject(:state) { described_class.new('weird-project-id') }
+
   let(:key) { 'phabricator-import/jobs/project-weird-project-id/job-count' }
 
   describe '#add_job' do
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 7513dbeeb6f373a36f80a37fe3795b55116bf104..6bc9b6365d163ba8d6d9db8df45f03d616058579 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -259,13 +259,15 @@
 
   describe '.references_pattern' do
     subject { described_class.references_pattern }
+
     it { is_expected.to be_kind_of Regexp }
   end
 
   describe 'referables prefixes' do
     def prefixes
       described_class::REFERABLES.each_with_object({}) do |referable, result|
-        klass = referable.to_s.camelize.constantize
+        class_name = referable.to_s.camelize
+        klass = class_name.constantize if Object.const_defined?(class_name)
 
         next unless klass.respond_to?(:reference_prefix)
 
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index 3036e3a9754315770172c5958dee86f7afa2b8a5..b557baed258a9f50b867da88f81912f79904a733 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -64,4 +64,15 @@
     it { is_expected.not_to match('.my/image') }
     it { is_expected.not_to match('my/image.') }
   end
+
+  describe '.aws_account_id_regex' do
+    subject { described_class.aws_arn_regex }
+
+    it { is_expected.to match('arn:aws:iam::123456789012:role/role-name') }
+    it { is_expected.to match('arn:aws:s3:::bucket/key') }
+    it { is_expected.to match('arn:aws:ec2:us-east-1:123456789012:volume/vol-1') }
+    it { is_expected.to match('arn:aws:rds:us-east-1:123456789012:pg:prod') }
+    it { is_expected.not_to match('123456789012') }
+    it { is_expected.not_to match('role/role-name') }
+  end
 end
diff --git a/spec/lib/gitlab/request_context_spec.rb b/spec/lib/gitlab/request_context_spec.rb
index a744f48da1f6b705e71742723d197b2ab68a4674..cde12d4b3104f561098bf5057a5e9cf19d0585d1 100644
--- a/spec/lib/gitlab/request_context_spec.rb
+++ b/spec/lib/gitlab/request_context_spec.rb
@@ -5,6 +5,7 @@
 describe Gitlab::RequestContext do
   describe '#client_ip' do
     subject { described_class.client_ip }
+
     let(:app) { -> (env) {} }
     let(:env) { Hash.new }
 
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index ff9e31ec34691eba91744d9a8bfda614b0950afc..a17e9a312128ddcb739c36b62b6247cb770b1b35 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -396,6 +396,7 @@
 
   describe 'namespace actions' do
     subject { described_class.new }
+
     let(:storage) { Gitlab.config.repositories.storages.keys.first }
 
     describe '#add_namespace' do
diff --git a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb
index 263cc821c1aee8715eb5fc56f517a6af6c8539de..45bcc71dfcb0fe9cc18039e2d925ab28d7820e0d 100644
--- a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb
+++ b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb
@@ -7,13 +7,16 @@
   let(:pid) { 12345 }
 
   before do
-    allow(memory_killer).to receive(:pid).and_return(pid)
     allow(Sidekiq.logger).to receive(:info)
     allow(Sidekiq.logger).to receive(:warn)
+    allow(memory_killer).to receive(:pid).and_return(pid)
+
+    # make sleep no-op
+    allow(memory_killer).to receive(:sleep) {}
   end
 
-  describe '#start_working' do
-    subject { memory_killer.send(:start_working) }
+  describe '#run_thread' do
+    subject { memory_killer.send(:run_thread) }
 
     before do
       # let enabled? return 3 times: true, true, false
@@ -37,10 +40,11 @@
           .with(
             class: described_class.to_s,
             pid: pid,
-            message: "Exception from start_working: My Exception")
+            message: "Exception from run_thread: My Exception")
 
-        expect(memory_killer).to receive(:rss_within_range?).twice.and_raise(StandardError, 'My Exception')
-        expect(memory_killer).to receive(:sleep).twice.with(Gitlab::SidekiqDaemon::MemoryKiller::CHECK_INTERVAL_SECONDS)
+        expect(memory_killer).to receive(:rss_within_range?)
+          .twice
+          .and_raise(StandardError, 'My Exception')
 
         expect { subject }.not_to raise_exception
       end
@@ -50,9 +54,11 @@
           .with(
             class: described_class.to_s,
             pid: pid,
-            message: "Exception from start_working: My Exception")
+            message: "Exception from run_thread: My Exception")
 
-        expect(memory_killer).to receive(:rss_within_range?).once.and_raise(Exception, 'My Exception')
+        expect(memory_killer).to receive(:rss_within_range?)
+          .once
+          .and_raise(Exception, 'My Exception')
 
         expect(memory_killer).to receive(:sleep).with(Gitlab::SidekiqDaemon::MemoryKiller::CHECK_INTERVAL_SECONDS)
         expect(Sidekiq.logger).to receive(:warn).once
@@ -78,7 +84,9 @@
     end
 
     it 'not invoke restart_sidekiq when rss in range' do
-      expect(memory_killer).to receive(:rss_within_range?).twice.and_return(true)
+      expect(memory_killer).to receive(:rss_within_range?)
+        .twice
+        .and_return(true)
 
       expect(memory_killer).not_to receive(:restart_sidekiq)
 
@@ -86,9 +94,12 @@
     end
 
     it 'invoke restart_sidekiq when rss not in range' do
-      expect(memory_killer).to receive(:rss_within_range?).at_least(:once).and_return(false)
+      expect(memory_killer).to receive(:rss_within_range?)
+        .at_least(:once)
+        .and_return(false)
 
-      expect(memory_killer).to receive(:restart_sidekiq).at_least(:once)
+      expect(memory_killer).to receive(:restart_sidekiq)
+        .at_least(:once)
 
       subject
     end
@@ -97,10 +108,9 @@
   describe '#stop_working' do
     subject { memory_killer.send(:stop_working)}
 
-    it 'changed enable? to false' do
-      expect(memory_killer.send(:enabled?)).to be true
-      subject
-      expect(memory_killer.send(:enabled?)).to be false
+    it 'changes enable? to false' do
+      expect { subject }.to change { memory_killer.send(:enabled?) }
+        .from(true).to(false)
     end
   end
 
@@ -121,8 +131,12 @@
 
     it 'return true when everything is within limit' do
       expect(memory_killer).to receive(:get_rss).and_return(100)
-      expect(memory_killer).to receive(:soft_limit_rss).and_return(200)
-      expect(memory_killer).to receive(:hard_limit_rss).and_return(300)
+      expect(memory_killer).to receive(:get_soft_limit_rss).and_return(200)
+      expect(memory_killer).to receive(:get_hard_limit_rss).and_return(300)
+
+      expect(memory_killer).to receive(:refresh_state)
+        .with(:running)
+        .and_call_original
 
       expect(Gitlab::Metrics::System).to receive(:monotonic_time).and_call_original
       expect(memory_killer).not_to receive(:log_rss_out_of_range)
@@ -131,9 +145,17 @@
     end
 
     it 'return false when rss exceeds hard_limit_rss' do
-      expect(memory_killer).to receive(:get_rss).and_return(400)
-      expect(memory_killer).to receive(:soft_limit_rss).at_least(:once).and_return(200)
-      expect(memory_killer).to receive(:hard_limit_rss).at_least(:once).and_return(300)
+      expect(memory_killer).to receive(:get_rss).at_least(:once).and_return(400)
+      expect(memory_killer).to receive(:get_soft_limit_rss).at_least(:once).and_return(200)
+      expect(memory_killer).to receive(:get_hard_limit_rss).at_least(:once).and_return(300)
+
+      expect(memory_killer).to receive(:refresh_state)
+        .with(:running)
+        .and_call_original
+
+      expect(memory_killer).to receive(:refresh_state)
+        .with(:above_soft_limit)
+        .and_call_original
 
       expect(Gitlab::Metrics::System).to receive(:monotonic_time).and_call_original
 
@@ -143,13 +165,21 @@
     end
 
     it 'return false when rss exceed hard_limit_rss after a while' do
-      expect(memory_killer).to receive(:get_rss).and_return(250, 400)
-      expect(memory_killer).to receive(:soft_limit_rss).at_least(:once).and_return(200)
-      expect(memory_killer).to receive(:hard_limit_rss).at_least(:once).and_return(300)
+      expect(memory_killer).to receive(:get_rss).and_return(250, 400, 400)
+      expect(memory_killer).to receive(:get_soft_limit_rss).at_least(:once).and_return(200)
+      expect(memory_killer).to receive(:get_hard_limit_rss).at_least(:once).and_return(300)
+
+      expect(memory_killer).to receive(:refresh_state)
+        .with(:running)
+        .and_call_original
+
+      expect(memory_killer).to receive(:refresh_state)
+        .at_least(:once)
+        .with(:above_soft_limit)
+        .and_call_original
 
       expect(Gitlab::Metrics::System).to receive(:monotonic_time).twice.and_call_original
       expect(memory_killer).to receive(:sleep).with(check_interval_seconds)
-
       expect(memory_killer).to receive(:log_rss_out_of_range).with(400, 300, 200)
 
       expect(subject).to be false
@@ -157,8 +187,16 @@
 
     it 'return true when rss below soft_limit_rss after a while within GRACE_BALLOON_SECONDS' do
       expect(memory_killer).to receive(:get_rss).and_return(250, 100)
-      expect(memory_killer).to receive(:soft_limit_rss).and_return(200, 200)
-      expect(memory_killer).to receive(:hard_limit_rss).and_return(300, 300)
+      expect(memory_killer).to receive(:get_soft_limit_rss).and_return(200, 200)
+      expect(memory_killer).to receive(:get_hard_limit_rss).and_return(300, 300)
+
+      expect(memory_killer).to receive(:refresh_state)
+        .with(:running)
+        .and_call_original
+
+      expect(memory_killer).to receive(:refresh_state)
+        .with(:above_soft_limit)
+        .and_call_original
 
       expect(Gitlab::Metrics::System).to receive(:monotonic_time).twice.and_call_original
       expect(memory_killer).to receive(:sleep).with(check_interval_seconds)
@@ -168,17 +206,27 @@
       expect(subject).to be true
     end
 
-    it 'return false when rss exceed soft_limit_rss longer than GRACE_BALLOON_SECONDS' do
-      expect(memory_killer).to receive(:get_rss).exactly(4).times.and_return(250)
-      expect(memory_killer).to receive(:soft_limit_rss).exactly(5).times.and_return(200)
-      expect(memory_killer).to receive(:hard_limit_rss).exactly(5).times.and_return(300)
+    context 'when exceeding GRACE_BALLOON_SECONDS' do
+      let(:grace_balloon_seconds) { 0 }
 
-      expect(Gitlab::Metrics::System).to receive(:monotonic_time).exactly(5).times.and_call_original
-      expect(memory_killer).to receive(:sleep).exactly(3).times.with(check_interval_seconds).and_call_original
+      it 'return false when rss exceed soft_limit_rss' do
+        allow(memory_killer).to receive(:get_rss).and_return(250)
+        allow(memory_killer).to receive(:get_soft_limit_rss).and_return(200)
+        allow(memory_killer).to receive(:get_hard_limit_rss).and_return(300)
 
-      expect(memory_killer).to receive(:log_rss_out_of_range).with(250, 300, 200)
+        expect(memory_killer).to receive(:refresh_state)
+          .with(:running)
+          .and_call_original
 
-      expect(subject).to be false
+        expect(memory_killer).to receive(:refresh_state)
+          .with(:above_soft_limit)
+          .and_call_original
+
+        expect(memory_killer).to receive(:log_rss_out_of_range)
+          .with(250, 300, 200)
+
+        expect(subject).to be false
+      end
     end
   end
 
@@ -190,19 +238,42 @@
     before do
       stub_const("#{described_class}::SHUTDOWN_TIMEOUT_SECONDS", shutdown_timeout_seconds)
       allow(Sidekiq).to receive(:options).and_return(timeout: 9)
+      allow(memory_killer).to receive(:get_rss).and_return(100)
+      allow(memory_killer).to receive(:get_soft_limit_rss).and_return(200)
+      allow(memory_killer).to receive(:get_hard_limit_rss).and_return(300)
     end
 
     it 'send signal' do
-      expect(memory_killer).to receive(:signal_and_wait).with(shutdown_timeout_seconds, 'SIGTSTP', 'stop fetching new jobs').ordered
-      expect(memory_killer).to receive(:signal_and_wait).with(11, 'SIGTERM', 'gracefully shut down').ordered
-      expect(memory_killer).to receive(:signal_pgroup).with('SIGKILL', 'die').ordered
+      expect(memory_killer).to receive(:refresh_state)
+        .with(:stop_fetching_new_jobs)
+        .ordered
+        .and_call_original
+      expect(memory_killer).to receive(:signal_and_wait)
+        .with(shutdown_timeout_seconds, 'SIGTSTP', 'stop fetching new jobs')
+        .ordered
+
+      expect(memory_killer).to receive(:refresh_state)
+        .with(:shutting_down)
+        .ordered
+        .and_call_original
+      expect(memory_killer).to receive(:signal_and_wait)
+        .with(11, 'SIGTERM', 'gracefully shut down')
+        .ordered
+
+      expect(memory_killer).to receive(:refresh_state)
+        .with(:killing_sidekiq)
+        .ordered
+        .and_call_original
+      expect(memory_killer).to receive(:signal_pgroup)
+        .with('SIGKILL', 'die')
+        .ordered
 
       subject
     end
   end
 
   describe '#signal_and_wait' do
-    let(:time) { 7 }
+    let(:time) { 0 }
     let(:signal) { 'my-signal' }
     let(:explanation) { 'my-explanation' }
     let(:check_interval_seconds) { 2 }
@@ -226,14 +297,17 @@
     end
 
     it 'send signal and wait till deadline if any job not finished' do
-      expect(Process).to receive(:kill).with(signal, pid).ordered
-      expect(Gitlab::Metrics::System).to receive(:monotonic_time).and_call_original.at_least(:once)
+      expect(Process).to receive(:kill)
+        .with(signal, pid)
+        .ordered
+
+      expect(Gitlab::Metrics::System).to receive(:monotonic_time)
+        .and_call_original
+        .at_least(:once)
 
       expect(memory_killer).to receive(:enabled?).and_return(true).at_least(:once)
       expect(memory_killer).to receive(:any_jobs?).and_return(true).at_least(:once)
 
-      expect(memory_killer).to receive(:sleep).and_call_original.exactly(4).times
-
       subject
     end
   end
@@ -401,4 +475,27 @@
       expect(subject).to eq(10)
     end
   end
+
+  describe '#refresh_state' do
+    let(:metrics) { memory_killer.instance_variable_get(:@metrics) }
+
+    subject { memory_killer.send(:refresh_state, :shutting_down) }
+
+    it 'calls gitlab metrics gauge set methods' do
+      expect(memory_killer).to receive(:get_rss) { 1010 }
+      expect(memory_killer).to receive(:get_soft_limit_rss) { 1020 }
+      expect(memory_killer).to receive(:get_hard_limit_rss) { 1040 }
+
+      expect(metrics[:sidekiq_memory_killer_phase]).to receive(:set)
+        .with({}, described_class::PHASE[:shutting_down])
+      expect(metrics[:sidekiq_current_rss]).to receive(:set)
+        .with({}, 1010)
+      expect(metrics[:sidekiq_memory_killer_soft_limit_rss]).to receive(:set)
+        .with({}, 1020)
+      expect(metrics[:sidekiq_memory_killer_hard_limit_rss]).to receive(:set)
+        .with({}, 1040)
+
+      subject
+    end
+  end
 end
diff --git a/spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb b/spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb
index 397098ed5a4ccb0b41f92f44de6d5c65c40d75a9..3f49ef0e9a7b4f805ebe758ce6a2d3fe866c22eb 100644
--- a/spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb
+++ b/spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb
@@ -37,8 +37,8 @@
     end
   end
 
-  describe '#start_working when notification channel not enabled' do
-    subject { monitor.send(:start_working) }
+  describe '#run_thread when notification channel not enabled' do
+    subject { monitor.send(:run_thread) }
 
     it 'return directly' do
       allow(monitor).to receive(:notification_channel_enabled?).and_return(nil)
@@ -52,8 +52,8 @@
     end
   end
 
-  describe '#start_working when notification channel enabled' do
-    subject { monitor.send(:start_working) }
+  describe '#run_thread when notification channel enabled' do
+    subject { monitor.send(:run_thread) }
 
     before do
       # we want to run at most once cycle
diff --git a/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb
index bf3bc8e1adde2ab05d071e53c8b09f2efa11dad0..b5be43ec96c6377c4602e7bcbd8ced77c706493f 100644
--- a/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb
@@ -4,6 +4,7 @@
 
 describe Gitlab::SidekiqMiddleware::MemoryKiller do
   subject { described_class.new }
+
   let(:pid) { 999 }
 
   let(:worker) { double(:worker, class: ProjectCacheWorker) }
diff --git a/spec/lib/gitlab/submodule_links_spec.rb b/spec/lib/gitlab/submodule_links_spec.rb
index d4420c5b5134142eedb863c5849c33a48750d79f..f0c8825de74fa215db2db4decb3d090519714c30 100644
--- a/spec/lib/gitlab/submodule_links_spec.rb
+++ b/spec/lib/gitlab/submodule_links_spec.rb
@@ -8,7 +8,9 @@
   let(:links) { described_class.new(repo) }
 
   describe '#for' do
-    subject { links.for(submodule_item, 'ref') }
+    let(:ref) { 'ref' }
+
+    subject { links.for(submodule_item, ref) }
 
     context 'when there is no .gitmodules file' do
       before do
@@ -35,8 +37,20 @@
         stub_urls({ 'gitlab-foss' => 'git@gitlab.com:gitlab-org/gitlab-foss.git' })
       end
 
-      it 'returns links' do
+      it 'returns links and caches the by ref' do
         expect(subject).to eq(['https://gitlab.com/gitlab-org/gitlab-foss', 'https://gitlab.com/gitlab-org/gitlab-foss/tree/hash'])
+
+        cache_store = links.instance_variable_get("@cache_store")
+
+        expect(cache_store[ref]).to eq({ "gitlab-foss" => "git@gitlab.com:gitlab-org/gitlab-foss.git" })
+      end
+
+      context 'when ref name contains a dash' do
+        let(:ref) { 'signed-commits' }
+
+        it 'returns links' do
+          expect(subject).to eq(['https://gitlab.com/gitlab-org/gitlab-foss', 'https://gitlab.com/gitlab-org/gitlab-foss/tree/hash'])
+        end
       end
     end
   end
diff --git a/spec/lib/gitlab/tracking/incident_management_spec.rb b/spec/lib/gitlab/tracking/incident_management_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6f7e04b7c169dc9f8d0843f891f16fb150ef8030
--- /dev/null
+++ b/spec/lib/gitlab/tracking/incident_management_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Tracking::IncidentManagement do
+  describe '.track_from_params' do
+    shared_examples 'a tracked event' do |label, value = nil|
+      it 'creates the tracking event with the correct details' do
+        expect(::Gitlab::Tracking)
+          .to receive(:event)
+          .with(
+            'IncidentManagement::Settings',
+            label,
+            value || kind_of(Hash)
+          )
+      end
+    end
+
+    after do
+      described_class.track_from_params(params)
+    end
+
+    context 'known params' do
+      known_params = described_class.tracking_keys
+
+      known_params.each do |key, values|
+        context "param #{key}" do
+          let(:params) { { key => '1' } }
+
+          it_behaves_like 'a tracked event', "enabled_#{known_params[key][:name]}"
+        end
+      end
+
+      context 'different input values' do
+        shared_examples 'the correct prefixed event name' do |input, enabled|
+          let(:params) { { issue_template_key: input } }
+
+          it 'matches' do
+            expect(::Gitlab::Tracking)
+            .to receive(:event)
+            .with(
+              anything,
+              "#{enabled}_issue_template_on_alerts",
+              anything
+            )
+          end
+        end
+
+        it_behaves_like 'the correct prefixed event name', 1,          'enabled'
+        it_behaves_like 'the correct prefixed event name', '1',        'enabled'
+        it_behaves_like 'the correct prefixed event name', 'template', 'enabled'
+        it_behaves_like 'the correct prefixed event name', '',         'disabled'
+        it_behaves_like 'the correct prefixed event name', nil,        'disabled'
+      end
+
+      context 'param with label' do
+        let(:params) { { issue_template_key: '1' } }
+
+        it_behaves_like 'a tracked event', "enabled_issue_template_on_alerts", { label: 'Template name', property: '1' }
+      end
+
+      context 'param without label' do
+        let(:params) { { create_issue: '1' } }
+
+        it_behaves_like 'a tracked event', "enabled_issue_auto_creation_on_alerts", {}
+      end
+    end
+
+    context 'unknown params' do
+      let(:params) { { 'unknown' => '1' } }
+
+      it 'does not create the tracking event' do
+        expect(::Gitlab::Tracking)
+          .not_to receive(:event)
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/utils/override_spec.rb b/spec/lib/gitlab/utils/override_spec.rb
index 5855c4374a97333434333cb130b274a4ac0fba85..e2776efac8564c5e5b2fb446667321d025d71721 100644
--- a/spec/lib/gitlab/utils/override_spec.rb
+++ b/spec/lib/gitlab/utils/override_spec.rb
@@ -151,6 +151,7 @@ def bad
 
         context 'when subject is a module, and class is prepending it' do
           subject { extension }
+
           let(:klass) { prepending_class }
 
           it_behaves_like 'checking as intended'
@@ -158,6 +159,7 @@ def bad
 
         context 'when subject is a module, and class is including it' do
           subject { extension }
+
           let(:klass) { including_class }
 
           it_behaves_like 'checking as intended, nothing was overridden'
@@ -177,6 +179,7 @@ def bad
 
         context 'when subject is a module, and class is prepending it' do
           subject { extension }
+
           let(:klass) { prepending_class }
 
           it_behaves_like 'nothing happened'
@@ -184,6 +187,7 @@ def bad
 
         context 'when subject is a module, and class is including it' do
           subject { extension }
+
           let(:klass) { including_class }
 
           it 'does not complain when it is overriding something' do
@@ -215,6 +219,7 @@ def bad
 
         context 'when subject is a module, and class is prepending it' do
           subject { extension }
+
           let(:klass) { prepending_class_methods }
 
           it_behaves_like 'checking as intended'
@@ -222,6 +227,7 @@ def bad
 
         context 'when subject is a module, and class is extending it' do
           subject { extension }
+
           let(:klass) { extending_class_methods }
 
           it_behaves_like 'checking as intended, nothing was overridden'
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index c1d171815ba0d88c050316205cddd91b7db6c96d..6bf837f1d3fa9ca91936ab322b55e8ac7a6d07ea 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -146,7 +146,7 @@
 
   describe '.ee?' do
     before do
-      stub_env('IS_GITLAB_EE', nil) # Make sure the ENV is clean
+      stub_env('FOSS_ONLY', nil) # Make sure the ENV is clean
       described_class.instance_variable_set(:@is_ee, nil)
     end
 
@@ -154,42 +154,66 @@
       described_class.instance_variable_set(:@is_ee, nil)
     end
 
-    it 'returns true when using Enterprise Edition' do
-      root = Pathname.new('dummy')
-      license_path = double(:path, exist?: true)
+    context 'for EE' do
+      before do
+        root = Pathname.new('dummy')
+        license_path = double(:path, exist?: true)
 
-      allow(described_class)
-        .to receive(:root)
-              .and_return(root)
+        allow(described_class)
+          .to receive(:root)
+                .and_return(root)
 
-      allow(root)
-        .to receive(:join)
-              .with('ee/app/models/license.rb')
-              .and_return(license_path)
+        allow(root)
+          .to receive(:join)
+                .with('ee/app/models/license.rb')
+                .and_return(license_path)
+      end
 
-      expect(described_class.ee?).to eq(true)
-    end
+      context 'when using FOSS_ONLY=1' do
+        before do
+          stub_env('FOSS_ONLY', '1')
+        end
 
-    it 'returns false when using Community Edition' do
-      root = double(:path)
-      license_path = double(:path, exists?: false)
+        it 'returns not to be EE' do
+          expect(described_class).not_to be_ee
+        end
+      end
 
-      allow(described_class)
-        .to receive(:root)
-              .and_return(Pathname.new('dummy'))
+      context 'when using FOSS_ONLY=0' do
+        before do
+          stub_env('FOSS_ONLY', '0')
+        end
 
-      allow(root)
-        .to receive(:join)
-              .with('ee/app/models/license.rb')
-              .and_return(license_path)
+        it 'returns to be EE' do
+          expect(described_class).to be_ee
+        end
+      end
 
-      expect(described_class.ee?).to eq(false)
+      context 'when using default FOSS_ONLY' do
+        it 'returns to be EE' do
+          expect(described_class).to be_ee
+        end
+      end
     end
 
-    it 'returns true when the IS_GITLAB_EE variable is not empty' do
-      stub_env('IS_GITLAB_EE', '1')
+    context 'for CE' do
+      before do
+        root = double(:path)
+        license_path = double(:path, exists?: false)
 
-      expect(described_class.ee?).to eq(true)
+        allow(described_class)
+          .to receive(:root)
+                .and_return(Pathname.new('dummy'))
+
+        allow(root)
+          .to receive(:join)
+                .with('ee/app/models/license.rb')
+                .and_return(license_path)
+      end
+
+      it 'returns not to be EE' do
+        expect(described_class).not_to be_ee
+      end
     end
   end
 
diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb
index 91b076c31d608f2fe68685e95dd9c2f2213732eb..0f7f57095df1019f53e4320d11c219201dc08021 100644
--- a/spec/lib/google_api/cloud_platform/client_spec.rb
+++ b/spec/lib/google_api/cloud_platform/client_spec.rb
@@ -54,6 +54,7 @@
 
   describe '#projects_zones_clusters_get' do
     subject { client.projects_zones_clusters_get(spy, spy, spy) }
+
     let(:gke_cluster) { double }
 
     before do
@@ -160,6 +161,7 @@
 
   describe '#projects_zones_operations' do
     subject { client.projects_zones_operations(spy, spy, spy) }
+
     let(:operation) { double }
 
     before do
diff --git a/spec/lib/grafana/client_spec.rb b/spec/lib/grafana/client_spec.rb
index bd93a3c59a26f0e715c7d0e1d10ae8b2538472ba..699344e940ef700f242515c9b42ec4b04232dfd1 100644
--- a/spec/lib/grafana/client_spec.rb
+++ b/spec/lib/grafana/client_spec.rb
@@ -35,7 +35,7 @@
     it 'does not follow redirects' do
       expect { subject }.to raise_exception(
         Grafana::Client::Error,
-        'Grafana response status code: 302'
+        'Grafana response status code: 302, Message: {}'
       )
 
       expect(redirect_req_stub).to have_been_requested
@@ -67,6 +67,30 @@
     end
   end
 
+  describe '#get_dashboard' do
+    let(:grafana_api_url) { 'https://grafanatest.com/-/grafana-project/api/dashboards/uid/FndfgnX' }
+
+    subject do
+      client.get_dashboard(uid: 'FndfgnX')
+    end
+
+    it_behaves_like 'calls grafana api'
+    it_behaves_like 'no redirects'
+    it_behaves_like 'handles exceptions'
+  end
+
+  describe '#get_datasource' do
+    let(:grafana_api_url) { 'https://grafanatest.com/-/grafana-project/api/datasources/name/Test%20Name' }
+
+    subject do
+      client.get_datasource(name: 'Test Name')
+    end
+
+    it_behaves_like 'calls grafana api'
+    it_behaves_like 'no redirects'
+    it_behaves_like 'handles exceptions'
+  end
+
   describe '#proxy_datasource' do
     let(:grafana_api_url) do
       'https://grafanatest.com/-/grafana-project/' \
diff --git a/spec/lib/json_web_token/token_spec.rb b/spec/lib/json_web_token/token_spec.rb
index 916d11ce0edd74ce9635860595502bb95130ff9d..ca587a6ebcd7992b06a2343090fe94569072587b 100644
--- a/spec/lib/json_web_token/token_spec.rb
+++ b/spec/lib/json_web_token/token_spec.rb
@@ -16,6 +16,7 @@
 
   context 'embeds default payload' do
     subject { token.payload }
+
     let(:default) { token.send(:default_payload) }
 
     it { is_expected.to include(default) }
diff --git a/spec/lib/omni_auth/strategies/jwt_spec.rb b/spec/lib/omni_auth/strategies/jwt_spec.rb
index bdf3ea6be98f1ce97ec371afaba4a0e09a6e885f..a8c565aa7053c759ae9de5bb42670f281e9bd716 100644
--- a/spec/lib/omni_auth/strategies/jwt_spec.rb
+++ b/spec/lib/omni_auth/strategies/jwt_spec.rb
@@ -8,6 +8,7 @@
 
   context '#decoded' do
     subject { described_class.new({}) }
+
     let(:timestamp) { Time.now.to_i }
     let(:jwt_config) { Devise.omniauth_configs[:jwt] }
     let(:claims) do
diff --git a/spec/lib/uploaded_file_spec.rb b/spec/lib/uploaded_file_spec.rb
index 2cb4727bd4b0375b7cbc5d25b0d9836141cf943c..2bbbd67b13ce33a55116ff2556a5d049c6d42e92 100644
--- a/spec/lib/uploaded_file_spec.rb
+++ b/spec/lib/uploaded_file_spec.rb
@@ -72,16 +72,6 @@
       end
     end
 
-    context 'when only remote id is specified' do
-      let(:params) do
-        { 'file.remote_id' => 'remote_id' }
-      end
-
-      it "raises an error" do
-        expect { subject }.to raise_error(UploadedFile::InvalidPathError, /file is invalid/)
-      end
-    end
-
     context 'when verifying allowed paths' do
       let(:params) do
         { 'file.path' => temp_file.path }
@@ -120,6 +110,52 @@
     end
   end
 
+  describe '.initialize' do
+    context 'when no size is provided' do
+      it 'determine size from local path' do
+        file = described_class.new(temp_file.path)
+
+        expect(file.size).to eq(temp_file.size)
+      end
+
+      it 'raises an exception if is a remote file' do
+        expect do
+          described_class.new(nil, remote_id: 'id')
+        end.to raise_error(UploadedFile::UnknownSizeError, 'Unable to determine file size')
+      end
+    end
+
+    context 'when size is a number' do
+      let_it_be(:size) { 1.gigabyte }
+
+      it 'is overridden by the size of the local file' do
+        file = described_class.new(temp_file.path, size: size)
+
+        expect(file.size).to eq(temp_file.size)
+      end
+
+      it 'is respected if is a remote file' do
+        file = described_class.new(nil, remote_id: 'id', size: size)
+
+        expect(file.size).to eq(size)
+      end
+    end
+
+    context 'when size is a string' do
+      it 'is converted to a number' do
+        file = described_class.new(nil, remote_id: 'id', size: '1')
+
+        expect(file.size).to eq(1)
+      end
+
+      it 'raises an exception if does not represent a number' do
+        expect do
+          described_class.new(nil, remote_id: 'id', size: 'not a number')
+        end.to raise_error(UploadedFile::UnknownSizeError, 'Unable to determine file size')
+      end
+    end
+  end
+
   describe '#sanitize_filename' do
     it { expect(described_class.new(temp_file.path).sanitize_filename('spaced name')).to eq('spaced_name') }
     it { expect(described_class.new(temp_file.path).sanitize_filename('#$%^&')).to eq('_____') }
diff --git a/spec/mailers/abuse_report_mailer_spec.rb b/spec/mailers/abuse_report_mailer_spec.rb
index 86153071cd382753e111d8db5a3507b16368f87b..fcbffb52849fec047cb2f34a95454ea392beca50 100644
--- a/spec/mailers/abuse_report_mailer_spec.rb
+++ b/spec/mailers/abuse_report_mailer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe AbuseReportMailer do
diff --git a/spec/mailers/emails/merge_requests_spec.rb b/spec/mailers/emails/merge_requests_spec.rb
index 2ad572bb5c75f728828ce8c439b285131bc12811..541acc47172cf908e1df23d93c8ae7e33766263e 100644
--- a/spec/mailers/emails/merge_requests_spec.rb
+++ b/spec/mailers/emails/merge_requests_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 require 'email_spec'
 
diff --git a/spec/mailers/emails/pages_domains_spec.rb b/spec/mailers/emails/pages_domains_spec.rb
index c52e3c2191d5b1e6d16609c9a236d828f007a799..e360e38256e5952b4adb3e227db9ac381a788255 100644
--- a/spec/mailers/emails/pages_domains_spec.rb
+++ b/spec/mailers/emails/pages_domains_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 require 'email_spec'
 
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index 1f7be415e35385957aee7ed2a50409e951b2877e..d340f207dc78b2bad49c063d3bae2252eae83f9a 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 require 'email_spec'
 
diff --git a/spec/mailers/emails/releases_spec.rb b/spec/mailers/emails/releases_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..19f404db2a62f3d441d33e5d140fe69017a2e5be
--- /dev/null
+++ b/spec/mailers/emails/releases_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'email_spec'
+
+describe Emails::Releases do
+  include EmailSpec::Matchers
+  include_context 'gitlab email notification'
+
+  describe '#new_release_email' do
+    let_it_be(:user) { create(:user) }
+    let_it_be(:project) { create(:project) }
+    let(:release) { create(:release, project: project) }
+
+    subject { Notify.new_release_email(user.id, release) }
+
+    it_behaves_like 'an email sent from GitLab'
+
+    context 'when the release has a name' do
+      it 'shows the correct subject' do
+        expected_subject = "#{release.project.name} | New release: #{release.name} - #{release.tag}"
+        is_expected.to have_subject(expected_subject)
+      end
+    end
+
+    context 'when the release does not have a name' do
+      it 'shows the correct subject' do
+        release.name = nil
+        expected_subject = "#{release.project.name} | New release: #{release.tag}"
+
+        is_expected.to have_subject(expected_subject)
+      end
+    end
+
+    it 'contains a message with the new release tag' do
+      message = "A new Release #{release.tag} for #{release.project.name} was published."
+      is_expected.to have_body_text(message)
+    end
+
+    it 'contains the release assets' do
+      is_expected.to have_body_text('Assets:')
+      release.sources do |source|
+        is_expected.to have_body_text("Download #{source.format}")
+      end
+    end
+
+    it 'contains the release notes' do
+      is_expected.to have_body_text('Release notes:')
+      is_expected.to have_body_text(release.description)
+    end
+  end
+end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 56fa26d5f23bc090a676b8f442d81b55830f9a81..cafb96898b3d43e5a093244da3d759fde62b8fa5 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 require 'email_spec'
 
@@ -714,7 +716,7 @@ def id
 
     describe 'project access requested' do
       let(:project) do
-        create(:project, :public, :access_requestable) do |project|
+        create(:project, :public) do |project|
           project.add_maintainer(project.owner)
         end
       end
@@ -743,7 +745,7 @@ def id
     end
 
     describe 'project access denied' do
-      let(:project) { create(:project, :public, :access_requestable) }
+      let(:project) { create(:project, :public) }
       let(:project_member) do
         project.request_access(user)
         project.requesters.find_by(user_id: user.id)
@@ -765,7 +767,7 @@ def id
 
     describe 'project access changed' do
       let(:owner) { create(:user, name: "Chang O'Keefe") }
-      let(:project) { create(:project, :public, :access_requestable, namespace: owner.namespace) }
+      let(:project) { create(:project, :public, namespace: owner.namespace) }
       let(:project_member) { create(:project_member, project: project, user: user) }
       subject { described_class.member_access_granted_email('project', project_member.id) }
 
@@ -1167,7 +1169,7 @@ def invite_to_project(project, inviter:)
 
   context 'for a group' do
     describe 'group access requested' do
-      let(:group) { create(:group, :public, :access_requestable) }
+      let(:group) { create(:group, :public) }
       let(:group_member) do
         group.request_access(user)
         group.requesters.find_by(user_id: user.id)
diff --git a/spec/mailers/repository_check_mailer_spec.rb b/spec/mailers/repository_check_mailer_spec.rb
index 757d3dfa797af79befd7d1ebecabefad790a1c95..1fd4d28ca53e7fda4d169cdd9fa4c323a62e799d 100644
--- a/spec/mailers/repository_check_mailer_spec.rb
+++ b/spec/mailers/repository_check_mailer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe RepositoryCheckMailer do
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 84c25b93fc652925d7475dbdb0c48c9454655a0d..7bef3d30064c0e333ff27b849f1d9634f089ab3f 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -56,6 +56,14 @@
     it { is_expected.not_to allow_value(nil).for(:protected_paths) }
     it { is_expected.to allow_value([]).for(:protected_paths) }
 
+    it { is_expected.to allow_value(3).for(:push_event_hooks_limit) }
+    it { is_expected.not_to allow_value('three').for(:push_event_hooks_limit) }
+    it { is_expected.not_to allow_value(nil).for(:push_event_hooks_limit) }
+
+    it { is_expected.to allow_value(3).for(:push_event_activities_limit) }
+    it { is_expected.not_to allow_value('three').for(:push_event_activities_limit) }
+    it { is_expected.not_to allow_value(nil).for(:push_event_activities_limit) }
+
     context "when user accepted let's encrypt terms of service" do
       before do
         setting.update(lets_encrypt_terms_of_service_accepted: true)
diff --git a/spec/models/aws/role_spec.rb b/spec/models/aws/role_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c40752e40a687118a16344520d30a20cbd34796b
--- /dev/null
+++ b/spec/models/aws/role_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Aws::Role do
+  it { is_expected.to belong_to(:user) }
+  it { is_expected.to validate_length_of(:role_external_id).is_at_least(1).is_at_most(64) }
+
+  describe 'custom validations' do
+    subject { role.valid? }
+
+    context ':role_arn' do
+      let(:role) { build(:aws_role, role_arn: role_arn) }
+
+      context 'length is zero' do
+        let(:role_arn) { '' }
+
+        it { is_expected.to be_falsey }
+      end
+
+      context 'length is longer than 2048' do
+        let(:role_arn) { '1' * 2049 }
+
+        it { is_expected.to be_falsey }
+      end
+
+      context 'ARN is valid' do
+        let(:role_arn) { 'arn:aws:iam::123456789012:role/test-role' }
+
+        it { is_expected.to be_truthy }
+      end
+    end
+  end
+end
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index 2efab3076d819f8f435dcbe110e9b86cd31fb841..9e55fbcce20e47d28cdc409cd921c9553dc84062 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -320,6 +320,22 @@
         expect(blob.rich_viewer).to be_a(BlobViewer::Markup)
       end
     end
+
+    context 'when the blob is video' do
+      it 'returns a video viewer' do
+        blob = fake_blob(path: 'file.mp4', binary: true)
+
+        expect(blob.rich_viewer).to be_a(BlobViewer::Video)
+      end
+    end
+
+    context 'when the blob is audio' do
+      it 'returns an audio viewer' do
+        blob = fake_blob(path: 'file.wav', binary: true)
+
+        expect(blob.rich_viewer).to be_a(BlobViewer::Audio)
+      end
+    end
   end
 
   describe '#auxiliary_viewer' do
diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb
index 67cd939b4c6e577f19b9f58eb235da68c04044cd..da95a2d30f56941d719d5807c3335aa03c6852e6 100644
--- a/spec/models/ci/build_metadata_spec.rb
+++ b/spec/models/ci/build_metadata_spec.rb
@@ -4,7 +4,7 @@
 
 describe Ci::BuildMetadata do
   set(:user) { create(:user) }
-  set(:group) { create(:group, :access_requestable) }
+  set(:group) { create(:group) }
   set(:project) { create(:project, :repository, group: group, build_timeout: 2000) }
 
   set(:pipeline) do
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index de90e4c2fbaef51023de5a4299579cc6bc60505f..50b104c4095bd96079a02615e69d7f49cb86da92 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -4,7 +4,7 @@
 
 describe Ci::Build do
   set(:user) { create(:user) }
-  set(:group) { create(:group, :access_requestable) }
+  set(:group) { create(:group) }
   set(:project) { create(:project, :repository, group: group) }
 
   set(:pipeline) do
@@ -19,17 +19,24 @@
   it { is_expected.to belong_to(:runner) }
   it { is_expected.to belong_to(:trigger_request) }
   it { is_expected.to belong_to(:erased_by) }
+
   it { is_expected.to have_many(:trace_sections) }
   it { is_expected.to have_many(:needs) }
+  it { is_expected.to have_many(:sourced_pipelines) }
+  it { is_expected.to have_many(:job_variables) }
+
   it { is_expected.to have_one(:deployment) }
   it { is_expected.to have_one(:runner_session) }
-  it { is_expected.to have_many(:job_variables) }
+
   it { is_expected.to validate_presence_of(:ref) }
+
   it { is_expected.to respond_to(:has_trace?) }
   it { is_expected.to respond_to(:trace) }
+
   it { is_expected.to delegate_method(:merge_request_event?).to(:pipeline) }
   it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) }
   it { is_expected.to delegate_method(:legacy_detached_merge_request_pipeline?).to(:pipeline) }
+
   it { is_expected.to include_module(Ci::PipelineDelegator) }
 
   describe 'associations' do
@@ -199,6 +206,35 @@
     end
   end
 
+  describe '.with_exposed_artifacts' do
+    subject { described_class.with_exposed_artifacts }
+
+    let!(:job1) { create(:ci_build) }
+    let!(:job2) { create(:ci_build, options: options) }
+    let!(:job3) { create(:ci_build) }
+
+    context 'when some jobs have exposed artifacs and some not' do
+      let(:options) { { artifacts: { expose_as: 'test', paths: ['test'] } } }
+
+      before do
+        job1.ensure_metadata.update!(has_exposed_artifacts: nil)
+        job3.ensure_metadata.update!(has_exposed_artifacts: false)
+      end
+
+      it 'selects only the jobs with exposed artifacts' do
+        is_expected.to eq([job2])
+      end
+    end
+
+    context 'when job does not expose artifacts' do
+      let(:options) { nil }
+
+      it 'returns an empty array' do
+        is_expected.to be_empty
+      end
+    end
+  end
+
   describe '.with_reports' do
     subject { described_class.with_reports(Ci::JobArtifact.test_reports) }
 
@@ -567,6 +603,7 @@
 
   describe '#artifacts_metadata?' do
     subject { build.artifacts_metadata? }
+
     context 'artifacts metadata does not exist' do
       it { is_expected.to be_falsy }
     end
@@ -579,6 +616,7 @@
 
   describe '#artifacts_expire_in' do
     subject { build.artifacts_expire_in }
+
     it { is_expected.to be_nil }
 
     context 'when artifacts_expire_at is specified' do
@@ -1258,6 +1296,7 @@
 
         describe '#erasable?' do
           subject { build.erasable? }
+
           it { is_expected.to be_truthy }
         end
 
@@ -1834,6 +1873,14 @@ def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
         expect(build.metadata.read_attribute(:config_options)).to be_nil
       end
     end
+
+    context 'when options include artifacts:expose_as' do
+      let(:build) { create(:ci_build, options: { artifacts: { expose_as: 'test' } }) }
+
+      it 'saves the presence of expose_as into build metadata' do
+        expect(build.metadata).to have_exposed_artifacts
+      end
+    end
   end
 
   describe '#other_manual_actions' do
@@ -2188,6 +2235,7 @@ def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
           { key: 'CI_COMMIT_REF_NAME', value: build.ref, public: true, masked: false },
           { key: 'CI_COMMIT_REF_SLUG', value: build.ref_slug, public: true, masked: false },
           { key: 'CI_NODE_TOTAL', value: '1', public: true, masked: false },
+          { key: 'CI_DEFAULT_BRANCH', value: project.default_branch, public: true, masked: false },
           { key: 'CI_BUILD_REF', value: build.sha, public: true, masked: false },
           { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true, masked: false },
           { key: 'CI_BUILD_REF_NAME', value: build.ref, public: true, masked: false },
@@ -3911,4 +3959,14 @@ def run_job_without_exception
       end
     end
   end
+
+  describe '#invalid_dependencies' do
+    let!(:pre_stage_job_valid) { create(:ci_build, :manual, pipeline: pipeline, name: 'test1', stage_idx: 0) }
+    let!(:pre_stage_job_invalid) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test2', stage_idx: 1) }
+    let!(:job) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 2, options: { dependencies: %w(test1 test2) }) }
+
+    it 'returns invalid dependencies' do
+      expect(job.invalid_dependencies).to eq([pre_stage_job_invalid])
+    end
+  end
 end
diff --git a/spec/models/ci/build_trace_spec.rb b/spec/models/ci/build_trace_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2471a6fa82720540c3b534c9c235557c4ba8028b
--- /dev/null
+++ b/spec/models/ci/build_trace_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::BuildTrace do
+  let(:build) { build_stubbed(:ci_build) }
+  let(:state) { nil }
+  let(:data) { StringIO.new('the-stream') }
+
+  let(:stream) do
+    Gitlab::Ci::Trace::Stream.new { data }
+  end
+
+  subject { described_class.new(build: build, stream: stream, state: state, content_format: content_format) }
+
+  shared_examples 'delegates methods' do
+    it { is_expected.to delegate_method(:state).to(:trace) }
+    it { is_expected.to delegate_method(:append).to(:trace) }
+    it { is_expected.to delegate_method(:truncated).to(:trace) }
+    it { is_expected.to delegate_method(:offset).to(:trace) }
+    it { is_expected.to delegate_method(:size).to(:trace) }
+    it { is_expected.to delegate_method(:total).to(:trace) }
+    it { is_expected.to delegate_method(:id).to(:build).with_prefix }
+    it { is_expected.to delegate_method(:status).to(:build).with_prefix }
+    it { is_expected.to delegate_method(:complete?).to(:build).with_prefix }
+  end
+
+  context 'with :json content format' do
+    let(:content_format) { :json }
+
+    it_behaves_like 'delegates methods'
+
+    it { is_expected.to be_json }
+
+    it 'returns formatted trace' do
+      expect(subject.trace.lines).to eq([
+        { offset: 0, content: [{ text: 'the-stream' }] }
+      ])
+    end
+  end
+
+  context 'with :html content format' do
+    let(:content_format) { :html }
+
+    it_behaves_like 'delegates methods'
+
+    it { is_expected.to be_html }
+
+    it 'returns formatted trace' do
+      expect(subject.trace.html).to eq('<span>the-stream</span>')
+    end
+  end
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 0e11c595388d09a2dcd92e517435a23ed2356082..de0ce9932e893dec58fb5d72f9bdb21d9534d492 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -28,7 +28,13 @@
   it { is_expected.to have_many(:builds) }
   it { is_expected.to have_many(:auto_canceled_pipelines) }
   it { is_expected.to have_many(:auto_canceled_jobs) }
+  it { is_expected.to have_many(:sourced_pipelines) }
+  it { is_expected.to have_many(:triggered_pipelines) }
+
   it { is_expected.to have_one(:chat_data) }
+  it { is_expected.to have_one(:source_pipeline) }
+  it { is_expected.to have_one(:triggered_by_pipeline) }
+  it { is_expected.to have_one(:source_job) }
 
   it { is_expected.to validate_presence_of(:sha) }
   it { is_expected.to validate_presence_of(:status) }
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 70ff3cf5dc41b536a15a537ae7c524c255443b17..ac438f7d4739a16a3109a544205ee95ff3118835 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -686,11 +686,13 @@ def does_db_update
   describe '#has_tags?' do
     context 'when runner has tags' do
       subject { create(:ci_runner, tag_list: ['tag']) }
+
       it { is_expected.to have_tags }
     end
 
     context 'when runner does not have tags' do
       subject { create(:ci_runner, tag_list: []) }
+
       it { is_expected.not_to have_tags }
     end
   end
diff --git a/spec/models/ci/sources/pipeline_spec.rb b/spec/models/ci/sources/pipeline_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..63bee5bfb55890306f716a352ab71172771661ba
--- /dev/null
+++ b/spec/models/ci/sources/pipeline_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::Sources::Pipeline do
+  it { is_expected.to belong_to(:project) }
+  it { is_expected.to belong_to(:pipeline) }
+
+  it { is_expected.to belong_to(:source_project) }
+  it { is_expected.to belong_to(:source_job) }
+  it { is_expected.to belong_to(:source_pipeline) }
+
+  it { is_expected.to validate_presence_of(:project) }
+  it { is_expected.to validate_presence_of(:pipeline) }
+
+  it { is_expected.to validate_presence_of(:source_project) }
+  it { is_expected.to validate_presence_of(:source_job) }
+  it { is_expected.to validate_presence_of(:source_pipeline) }
+end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index b2379283aa76c78ce6f5447433a51de66677473d..48e3b4d6baea3d643cdc2437e9342134ed183127 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -17,6 +17,7 @@
   it { is_expected.to have_many(:cluster_groups) }
   it { is_expected.to have_many(:groups) }
   it { is_expected.to have_one(:provider_gcp) }
+  it { is_expected.to have_one(:provider_aws) }
   it { is_expected.to have_one(:platform_kubernetes) }
   it { is_expected.to have_one(:application_helm) }
   it { is_expected.to have_one(:application_ingress) }
@@ -108,6 +109,31 @@
     it { is_expected.to contain_exactly(cluster) }
   end
 
+  describe '.aws_provided' do
+    subject { described_class.aws_provided }
+
+    let!(:cluster) { create(:cluster, :provided_by_aws) }
+
+    before do
+      create(:cluster, :provided_by_user)
+    end
+
+    it { is_expected.to contain_exactly(cluster) }
+  end
+
+  describe '.aws_installed' do
+    subject { described_class.aws_installed }
+
+    let!(:cluster) { create(:cluster, :provided_by_aws) }
+
+    before do
+      errored_cluster = create(:cluster, :provided_by_aws)
+      errored_cluster.provider.make_errored!("Error message")
+    end
+
+    it { is_expected.to contain_exactly(cluster) }
+  end
+
   describe '.managed' do
     subject do
       described_class.managed
@@ -398,7 +424,14 @@
 
       it 'returns a provider' do
         is_expected.to eq(cluster.provider_gcp)
-        expect(subject.class.name.deconstantize).to eq(Clusters::Providers.to_s)
+      end
+    end
+
+    context 'when provider is aws' do
+      let(:cluster) { create(:cluster, :provided_by_aws) }
+
+      it 'returns a provider' do
+        is_expected.to eq(cluster.provider_aws)
       end
     end
 
diff --git a/spec/models/clusters/providers/aws_spec.rb b/spec/models/clusters/providers/aws_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ec8159a7ee0f3b065fd23dcaab72789086a239e6
--- /dev/null
+++ b/spec/models/clusters/providers/aws_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Providers::Aws do
+  it { is_expected.to belong_to(:cluster) }
+  it { is_expected.to belong_to(:created_by_user) }
+
+  it { is_expected.to validate_length_of(:key_name).is_at_least(1).is_at_most(255) }
+  it { is_expected.to validate_length_of(:region).is_at_least(1).is_at_most(255) }
+  it { is_expected.to validate_length_of(:instance_type).is_at_least(1).is_at_most(255) }
+  it { is_expected.to validate_length_of(:security_group_id).is_at_least(1).is_at_most(255) }
+  it { is_expected.to validate_presence_of(:subnet_ids) }
+
+  include_examples 'provider status', :cluster_provider_aws
+
+  describe 'default_value_for' do
+    let(:provider) { build(:cluster_provider_aws) }
+
+    it "sets default values" do
+      expect(provider.region).to eq('us-east-1')
+      expect(provider.num_nodes).to eq(3)
+      expect(provider.instance_type).to eq('m5.large')
+    end
+  end
+
+  describe 'custom validations' do
+    subject { provider.valid? }
+
+    context ':num_nodes' do
+      let(:provider) { build(:cluster_provider_aws, num_nodes: num_nodes) }
+
+      context 'contains non-digit characters' do
+        let(:num_nodes) { 'A3' }
+
+        it { is_expected.to be_falsey }
+      end
+
+      context 'is blank' do
+        let(:num_nodes) { nil }
+
+        it { is_expected.to be_falsey }
+      end
+
+      context 'is less than 1' do
+        let(:num_nodes) { 0 }
+
+        it { is_expected.to be_falsey }
+      end
+
+      context 'is a positive integer' do
+        let(:num_nodes) { 3 }
+
+        it { is_expected.to be_truthy }
+      end
+    end
+  end
+
+  describe '#nullify_credentials' do
+    let(:provider) { create(:cluster_provider_aws, :scheduled) }
+
+    subject { provider.nullify_credentials }
+
+    before do
+      expect(provider.access_key_id).to be_present
+      expect(provider.secret_access_key).to be_present
+    end
+
+    it 'removes access_key_id and secret_access_key' do
+      subject
+
+      expect(provider.access_key_id).to be_nil
+      expect(provider.secret_access_key).to be_nil
+    end
+  end
+end
diff --git a/spec/models/concerns/access_requestable_spec.rb b/spec/models/concerns/access_requestable_spec.rb
index de2bc3a387b0def0ab840490a6698c5e91a1bc0b..5c1694e3737a6e90e494861955199d97cb70c4ef 100644
--- a/spec/models/concerns/access_requestable_spec.rb
+++ b/spec/models/concerns/access_requestable_spec.rb
@@ -5,7 +5,7 @@
 describe AccessRequestable do
   describe 'Group' do
     describe '#request_access' do
-      let(:group) { create(:group, :public, :access_requestable) }
+      let(:group) { create(:group, :public) }
       let(:user) { create(:user) }
 
       it { expect(group.request_access(user)).to be_a(GroupMember) }
@@ -13,7 +13,7 @@
     end
 
     describe '#access_requested?' do
-      let(:group) { create(:group, :public, :access_requestable) }
+      let(:group) { create(:group, :public) }
       let(:user) { create(:user) }
 
       before do
@@ -26,14 +26,14 @@
 
   describe 'Project' do
     describe '#request_access' do
-      let(:project) { create(:project, :public, :access_requestable) }
+      let(:project) { create(:project, :public) }
       let(:user) { create(:user) }
 
       it { expect(project.request_access(user)).to be_a(ProjectMember) }
     end
 
     describe '#access_requested?' do
-      let(:project) { create(:project, :public, :access_requestable) }
+      let(:project) { create(:project, :public) }
       let(:user) { create(:user) }
 
       before do
diff --git a/spec/models/concerns/atomic_internal_id_spec.rb b/spec/models/concerns/atomic_internal_id_spec.rb
index 80f296d8825ef7dde8e8972c3e50180f4d39af2d..0605392c0aaddeb0b645a149d5fa0d5570e24184 100644
--- a/spec/models/concerns/atomic_internal_id_spec.rb
+++ b/spec/models/concerns/atomic_internal_id_spec.rb
@@ -22,41 +22,22 @@
     end
 
     context 'when value is set by ensure_project_iid!' do
-      context 'with iid_always_track false' do
-        before do
-          stub_feature_flags(iid_always_track: false)
-        end
+      it 'does not track the value' do
+        expect(InternalId).not_to receive(:track_greatest)
 
-        it 'does not track the value' do
-          expect(InternalId).not_to receive(:track_greatest)
-
-          milestone.ensure_project_iid!
-          subject
-        end
-
-        it 'tracks the iid for the scope that is actually present' do
-          milestone.iid = external_iid
-
-          expect(InternalId).to receive(:track_greatest).once.with(milestone, scope_attrs, usage, external_iid, anything)
-          expect(InternalId).not_to receive(:generate_next)
-
-          # group scope is not present here, the milestone does not have a group
-          milestone.track_group_iid!
-          subject
-        end
+        milestone.ensure_project_iid!
+        subject
       end
 
-      context 'with iid_always_track enabled' do
-        before do
-          stub_feature_flags(iid_always_track: true)
-        end
+      it 'tracks the iid for the scope that is actually present' do
+        milestone.iid = external_iid
 
-        it 'does not track the value' do
-          expect(InternalId).to receive(:track_greatest)
+        expect(InternalId).to receive(:track_greatest).once.with(milestone, scope_attrs, usage, external_iid, anything)
+        expect(InternalId).not_to receive(:generate_next)
 
-          milestone.ensure_project_iid!
-          subject
-        end
+        # group scope is not present here, the milestone does not have a group
+        milestone.track_group_iid!
+        subject
       end
     end
   end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index e64af9206f6444439273979cc6e383fc10ecb934..e8116f0a301fdc07abc84f5eb5790ad02bcc5d5c 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -45,8 +45,8 @@
       it { is_expected.to validate_presence_of(:iid) }
       it { is_expected.to validate_presence_of(:author) }
       it { is_expected.to validate_presence_of(:title) }
-      it { is_expected.to validate_length_of(:title).is_at_most(255) }
-      it { is_expected.to validate_length_of(:description).is_at_most(16_000).on(:create) }
+      it { is_expected.to validate_length_of(:title).is_at_most(described_class::TITLE_LENGTH_MAX) }
+      it { is_expected.to validate_length_of(:description).is_at_most(described_class::DESCRIPTION_LENGTH_MAX).on(:create) }
 
       it_behaves_like 'validates description length with custom validation'
       it_behaves_like 'truncates the description to its allowed maximum length on import'
diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb
index 929b5f52c7cf473616bb3f6d7748d18addecfd2d..f823ac0165f443f5428e5ae3ccc1cf8555118e42 100644
--- a/spec/models/concerns/noteable_spec.rb
+++ b/spec/models/concerns/noteable_spec.rb
@@ -6,6 +6,7 @@
   let!(:active_diff_note1) { create(:diff_note_on_merge_request) }
   let(:project) { active_diff_note1.project }
   subject { active_diff_note1.noteable }
+
   let!(:active_diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: subject, in_reply_to: active_diff_note1) }
   let!(:active_diff_note3) { create(:diff_note_on_merge_request, project: project, noteable: subject, position: active_position2) }
   let!(:outdated_diff_note1) { create(:diff_note_on_merge_request, project: project, noteable: subject, position: outdated_position) }
diff --git a/spec/models/concerns/stepable_spec.rb b/spec/models/concerns/stepable_spec.rb
index 5685de6a9bfb7321902641011545d8e47e19998a..51356c3eaf6b76ae3ccdfeacc64eb21c343bafae 100644
--- a/spec/models/concerns/stepable_spec.rb
+++ b/spec/models/concerns/stepable_spec.rb
@@ -7,6 +7,8 @@
     Class.new do
       include Stepable
 
+      attr_writer :return_non_success
+
       steps :method1, :method2, :method3
 
       def execute
@@ -15,18 +17,18 @@ def execute
 
       private
 
-      def method1
+      def method1(_result)
         { status: :success }
       end
 
-      def method2
-        return { status: :error } unless @pass
+      def method2(result)
+        return { status: :not_a_success } if @return_non_success
 
-        { status: :success, variable1: 'var1' }
+        result.merge({ status: :success, variable1: 'var1', excluded_variable: 'a' })
       end
 
-      def method3
-        { status: :success, variable2: 'var2' }
+      def method3(result)
+        result.except(:excluded_variable).merge({ status: :success, variable2: 'var2' })
       end
     end
   end
@@ -41,8 +43,8 @@ def method3
 
       private
 
-      def appended_method1
-        { status: :success }
+      def appended_method1(previous_result)
+        previous_result.merge({ status: :success })
       end
     end
   end
@@ -51,21 +53,19 @@ def appended_method1
     described_class.prepend(prepended_module)
   end
 
-  it 'stops after the first error' do
+  it 'stops after the first non success status' do
+    subject.return_non_success = true
+
     expect(subject).not_to receive(:method3)
     expect(subject).not_to receive(:appended_method1)
 
     expect(subject.execute).to eq(
-      status: :error,
-      failed_step: :method2
+      status: :not_a_success,
+      last_step: :method2
     )
   end
 
   context 'when all methods return success' do
-    before do
-      subject.instance_variable_set(:@pass, true)
-    end
-
     it 'calls all methods in order' do
       expect(subject).to receive(:method1).and_call_original.ordered
       expect(subject).to receive(:method2).and_call_original.ordered
@@ -82,6 +82,10 @@ def appended_method1
         variable2: 'var2'
       )
     end
+
+    it 'can modify results of previous steps' do
+      expect(subject.execute).not_to include(excluded_variable: 'a')
+    end
   end
 
   context 'with multiple stepable classes' do
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 51e28974ae0afbed0890acee0363df400c5bda54..43b894b59576e6392d274196bb7d367ca209a5d6 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -17,6 +17,7 @@
 
   describe 'ensures authentication token' do
     subject { create(:user).send(token_field) }
+
     it { is_expected.to be_a String }
   end
 end
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index 935838ce294b0739514cb617e8d673f2ab2f0355..eea539746a5bc8bc57afdd5e6dc030c63d7e08a9 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -78,7 +78,7 @@
   describe '#delete_tags!' do
     let(:repository) do
       create(:container_repository, name: 'my_image',
-                                    tags: %w[latest rc1],
+                                    tags: { latest: '123', rc1: '234' },
                                     project: project)
     end
 
@@ -86,6 +86,7 @@
       it 'returns status that indicates success' do
         expect(repository.client)
           .to receive(:delete_repository_tag)
+          .twice
           .and_return(true)
 
         expect(repository.delete_tags!).to be_truthy
@@ -96,6 +97,7 @@
       it 'returns status that indicates failure' do
         expect(repository.client)
           .to receive(:delete_repository_tag)
+          .twice
           .and_return(false)
 
         expect(repository.delete_tags!).to be_falsey
diff --git a/spec/models/deploy_keys_project_spec.rb b/spec/models/deploy_keys_project_spec.rb
index c137444763bbf86411eaa818d34ae2144d83fa44..1dbae78a01de36d014087333d9d90ef04ae918a6 100644
--- a/spec/models/deploy_keys_project_spec.rb
+++ b/spec/models/deploy_keys_project_spec.rb
@@ -16,6 +16,7 @@
   describe "Destroying" do
     let(:project)     { create(:project) }
     subject           { create(:deploy_keys_project, project: project) }
+
     let(:deploy_key)  { subject.deploy_key }
 
     context "when the deploy key is only used by this project" do
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 51ed8e9421b9932cc74b5ee181c139a38c2cf35c..3a0b3c46ad0c99b7abf26ece5b97df521d354275 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -348,4 +348,17 @@
       expect(deployment.deployed_by).to eq(build_user)
     end
   end
+
+  describe '.find_successful_deployment!' do
+    it 'returns a successful deployment' do
+      deploy = create(:deployment, :success)
+
+      expect(described_class.find_successful_deployment!(deploy.iid)).to eq(deploy)
+    end
+
+    it 'raises when no deployment is found' do
+      expect { described_class.find_successful_deployment!(-1) }
+        .to raise_error(ActiveRecord::RecordNotFound)
+    end
+  end
 end
diff --git a/spec/models/description_version_spec.rb b/spec/models/description_version_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5ec34c0cde4debeaaef3c4afbdf0513d9fa69af5
--- /dev/null
+++ b/spec/models/description_version_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DescriptionVersion do
+  describe 'associations' do
+    it { is_expected.to belong_to :issue }
+    it { is_expected.to belong_to :merge_request }
+  end
+
+  describe 'validations' do
+    describe 'exactly_one_issuable' do
+      using RSpec::Parameterized::TableSyntax
+
+      subject { described_class.new(issue_id: issue_id, merge_request_id: merge_request_id).valid? }
+
+      where(:issue_id, :merge_request_id, :valid?) do
+        nil | 1   | true
+        1   | nil | true
+        nil | nil | false
+        1   | 1   | false
+      end
+
+      with_them do
+        it { is_expected.to eq(valid?) }
+      end
+    end
+  end
+end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 521c4704c8758c5c4e0f942765e9f37c3064ec6b..786f3b832c442c440c6ae39a5ef53831d9d8d598 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -882,4 +882,19 @@
       end
     end
   end
+
+  describe '.find_or_create_by_name' do
+    it 'finds an existing environment if it exists' do
+      env = create(:environment)
+
+      expect(described_class.find_or_create_by_name(env.name)).to eq(env)
+    end
+
+    it 'creates an environment if it does not exist' do
+      env = project.environments.find_or_create_by_name('kittens')
+
+      expect(env).to be_an_instance_of(described_class)
+      expect(env).to be_persisted
+    end
+  end
 end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 62663c247d132f5541dfc76d16f8a851dcb92422..ff2e1aa047efad9028ea628e7b9f151bca5d6e5c 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -100,26 +100,31 @@
   describe '#membership_changed?' do
     context "created" do
       subject { build(:event, :created).membership_changed? }
+
       it { is_expected.to be_falsey }
     end
 
     context "updated" do
       subject { build(:event, :updated).membership_changed? }
+
       it { is_expected.to be_falsey }
     end
 
     context "expired" do
       subject { build(:event, :expired).membership_changed? }
+
       it { is_expected.to be_truthy }
     end
 
     context "left" do
       subject { build(:event, :left).membership_changed? }
+
       it { is_expected.to be_truthy }
     end
 
     context "joined" do
       subject { build(:event, :joined).membership_changed? }
+
       it { is_expected.to be_truthy }
     end
   end
diff --git a/spec/models/evidence_spec.rb b/spec/models/evidence_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..00788c2c391ad4a2d48148807d9d98e06be0195e
--- /dev/null
+++ b/spec/models/evidence_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Evidence do
+  let_it_be(:project) { create(:project) }
+  let(:release) { create(:release, project: project) }
+  let(:schema_file) { 'evidences/evidence' }
+  let(:summary_json) { described_class.last.summary.to_json }
+
+  describe 'associations' do
+    it { is_expected.to belong_to(:release) }
+  end
+
+  describe 'summary_sha' do
+    it 'returns nil if summary is nil' do
+      expect(build(:evidence, summary: nil).summary_sha).to be_nil
+    end
+  end
+
+  describe '#generate_summary_and_sha' do
+    before do
+      described_class.create!(release: release)
+    end
+
+    context 'when a release name is not provided' do
+      let(:release) { create(:release, project: project, name: nil) }
+
+      it 'creates a valid JSON object' do
+        expect(release.name).to be_nil
+        expect(summary_json).to match_schema(schema_file)
+      end
+    end
+
+    context 'when a release is associated to a milestone' do
+      let(:milestone) { create(:milestone, project: project) }
+      let(:release) { create(:release, project: project, milestones: [milestone]) }
+
+      context 'when a milestone has no issue associated with it' do
+        it 'creates a valid JSON object' do
+          expect(milestone.issues).to be_empty
+          expect(summary_json).to match_schema(schema_file)
+        end
+      end
+
+      context 'when a milestone has no description' do
+        let(:milestone) { create(:milestone, project: project, description: nil) }
+
+        it 'creates a valid JSON object' do
+          expect(milestone.description).to be_nil
+          expect(summary_json).to match_schema(schema_file)
+        end
+      end
+
+      context 'when a milestone has no due_date' do
+        let(:milestone) { create(:milestone, project: project, due_date: nil) }
+
+        it 'creates a valid JSON object' do
+          expect(milestone.due_date).to be_nil
+          expect(summary_json).to match_schema(schema_file)
+        end
+      end
+
+      context 'when a milestone has an issue' do
+        context 'when the issue has no description' do
+          let(:issue) { create(:issue, project: project, description: nil, state: 'closed') }
+
+          before do
+            milestone.issues << issue
+          end
+
+          it 'creates a valid JSON object' do
+            expect(milestone.issues.first.description).to be_nil
+            expect(summary_json).to match_schema(schema_file)
+          end
+        end
+      end
+    end
+
+    context 'when a release is not associated to any milestone' do
+      it 'creates a valid JSON object' do
+        expect(release.milestones).to be_empty
+        expect(summary_json).to match_schema(schema_file)
+      end
+    end
+  end
+end
diff --git a/spec/models/gpg_signature_spec.rb b/spec/models/gpg_signature_spec.rb
index 4911375c962c79233057a4d67423d8500ab242c3..a780b8bfdf53d5a2e32bc1d5e8f3d275e64b5d22 100644
--- a/spec/models/gpg_signature_spec.rb
+++ b/spec/models/gpg_signature_spec.rb
@@ -20,6 +20,7 @@
 
   describe 'validation' do
     subject { described_class.new }
+
     it { is_expected.to validate_presence_of(:commit_sha) }
     it { is_expected.to validate_presence_of(:project_id) }
     it { is_expected.to validate_presence_of(:gpg_key_primary_keyid) }
@@ -60,6 +61,18 @@
     end
   end
 
+  describe '.by_commit_sha scope' do
+    let(:gpg_key) { create(:gpg_key, key: GpgHelpers::User2.public_key) }
+    let!(:another_gpg_signature) { create(:gpg_signature, gpg_key: gpg_key) }
+
+    it 'returns all gpg signatures by sha' do
+      expect(described_class.by_commit_sha(commit_sha)).to eq([gpg_signature])
+      expect(
+        described_class.by_commit_sha([commit_sha, another_gpg_signature.commit_sha])
+      ).to contain_exactly(gpg_signature, another_gpg_signature)
+    end
+  end
+
   describe '#commit' do
     it 'fetches the commit through the project' do
       expect_any_instance_of(Project).to receive(:commit).with(commit_sha).and_return(commit)
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 3f149f9d7ee33fcf8493bd8462ecdac4220bed53..520421ac5e3cda42c58322c6aefdb6c3056f45d4 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -3,7 +3,7 @@
 require 'spec_helper'
 
 describe Group do
-  let!(:group) { create(:group, :access_requestable) }
+  let!(:group) { create(:group) }
 
   describe 'associations' do
     it { is_expected.to have_many :projects }
@@ -331,7 +331,7 @@
   end
 
   describe '#avatar_url' do
-    let!(:group) { create(:group, :access_requestable, :with_avatar) }
+    let!(:group) { create(:group, :with_avatar) }
     let(:user) { create(:user) }
 
     context 'when avatar file is uploaded' do
@@ -1042,4 +1042,21 @@ def setup_group_members(group)
       expect(group.access_request_approvers_to_be_notified).to eq(active_owners_in_recent_sign_in_desc_order)
     end
   end
+
+  describe '.groups_including_descendants_by' do
+    it 'returns the expected groups for a group and its descendants' do
+      parent_group1 = create(:group)
+      child_group1 = create(:group, parent: parent_group1)
+      child_group2 = create(:group, parent: parent_group1)
+
+      parent_group2 = create(:group)
+      child_group3 = create(:group, parent: parent_group2)
+
+      create(:group)
+
+      groups = described_class.groups_including_descendants_by([parent_group2.id, parent_group1.id])
+
+      expect(groups).to contain_exactly(parent_group1, parent_group2, child_group1, child_group2, child_group3)
+    end
+  end
 end
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index fe08dc4f5e6a0d7acabddd6627f071fe445bca73..025c11d640769c0f5c56b54103ddf5be47820875 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -6,7 +6,7 @@
   let(:hook) { build(:project_hook) }
 
   describe 'associations' do
-    it { is_expected.to have_many(:web_hook_logs).dependent(:destroy) }
+    it { is_expected.to have_many(:web_hook_logs) }
   end
 
   describe 'validations' do
@@ -85,4 +85,13 @@
       hook.async_execute(data, hook_name)
     end
   end
+
+  describe '#destroy' do
+    it 'cascades to web_hook_logs' do
+      web_hook = create(:project_hook)
+      create_list(:web_hook_log, 3, web_hook: web_hook)
+
+      expect { web_hook.destroy }.to change(web_hook.web_hook_logs, :count).by(-3)
+    end
+  end
 end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 9c58d307c4c7d47878f27d2ef59a08ba2bffaf5a..18a1a30eee54c5aa6f1fd3593e741582692a9218 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -138,7 +138,10 @@
     end
 
     it 'changes the state to closed' do
-      expect { issue.close }.to change { issue.state }.from('opened').to('closed')
+      open_state = described_class.available_states[:opened]
+      closed_state = described_class.available_states[:closed]
+
+      expect { issue.close }.to change { issue.state_id }.from(open_state).to(closed_state)
     end
   end
 
@@ -155,7 +158,7 @@
     end
 
     it 'changes the state to opened' do
-      expect { issue.reopen }.to change { issue.state }.from('closed').to('opened')
+      expect { issue.reopen }.to change { issue.state_id }.from(described_class.available_states[:closed]).to(described_class.available_states[:opened])
     end
   end
 
@@ -277,6 +280,7 @@
 
       context 'checking destination project also' do
         subject { issue.can_move?(user, to_project) }
+
         let(:to_project) { create(:project) }
 
         context 'destination project allowed' do
@@ -899,4 +903,6 @@
       let(:default_params) { { project: project } }
     end
   end
+
+  it_behaves_like 'versioned description'
 end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 2cb4f222ea4399f20c847dcc21f682255d7f3d76..e7f032268261b8329e01e3b8128a6b3928dd0451 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -92,7 +92,7 @@
 
   describe 'Scopes & finders' do
     before do
-      project = create(:project, :public, :access_requestable)
+      project = create(:project, :public)
       group = create(:group)
       @owner_user = create(:user).tap { |u| group.add_owner(u) }
       @owner = group.members.find_by(user_id: @owner_user.id)
@@ -230,7 +230,7 @@
   describe '.add_user' do
     %w[project group].each do |source_type|
       context "when source is a #{source_type}" do
-        let!(:source) { create(source_type, :public, :access_requestable) }
+        let!(:source) { create(source_type, :public) }
         let!(:user) { create(:user) }
         let!(:admin) { create(:admin) }
 
@@ -437,7 +437,7 @@
   describe '.add_users' do
     %w[project group].each do |source_type|
       context "when source is a #{source_type}" do
-        let!(:source) { create(source_type, :public, :access_requestable) }
+        let!(:source) { create(source_type, :public) }
         let!(:admin) { create(:admin) }
         let(:user1) { create(:user) }
         let(:user2) { create(:user) }
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index b146c767f824b7e3c1871e74263f54eb45f36635..b8d09f03fe6bd3bba28ec84d62881509fd19e9a0 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -470,7 +470,7 @@ def create_merge_request_with_diffs(source_branch, diffs: 2)
       commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
 
       allow(subject).to receive(:commits).and_return([commit])
-      allow(subject).to receive(:state).and_return("closed")
+      allow(subject).to receive(:state_id).and_return(described_class.available_states[:closed])
 
       expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
     end
@@ -479,7 +479,7 @@ def create_merge_request_with_diffs(source_branch, diffs: 2)
       issue  = create :issue, project: subject.project
       commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
       allow(subject).to receive(:commits).and_return([commit])
-      allow(subject).to receive(:state).and_return("merged")
+      allow(subject).to receive(:state_id).and_return(described_class.available_states[:merged])
 
       expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
     end
@@ -541,6 +541,7 @@ def create_merge_request_with_diffs(source_branch, diffs: 2)
 
     context 'with diffs' do
       subject { create(:merge_request, :with_diffs) }
+
       it 'returns the sha of the source branch last commit' do
         expect(subject.source_branch_sha).to eq(last_branch_commit.sha)
       end
@@ -548,6 +549,7 @@ def create_merge_request_with_diffs(source_branch, diffs: 2)
 
     context 'without diffs' do
       subject { create(:merge_request, :without_diffs) }
+
       it 'returns the sha of the source branch last commit' do
         expect(subject.source_branch_sha).to eq(last_branch_commit.sha)
       end
@@ -570,6 +572,7 @@ def create_merge_request_with_diffs(source_branch, diffs: 2)
 
     context 'when the merge request is being created' do
       subject { build(:merge_request, source_branch: nil, compare_commits: []) }
+
       it 'returns nil' do
         expect(subject.source_branch_sha).to be_nil
       end
@@ -1671,6 +1674,63 @@ def set_compare(merge_request)
     end
   end
 
+  describe '#find_exposed_artifacts' do
+    let(:project) { create(:project, :repository) }
+    let(:merge_request) { create(:merge_request, :with_test_reports, source_project: project) }
+    let(:pipeline) { merge_request.head_pipeline }
+
+    subject { merge_request.find_exposed_artifacts }
+
+    context 'when head pipeline has exposed artifacts' do
+      let!(:job) do
+        create(:ci_build, options: { artifacts: { expose_as: 'artifact', paths: ['ci_artifacts.txt'] } }, pipeline: pipeline)
+      end
+
+      let!(:artifacts_metadata) { create(:ci_job_artifact, :metadata, job: job) }
+
+      context 'when reactive cache worker is parsing results asynchronously' do
+        it 'returns status' do
+          expect(subject[:status]).to eq(:parsing)
+        end
+      end
+
+      context 'when reactive cache worker is inline' do
+        before do
+          synchronous_reactive_cache(merge_request)
+        end
+
+        it 'returns status and data' do
+          expect(subject[:status]).to eq(:parsed)
+        end
+
+        context 'when an error occurrs' do
+          before do
+            expect_next_instance_of(Ci::FindExposedArtifactsService) do |service|
+              expect(service).to receive(:for_pipeline)
+                .and_raise(StandardError.new)
+            end
+          end
+
+          it 'returns an error message' do
+            expect(subject[:status]).to eq(:error)
+          end
+        end
+
+        context 'when cached results is not latest' do
+          before do
+            allow_next_instance_of(Ci::GenerateExposedArtifactsReportService) do |service|
+              allow(service).to receive(:latest?).and_return(false)
+            end
+          end
+
+          it 'raises and InvalidateReactiveCache error' do
+            expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
+          end
+        end
+      end
+    end
+  end
+
   describe '#compare_test_reports' do
     subject { merge_request.compare_test_reports }
 
@@ -2072,7 +2132,7 @@ def set_compare(merge_request)
     end
 
     it 'refuses to enqueue a job if the MR is not open' do
-      merge_request.update_column(:state, 'foo')
+      merge_request.update_column(:state_id, 5)
 
       expect(RebaseWorker).not_to receive(:perform_async)
 
@@ -2495,6 +2555,7 @@ def set_compare(merge_request)
   describe "#diff_refs" do
     context "with diffs" do
       subject { create(:merge_request, :with_diffs) }
+
       let(:expected_diff_refs) do
         Gitlab::Diff::DiffRefs.new(
           base_sha:  subject.merge_request_diff.base_commit_sha,
@@ -2567,32 +2628,32 @@ def set_compare(merge_request)
 
   describe '#merge_ongoing?' do
     it 'returns true when the merge request is locked' do
-      merge_request = build_stubbed(:merge_request, state: :locked)
+      merge_request = build_stubbed(:merge_request, state_id: described_class.available_states[:locked])
 
       expect(merge_request.merge_ongoing?).to be(true)
     end
 
     it 'returns true when merge_id, MR is not merged and it has no running job' do
-      merge_request = build_stubbed(:merge_request, state: :open, merge_jid: 'foo')
+      merge_request = build_stubbed(:merge_request, state_id: described_class.available_states[:opened], merge_jid: 'foo')
       allow(Gitlab::SidekiqStatus).to receive(:running?).with('foo') { true }
 
       expect(merge_request.merge_ongoing?).to be(true)
     end
 
     it 'returns false when merge_jid is nil' do
-      merge_request = build_stubbed(:merge_request, state: :open, merge_jid: nil)
+      merge_request = build_stubbed(:merge_request, state_id: described_class.available_states[:opened], merge_jid: nil)
 
       expect(merge_request.merge_ongoing?).to be(false)
     end
 
     it 'returns false if MR is merged' do
-      merge_request = build_stubbed(:merge_request, state: :merged, merge_jid: 'foo')
+      merge_request = build_stubbed(:merge_request, state_id: described_class.available_states[:merged], merge_jid: 'foo')
 
       expect(merge_request.merge_ongoing?).to be(false)
     end
 
     it 'returns false if there is no merge job running' do
-      merge_request = build_stubbed(:merge_request, state: :open, merge_jid: 'foo')
+      merge_request = build_stubbed(:merge_request, state_id: described_class.available_states[:opened], merge_jid: 'foo')
       allow(Gitlab::SidekiqStatus).to receive(:running?).with('foo') { false }
 
       expect(merge_request.merge_ongoing?).to be(false)
@@ -2726,7 +2787,7 @@ def create_pipeline(status)
 
       context 'closed MR' do
         before do
-          merge_request.update_attribute(:state, :closed)
+          merge_request.update_attribute(:state_id, described_class.available_states[:closed])
         end
 
         it 'is not mergeable' do
@@ -2840,6 +2901,7 @@ def create_pipeline(status)
 
   describe '#merge_request_diff_for' do
     subject { create(:merge_request, importing: true) }
+
     let!(:merge_request_diff1) { subject.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
     let!(:merge_request_diff2) { subject.merge_request_diffs.create(head_commit_sha: nil) }
     let!(:merge_request_diff3) { subject.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
@@ -2870,6 +2932,7 @@ def create_pipeline(status)
 
   describe '#version_params_for' do
     subject { create(:merge_request, importing: true) }
+
     let(:project) { subject.project }
     let!(:merge_request_diff1) { subject.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
     let!(:merge_request_diff2) { subject.merge_request_diffs.create(head_commit_sha: nil) }
@@ -3331,4 +3394,6 @@ def create_pipeline(status)
 
     it { expect(query).to contain_exactly(merge_request1, merge_request2) }
   end
+
+  it_behaves_like 'versioned description'
 end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 989024dee60430009013c5eb3240b5560c03026f..4c320b4b145c6d7dcbdc7ec93f99432c37b77c58 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -55,11 +55,13 @@
 
     context 'when noteable and note project are the same' do
       subject { create(:note) }
+
       it { is_expected.to be_valid }
     end
 
     context 'when project is missing for a project related note' do
       subject { build(:note, project: nil, noteable: build_stubbed(:issue)) }
+
       it { is_expected.to be_invalid }
     end
 
@@ -741,6 +743,7 @@ def retrieve_participants
 
   describe '#to_discussion' do
     subject { create(:discussion_note_on_merge_request) }
+
     let!(:note2) { create(:discussion_note_on_merge_request, project: subject.project, noteable: subject.noteable, in_reply_to: subject) }
 
     it "returns a discussion with just this note" do
@@ -808,6 +811,7 @@ def retrieve_participants
     context 'for a note' do
       context 'when part of a discussion' do
         subject { create(:discussion_note_on_issue) }
+
         let(:note) { create(:discussion_note_on_issue, in_reply_to: subject) }
 
         it 'checks if the note is in reply to the other discussion' do
@@ -821,6 +825,7 @@ def retrieve_participants
 
       context 'when not part of a discussion' do
         subject { create(:note) }
+
         let(:note) { create(:note, in_reply_to: subject) }
 
         it 'checks if the note is in reply to the other noteable' do
@@ -835,6 +840,7 @@ def retrieve_participants
     context 'for a discussion' do
       context 'when part of the same discussion' do
         subject { create(:diff_note_on_merge_request) }
+
         let(:note) { create(:diff_note_on_merge_request, in_reply_to: subject) }
 
         it 'returns true' do
@@ -844,6 +850,7 @@ def retrieve_participants
 
       context 'when not part of the same discussion' do
         subject { create(:diff_note_on_merge_request) }
+
         let(:note) { create(:diff_note_on_merge_request) }
 
         it 'returns false' do
@@ -855,6 +862,7 @@ def retrieve_participants
     context 'for a noteable' do
       context 'when a comment on the same noteable' do
         subject { create(:note) }
+
         let(:note) { create(:note, in_reply_to: subject) }
 
         it 'returns true' do
@@ -864,6 +872,7 @@ def retrieve_participants
 
       context 'when not a comment on the same noteable' do
         subject { create(:note) }
+
         let(:note) { create(:note) }
 
         it 'returns false' do
@@ -887,6 +896,7 @@ def retrieve_participants
 
     context 'when not part of a discussion' do
       subject { create(:note) }
+
       let(:note) { create(:note, in_reply_to: subject) }
 
       it 'returns the noteable' do
@@ -972,13 +982,64 @@ def expect_expiration(note)
       project = create(:project)
       note = create(:note_on_issue, project: project)
 
-      expect(note.parent).to eq(project)
+      expect(note.resource_parent).to eq(project)
     end
 
     it 'returns nil for personal snippet note' do
       note = create(:note_on_personal_snippet)
 
-      expect(note.parent).to be_nil
+      expect(note.resource_parent).to be_nil
+    end
+  end
+
+  describe 'scopes' do
+    let_it_be(:note1) { create(:note, note: 'Test 345') }
+    let_it_be(:note2) { create(:note, note: 'Test 789') }
+
+    describe '#for_note_or_capitalized_note' do
+      it 'returns the expected matching note' do
+        notes = described_class.for_note_or_capitalized_note('Test 345')
+
+        expect(notes.count).to eq(1)
+        expect(notes.first.id).to eq(note1.id)
+      end
+
+      it 'returns the expected capitalized note' do
+        notes = described_class.for_note_or_capitalized_note('test 345')
+
+        expect(notes.count).to eq(1)
+        expect(notes.first.id).to eq(note1.id)
+      end
+
+      it 'does not support pattern matching' do
+        notes = described_class.for_note_or_capitalized_note('test%')
+
+        expect(notes.count).to eq(0)
+      end
+    end
+
+    describe '#like_note_or_capitalized_note' do
+      it 'returns the expected matching note' do
+        notes = described_class.like_note_or_capitalized_note('Test 345')
+
+        expect(notes.count).to eq(1)
+        expect(notes.first.id).to eq(note1.id)
+      end
+
+      it 'returns the expected capitalized note' do
+        notes = described_class.like_note_or_capitalized_note('test 345')
+
+        expect(notes.count).to eq(1)
+        expect(notes.first.id).to eq(note1.id)
+      end
+
+      it 'supports pattern matching' do
+        notes = described_class.like_note_or_capitalized_note('test%')
+
+        expect(notes.count).to eq(2)
+        expect(notes.first.id).to eq(note1.id)
+        expect(notes.second.id).to eq(note2.id)
+      end
     end
   end
 end
diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb
index 820d233dbdcf9a2cef2b26cbef8f7ca021e39b34..094c60e3e09e9f008af218cb24fd35a951bede1f 100644
--- a/spec/models/notification_setting_spec.rb
+++ b/spec/models/notification_setting_spec.rb
@@ -98,6 +98,7 @@
 
     it 'returns email events' do
       expect(subject).to include(
+        :new_release,
         :new_note,
         :new_issue,
         :reopen_issue,
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 2e7b2b88432c501387bb4904e6e509a4875832d1..4b65bf032d1cc0135e62bbfaec5017497fc4f436 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -293,11 +293,13 @@
   describe "#https?" do
     context "when a certificate is present" do
       subject { build(:pages_domain) }
+
       it { is_expected.to be_https }
     end
 
     context "when no certificate is present" do
       subject { build(:pages_domain, :without_certificate) }
+
       it { is_expected.not_to be_https }
     end
   end
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index bdb7c7aeff49cd6a0263c48508b757769032844c..5feb8ca7839d8753d1aedc1eaa82fd278be2b333 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -15,20 +15,22 @@
   let(:transition_id) { 'test27' }
 
   describe '#options' do
-    let(:service) do
-      described_class.create(
+    let(:options) do
+      {
         project: create(:project),
         active: true,
-        username: 'username ',
+        username: 'username',
         password: 'test',
         jira_issue_transition_id: 24,
         url: 'http://jira.test.com/path/'
-      )
+      }
     end
 
+    let(:service) { described_class.create(options) }
+
     it 'sets the URL properly' do
-      # jira-ruby gem parses the URI and handles trailing slashes
-      # fine: https://github.com/sumoheavy/jira-ruby/blob/v1.4.1/lib/jira/http_client.rb#L59
+      # jira-ruby gem parses the URI and handles trailing slashes fine:
+      # https://github.com/sumoheavy/jira-ruby/blob/v1.7.0/lib/jira/http_client.rb#L62
       expect(service.options[:site]).to eq('http://jira.test.com/')
     end
 
@@ -36,14 +38,30 @@
       expect(service.options[:context_path]).to eq('/path')
     end
 
-    it 'leaves out trailing whitespaces in username' do
-      expect(service.options[:username]).to eq('username')
+    context 'username with trailing whitespaces' do
+      before do
+        options.merge!(username: 'username ')
+      end
+
+      it 'leaves out trailing whitespaces in username' do
+        expect(service.options[:username]).to eq('username')
+      end
     end
 
     it 'provides additional cookies to allow basic auth with oracle webgate' do
       expect(service.options[:use_cookies]).to eq(true)
       expect(service.options[:additional_cookies]).to eq(['OBBasicAuth=fromDialog'])
     end
+
+    context 'using api URL' do
+      before do
+        options.merge!(api_url: 'http://jira.test.com/api_path/')
+      end
+
+      it 'leaves out trailing slashes in context' do
+        expect(service.options[:context_path]).to eq('/api_path')
+      end
+    end
   end
 
   describe 'Associations' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 1490955f4a368b4aae8624f479d7169745f7abd7..9f3313e67b5e802e44091792998f24a31fcfa836 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -101,6 +101,8 @@
     it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) }
     it { is_expected.to have_many(:cycle_analytics_stages) }
     it { is_expected.to have_many(:external_pull_requests) }
+    it { is_expected.to have_many(:sourced_pipelines) }
+    it { is_expected.to have_many(:source_pipelines) }
 
     it 'has an inverse relationship with merge requests' do
       expect(described_class.reflect_on_association(:merge_requests).has_inverse?).to eq(:target_project)
@@ -151,7 +153,7 @@
     end
 
     describe '#members & #requesters' do
-      let(:project) { create(:project, :public, :access_requestable) }
+      let(:project) { create(:project, :public) }
       let(:requester) { create(:user) }
       let(:developer) { create(:user) }
       before do
@@ -629,8 +631,38 @@
   describe "#web_url" do
     let(:project) { create(:project, path: "somewhere") }
 
-    it 'returns the full web URL for this repo' do
-      expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.full_path}/somewhere")
+    context 'when given the only_path option' do
+      subject { project.web_url(only_path: only_path) }
+
+      context 'when only_path is false' do
+        let(:only_path) { false }
+
+        it 'returns the full web URL for this repo' do
+          expect(subject).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.full_path}/somewhere")
+        end
+      end
+
+      context 'when only_path is true' do
+        let(:only_path) { true }
+
+        it 'returns the relative web URL for this repo' do
+          expect(subject).to eq("/#{project.namespace.full_path}/somewhere")
+        end
+      end
+
+      context 'when only_path is nil' do
+        let(:only_path) { nil }
+
+        it 'returns the full web URL for this repo' do
+          expect(subject).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.full_path}/somewhere")
+        end
+      end
+    end
+
+    context 'when not given the only_path option' do
+      it 'returns the full web URL for this repo' do
+        expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.full_path}/somewhere")
+      end
     end
   end
 
@@ -3224,20 +3256,78 @@ def enable_lfs
   describe '#http_url_to_repo' do
     let(:project) { create(:project) }
 
-    it 'returns the url to the repo without a username' do
-      expect(project.http_url_to_repo).to eq("#{project.web_url}.git")
-      expect(project.http_url_to_repo).not_to include('@')
+    context 'when a custom HTTP clone URL root is not set' do
+      it 'returns the url to the repo without a username' do
+        expect(project.http_url_to_repo).to eq("#{project.web_url}.git")
+        expect(project.http_url_to_repo).not_to include('@')
+      end
+    end
+
+    context 'when a custom HTTP clone URL root is set' do
+      before do
+        stub_application_setting(custom_http_clone_url_root: custom_http_clone_url_root)
+      end
+
+      context 'when custom HTTP clone URL root has a relative URL root' do
+        context 'when custom HTTP clone URL root ends with a slash' do
+          let(:custom_http_clone_url_root) { 'https://git.example.com:51234/mygitlab/' }
+
+          it 'returns the url to the repo, with the root replaced with the custom one' do
+            expect(project.http_url_to_repo).to eq("https://git.example.com:51234/mygitlab/#{project.full_path}.git")
+          end
+        end
+
+        context 'when custom HTTP clone URL root does not end with a slash' do
+          let(:custom_http_clone_url_root) { 'https://git.example.com:51234/mygitlab' }
+
+          it 'returns the url to the repo, with the root replaced with the custom one' do
+            expect(project.http_url_to_repo).to eq("https://git.example.com:51234/mygitlab/#{project.full_path}.git")
+          end
+        end
+      end
+
+      context 'when custom HTTP clone URL root does not have a relative URL root' do
+        context 'when custom HTTP clone URL root ends with a slash' do
+          let(:custom_http_clone_url_root) { 'https://git.example.com:51234/' }
+
+          it 'returns the url to the repo, with the root replaced with the custom one' do
+            expect(project.http_url_to_repo).to eq("https://git.example.com:51234/#{project.full_path}.git")
+          end
+        end
+
+        context 'when custom HTTP clone URL root does not end with a slash' do
+          let(:custom_http_clone_url_root) { 'https://git.example.com:51234' }
+
+          it 'returns the url to the repo, with the root replaced with the custom one' do
+            expect(project.http_url_to_repo).to eq("https://git.example.com:51234/#{project.full_path}.git")
+          end
+        end
+      end
     end
   end
 
   describe '#lfs_http_url_to_repo' do
     let(:project) { create(:project) }
 
-    it 'returns the url to the repo without a username' do
-      lfs_http_url_to_repo = project.lfs_http_url_to_repo('operation_that_doesnt_matter')
+    context 'when a custom HTTP clone URL root is not set' do
+      it 'returns the url to the repo without a username' do
+        lfs_http_url_to_repo = project.lfs_http_url_to_repo('operation_that_doesnt_matter')
+
+        expect(lfs_http_url_to_repo).to eq("#{project.web_url}.git")
+        expect(lfs_http_url_to_repo).not_to include('@')
+      end
+    end
+
+    context 'when a custom HTTP clone URL root is set' do
+      before do
+        stub_application_setting(custom_http_clone_url_root: 'https://git.example.com:51234')
+      end
+
+      it 'returns the url to the repo, with the root replaced with the custom one' do
+        lfs_http_url_to_repo = project.lfs_http_url_to_repo('operation_that_doesnt_matter')
 
-      expect(lfs_http_url_to_repo).to eq("#{project.web_url}.git")
-      expect(lfs_http_url_to_repo).not_to include('@')
+        expect(lfs_http_url_to_repo).to eq("https://git.example.com:51234/#{project.full_path}.git")
+      end
     end
   end
 
@@ -4924,6 +5014,7 @@ def enable_lfs
 
   describe '#git_objects_poolable?' do
     subject { project }
+
     context 'when not using hashed storage' do
       let(:project) { create(:project, :legacy_storage, :public, :repository) }
 
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 77c88a04cde59795a80f97a3ece2d7920c20bbfc..d62fa58739acc294dc0cf3eafd3d621fe3f98aad 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -141,7 +141,7 @@
   describe '#find_member' do
     context 'personal project' do
       let(:project) do
-        create(:project, :public, :access_requestable)
+        create(:project, :public)
       end
 
       let(:requester) { create(:user) }
@@ -161,7 +161,7 @@
     end
 
     context 'group project' do
-      let(:group) { create(:group, :access_requestable) }
+      let(:group) { create(:group) }
       let(:project) { create(:project, group: group) }
       let(:requester) { create(:user) }
 
@@ -246,7 +246,7 @@
 
     context 'personal project' do
       let(:project) do
-        create(:project, :public, :access_requestable)
+        create(:project, :public)
       end
 
       context 'when project is not shared with group' do
@@ -292,7 +292,7 @@
     end
 
     context 'group project' do
-      let(:group) { create(:group, :access_requestable) }
+      let(:group) { create(:group) }
       let!(:project) do
         create(:project, group: group)
       end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index d12dd97bb9e68e3de1a14ec2d9adf3f2ed97105c..31d1d1fd7d1d143c44ab58d5b1b2c72a55c61ea8 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -47,11 +47,25 @@
   describe "#http_url_to_repo" do
     let(:project) { create :project }
 
-    it 'returns the full http url to the repo' do
-      expected_url = "#{Gitlab.config.gitlab.url}/#{subject.full_path}.git"
+    context 'when a custom HTTP clone URL root is not set' do
+      it 'returns the full http url to the repo' do
+        expected_url = "#{Gitlab.config.gitlab.url}/#{subject.full_path}.git"
 
-      expect(project_wiki.http_url_to_repo).to eq(expected_url)
-      expect(project_wiki.http_url_to_repo).not_to include('@')
+        expect(project_wiki.http_url_to_repo).to eq(expected_url)
+        expect(project_wiki.http_url_to_repo).not_to include('@')
+      end
+    end
+
+    context 'when a custom HTTP clone URL root is set' do
+      before do
+        stub_application_setting(custom_http_clone_url_root: 'https://git.example.com:51234')
+      end
+
+      it 'returns the full http url to the repo, with the root replaced with the custom one' do
+        expected_url = "https://git.example.com:51234/#{subject.full_path}.git"
+
+        expect(project_wiki.http_url_to_repo).to eq(expected_url)
+      end
     end
   end
 
@@ -95,6 +109,7 @@
     context "when the wiki repository is empty" do
       describe '#empty?' do
         subject { super().empty? }
+
         it { is_expected.to be_truthy }
       end
     end
@@ -107,6 +122,7 @@
 
       describe '#empty?' do
         subject { super().empty? }
+
         it { is_expected.to be_falsey }
 
         it 'only instantiates a Wiki page once' do
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
index e7a8d27a036094501105022832133c193014f890..0aac325c2b2f9eff32e90eb2a12fa84ff593d86e 100644
--- a/spec/models/release_spec.rb
+++ b/spec/models/release_spec.rb
@@ -15,11 +15,13 @@
     it { is_expected.to have_many(:links).class_name('Releases::Link') }
     it { is_expected.to have_many(:milestones) }
     it { is_expected.to have_many(:milestone_releases) }
+    it { is_expected.to have_one(:evidence) }
   end
 
   describe 'validation' do
     it { is_expected.to validate_presence_of(:project) }
     it { is_expected.to validate_presence_of(:description) }
+    it { is_expected.to validate_presence_of(:tag) }
 
     context 'when a release exists in the database without a name' do
       it 'does not require name' do
@@ -89,4 +91,42 @@
       end
     end
   end
+
+  describe 'evidence' do
+    describe '#create_evidence!' do
+      context 'when a release is created' do
+        it 'creates one Evidence object too' do
+          expect { release }.to change(Evidence, :count).by(1)
+        end
+      end
+    end
+
+    context 'when a release is deleted' do
+      it 'also deletes the associated evidence' do
+        release = create(:release)
+
+        expect { release.destroy }.to change(Evidence, :count).by(-1)
+      end
+    end
+  end
+
+  describe '#notify_new_release' do
+    context 'when a release is created' do
+      it 'instantiates NewReleaseWorker to send notifications' do
+        expect(NewReleaseWorker).to receive(:perform_async)
+
+        create(:release)
+      end
+    end
+
+    context 'when a release is updated' do
+      let!(:release) { create(:release) }
+
+      it 'does not send any new notification' do
+        expect(NewReleaseWorker).not_to receive(:perform_async)
+
+        release.update!(description: 'new description')
+      end
+    end
+  end
 end
diff --git a/spec/models/resource_label_event_spec.rb b/spec/models/resource_label_event_spec.rb
index f4023dcb95aeadd6daaf1d8c2c30484a85317f56..f51041c9ddcc3d6e03d1bb5d4e0ce3e1047707c4 100644
--- a/spec/models/resource_label_event_spec.rb
+++ b/spec/models/resource_label_event_spec.rb
@@ -4,6 +4,7 @@
 
 RSpec.describe ResourceLabelEvent, type: :model do
   subject { build(:resource_label_event, issue: issue) }
+
   let(:issue) { create(:issue) }
   let(:merge_request) { create(:merge_request) }
 
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 3524cdae3b89bcd027c761bb97a46a0d1f260103..f4dcbfbc190299a7d7da16b4f794593a03ff6622 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -133,6 +133,32 @@
     end
   end
 
+  describe 'when default snippet visibility set to internal' do
+    using RSpec::Parameterized::TableSyntax
+
+    before do
+      stub_application_setting(default_snippet_visibility: Gitlab::VisibilityLevel::INTERNAL)
+    end
+
+    where(:attribute_name, :value) do
+      :visibility | 'private'
+      :visibility_level | Gitlab::VisibilityLevel::PRIVATE
+      'visibility' | 'private'
+      'visibility_level' | Gitlab::VisibilityLevel::PRIVATE
+    end
+
+    with_them do
+      it 'sets the visibility level' do
+        snippet = described_class.new(attribute_name => value, title: 'test', file_name: 'test.rb', content: 'test data')
+
+        expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+        expect(snippet.title).to eq('test')
+        expect(snippet.file_name).to eq('test.rb')
+        expect(snippet.content).to eq('test data')
+      end
+    end
+  end
+
   describe '.with_optional_visibility' do
     context 'when a visibility level is provided' do
       it 'returns snippets with the given visibility' do
@@ -157,12 +183,12 @@
     end
   end
 
-  describe '.only_global_snippets' do
+  describe '.only_personal_snippets' do
     it 'returns snippets not associated with any projects' do
       create(:project_snippet)
 
       snippet = create(:snippet)
-      snippets = described_class.only_global_snippets
+      snippets = described_class.only_personal_snippets
 
       expect(snippets).to eq([snippet])
     end
diff --git a/spec/models/system_note_metadata_spec.rb b/spec/models/system_note_metadata_spec.rb
index bcd3c03f947cc21be120a0623aa8567d9640df49..801f139355bd6a856c4207cc62e4731893c173e0 100644
--- a/spec/models/system_note_metadata_spec.rb
+++ b/spec/models/system_note_metadata_spec.rb
@@ -5,6 +5,7 @@
 describe SystemNoteMetadata do
   describe 'associations' do
     it { is_expected.to belong_to(:note) }
+    it { is_expected.to belong_to(:description_version) }
   end
 
   describe 'validation' do
diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb
index 28fc82f2a323695320daa14feb5ba6d441e705af..7321a458817cfbebea7a4c61e5c1e655c9380a73 100644
--- a/spec/models/timelog_spec.rb
+++ b/spec/models/timelog_spec.rb
@@ -4,6 +4,7 @@
 
 RSpec.describe Timelog do
   subject { build(:timelog) }
+
   let(:issue) { create(:issue) }
   let(:merge_request) { create(:merge_request) }
 
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index c2566ccd0473b83a7c81d5b0ebc5a7fc38884042..487a1c619c6f66b7a429d5a83e3d3ebf48cef88f 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -253,14 +253,14 @@
     end
   end
 
-  describe '.for_group_and_descendants' do
+  describe '.for_group_ids_and_descendants' do
     it 'returns the todos for a group and its descendants' do
       parent_group = create(:group)
       child_group = create(:group, parent: parent_group)
 
       todo1 = create(:todo, group: parent_group)
       todo2 = create(:todo, group: child_group)
-      todos = described_class.for_group_and_descendants(parent_group)
+      todos = described_class.for_group_ids_and_descendants([parent_group.id])
 
       expect(todos).to contain_exactly(todo1, todo2)
     end
diff --git a/spec/models/user_interacted_project_spec.rb b/spec/models/user_interacted_project_spec.rb
index 47d919c1d12ad9907c282b494f23050be087e999..b96ff08e22d48c65b6ba1639919fa0bc5d3c3af4 100644
--- a/spec/models/user_interacted_project_spec.rb
+++ b/spec/models/user_interacted_project_spec.rb
@@ -5,6 +5,7 @@
 describe UserInteractedProject do
   describe '.track' do
     subject { described_class.track(event) }
+
     let(:event) { build(:event) }
 
     Event::ACTIONS.each do |action|
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 12292dad142dafe5bc3e2bda139d2a28abd2e190..8eb2f9b5bc060d0b0eace99d3d941b17d791350e 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -79,7 +79,7 @@
     describe '#group_members' do
       it 'does not include group memberships for which user is a requester' do
         user = create(:user)
-        group = create(:group, :public, :access_requestable)
+        group = create(:group, :public)
         group.request_access(user)
 
         expect(user.group_members).to be_empty
@@ -89,7 +89,7 @@
     describe '#project_members' do
       it 'does not include project memberships for which user is a requester' do
         user = create(:user)
-        project = create(:project, :public, :access_requestable)
+        project = create(:project, :public)
         project.request_access(user)
 
         expect(user.project_members).to be_empty
@@ -1191,7 +1191,7 @@
   end
 
   describe '.without_projects' do
-    let!(:project) { create(:project, :public, :access_requestable) }
+    let!(:project) { create(:project, :public) }
     let!(:user) { create(:user) }
     let!(:user_without_project) { create(:user) }
     let!(:user_without_project2) { create(:user) }
@@ -2170,6 +2170,7 @@
 
   describe "#contributed_projects" do
     subject { create(:user) }
+
     let!(:project1) { create(:project) }
     let!(:project2) { fork_project(project3) }
     let!(:project3) { create(:project) }
@@ -3734,6 +3735,80 @@ def access_levels(groups)
     end
   end
 
+  describe '#notification_settings_for' do
+    let(:user) { create(:user) }
+    let(:source) { nil }
+
+    subject { user.notification_settings_for(source) }
+
+    context 'when source is nil' do
+      it 'returns a blank global notification settings object' do
+        expect(subject.source).to eq(nil)
+        expect(subject.notification_email).to eq(nil)
+        expect(subject.level).to eq('global')
+      end
+    end
+
+    context 'when source is a Group' do
+      let(:group) { create(:group) }
+
+      subject { user.notification_settings_for(group, inherit: true) }
+
+      context 'when group has no existing notification settings' do
+        context 'when group has no ancestors' do
+          it 'will be a default Global notification setting' do
+            expect(subject.notification_email).to eq(nil)
+            expect(subject.level).to eq('global')
+          end
+        end
+
+        context 'when group has ancestors' do
+          context 'when an ancestor has a level other than Global' do
+            let(:ancestor) { create(:group) }
+            let(:group) { create(:group, parent: ancestor) }
+
+            before do
+              create(:notification_setting, user: user, source: ancestor, level: 'participating', notification_email: 'ancestor@example.com')
+            end
+
+            it 'has the same level set' do
+              expect(subject.level).to eq('participating')
+            end
+
+            it 'has the same email set' do
+              expect(subject.notification_email).to eq('ancestor@example.com')
+            end
+
+            context 'when inherit is false' do
+              subject { user.notification_settings_for(group) }
+
+              it 'does not inherit settings' do
+                expect(subject.notification_email).to eq(nil)
+                expect(subject.level).to eq('global')
+              end
+            end
+          end
+
+          context 'when an ancestor has a Global level but has an email set' do
+            let(:grand_ancestor) { create(:group) }
+            let(:ancestor) { create(:group, parent: grand_ancestor) }
+            let(:group) { create(:group, parent: ancestor) }
+
+            before do
+              create(:notification_setting, user: user, source: grand_ancestor, level: 'participating', notification_email: 'grand@example.com')
+              create(:notification_setting, user: user, source: ancestor, level: 'global', notification_email: 'ancestor@example.com')
+            end
+
+            it 'has the same email set' do
+              expect(subject.level).to eq('global')
+              expect(subject.notification_email).to eq('ancestor@example.com')
+            end
+          end
+        end
+      end
+    end
+  end
+
   describe '#notification_email_for' do
     let(:user) { create(:user) }
     let(:group) { create(:group) }
diff --git a/spec/models/zoom_meeting_spec.rb b/spec/models/zoom_meeting_spec.rb
index 9ace5f9ef1977151a06964af51cdfa742ee58710..3dad957a1ce221276d60ff57e156e06211af6214 100644
--- a/spec/models/zoom_meeting_spec.rb
+++ b/spec/models/zoom_meeting_spec.rb
@@ -24,6 +24,7 @@
     describe '.added_to_issue' do
       it 'gets only added meetings' do
         meetings_added = described_class.added_to_issue.pluck(:id)
+
         expect(meetings_added).to include(added_meeting.id)
         expect(meetings_added).not_to include(removed_meeting.id)
       end
@@ -31,6 +32,7 @@
     describe '.removed_from_issue' do
       it 'gets only removed meetings' do
         meetings_removed = described_class.removed_from_issue.pluck(:id)
+
         expect(meetings_removed).to include(removed_meeting.id)
         expect(meetings_removed).not_to include(added_meeting.id)
       end
diff --git a/spec/policies/deploy_keys_project_policy_spec.rb b/spec/policies/deploy_keys_project_policy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..952da86b7a7d1fe2c689761143ab37ab4ecaaf26
--- /dev/null
+++ b/spec/policies/deploy_keys_project_policy_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DeployKeysProjectPolicy do
+  subject { described_class.new(current_user, deploy_key.deploy_keys_project_for(project)) }
+
+  describe 'updating a deploy_keys_project' do
+    context 'when a project maintainer' do
+      let(:current_user) { create(:user) }
+
+      context 'tries to update private deploy key attached to project' do
+        let(:deploy_key) { create(:deploy_key, public: false) }
+        let(:project) { create(:project_empty_repo) }
+
+        before do
+          project.add_maintainer(current_user)
+          project.deploy_keys << deploy_key
+        end
+
+        it { is_expected.to be_disallowed(:update_deploy_keys_project) }
+      end
+
+      context 'tries to update public deploy key attached to project' do
+        let(:deploy_key) { create(:deploy_key, public: true) }
+        let(:project) { create(:project_empty_repo) }
+
+        before do
+          project.add_maintainer(current_user)
+          project.deploy_keys << deploy_key
+        end
+
+        it { is_expected.to be_allowed(:update_deploy_keys_project) }
+      end
+    end
+
+    context 'when a non-maintainer project member' do
+      let(:current_user) { create(:user) }
+      let(:project) { create(:project_empty_repo) }
+
+      before do
+        project.add_developer(current_user)
+        project.deploy_keys << deploy_key
+      end
+
+      context 'tries to update private deploy key attached to project' do
+        let(:deploy_key) { create(:deploy_key, public: false) }
+
+        it { is_expected.to be_disallowed(:update_deploy_keys_project) }
+      end
+
+      context 'tries to update public deploy key attached to project' do
+        let(:deploy_key) { create(:deploy_key, public: true) }
+
+        it { is_expected.to be_disallowed(:update_deploy_keys_project) }
+      end
+    end
+
+    context 'when a user is not a project member' do
+      let(:current_user) { create(:user) }
+      let(:project) { create(:project_empty_repo) }
+      let(:deploy_key) { create(:deploy_key, public: true) }
+
+      before do
+        project.deploy_keys << deploy_key
+      end
+
+      context 'tries to update public deploy key attached to project' do
+        it { is_expected.to be_disallowed(:update_deploy_keys_project) }
+      end
+    end
+  end
+end
diff --git a/spec/policies/identity_provider_policy_spec.rb b/spec/policies/identity_provider_policy_spec.rb
index 2520469d4e797cbfb702e2b6387990d9403e24d4..52b6d2c89ba716ae5918090990572c8ba15691ab 100644
--- a/spec/policies/identity_provider_policy_spec.rb
+++ b/spec/policies/identity_provider_policy_spec.rb
@@ -4,6 +4,7 @@
 
 describe IdentityProviderPolicy do
   subject(:policy) { described_class.new(user, provider) }
+
   let(:user) { User.new }
   let(:provider) { :a_provider }
 
diff --git a/spec/policies/merge_request_policy_spec.rb b/spec/policies/merge_request_policy_spec.rb
index 87205f565893196f43a89eba8fe36cc5d7ed2c75..af4c9703eb473d1c3cd5becb4dbf295b3bd953e8 100644
--- a/spec/policies/merge_request_policy_spec.rb
+++ b/spec/policies/merge_request_policy_spec.rb
@@ -53,21 +53,25 @@ def permissions(user, merge_request)
 
     describe 'the author' do
       subject { author }
+
       it_behaves_like 'a denied user'
     end
 
     describe 'a guest' do
       subject { guest }
+
       it_behaves_like 'a denied user'
     end
 
     describe 'a developer' do
       subject { developer }
+
       it_behaves_like 'a denied user'
     end
 
     describe 'any other user' do
       subject { non_team_member }
+
       it_behaves_like 'a denied user'
     end
   end
@@ -82,11 +86,13 @@ def permissions(user, merge_request)
 
     describe 'a non-team-member' do
       subject { non_team_member }
+
       it_behaves_like 'a denied user'
     end
 
     describe 'a developer' do
       subject { developer }
+
       it_behaves_like 'a user with access'
     end
   end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 6093464c94930561f3591adc720ba126182baec9..e61a064e82cf8be264a6cba1f9c844b1bf574ce0 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -40,14 +40,14 @@
       update_commit_status create_build update_build create_pipeline
       update_pipeline create_merge_request_from create_wiki push_code
       resolve_note create_container_image update_container_image destroy_container_image
-      create_environment create_deployment create_release update_release
+      create_environment create_deployment update_deployment create_release update_release
     ]
   end
 
   let(:base_maintainer_permissions) do
     %i[
       push_to_delete_protected_branch update_project_snippet update_environment
-      update_deployment admin_project_snippet admin_project_member admin_note admin_wiki admin_project
+      admin_project_snippet admin_project_member admin_note admin_wiki admin_project
       admin_commit_status admin_build admin_container_image
       admin_pipeline admin_environment admin_deployment destroy_release add_cluster
       daily_statistics
diff --git a/spec/policies/todo_policy_spec.rb b/spec/policies/todo_policy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..be6fecd104584aacb749e347c293ff2bbd08769a
--- /dev/null
+++ b/spec/policies/todo_policy_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe TodoPolicy do
+  let_it_be(:author) { create(:user) }
+
+  let_it_be(:user1) { create(:user) }
+  let_it_be(:user2) { create(:user) }
+  let_it_be(:user3) { create(:user) }
+
+  let_it_be(:todo1) { create(:todo, author: author, user: user1) }
+  let_it_be(:todo2) { create(:todo, author: author, user: user2) }
+  let_it_be(:todo3) { create(:todo, author: author, user: user2) }
+  let_it_be(:todo4) { create(:todo, author: author, user: user3) }
+
+  def permissions(user, todo)
+    described_class.new(user, todo)
+  end
+
+  describe 'own_todo' do
+    it 'allows owners to access their own todos' do
+      [
+        [user1, todo1],
+        [user2, todo2],
+        [user2, todo3],
+        [user3, todo4]
+      ].each do |user, todo|
+        expect(permissions(user, todo)).to be_allowed(:read_todo)
+      end
+    end
+
+    it 'does not allow users to access todos of other users' do
+      [
+        [user1, todo2],
+        [user1, todo3],
+        [user2, todo1],
+        [user2, todo4],
+        [user3, todo1],
+        [user3, todo2],
+        [user3, todo3]
+      ].each do |user, todo|
+        expect(permissions(user, todo)).to be_disallowed(:read_todo)
+      end
+    end
+  end
+end
diff --git a/spec/presenters/conversational_development_index/metric_presenter_spec.rb b/spec/presenters/conversational_development_index/metric_presenter_spec.rb
index 81eb05a9a6b6a1e18d8b758c3aada6d106693391..b8b68a676e6c53b5b3a41305f43610c85037946e 100644
--- a/spec/presenters/conversational_development_index/metric_presenter_spec.rb
+++ b/spec/presenters/conversational_development_index/metric_presenter_spec.rb
@@ -2,6 +2,7 @@
 
 describe ConversationalDevelopmentIndex::MetricPresenter do
   subject { described_class.new(metric) }
+
   let(:metric) { build(:conversational_development_index_metric) }
 
   describe '#cards' do
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index 1af6602ea9e60b83d063e21c89026985ca4cf5c5..100f3d33c7b516817ff2a01ccc450e9d53d344f8 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -7,7 +7,7 @@
   set(:stranger) { create(:user) }
 
   set(:project) do
-    create(:project, :public, :access_requestable, creator_id: maintainer.id, namespace: maintainer.namespace) do |project|
+    create(:project, :public, creator_id: maintainer.id, namespace: maintainer.namespace) do |project|
       project.add_developer(developer)
       project.add_maintainer(maintainer)
       project.request_access(access_requester)
@@ -15,7 +15,7 @@
   end
 
   set(:group) do
-    create(:group, :public, :access_requestable) do |group|
+    create(:group, :public) do |group|
       group.add_developer(developer)
       group.add_owner(maintainer)
       group.request_access(access_requester)
diff --git a/spec/requests/api/badges_spec.rb b/spec/requests/api/badges_spec.rb
index 1dd0cb4817c40cd4984b4323d1c9b99ef8c150ce..771a78a2d91622b6510ce72f158154438e24a655 100644
--- a/spec/requests/api/badges_spec.rb
+++ b/spec/requests/api/badges_spec.rb
@@ -345,7 +345,7 @@ def get_source(source_type)
   end
 
   def setup_project
-    create(:project, :public, :access_requestable, creator_id: maintainer.id, namespace: project_group) do |project|
+    create(:project, :public, creator_id: maintainer.id, namespace: project_group) do |project|
       project.add_developer(developer)
       project.add_maintainer(maintainer)
       project.request_access(access_requester)
@@ -356,7 +356,7 @@ def setup_project
   end
 
   def setup_group
-    create(:group, :public, :access_requestable) do |group|
+    create(:group, :public) do |group|
       group.add_developer(developer)
       group.add_owner(maintainer)
       group.request_access(access_requester)
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index f9c8b42afa812901d7a982e924b0f1db2a4ffbb5..d1e20cb17707c7e6ec1a4b3d99e091e9086b49ee 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -602,7 +602,7 @@ def check_merge_status(json_response)
       post api(route, user), params: { branch: 'new_design3', ref: 'foo' }
 
       expect(response).to have_gitlab_http_status(400)
-      expect(json_response['message']).to eq('Invalid reference name')
+      expect(json_response['message']).to eq('Invalid reference name: new_design3')
     end
   end
 
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index 1be8883bd3c05f5025266b16502e6d74294cce25..6cb02ba2f6ba2d838f55b5dd9be5b24b08976672 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -125,25 +125,55 @@ def create_status(commit, opts = {})
     let(:post_url) { "/projects/#{project.id}/statuses/#{sha}" }
 
     context 'developer user' do
-      %w[pending running success failed canceled].each do |status|
-        context "for #{status}" do
-          context 'uses only required parameters' do
-            it 'creates commit status' do
-              post api(post_url, developer), params: { state: status }
+      context 'uses only required parameters' do
+        %w[pending running success failed canceled].each do |status|
+          context "for #{status}" do
+            context 'when pipeline for sha does not exists' do
+              it 'creates commit status' do
+                post api(post_url, developer), params: { state: status }
+
+                expect(response).to have_gitlab_http_status(201)
+                expect(json_response['sha']).to eq(commit.id)
+                expect(json_response['status']).to eq(status)
+                expect(json_response['name']).to eq('default')
+                expect(json_response['ref']).not_to be_empty
+                expect(json_response['target_url']).to be_nil
+                expect(json_response['description']).to be_nil
+
+                if status == 'failed'
+                  expect(CommitStatus.find(json_response['id'])).to be_api_failure
+                end
+              end
+            end
+          end
+        end
+
+        context 'when pipeline already exists for the specified sha' do
+          let!(:pipeline) { create(:ci_pipeline, project: project, sha: sha, ref: 'ref') }
+          let(:params) { { state: 'pending' } }
+
+          shared_examples_for 'creates a commit status for the existing pipeline' do
+            it do
+              expect do
+                post api(post_url, developer), params: params
+              end.not_to change { Ci::Pipeline.count }
+
+              job = pipeline.statuses.find_by_name(json_response['name'])
 
               expect(response).to have_gitlab_http_status(201)
-              expect(json_response['sha']).to eq(commit.id)
-              expect(json_response['status']).to eq(status)
-              expect(json_response['name']).to eq('default')
-              expect(json_response['ref']).not_to be_empty
-              expect(json_response['target_url']).to be_nil
-              expect(json_response['description']).to be_nil
-
-              if status == 'failed'
-                expect(CommitStatus.find(json_response['id'])).to be_api_failure
-              end
+              expect(job.status).to eq('pending')
             end
           end
+
+          it_behaves_like 'creates a commit status for the existing pipeline'
+
+          context 'with pipeline for merge request' do
+            let!(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_project: project) }
+            let!(:pipeline) { merge_request.all_pipelines.last }
+            let(:sha) { pipeline.sha }
+
+            it_behaves_like 'creates a commit status for the existing pipeline'
+          end
         end
       end
 
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index b93ee148736aa30cdbeadbfd59c1d78ef34ad8a5..e0cc18abcca81be6f135bbcbd5601c48ebf526fd 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -2,6 +2,7 @@
 
 describe API::DeployKeys do
   let(:user)        { create(:user) }
+  let(:maintainer)  { create(:user) }
   let(:admin)       { create(:admin) }
   let(:project)     { create(:project, creator_id: user.id) }
   let(:project2)    { create(:project, creator_id: user.id) }
@@ -124,45 +125,109 @@
   end
 
   describe 'PUT /projects/:id/deploy_keys/:key_id' do
-    let(:private_deploy_key) { create(:another_deploy_key, public: false) }
-    let(:project_private_deploy_key) do
-      create(:deploy_keys_project, project: project, deploy_key: private_deploy_key)
+    let(:extra_params) { {} }
+
+    subject do
+      put api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", api_user), params: extra_params
     end
 
-    it 'updates a public deploy key as admin' do
-      expect do
-        put api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin), params: { title: 'new title' }
-      end.not_to change(deploy_key, :title)
+    context 'with non-admin' do
+      let(:api_user) { user }
 
-      expect(response).to have_gitlab_http_status(200)
+      it 'does not update a public deploy key' do
+        expect { subject }.not_to change(deploy_key, :title)
+
+        expect(response).to have_gitlab_http_status(404)
+      end
     end
 
-    it 'does not update a public deploy key as non admin' do
-      expect do
-        put api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", user), params: { title: 'new title' }
-      end.not_to change(deploy_key, :title)
+    context 'with admin' do
+      let(:api_user) { admin }
 
-      expect(response).to have_gitlab_http_status(404)
+      context 'public deploy key attached to project' do
+        let(:extra_params) { { title: 'new title', can_push: true } }
+
+        it 'updates the title of the deploy key' do
+          expect { subject }.to change { deploy_key.reload.title }.to 'new title'
+          expect(response).to have_gitlab_http_status(200)
+        end
+
+        it 'updates can_push of deploy_keys_project' do
+          expect { subject }.to change { deploy_keys_project.reload.can_push }.from(false).to(true)
+          expect(response).to have_gitlab_http_status(200)
+        end
+      end
+
+      context 'private deploy key' do
+        let(:deploy_key) { create(:another_deploy_key, public: false) }
+        let(:deploy_keys_project) do
+          create(:deploy_keys_project, project: project, deploy_key: deploy_key)
+        end
+        let(:extra_params) { { title: 'new title', can_push: true } }
+
+        it 'updates the title of the deploy key' do
+          expect { subject }.to change { deploy_key.reload.title }.to 'new title'
+          expect(response).to have_gitlab_http_status(200)
+        end
+
+        it 'updates can_push of deploy_keys_project' do
+          expect { subject }.to change { deploy_keys_project.reload.can_push }.from(false).to(true)
+          expect(response).to have_gitlab_http_status(200)
+        end
+
+        context 'invalid title' do
+          let(:extra_params) { { title: '' } }
+
+          it 'does not update the title of the deploy key' do
+            expect { subject }.not_to change { deploy_key.reload.title }
+            expect(response).to have_gitlab_http_status(400)
+          end
+        end
+      end
     end
 
-    it 'does not update a private key with invalid title' do
-      project_private_deploy_key
+    context 'with admin as project maintainer' do
+      let(:api_user) { admin }
 
-      expect do
-        put api("/projects/#{project.id}/deploy_keys/#{private_deploy_key.id}", admin), params: { title: '' }
-      end.not_to change(deploy_key, :title)
+      before do
+        project.add_maintainer(admin)
+      end
 
-      expect(response).to have_gitlab_http_status(400)
+      context 'public deploy key attached to project' do
+        let(:extra_params) { { title: 'new title', can_push: true } }
+
+        it 'updates the title of the deploy key' do
+          expect { subject }.to change { deploy_key.reload.title }.to 'new title'
+          expect(response).to have_gitlab_http_status(200)
+        end
+
+        it 'updates can_push of deploy_keys_project' do
+          expect { subject }.to change { deploy_keys_project.reload.can_push }.from(false).to(true)
+          expect(response).to have_gitlab_http_status(200)
+        end
+      end
     end
 
-    it 'updates a private ssh key with correct attributes' do
-      project_private_deploy_key
+    context 'with maintainer' do
+      let(:api_user) { maintainer }
 
-      put api("/projects/#{project.id}/deploy_keys/#{private_deploy_key.id}", admin), params: { title: 'new title', can_push: true }
+      before do
+        project.add_maintainer(maintainer)
+      end
 
-      expect(json_response['id']).to eq(private_deploy_key.id)
-      expect(json_response['title']).to eq('new title')
-      expect(json_response['can_push']).to eq(true)
+      context 'public deploy key attached to project' do
+        let(:extra_params) { { title: 'new title', can_push: true } }
+
+        it 'does not update the title of the deploy key' do
+          expect { subject }.not_to change { deploy_key.reload.title }
+          expect(response).to have_gitlab_http_status(200)
+        end
+
+        it 'updates can_push of deploy_keys_project' do
+          expect { subject }.to change { deploy_keys_project.reload.can_push }.from(false).to(true)
+          expect(response).to have_gitlab_http_status(200)
+        end
+      end
     end
   end
 
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index 3dac7225b7ab67e5bb1ae372e7a9436b0113e9a6..ad7be531979016249a1a80677c25ab7f27cbc105 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'spec_helper'
 
 describe API::Deployments do
@@ -96,4 +98,164 @@ def expect_deployments(ordered_deployments)
       end
     end
   end
+
+  describe 'POST /projects/:id/deployments' do
+    let!(:project) { create(:project, :repository) }
+    let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
+
+    context 'as a maintainer' do
+      it 'creates a new deployment' do
+        post(
+          api("/projects/#{project.id}/deployments", user),
+          params: {
+            environment: 'production',
+            sha: sha,
+            ref: 'master',
+            tag: false,
+            status: 'success'
+          }
+        )
+
+        expect(response).to have_gitlab_http_status(201)
+
+        expect(json_response['sha']).to eq(sha)
+        expect(json_response['ref']).to eq('master')
+        expect(json_response['environment']['name']).to eq('production')
+      end
+
+      it 'errors when creating a deployment with an invalid name' do
+        post(
+          api("/projects/#{project.id}/deployments", user),
+          params: {
+            environment: 'a' * 300,
+            sha: sha,
+            ref: 'master',
+            tag: false,
+            status: 'success'
+          }
+        )
+
+        expect(response).to have_gitlab_http_status(500)
+      end
+    end
+
+    context 'as a developer' do
+      it 'creates a new deployment' do
+        developer = create(:user)
+
+        project.add_developer(developer)
+
+        post(
+          api("/projects/#{project.id}/deployments", developer),
+          params: {
+            environment: 'production',
+            sha: sha,
+            ref: 'master',
+            tag: false,
+            status: 'success'
+          }
+        )
+
+        expect(response).to have_gitlab_http_status(201)
+
+        expect(json_response['sha']).to eq(sha)
+        expect(json_response['ref']).to eq('master')
+      end
+    end
+
+    context 'as non member' do
+      it 'returns a 404 status code' do
+        post(
+          api( "/projects/#{project.id}/deployments", non_member),
+          params: {
+            environment: 'production',
+            sha: '123',
+            ref: 'master',
+            tag: false,
+            status: 'success'
+          }
+        )
+
+        expect(response).to have_gitlab_http_status(404)
+      end
+    end
+  end
+
+  describe 'PUT /projects/:id/deployments/:deployment_id' do
+    let(:project) { create(:project) }
+    let(:build) { create(:ci_build, :failed, project: project) }
+    let(:environment) { create(:environment, project: project) }
+    let(:deploy) do
+      create(
+        :deployment,
+        :failed,
+        project: project,
+        environment: environment,
+        deployable: nil
+      )
+    end
+
+    context 'as a maintainer' do
+      it 'returns a 403 when updating a deployment with a build' do
+        deploy.update(deployable: build)
+
+        put(
+          api("/projects/#{project.id}/deployments/#{deploy.id}", user),
+          params: { status: 'success' }
+        )
+
+        expect(response).to have_gitlab_http_status(403)
+      end
+
+      it 'updates a deployment without an associated build' do
+        put(
+          api("/projects/#{project.id}/deployments/#{deploy.id}", user),
+          params: { status: 'success' }
+        )
+
+        expect(response).to have_gitlab_http_status(200)
+        expect(json_response['status']).to eq('success')
+      end
+    end
+
+    context 'as a developer' do
+      let(:developer) { create(:user) }
+
+      before do
+        project.add_developer(developer)
+      end
+
+      it 'returns a 403 when updating a deployment with a build' do
+        deploy.update(deployable: build)
+
+        put(
+          api("/projects/#{project.id}/deployments/#{deploy.id}", developer),
+          params: { status: 'success' }
+        )
+
+        expect(response).to have_gitlab_http_status(403)
+      end
+
+      it 'updates a deployment without an associated build' do
+        put(
+          api("/projects/#{project.id}/deployments/#{deploy.id}", developer),
+          params: { status: 'success' }
+        )
+
+        expect(response).to have_gitlab_http_status(200)
+        expect(json_response['status']).to eq('success')
+      end
+    end
+
+    context 'as non member' do
+      it 'returns a 404 status code' do
+        put(
+          api("/projects/#{project.id}/deployments/#{deploy.id}", non_member),
+          params: { status: 'success' }
+        )
+
+        expect(response).to have_gitlab_http_status(404)
+      end
+    end
+  end
 end
diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb
index 2fc772b12af05bdda064451367869f5562382568..992fd5e9c660695c325815280861fe28e0c38738 100644
--- a/spec/requests/api/events_spec.rb
+++ b/spec/requests/api/events_spec.rb
@@ -122,6 +122,7 @@
           expect(payload_hash['action']).to eq(payload.action)
           expect(payload_hash['ref_type']).to eq(payload.ref_type)
           expect(payload_hash['commit_to']).to eq(payload.commit_to)
+          expect(payload_hash['ref_count']).to eq(payload.ref_count)
         end
       end
 
diff --git a/spec/requests/api/graphql/namespace/projects_spec.rb b/spec/requests/api/graphql/namespace/projects_spec.rb
index 815e9531ecf87c5c37e17be6c918696e26af00ce..2a95b99572fbc9343ab23496fc32fc3b663849a5 100644
--- a/spec/requests/api/graphql/namespace/projects_spec.rb
+++ b/spec/requests/api/graphql/namespace/projects_spec.rb
@@ -67,6 +67,7 @@
 
   context 'when the namespace is a user' do
     subject { user.namespace }
+
     let(:include_subgroups) { false }
 
     it_behaves_like 'a graphql namespace'
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 26f6e7055287f56c9ca68a913392fb94dde77f91..eb55d747179dc33ae53076985aed13a1980e5c6f 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -7,7 +7,7 @@
   let(:stranger) { create(:user) }
 
   let(:project) do
-    create(:project, :public, :access_requestable, creator_id: maintainer.id, namespace: maintainer.namespace) do |project|
+    create(:project, :public, creator_id: maintainer.id, namespace: maintainer.namespace) do |project|
       project.add_developer(developer)
       project.add_maintainer(maintainer)
       project.request_access(access_requester)
@@ -15,7 +15,7 @@
   end
 
   let!(:group) do
-    create(:group, :public, :access_requestable) do |group|
+    create(:group, :public) do |group|
       group.add_developer(developer)
       group.add_owner(maintainer)
       group.request_access(access_requester)
@@ -87,6 +87,15 @@
         expect(json_response.first['username']).to eq(maintainer.username)
       end
 
+      it 'finds members with the given user_ids' do
+        get api(members_url, developer), params: { user_ids: [maintainer.id, developer.id, stranger.id] }
+
+        expect(response).to have_gitlab_http_status(200)
+        expect(response).to include_pagination_headers
+        expect(json_response).to be_an Array
+        expect(json_response.map { |u| u['id'] }).to contain_exactly(maintainer.id, developer.id)
+      end
+
       it 'finds all members with no query specified' do
         get api(members_url, developer), params: { query: '' }
 
@@ -155,10 +164,10 @@
     end
   end
 
-  shared_examples 'GET /:source_type/:id/members/:user_id' do |source_type|
-    context "with :source_type == #{source_type.pluralize}" do
+  shared_examples 'GET /:source_type/:id/members/(all/):user_id' do |source_type, all|
+    context "with :source_type == #{source_type.pluralize} and all == #{all}" do
       it_behaves_like 'a 404 response when source is private' do
-        let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
+        let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members/#{all ? 'all/' : ''}#{developer.id}", stranger) }
       end
 
       context 'when authenticated as a non-member' do
@@ -166,7 +175,7 @@
           context "as a #{type}" do
             it 'returns 200' do
               user = public_send(type)
-              get api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+              get api("/#{source_type.pluralize}/#{source.id}/members/#{all ? 'all/' : ''}#{developer.id}", user)
 
               expect(response).to have_gitlab_http_status(200)
               # User attributes
@@ -434,12 +443,14 @@
     end
   end
 
-  it_behaves_like 'GET /:source_type/:id/members/:user_id', 'project' do
-    let(:source) { project }
-  end
+  [false, true].each do |all|
+    it_behaves_like 'GET /:source_type/:id/members/(all/):user_id', 'project', all do
+      let(:source) { all ? create(:project, :public, group: group) : project }
+    end
 
-  it_behaves_like 'GET /:source_type/:id/members/:user_id', 'group' do
-    let(:source) { group }
+    it_behaves_like 'GET /:source_type/:id/members/(all/):user_id', 'group', all do
+      let(:source) { all ? create(:group, parent: group) : group }
+    end
   end
 
   it_behaves_like 'POST /:source_type/:id/members', 'project' do
diff --git a/spec/requests/api/pages/internal_access_spec.rb b/spec/requests/api/pages/internal_access_spec.rb
index c41eabe0a485fbcd15bf12e51d883d5c8c806da2..28abe1a8456e4c5e8dda29b7193769b650d20547 100644
--- a/spec/requests/api/pages/internal_access_spec.rb
+++ b/spec/requests/api/pages/internal_access_spec.rb
@@ -27,6 +27,7 @@
   describe "Project should be internal" do
     describe '#internal?' do
       subject { project.internal? }
+
       it { is_expected.to be_truthy }
     end
   end
diff --git a/spec/requests/api/pages/private_access_spec.rb b/spec/requests/api/pages/private_access_spec.rb
index c647537038e4e5e6a0deda3106afad59294ac701..6af441caf744b0c96a980a1f3444909d00ceca42 100644
--- a/spec/requests/api/pages/private_access_spec.rb
+++ b/spec/requests/api/pages/private_access_spec.rb
@@ -27,6 +27,7 @@
   describe "Project should be private" do
     describe '#private?' do
       subject { project.private? }
+
       it { is_expected.to be_truthy }
     end
   end
diff --git a/spec/requests/api/pages/public_access_spec.rb b/spec/requests/api/pages/public_access_spec.rb
index 16cc5697f3011e7ba97443807f807bdf0aa81ca3..d99224eca5bcbd91b5bebb0ab9cb101e80297761 100644
--- a/spec/requests/api/pages/public_access_spec.rb
+++ b/spec/requests/api/pages/public_access_spec.rb
@@ -27,6 +27,7 @@
   describe "Project should be public" do
     describe '#public?' do
       subject { project.public? }
+
       it { is_expected.to be_truthy }
     end
   end
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index bc3a04420f99d2820542f06f80d737d37909c811..70a95663aeaf038fa1ab12f96bfc8342101cbbba 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -3,6 +3,7 @@
 describe API::Runner, :clean_gitlab_redis_shared_state do
   include StubGitlabCalls
   include RedisHelpers
+  include WorkhorseHelpers
 
   let(:registration_token) { 'abcdefg123456' }
 
@@ -1395,7 +1396,7 @@ def force_patch_the_trace
 
                   expect(response).to have_gitlab_http_status(200)
                   expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
-                  expect(json_response['TempPath']).to eq(JobArtifactUploader.workhorse_local_upload_path)
+                  expect(json_response).not_to have_key('TempPath')
                   expect(json_response['RemoteObject']).to have_key('ID')
                   expect(json_response['RemoteObject']).to have_key('GetURL')
                   expect(json_response['RemoteObject']).to have_key('StoreURL')
@@ -1562,15 +1563,16 @@ def authorize_artifacts_with_token_in_headers(params = {}, request_headers = hea
                 let!(:fog_connection) do
                   stub_artifacts_object_storage(direct_upload: true)
                 end
-
-                before do
+                let(:object) do
                   fog_connection.directories.new(key: 'artifacts').files.create(
                     key: 'tmp/uploads/12312300',
                     body: 'content'
                   )
+                end
+                let(:file_upload) { fog_to_uploaded_file(object) }
 
-                  upload_artifacts(file_upload, headers_with_token,
-                    { 'file.remote_id' => remote_id })
+                before do
+                  upload_artifacts(file_upload, headers_with_token, 'file.remote_id' => remote_id)
                 end
 
                 context 'when valid remote_id is used' do
@@ -1804,12 +1806,13 @@ def authorize_artifacts_with_token_in_headers(params = {}, request_headers = hea
         end
 
         def upload_artifacts(file, headers = {}, params = {})
-          params = params.merge({
-            'file.path' => file.path,
-            'file.name' => file.original_filename
-          })
-
-          post api("/jobs/#{job.id}/artifacts"), params: params, headers: headers
+          workhorse_finalize(
+            api("/jobs/#{job.id}/artifacts"),
+            method: :post,
+            file_key: :file,
+            params: params.merge(file: file),
+            headers: headers
+          )
         end
       end
 
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index d98b9be726a1a420984c1aa785e6a2e43a41fee3..f3bfb258029daa59a249548f0181ec0bfcad4108 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -72,7 +72,9 @@
             default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE,
             local_markdown_version: 3,
             allow_local_requests_from_web_hooks_and_services: true,
-            allow_local_requests_from_system_hooks: false
+            allow_local_requests_from_system_hooks: false,
+            push_event_hooks_limit: 2,
+            push_event_activities_limit: 2
           }
 
         expect(response).to have_gitlab_http_status(200)
@@ -102,6 +104,8 @@
         expect(json_response['local_markdown_version']).to eq(3)
         expect(json_response['allow_local_requests_from_web_hooks_and_services']).to eq(true)
         expect(json_response['allow_local_requests_from_system_hooks']).to eq(false)
+        expect(json_response['push_event_hooks_limit']).to eq(2)
+        expect(json_response['push_event_activities_limit']).to eq(2)
       end
     end
 
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 0d190ae069e8351248314e5d77600f91d2d3ad97..ee4e783e9acdadbe470d4512bedf0eac1b31f271 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -634,40 +634,21 @@
   end
 
   describe "GET /users/sign_up" do
-    context 'when experimental_separate_sign_up_flow is active' do
+    context 'when experimental signup_flow is active' do
       before do
-        stub_feature_flags(experimental_separate_sign_up_flow: true)
+        stub_experiment(signup_flow: true)
       end
 
-      context 'on gitlab.com' do
-        before do
-          allow(::Gitlab).to receive(:com?).and_return(true)
-        end
-
-        it "shows sign up page" do
-          get "/users/sign_up"
-          expect(response).to have_gitlab_http_status(200)
-          expect(response).to render_template(:new)
-        end
-      end
-
-      context 'not on gitlab.com' do
-        before do
-          allow(::Gitlab).to receive(:com?).and_return(false)
-        end
-
-        it "redirects to sign in page" do
-          get "/users/sign_up"
-          expect(response).to have_gitlab_http_status(302)
-          expect(response).to redirect_to(new_user_session_path(anchor: 'register-pane'))
-        end
+      it "shows sign up page" do
+        get "/users/sign_up"
+        expect(response).to have_gitlab_http_status(200)
+        expect(response).to render_template(:new)
       end
     end
 
-    context 'when experimental_separate_sign_up_flow is not active' do
+    context 'when experimental signup_flow is not active' do
       before do
-        allow(::Gitlab).to receive(:com?).and_return(true)
-        stub_feature_flags(experimental_separate_sign_up_flow: false)
+        stub_experiment(signup_flow: false)
       end
 
       it "redirects to sign in page" do
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index e58f1b7d9dc6b58810fb26cc02f4b6fcb5adcb2b..07e56619f40eba92a39c5b369858463f2e0e7cfa 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -843,8 +843,8 @@ def attempt_login(include_password)
             get "/#{project.full_path}/blob/master/info/refs"
           end
 
-          it "returns not found" do
-            expect(response).to have_gitlab_http_status(:not_found)
+          it "redirects" do
+            expect(response).to have_gitlab_http_status(302)
           end
         end
       end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index ae34f7d1f87c3b631ffc3815b644ae2d7b50e065..62b9ee1d361d7f5cec2af07ab5ed628611b6fd66 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -4,6 +4,7 @@
 describe 'Git LFS API and storage' do
   include LfsHttpHelpers
   include ProjectForksHelper
+  include WorkhorseHelpers
 
   set(:project) { create(:project, :repository) }
   set(:other_project) { create(:project, :repository) }
@@ -933,7 +934,7 @@ def authorization_in_action(action)
 
                 it_behaves_like 'a valid response' do
                   it 'responds with status 200, location of LFS remote store and object details' do
-                    expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
+                    expect(json_response).not_to have_key('TempPath')
                     expect(json_response['RemoteObject']).to have_key('ID')
                     expect(json_response['RemoteObject']).to have_key('GetURL')
                     expect(json_response['RemoteObject']).to have_key('StoreURL')
@@ -992,10 +993,17 @@ def authorization_in_action(action)
                   stub_lfs_object_storage(direct_upload: true)
                 end
 
+                let(:tmp_object) do
+                  fog_connection.directories.new(key: 'lfs-objects').files.create(
+                    key: 'tmp/uploads/12312300',
+                    body: 'content'
+                  )
+                end
+
                 ['123123', '../../123123'].each do |remote_id|
                   context "with invalid remote_id: #{remote_id}" do
                     subject do
-                      put_finalize(with_tempfile: true, args: {
+                      put_finalize(remote_object: tmp_object, args: {
                         'file.remote_id' => remote_id
                       })
                     end
@@ -1009,15 +1017,8 @@ def authorization_in_action(action)
                 end
 
                 context 'with valid remote_id' do
-                  before do
-                    fog_connection.directories.new(key: 'lfs-objects').files.create(
-                      key: 'tmp/uploads/12312300',
-                      body: 'content'
-                    )
-                  end
-
                   subject do
-                    put_finalize(with_tempfile: true, args: {
+                    put_finalize(remote_object: tmp_object, args: {
                       'file.remote_id' => '12312300',
                       'file.name' => 'name'
                     })
@@ -1027,6 +1028,10 @@ def authorization_in_action(action)
                     subject
 
                     expect(response).to have_gitlab_http_status(200)
+
+                    object = LfsObject.find_by_oid(sample_oid)
+                    expect(object).to be_present
+                    expect(object.file.read).to eq(tmp_object.body)
                   end
 
                   it 'schedules migration of file to object storage' do
@@ -1268,28 +1273,31 @@ def put_authorize(verified: true)
       put authorize_url(project, sample_oid, sample_size), params: {}, headers: authorize_headers
     end
 
-    def put_finalize(lfs_tmp = lfs_tmp_file, with_tempfile: false, verified: true, args: {})
-      upload_path = LfsObjectUploader.workhorse_local_upload_path
-      file_path = upload_path + '/' + lfs_tmp if lfs_tmp
+    def put_finalize(lfs_tmp = lfs_tmp_file, with_tempfile: false, verified: true, remote_object: nil, args: {})
+      uploaded_file = nil
 
       if with_tempfile
+        upload_path = LfsObjectUploader.workhorse_local_upload_path
+        file_path = upload_path + '/' + lfs_tmp if lfs_tmp
+
         FileUtils.mkdir_p(upload_path)
         FileUtils.touch(file_path)
-      end
-
-      extra_args = {
-        'file.path' => file_path,
-        'file.name' => File.basename(file_path)
-      }
 
-      put_finalize_with_args(args.merge(extra_args).compact, verified: verified)
-    end
+        uploaded_file = UploadedFile.new(file_path, filename: File.basename(file_path))
+      elsif remote_object
+        uploaded_file = fog_to_uploaded_file(remote_object)
+      end
 
-    def put_finalize_with_args(args, verified:)
       finalize_headers = headers
       finalize_headers.merge!(workhorse_internal_api_request_header) if verified
 
-      put objects_url(project, sample_oid, sample_size), params: args, headers: finalize_headers
+      workhorse_finalize(
+        objects_url(project, sample_oid, sample_size),
+        method: :put,
+        file_key: :file,
+        params: args.merge(file: uploaded_file),
+        headers: finalize_headers
+      )
     end
 
     def lfs_tmp_file
diff --git a/spec/rubocop/cop/gitlab/const_get_inherit_false_spec.rb b/spec/rubocop/cop/gitlab/const_get_inherit_false_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0ff06b431ebfdcf894e2aa296d3c7fc79ba6d494
--- /dev/null
+++ b/spec/rubocop/cop/gitlab/const_get_inherit_false_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../../rubocop/cop/gitlab/const_get_inherit_false'
+
+describe RuboCop::Cop::Gitlab::ConstGetInheritFalse do
+  include CopHelper
+
+  subject(:cop) { described_class.new }
+
+  context 'Object.const_get' do
+    it 'registers an offense with no 2nd argument' do
+      expect_offense(<<~PATTERN.strip_indent)
+        Object.const_get(:CONSTANT)
+               ^^^^^^^^^ Use inherit=false when using const_get.
+      PATTERN
+    end
+
+    it 'autocorrects' do
+      expect(autocorrect_source('Object.const_get(:CONSTANT)')).to eq('Object.const_get(:CONSTANT, false)')
+    end
+
+    context 'inherit=false' do
+      it 'does not register an offense' do
+        expect_no_offenses(<<~PATTERN.strip_indent)
+        Object.const_get(:CONSTANT, false)
+        PATTERN
+      end
+    end
+
+    context 'inherit=true' do
+      it 'registers an offense' do
+        expect_offense(<<~PATTERN.strip_indent)
+        Object.const_get(:CONSTANT, true)
+               ^^^^^^^^^ Use inherit=false when using const_get.
+        PATTERN
+      end
+
+      it 'autocorrects' do
+        expect(autocorrect_source('Object.const_get(:CONSTANT, true)')).to eq('Object.const_get(:CONSTANT, false)')
+      end
+    end
+  end
+
+  context 'const_get for a nested class' do
+    it 'registers an offense on reload usage' do
+      expect_offense(<<~PATTERN.strip_indent)
+        Nested::Blog.const_get(:CONSTANT)
+                     ^^^^^^^^^ Use inherit=false when using const_get.
+      PATTERN
+    end
+
+    it 'autocorrects' do
+      expect(autocorrect_source('Nested::Blag.const_get(:CONSTANT)')).to eq('Nested::Blag.const_get(:CONSTANT, false)')
+    end
+
+    context 'inherit=false' do
+      it 'does not register an offense' do
+        expect_no_offenses(<<~PATTERN.strip_indent)
+        Nested::Blog.const_get(:CONSTANT, false)
+        PATTERN
+      end
+    end
+
+    context 'inherit=true' do
+      it 'registers an offense if inherit is true' do
+        expect_offense(<<~PATTERN.strip_indent)
+        Nested::Blog.const_get(:CONSTANT, true)
+                     ^^^^^^^^^ Use inherit=false when using const_get.
+        PATTERN
+      end
+
+      it 'autocorrects' do
+        expect(autocorrect_source('Nested::Blag.const_get(:CONSTANT, true)')).to eq('Nested::Blag.const_get(:CONSTANT, false)')
+      end
+    end
+  end
+end
diff --git a/spec/rubocop/cop/migration/add_timestamps_spec.rb b/spec/rubocop/cop/migration/add_timestamps_spec.rb
index fae0177d5f5d5c907f9d8c4f826812bdcfe861ab..33f1bb85af828d5c1f69e7b00a91d0643a5fa751 100644
--- a/spec/rubocop/cop/migration/add_timestamps_spec.rb
+++ b/spec/rubocop/cop/migration/add_timestamps_spec.rb
@@ -9,6 +9,7 @@
   include CopHelper
 
   subject(:cop) { described_class.new }
+
   let(:migration_with_add_timestamps) do
     %q(
       class Users < ActiveRecord::Migration[4.2]
diff --git a/spec/rubocop/cop/migration/timestamps_spec.rb b/spec/rubocop/cop/migration/timestamps_spec.rb
index 1812818692a4d321599d6b7389aeff8036c30921..cafe255dc9a77e6edda5478ab5797658f51446d6 100644
--- a/spec/rubocop/cop/migration/timestamps_spec.rb
+++ b/spec/rubocop/cop/migration/timestamps_spec.rb
@@ -9,6 +9,7 @@
   include CopHelper
 
   subject(:cop) { described_class.new }
+
   let(:migration_with_timestamps) do
     %q(
       class Users < ActiveRecord::Migration[4.2]
diff --git a/spec/rubocop/cop/scalability/file_uploads_spec.rb b/spec/rubocop/cop/scalability/file_uploads_spec.rb
index 2a94fde5ba29fb7989de6f5ff2fdf260b85fe446..a35d423581c6398a1d7208fcc6fe3a92c26af256 100644
--- a/spec/rubocop/cop/scalability/file_uploads_spec.rb
+++ b/spec/rubocop/cop/scalability/file_uploads_spec.rb
@@ -10,6 +10,7 @@
   include ExpectOffense
 
   subject(:cop) { described_class.new }
+
   let(:message) { 'Do not upload files without workhorse acceleration. Please refer to https://docs.gitlab.com/ee/development/uploads.html' }
 
   context 'with required params' do
diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb
index f24036cf0c5a761e89d236ab30c0e82e8590a714..91c5fd6bf2cd7e40d4da331f3acbd7774cf7f8c6 100644
--- a/spec/serializers/build_details_entity_spec.rb
+++ b/spec/serializers/build_details_entity_spec.rb
@@ -123,6 +123,25 @@
       end
 
       it { is_expected.to include(failure_reason: 'unmet_prerequisites') }
+      it { is_expected.to include(callout_message: CommitStatusPresenter.callout_failure_messages[:unmet_prerequisites]) }
+    end
+
+    context 'when the build has failed due to a missing dependency' do
+      let!(:test1) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test1', stage_idx: 0) }
+      let!(:test2) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test2', stage_idx: 1) }
+      let!(:build) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 2, options: { dependencies: %w(test1 test2) }) }
+      let(:message) { subject[:callout_message] }
+
+      before do
+        build.drop!(:missing_dependency_failure)
+      end
+
+      it { is_expected.to include(failure_reason: 'missing_dependency_failure') }
+
+      it 'includes the failing dependencies in the callout message' do
+        expect(message).to include('test1')
+        expect(message).to include('test2')
+      end
     end
 
     context 'when a build has environment with latest deployment' do
diff --git a/spec/serializers/build_trace_entity_spec.rb b/spec/serializers/build_trace_entity_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bafead04a5121472d2052b91714bb19f18a0409c
--- /dev/null
+++ b/spec/serializers/build_trace_entity_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe BuildTraceEntity do
+  let(:build) { build_stubbed(:ci_build) }
+  let(:request) { double('request') }
+
+  let(:stream) do
+    Gitlab::Ci::Trace::Stream.new do
+      StringIO.new('the-trace')
+    end
+  end
+
+  let(:build_trace) do
+    Ci::BuildTrace.new(build: build, stream: stream, content_format: content_format, state: nil)
+  end
+
+  let(:entity) do
+    described_class.new(build_trace, request: request)
+  end
+
+  subject { entity.as_json }
+
+  shared_examples 'includes build and trace metadata' do
+    it 'includes build attributes' do
+      expect(subject[:id]).to eq(build.id)
+      expect(subject[:status]).to eq(build.status)
+      expect(subject[:complete]).to eq(build.complete?)
+    end
+
+    it 'includes trace metadata' do
+      expect(subject).to include(:state)
+      expect(subject).to include(:append)
+      expect(subject).to include(:truncated)
+      expect(subject).to include(:offset)
+      expect(subject).to include(:size)
+      expect(subject).to include(:total)
+    end
+  end
+
+  context 'when content format is :json' do
+    let(:content_format) { :json }
+
+    it_behaves_like 'includes build and trace metadata'
+
+    it 'includes the trace content in json' do
+      expect(subject[:lines]).to eq([
+        { offset: 0, content: [{ text: 'the-trace' }] }
+      ])
+    end
+  end
+
+  context 'when content format is :html' do
+    let(:content_format) { :html }
+
+    it_behaves_like 'includes build and trace metadata'
+
+    it 'includes the trace content in json' do
+      expect(subject[:html]).to eq('<span>the-trace</span>')
+    end
+  end
+end
diff --git a/spec/serializers/cluster_basic_entity_spec.rb b/spec/serializers/cluster_basic_entity_spec.rb
index ab5d54fca16b698ff2bf896d2df5edff4f28bd91..be03ee91784567e618b5d87031f160b086266ced 100644
--- a/spec/serializers/cluster_basic_entity_spec.rb
+++ b/spec/serializers/cluster_basic_entity_spec.rb
@@ -5,6 +5,7 @@
 describe ClusterBasicEntity do
   describe '#as_json' do
     subject { described_class.new(cluster, request: request).as_json }
+
     let(:maintainer) { create(:user) }
     let(:developer) { create(:user) }
     let(:current_user) { maintainer }
diff --git a/spec/serializers/container_repository_entity_spec.rb b/spec/serializers/container_repository_entity_spec.rb
index 5848dd64c3b20fda8d9ae912efd994dc40f343a8..799a8d5c12257854904b9ca5ebeb323403dc58d2 100644
--- a/spec/serializers/container_repository_entity_spec.rb
+++ b/spec/serializers/container_repository_entity_spec.rb
@@ -25,6 +25,18 @@
     expect(subject).to include(:id, :path, :location, :tags_path)
   end
 
+  context 'when project is not preset in the request' do
+    before do
+      allow(request).to receive(:respond_to?).and_return(false)
+      allow(request).to receive(:project).and_return(nil)
+    end
+
+    it 'uses project from the object' do
+      expect(request.project).not_to equal(project)
+      expect(subject).to include(:tags_path)
+    end
+  end
+
   context 'when user can manage repositories' do
     before do
       project.add_developer(user)
diff --git a/spec/serializers/deploy_key_entity_spec.rb b/spec/serializers/deploy_key_entity_spec.rb
index 9e76d36c302a9472381bf719ac1faa8f038df2ae..607adfc24886f098e7669cc3af902073ee060d44 100644
--- a/spec/serializers/deploy_key_entity_spec.rb
+++ b/spec/serializers/deploy_key_entity_spec.rb
@@ -8,14 +8,15 @@
   let(:user) { create(:user) }
   let(:project) { create(:project, :internal)}
   let(:project_private) { create(:project, :private)}
-  let!(:project_pending_delete) { create(:project, :internal, pending_delete: true) }
   let(:deploy_key) { create(:deploy_key) }
-  let!(:deploy_key_internal) { create(:deploy_keys_project, project: project, deploy_key: deploy_key) }
-  let!(:deploy_key_private)  { create(:deploy_keys_project, project: project_private, deploy_key: deploy_key) }
-  let!(:deploy_key_pending_delete) { create(:deploy_keys_project, project: project_pending_delete, deploy_key: deploy_key) }
 
   let(:entity) { described_class.new(deploy_key, user: user) }
 
+  before do
+    project.deploy_keys << deploy_key
+    project_private.deploy_keys << deploy_key
+  end
+
   describe 'returns deploy keys with projects a user can read' do
     let(:expected_result) do
       {
@@ -46,17 +47,30 @@
     it { expect(entity.as_json).to eq(expected_result) }
   end
 
-  describe 'returns can_edit true if user is a maintainer of project' do
+  context 'user is an admin' do
+    let(:user) { create(:user, :admin) }
+
+    it { expect(entity.as_json).to include(can_edit: true) }
+  end
+
+  context 'user is a project maintainer' do
     before do
       project.add_maintainer(user)
     end
 
-    it { expect(entity.as_json).to include(can_edit: true) }
-  end
+    context 'project deploy key' do
+      it { expect(entity.as_json).to include(can_edit: true) }
+    end
 
-  describe 'returns can_edit true if a user admin' do
-    let(:user) { create(:user, :admin) }
+    context 'public deploy key' do
+      let(:deploy_key_public) { create(:deploy_key, public: true) }
+      let(:entity_public) { described_class.new(deploy_key_public, { user: user, project: project }) }
 
-    it { expect(entity.as_json).to include(can_edit: true) }
+      before do
+        project.deploy_keys << deploy_key_public
+      end
+
+      it { expect(entity_public.as_json).to include(can_edit: true) }
+    end
   end
 end
diff --git a/spec/serializers/evidences/author_entity_spec.rb b/spec/serializers/evidences/author_entity_spec.rb
deleted file mode 100644
index 1d0fa95217cc5fc07a108866a29787e498a70d73..0000000000000000000000000000000000000000
--- a/spec/serializers/evidences/author_entity_spec.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Evidences::AuthorEntity do
-  let(:entity) { described_class.new(build(:author)) }
-
-  subject { entity.as_json }
-
-  it 'exposes the expected fields' do
-    expect(subject.keys).to contain_exactly(:id, :name, :email)
-  end
-end
diff --git a/spec/serializers/evidences/evidence_entity_spec.rb b/spec/serializers/evidences/evidence_entity_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..531708e3be676b173210031ff379d1251af034b1
--- /dev/null
+++ b/spec/serializers/evidences/evidence_entity_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Evidences::EvidenceEntity do
+  let(:evidence) { build(:evidence) }
+  let(:entity) { described_class.new(evidence) }
+
+  subject { entity.as_json }
+
+  it 'exposes the expected fields' do
+    expect(subject.keys).to contain_exactly(:release)
+  end
+end
diff --git a/spec/serializers/evidences/evidence_serializer_spec.rb b/spec/serializers/evidences/evidence_serializer_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5322f6a43fcb7068f484deeb1438d889cfbeb983
--- /dev/null
+++ b/spec/serializers/evidences/evidence_serializer_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Evidences::EvidenceSerializer do
+  it 'represents an EvidenceEntity entity' do
+    expect(described_class.entity_class).to eq(Evidences::EvidenceEntity)
+  end
+end
diff --git a/spec/serializers/evidences/issue_entity_spec.rb b/spec/serializers/evidences/issue_entity_spec.rb
index a14028087571def31599d5dfbae53d4147c0acae..915df9868878a88f7d5100fa027a649468082d5e 100644
--- a/spec/serializers/evidences/issue_entity_spec.rb
+++ b/spec/serializers/evidences/issue_entity_spec.rb
@@ -8,6 +8,6 @@
   subject { entity.as_json }
 
   it 'exposes the expected fields' do
-    expect(subject.keys).to contain_exactly(:id, :title, :description, :author, :state, :iid, :confidential, :created_at, :due_date)
+    expect(subject.keys).to contain_exactly(:id, :title, :description, :state, :iid, :confidential, :created_at, :due_date)
   end
 end
diff --git a/spec/serializers/evidences/milestone_entity_spec.rb b/spec/serializers/evidences/milestone_entity_spec.rb
index 082e178618ea6014d184c91a828aac6e83dc418b..68eb12093daa1e1dde2a32e7d367f29dd510ee85 100644
--- a/spec/serializers/evidences/milestone_entity_spec.rb
+++ b/spec/serializers/evidences/milestone_entity_spec.rb
@@ -12,7 +12,7 @@
     expect(subject.keys).to contain_exactly(:id, :title, :description, :state, :iid, :created_at, :due_date, :issues)
   end
 
-  context 'when there issues linked to this milestone' do
+  context 'when there are issues linked to this milestone' do
     let(:issue_1) { build(:issue) }
     let(:issue_2) { build(:issue) }
     let(:milestone) { build(:milestone, issues: [issue_1, issue_2]) }
diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb
index 4872b23d26bdf88fdd40bf854ca82d8f9a0113a4..35940ac062efb2231ec108fd0bf98ed542d48852 100644
--- a/spec/serializers/merge_request_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_widget_entity_spec.rb
@@ -358,4 +358,26 @@ def find_matching_commit(short_id)
       end
     end
   end
+
+  describe 'exposed_artifacts_path' do
+    context 'when merge request has exposed artifacts' do
+      before do
+        expect(resource).to receive(:has_exposed_artifacts?).and_return(true)
+      end
+
+      it 'set the path to poll data' do
+        expect(subject[:exposed_artifacts_path]).to be_present
+      end
+    end
+
+    context 'when merge request has no exposed artifacts' do
+      before do
+        expect(resource).to receive(:has_exposed_artifacts?).and_return(false)
+      end
+
+      it 'set the path to poll data' do
+        expect(subject[:exposed_artifacts_path]).to be_nil
+      end
+    end
+  end
 end
diff --git a/spec/serializers/pipeline_details_entity_spec.rb b/spec/serializers/pipeline_details_entity_spec.rb
index d7c40b8e7b987283bb6221ef75ebb2ea6908cdcc..b180ede51eb8298631d788436cd7dfbfecaa4833 100644
--- a/spec/serializers/pipeline_details_entity_spec.rb
+++ b/spec/serializers/pipeline_details_entity_spec.rb
@@ -138,5 +138,40 @@
         expect(subject[:flags][:yaml_errors]).to be false
       end
     end
+
+    context 'when pipeline is triggered by other pipeline' do
+      let(:pipeline) { create(:ci_empty_pipeline) }
+
+      before do
+        create(:ci_sources_pipeline, pipeline: pipeline)
+      end
+
+      it 'contains an information about depedent pipeline' do
+        expect(subject[:triggered_by]).to be_a(Hash)
+        expect(subject[:triggered_by][:path]).not_to be_nil
+        expect(subject[:triggered_by][:details]).not_to be_nil
+        expect(subject[:triggered_by][:details][:status]).not_to be_nil
+        expect(subject[:triggered_by][:project]).not_to be_nil
+      end
+    end
+
+    context 'when pipeline triggered other pipeline' do
+      let(:pipeline) { create(:ci_empty_pipeline) }
+      let(:build) { create(:ci_build, pipeline: pipeline) }
+
+      before do
+        create(:ci_sources_pipeline, source_job: build)
+        create(:ci_sources_pipeline, source_job: build)
+      end
+
+      it 'contains an information about depedent pipeline' do
+        expect(subject[:triggered]).to be_a(Array)
+        expect(subject[:triggered].length).to eq(2)
+        expect(subject[:triggered].first[:path]).not_to be_nil
+        expect(subject[:triggered].first[:details]).not_to be_nil
+        expect(subject[:triggered].first[:details][:status]).not_to be_nil
+        expect(subject[:triggered].first[:project]).not_to be_nil
+      end
+    end
   end
 end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index a1d275cfa2a73c11ab1d7fb933cbda4e7d7623fa..ce5264ec8bbf53c25ac64efedcb6210350569675 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -139,6 +139,7 @@
 
     describe 'number of queries when preloaded' do
       subject { serializer.represent(resource, preload: true) }
+
       let(:resource) { Ci::Pipeline.all }
 
       before do
@@ -158,7 +159,7 @@
 
         it 'verifies number of queries', :request_store do
           recorded = ActiveRecord::QueryRecorder.new { subject }
-          expected_queries = Gitlab.ee? ? 38 : 31
+          expected_queries = Gitlab.ee? ? 38 : 35
 
           expect(recorded.count).to be_within(2).of(expected_queries)
           expect(recorded.cached_count).to eq(0)
@@ -179,7 +180,8 @@ def ref
           # pipeline. With the same ref this check is cached but if refs are
           # different then there is an extra query per ref
           # https://gitlab.com/gitlab-org/gitlab-foss/issues/46368
-          expected_queries = Gitlab.ee? ? 44 : 38
+          expected_queries = Gitlab.ee? ? 44 : 41
+
           expect(recorded.count).to be_within(2).of(expected_queries)
           expect(recorded.cached_count).to eq(0)
         end
diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb
index ea7ea02cee3409e5b94864ecee375444cfa9a3e2..6e1fdb7aad0dbdf266155afa9c1485c3b69e6d23 100644
--- a/spec/services/application_settings/update_service_spec.rb
+++ b/spec/services/application_settings/update_service_spec.rb
@@ -147,35 +147,44 @@
     using RSpec::Parameterized::TableSyntax
 
     where(:params_performance_bar_enabled,
-      :params_performance_bar_allowed_group_path,
-      :previous_performance_bar_allowed_group_id,
-      :expected_performance_bar_allowed_group_id) do
-      true | '' | nil | nil
-      true | '' | 42_000_000 | nil
-      true | nil | nil | nil
-      true | nil | 42_000_000 | nil
-      true | 'foo' | nil | nil
-      true | 'foo' | 42_000_000 | nil
-      true | 'group_a' | nil | 42_000_000
-      true | 'group_b' | 42_000_000 | 43_000_000
-      true | 'group_a' | 42_000_000 | 42_000_000
-      false | '' | nil | nil
-      false | '' | 42_000_000 | nil
-      false | nil | nil | nil
-      false | nil | 42_000_000 | nil
-      false | 'foo' | nil | nil
-      false | 'foo' | 42_000_000 | nil
-      false | 'group_a' | nil | nil
-      false | 'group_b' | 42_000_000 | nil
-      false | 'group_a' | 42_000_000 | nil
+          :params_performance_bar_allowed_group_path,
+          :previous_performance_bar_allowed_group_id,
+          :expected_performance_bar_allowed_group_id,
+          :expected_valid) do
+      true | '' | nil | nil | true
+      true | '' | 42_000_000 | nil | true
+      true | nil | nil | nil | true
+      true | nil | 42_000_000 | nil | true
+      true | 'foo' | nil | nil | false
+      true | 'foo' | 42_000_000 | 42_000_000 | false
+      true | 'group_a' | nil | 42_000_000 | true
+      true | 'group_b' | 42_000_000 | 43_000_000 | true
+      true | 'group_b/' | 42_000_000 | 43_000_000 | true
+      true | 'group_a' | 42_000_000 | 42_000_000 | true
+      false | '' | nil | nil | true
+      false | '' | 42_000_000 | nil | true
+      false | nil | nil | nil | true
+      false | nil | 42_000_000 | nil | true
+      false | 'foo' | nil | nil | true
+      false | 'foo' | 42_000_000 | nil | true
+      false | 'group_a' | nil | nil | true
+      false | 'group_b' | 42_000_000 | nil | true
+      false | 'group_a' | 42_000_000 | nil | true
+      nil | '' | nil | nil | true
+      nil | 'foo' | nil | nil | false
+      nil | 'group_a' | nil | 42_000_000 | true
     end
 
     with_them do
       let(:params) do
         {
-          performance_bar_enabled: params_performance_bar_enabled,
           performance_bar_allowed_group_path: params_performance_bar_allowed_group_path
-        }
+        }.tap do |params_hash|
+          # Treat nil in the table as missing
+          unless params_performance_bar_enabled.nil?
+            params_hash[:performance_bar_enabled] = params_performance_bar_enabled
+          end
+        end
       end
 
       before do
@@ -202,6 +211,14 @@
             .not_to change(application_settings, :performance_bar_allowed_group_id)
         end
       end
+
+      it 'adds errors to the model for invalid params' do
+        expect(subject.execute).to eq(expected_valid)
+
+        unless expected_valid
+          expect(application_settings.errors[:performance_bar_allowed_group_id]).to be_present
+        end
+      end
     end
 
     context 'when :performance_bar_allowed_group_path is not present' do
@@ -221,7 +238,7 @@
       let(:group) { create(:group) }
       let(:params) { { performance_bar_allowed_group_path: group.full_path } }
 
-      it 'implicitely defaults to true' do
+      it 'implicitly defaults to true' do
         expect { subject.execute }
           .to change(application_settings, :performance_bar_allowed_group_id)
           .from(nil).to(group.id)
diff --git a/spec/services/boards/issues/create_service_spec.rb b/spec/services/boards/issues/create_service_spec.rb
index 33637419f83a3d9525798778e91ac1f78536ee07..ef7b7fdbaacf1d027a9f979745b63eb6fa07e69f 100644
--- a/spec/services/boards/issues/create_service_spec.rb
+++ b/spec/services/boards/issues/create_service_spec.rb
@@ -10,7 +10,7 @@
     let(:label)   { create(:label, project: project, name: 'in-progress') }
     let!(:list)   { create(:list, board: board, label: label, position: 0) }
 
-    subject(:service) { described_class.new(board.parent, project, user, board_id: board.id, list_id: list.id, title: 'New issue') }
+    subject(:service) { described_class.new(board.resource_parent, project, user, board_id: board.id, list_id: list.id, title: 'New issue') }
 
     before do
       project.add_developer(user)
diff --git a/spec/services/boards/lists/update_service_spec.rb b/spec/services/boards/lists/update_service_spec.rb
index a5411a2fb3a38b3008ab6d30b9617deaab165e94..243e0fc50ad46c7acc33781e3191569067f6e248 100644
--- a/spec/services/boards/lists/update_service_spec.rb
+++ b/spec/services/boards/lists/update_service_spec.rb
@@ -9,9 +9,9 @@
   shared_examples 'moving list' do
     context 'when user can admin list' do
       it 'calls Lists::MoveService to update list position' do
-        board.parent.add_developer(user)
+        board.resource_parent.add_developer(user)
 
-        expect(Boards::Lists::MoveService).to receive(:new).with(board.parent, user, params).and_call_original
+        expect(Boards::Lists::MoveService).to receive(:new).with(board.resource_parent, user, params).and_call_original
         expect_any_instance_of(Boards::Lists::MoveService).to receive(:execute).with(list)
 
         service.execute(list)
@@ -30,7 +30,7 @@
   shared_examples 'updating list preferences' do
     context 'when user can read list' do
       it 'updates list preference for user' do
-        board.parent.add_guest(user)
+        board.resource_parent.add_guest(user)
 
         service.execute(list)
 
@@ -48,7 +48,7 @@
   end
 
   describe '#execute' do
-    let(:service) { described_class.new(board.parent, user, params) }
+    let(:service) { described_class.new(board.resource_parent, user, params) }
 
     context 'when position parameter is present' do
       let(:params) { { position: 1 } }
diff --git a/spec/services/boards/visits/create_service_spec.rb b/spec/services/boards/visits/create_service_spec.rb
index 6baf7ac9debf42e51fffe474c3628253e9fe4716..203c287f396dac140153e0bf6ae5dbaf752626f3 100644
--- a/spec/services/boards/visits/create_service_spec.rb
+++ b/spec/services/boards/visits/create_service_spec.rb
@@ -10,7 +10,7 @@
       let(:project)       { create(:project) }
       let(:project_board) { create(:board, project: project) }
 
-      subject(:service) { described_class.new(project_board.parent, user) }
+      subject(:service) { described_class.new(project_board.resource_parent, user) }
 
       it 'returns nil when there is no user' do
         service.current_user = nil
@@ -35,7 +35,7 @@
       let(:group)       { create(:group) }
       let(:group_board) { create(:board, group: group) }
 
-      subject(:service) { described_class.new(group_board.parent, user) }
+      subject(:service) { described_class.new(group_board.resource_parent, user) }
 
       it 'returns nil when there is no user' do
         service.current_user = nil
diff --git a/spec/services/bulk_push_event_payload_service_spec.rb b/spec/services/bulk_push_event_payload_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..661c3540aa0b4ad1d6c75d04db7351b36add8262
--- /dev/null
+++ b/spec/services/bulk_push_event_payload_service_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe BulkPushEventPayloadService do
+  let(:event) { create(:push_event) }
+
+  let(:push_data) do
+    {
+      action: :created,
+      ref_count: 4,
+      ref_type: :branch
+    }
+  end
+
+  subject { described_class.new(event, push_data) }
+
+  it 'creates a PushEventPayload' do
+    push_event_payload = subject.execute
+
+    expect(push_event_payload).to be_persisted
+    expect(push_event_payload.action).to eq(push_data[:action].to_s)
+    expect(push_event_payload.commit_count).to eq(0)
+    expect(push_event_payload.ref_count).to eq(push_data[:ref_count])
+    expect(push_event_payload.ref_type).to eq(push_data[:ref_type].to_s)
+  end
+end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 4d6269f0e015a48887feba2005fb33d36d19db83..fd5f72c4c46e21c6b6adad10788151cf35c642e3 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -736,6 +736,28 @@ def previous_commit_sha_from_ref(ref)
       end
     end
 
+    context 'when environment with duplicate names' do
+      let(:ci_yaml) do
+        {
+          deploy: { environment: { name: 'production' }, script: 'ls' },
+          deploy_2: { environment: { name: 'production' }, script: 'ls' }
+        }
+      end
+
+      before do
+        stub_ci_pipeline_yaml_file(YAML.dump(ci_yaml))
+      end
+
+      it 'creates a pipeline with the environment' do
+        result = execute_service
+
+        expect(result).to be_persisted
+        expect(Environment.find_by(name: 'production')).to be_present
+        expect(result.builds.first.deployment).to be_persisted
+        expect(result.builds.first.deployment.deployable).to be_a(Ci::Build)
+      end
+    end
+
     context 'when builds with auto-retries are configured' do
       let(:pipeline)  { execute_service }
       let(:rspec_job) { pipeline.builds.find_by(name: 'rspec') }
diff --git a/spec/services/ci/find_exposed_artifacts_service_spec.rb b/spec/services/ci/find_exposed_artifacts_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f6309822fe079c90a410f7e12624f897c43845f4
--- /dev/null
+++ b/spec/services/ci/find_exposed_artifacts_service_spec.rb
@@ -0,0 +1,147 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::FindExposedArtifactsService do
+  include Gitlab::Routing
+
+  let(:metadata) do
+    Gitlab::Ci::Build::Artifacts::Metadata
+      .new(metadata_file_stream, path, { recursive: true })
+  end
+
+  let(:metadata_file_stream) do
+    File.open(Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz')
+  end
+
+  let_it_be(:project) { create(:project) }
+  let(:user) { nil }
+
+  after do
+    metadata_file_stream&.close
+  end
+
+  def create_job_with_artifacts(options)
+    create(:ci_build, pipeline: pipeline, options: options).tap do |job|
+      create(:ci_job_artifact, :metadata, job: job)
+    end
+  end
+
+  describe '#for_pipeline' do
+    shared_examples 'finds a single match' do
+      it 'returns the artifact with exact location' do
+        expect(subject).to eq([{
+          text: 'Exposed artifact',
+          url: file_project_job_artifacts_path(project, job, 'other_artifacts_0.1.2/doc_sample.txt'),
+          job_name: job.name,
+          job_path: project_job_path(project, job)
+        }])
+      end
+    end
+
+    shared_examples 'finds multiple matches' do
+      it 'returns the path to the artifacts browser' do
+        expect(subject).to eq([{
+          text: 'Exposed artifact',
+          url: browse_project_job_artifacts_path(project, job),
+          job_name: job.name,
+          job_path: project_job_path(project, job)
+        }])
+      end
+    end
+
+    let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+
+    subject { described_class.new(project, user).for_pipeline(pipeline) }
+
+    context 'with jobs having at most 1 matching exposed artifact' do
+      let!(:job) do
+        create_job_with_artifacts(artifacts: {
+          expose_as: 'Exposed artifact',
+          paths: ['other_artifacts_0.1.2/doc_sample.txt', 'something-else.html']
+        })
+      end
+
+      it_behaves_like 'finds a single match'
+    end
+
+    context 'with jobs having more than 1 matching exposed artifacts' do
+      let!(:job) do
+        create_job_with_artifacts(artifacts: {
+          expose_as: 'Exposed artifact',
+          paths: [
+            'ci_artifacts.txt',
+            'other_artifacts_0.1.2/doc_sample.txt',
+            'something-else.html'
+          ]
+        })
+      end
+
+      it_behaves_like 'finds multiple matches'
+    end
+
+    context 'with jobs having more than 1 matching exposed artifacts inside a directory' do
+      let!(:job) do
+        create_job_with_artifacts(artifacts: {
+          expose_as: 'Exposed artifact',
+          paths: ['tests_encoding/']
+        })
+      end
+
+      it_behaves_like 'finds multiple matches'
+    end
+
+    context 'with jobs having paths with glob expression' do
+      let!(:job) do
+        create_job_with_artifacts(artifacts: {
+          expose_as: 'Exposed artifact',
+          paths: ['other_artifacts_0.1.2/doc_sample.txt', 'tests_encoding/*.*']
+        })
+      end
+
+      it_behaves_like 'finds a single match' # because those with * are ignored
+    end
+
+    context 'limiting results' do
+      let!(:job1) do
+        create_job_with_artifacts(artifacts: {
+          expose_as: 'artifact 1',
+          paths: ['ci_artifacts.txt']
+        })
+      end
+
+      let!(:job2) do
+        create_job_with_artifacts(artifacts: {
+          expose_as: 'artifact 2',
+          paths: ['tests_encoding/']
+        })
+      end
+
+      let!(:job3) do
+        create_job_with_artifacts(artifacts: {
+          expose_as: 'should not be exposed',
+          paths: ['other_artifacts_0.1.2/doc_sample.txt']
+        })
+      end
+
+      subject { described_class.new(project, user).for_pipeline(pipeline, limit: 2) }
+
+      it 'returns first 2 results' do
+        expect(subject).to eq([
+          {
+            text: 'artifact 1',
+            url: file_project_job_artifacts_path(project, job1, 'ci_artifacts.txt'),
+            job_name: job1.name,
+            job_path: project_job_path(project, job1)
+          },
+          {
+            text: 'artifact 2',
+            url: browse_project_job_artifacts_path(project, job2),
+            job_name: job2.name,
+            job_path: project_job_path(project, job2)
+          }
+        ])
+      end
+    end
+  end
+end
diff --git a/spec/services/ci/pipeline_trigger_service_spec.rb b/spec/services/ci/pipeline_trigger_service_spec.rb
index 76251b5b55706dfdd0b1bd6797aa261aed45a795..24d42f402f40dde5ea57435b299322c773e67df8 100644
--- a/spec/services/ci/pipeline_trigger_service_spec.rb
+++ b/spec/services/ci/pipeline_trigger_service_spec.rb
@@ -11,76 +11,158 @@
 
   describe '#execute' do
     let(:user) { create(:user) }
-    let(:trigger) { create(:ci_trigger, project: project, owner: user) }
     let(:result) { described_class.new(project, user, params).execute }
 
     before do
       project.add_developer(user)
     end
 
-    context 'when trigger belongs to a different project' do
-      let(:params) { { token: trigger.token, ref: 'master', variables: nil } }
-      let(:trigger) { create(:ci_trigger, project: create(:project), owner: user) }
+    context 'with a trigger token' do
+      let(:trigger) { create(:ci_trigger, project: project, owner: user) }
 
-      it 'does nothing' do
-        expect { result }.not_to change { Ci::Pipeline.count }
-      end
-    end
-
-    context 'when params have an existsed trigger token' do
-      context 'when params have an existsed ref' do
+      context 'when trigger belongs to a different project' do
         let(:params) { { token: trigger.token, ref: 'master', variables: nil } }
+        let(:trigger) { create(:ci_trigger, project: create(:project), owner: user) }
 
-        it 'triggers a pipeline' do
-          expect { result }.to change { Ci::Pipeline.count }.by(1)
-          expect(result[:pipeline].ref).to eq('master')
-          expect(result[:pipeline].project).to eq(project)
-          expect(result[:pipeline].user).to eq(trigger.owner)
-          expect(result[:pipeline].trigger_requests.to_a)
-            .to eq(result[:pipeline].builds.map(&:trigger_request).uniq)
-          expect(result[:status]).to eq(:success)
+        it 'does nothing' do
+          expect { result }.not_to change { Ci::Pipeline.count }
         end
+      end
 
-        context 'when commit message has [ci skip]' do
-          before do
-            allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { '[ci skip]' }
-          end
+      context 'when params have an existsed trigger token' do
+        context 'when params have an existsed ref' do
+          let(:params) { { token: trigger.token, ref: 'master', variables: nil } }
 
-          it 'ignores [ci skip] and create as general' do
+          it 'triggers a pipeline' do
             expect { result }.to change { Ci::Pipeline.count }.by(1)
+            expect(result[:pipeline].ref).to eq('master')
+            expect(result[:pipeline].project).to eq(project)
+            expect(result[:pipeline].user).to eq(trigger.owner)
+            expect(result[:pipeline].trigger_requests.to_a)
+              .to eq(result[:pipeline].builds.map(&:trigger_request).uniq)
             expect(result[:status]).to eq(:success)
           end
+
+          context 'when commit message has [ci skip]' do
+            before do
+              allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { '[ci skip]' }
+            end
+
+            it 'ignores [ci skip] and create as general' do
+              expect { result }.to change { Ci::Pipeline.count }.by(1)
+              expect(result[:status]).to eq(:success)
+            end
+          end
+
+          context 'when params have a variable' do
+            let(:params) { { token: trigger.token, ref: 'master', variables: variables } }
+            let(:variables) { { 'AAA' => 'AAA123' } }
+
+            it 'has a variable' do
+              expect { result }.to change { Ci::PipelineVariable.count }.by(1)
+                               .and change { Ci::TriggerRequest.count }.by(1)
+              expect(result[:pipeline].variables.map { |v| { v.key => v.value } }.first).to eq(variables)
+              expect(result[:pipeline].trigger_requests.last.variables).to be_nil
+            end
+          end
         end
 
-        context 'when params have a variable' do
-          let(:params) { { token: trigger.token, ref: 'master', variables: variables } }
-          let(:variables) { { 'AAA' => 'AAA123' } }
+        context 'when params have a non-existsed ref' do
+          let(:params) { { token: trigger.token, ref: 'invalid-ref', variables: nil } }
 
-          it 'has a variable' do
-            expect { result }.to change { Ci::PipelineVariable.count }.by(1)
-                             .and change { Ci::TriggerRequest.count }.by(1)
-            expect(result[:pipeline].variables.map { |v| { v.key => v.value } }.first).to eq(variables)
-            expect(result[:pipeline].trigger_requests.last.variables).to be_nil
+          it 'does not trigger a pipeline' do
+            expect { result }.not_to change { Ci::Pipeline.count }
+            expect(result[:http_status]).to eq(400)
           end
         end
       end
 
-      context 'when params have a non-existsed ref' do
-        let(:params) { { token: trigger.token, ref: 'invalid-ref', variables: nil } }
+      context 'when params have a non-existsed trigger token' do
+        let(:params) { { token: 'invalid-token', ref: nil, variables: nil } }
 
         it 'does not trigger a pipeline' do
           expect { result }.not_to change { Ci::Pipeline.count }
-          expect(result[:http_status]).to eq(400)
+          expect(result).to be_nil
         end
       end
     end
 
-    context 'when params have a non-existsed trigger token' do
-      let(:params) { { token: 'invalid-token', ref: nil, variables: nil } }
+    context 'with a pipeline job token' do
+      let!(:pipeline) { create(:ci_empty_pipeline, project: project) }
+      let(:job) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+      context 'when job user does not have a permission to read a project' do
+        let(:params) { { token: job.token, ref: 'master', variables: nil } }
+        let(:job) { create(:ci_build, pipeline: pipeline, user: create(:user)) }
+
+        it 'does nothing' do
+          expect { result }.not_to change { Ci::Pipeline.count }
+        end
+      end
+
+      context 'when job is not running' do
+        let(:params) { { token: job.token, ref: 'master', variables: nil } }
+        let(:job) { create(:ci_build, :success, pipeline: pipeline, user: user) }
+
+        it 'does nothing' do
+          expect { result }.not_to change { Ci::Pipeline.count }
+          expect(result[:message]).to eq('400 Job has to be running')
+        end
+      end
 
-      it 'does not trigger a pipeline' do
-        expect { result }.not_to change { Ci::Pipeline.count }
-        expect(result).to be_nil
+      context 'when params have an existsed job token' do
+        context 'when params have an existsed ref' do
+          let(:params) { { token: job.token, ref: 'master', variables: nil } }
+
+          it 'triggers a pipeline' do
+            expect { result }.to change { Ci::Pipeline.count }.by(1)
+            expect(result[:pipeline].ref).to eq('master')
+            expect(result[:pipeline].project).to eq(project)
+            expect(result[:pipeline].user).to eq(job.user)
+            expect(result[:status]).to eq(:success)
+          end
+
+          context 'when commit message has [ci skip]' do
+            before do
+              allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { '[ci skip]' }
+            end
+
+            it 'ignores [ci skip] and create as general' do
+              expect { result }.to change { Ci::Pipeline.count }.by(1)
+              expect(result[:status]).to eq(:success)
+            end
+          end
+
+          context 'when params have a variable' do
+            let(:params) { { token: job.token, ref: 'master', variables: variables } }
+            let(:variables) { { 'AAA' => 'AAA123' } }
+
+            it 'has a variable' do
+              expect { result }.to change { Ci::PipelineVariable.count }.by(1)
+                               .and change { Ci::Sources::Pipeline.count }.by(1)
+              expect(result[:pipeline].variables.map { |v| { v.key => v.value } }.first).to eq(variables)
+              expect(job.sourced_pipelines.last.pipeline_id).to eq(result[:pipeline].id)
+            end
+          end
+        end
+
+        context 'when params have a non-existsed ref' do
+          let(:params) { { token: job.token, ref: 'invalid-ref', variables: nil } }
+
+          it 'does not job a pipeline' do
+            expect { result }.not_to change { Ci::Pipeline.count }
+            expect(result[:http_status]).to eq(400)
+          end
+        end
+      end
+
+      context 'when params have a non-existsed trigger token' do
+        let(:params) { { token: 'invalid-token', ref: nil, variables: nil } }
+
+        it 'does not trigger a pipeline' do
+          expect { result }.not_to change { Ci::Pipeline.count }
+          expect(result).to be_nil
+        end
       end
     end
   end
diff --git a/spec/services/create_branch_service_spec.rb b/spec/services/create_branch_service_spec.rb
index 0d34c7f9a8201944403f10d83e77b2e4157a8673..9661173c9e793947a56875164004cf9feab562c5 100644
--- a/spec/services/create_branch_service_spec.rb
+++ b/spec/services/create_branch_service_spec.rb
@@ -22,5 +22,20 @@
         expect(project.repository.branch_exists?('my-feature')).to be_truthy
       end
     end
+
+    context 'when creating a branch fails' do
+      let(:project) { create(:project_empty_repo) }
+
+      before do
+        allow(project.repository).to receive(:add_branch).and_return(false)
+      end
+
+      it 'retruns an error with the branch name' do
+        result = service.execute('my-feature', 'master')
+
+        expect(result[:status]).to eq(:error)
+        expect(result[:message]).to eq("Invalid reference name: my-feature")
+      end
+    end
   end
 end
diff --git a/spec/services/update_deployment_service_spec.rb b/spec/services/deployments/after_create_service_spec.rb
similarity index 99%
rename from spec/services/update_deployment_service_spec.rb
rename to spec/services/deployments/after_create_service_spec.rb
index 343dab8a9747b4b614f7b94fa54406c1c617a551..b34483ea85bf62a9721afcf2b62334136f582e8f 100644
--- a/spec/services/update_deployment_service_spec.rb
+++ b/spec/services/deployments/after_create_service_spec.rb
@@ -2,7 +2,7 @@
 
 require 'spec_helper'
 
-describe UpdateDeploymentService do
+describe Deployments::AfterCreateService do
   let(:user) { create(:user) }
   let(:project) { create(:project, :repository) }
   let(:options) { { name: 'production' } }
diff --git a/spec/services/deployments/create_service_spec.rb b/spec/services/deployments/create_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e41c8259ea9292f5dbefbc93b73a70bd192c3f0e
--- /dev/null
+++ b/spec/services/deployments/create_service_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Deployments::CreateService do
+  let(:environment) do
+    double(
+      :environment,
+      deployment_platform: double(:platform, cluster_id: 1),
+      project_id: 2,
+      id: 3
+    )
+  end
+
+  let(:user) { double(:user) }
+
+  describe '#execute' do
+    let(:service) { described_class.new(environment, user, {}) }
+
+    it 'does not run the AfterCreateService service if the deployment is not persisted' do
+      deploy = double(:deployment, persisted?: false)
+
+      expect(service)
+        .to receive(:create_deployment)
+        .and_return(deploy)
+
+      expect(Deployments::AfterCreateService)
+        .not_to receive(:new)
+
+      expect(service.execute).to eq(deploy)
+    end
+
+    it 'runs the AfterCreateService service if the deployment is persisted' do
+      deploy = double(:deployment, persisted?: true)
+      after_service = double(:after_create_service)
+
+      expect(service)
+        .to receive(:create_deployment)
+        .and_return(deploy)
+
+      expect(Deployments::AfterCreateService)
+        .to receive(:new)
+        .with(deploy)
+        .and_return(after_service)
+
+      expect(after_service)
+        .to receive(:execute)
+
+      expect(service.execute).to eq(deploy)
+    end
+  end
+
+  describe '#create_deployment' do
+    it 'creates a deployment' do
+      environment = build(:environment)
+      service = described_class.new(environment, user, {})
+
+      expect(environment.deployments)
+        .to receive(:create)
+        .with(an_instance_of(Hash))
+
+      service.create_deployment
+    end
+  end
+
+  describe '#deployment_attributes' do
+    it 'only includes attributes that we want to persist' do
+      service = described_class.new(
+        environment,
+        user,
+        ref: 'master',
+        tag: true,
+        sha: '123',
+        foo: 'bar',
+        on_stop: 'stop',
+        status: 'running'
+      )
+
+      expect(service.deployment_attributes).to eq(
+        cluster_id: 1,
+        project_id: 2,
+        environment_id: 3,
+        ref: 'master',
+        tag: true,
+        sha: '123',
+        user: user,
+        on_stop: 'stop',
+        status: 'running'
+      )
+    end
+  end
+end
diff --git a/spec/services/deployments/update_service_spec.rb b/spec/services/deployments/update_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a923099b82cbab3b8f5c22d8a5498f88d7b76d1f
--- /dev/null
+++ b/spec/services/deployments/update_service_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Deployments::UpdateService do
+  let(:deploy) { create(:deployment, :running) }
+  let(:service) { described_class.new(deploy, status: 'success') }
+
+  describe '#execute' do
+    it 'updates the status of a deployment' do
+      expect(service.execute).to eq(true)
+      expect(deploy.status).to eq('success')
+    end
+  end
+end
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index 9f2c3fec62c658a98793184fc0d96aa55addc3ba..eb738ac80b192f09e12e813d7e8529c01caacce3 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -113,40 +113,21 @@
     end
   end
 
-  describe '#push', :clean_gitlab_redis_shared_state do
-    let(:project) { create(:project) }
-    let(:user) { create(:user) }
-
-    let(:push_data) do
-      {
-        commits: [
-          {
-            id: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
-            message: 'This is a commit'
-          }
-        ],
-        before: '0000000000000000000000000000000000000000',
-        after: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
-        total_commits_count: 1,
-        ref: 'refs/heads/my-branch'
-      }
-    end
-
+  shared_examples_for 'service for creating a push event' do |service_class|
     it 'creates a new event' do
-      expect { service.push(project, user, push_data) }.to change { Event.count }
+      expect { subject }.to change { Event.count }
     end
 
     it 'creates the push event payload' do
-      expect(PushEventPayloadService).to receive(:new)
+      expect(service_class).to receive(:new)
         .with(an_instance_of(PushEvent), push_data)
         .and_call_original
 
-      service.push(project, user, push_data)
+      subject
     end
 
     it 'updates user last activity' do
-      expect { service.push(project, user, push_data) }
-        .to change { user.last_activity_on }.to(Date.today)
+      expect { subject }.to change { user.last_activity_on }.to(Date.today)
     end
 
     it 'caches the last push event for the user' do
@@ -154,7 +135,7 @@
         .to receive(:cache_last_push_event)
         .with(an_instance_of(PushEvent))
 
-      service.push(project, user, push_data)
+      subject
     end
 
     it 'does not create any event data when an error is raised' do
@@ -163,17 +144,56 @@
       allow(payload_service).to receive(:execute)
         .and_raise(RuntimeError)
 
-      allow(PushEventPayloadService).to receive(:new)
+      allow(service_class).to receive(:new)
         .and_return(payload_service)
 
-      expect { service.push(project, user, push_data) }
-        .to raise_error(RuntimeError)
-
+      expect { subject }.to raise_error(RuntimeError)
       expect(Event.count).to eq(0)
       expect(PushEventPayload.count).to eq(0)
     end
   end
 
+  describe '#push', :clean_gitlab_redis_shared_state do
+    let(:project) { create(:project) }
+    let(:user) { create(:user) }
+
+    let(:push_data) do
+      {
+        commits: [
+          {
+            id: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+            message: 'This is a commit'
+          }
+        ],
+        before: '0000000000000000000000000000000000000000',
+        after: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+        total_commits_count: 1,
+        ref: 'refs/heads/my-branch'
+      }
+    end
+
+    subject { service.push(project, user, push_data) }
+
+    it_behaves_like 'service for creating a push event', PushEventPayloadService
+  end
+
+  describe '#bulk_push', :clean_gitlab_redis_shared_state do
+    let(:project) { create(:project) }
+    let(:user) { create(:user) }
+
+    let(:push_data) do
+      {
+        action: :created,
+        ref_count: 4,
+        ref_type: :branch
+      }
+    end
+
+    subject { service.bulk_push(project, user, push_data) }
+
+    it_behaves_like 'service for creating a push event', BulkPushEventPayloadService
+  end
+
   describe 'Project' do
     let(:user) { create :user }
     let(:project) { create(:project) }
diff --git a/spec/services/git/base_hooks_service_spec.rb b/spec/services/git/base_hooks_service_spec.rb
index e71900e3c0dae32130684178f6dc60ba88e746a7..f3f6b36a18d7c263eb92cd8dc5622bb833f88a73 100644
--- a/spec/services/git/base_hooks_service_spec.rb
+++ b/spec/services/git/base_hooks_service_spec.rb
@@ -8,13 +8,12 @@
 
   let(:user) { create(:user) }
   let(:project) { create(:project, :repository) }
-
   let(:oldrev) { Gitlab::Git::BLANK_SHA }
   let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0
   let(:ref) { 'refs/tags/v1.1.0' }
 
-  describe '#execute_project_hooks' do
-    class TestService < described_class
+  let(:test_service) do
+    Class.new(described_class) do
       def hook_name
         :push_hooks
       end
@@ -23,12 +22,44 @@ def commits
         []
       end
     end
+  end
+
+  subject { test_service.new(project, user, params) }
 
-    let(:project) { create(:project, :repository) }
+  let(:params) do
+    {
+      change: {
+        oldrev: oldrev,
+        newrev: newrev,
+        ref: ref
+      }
+    }
+  end
+
+  describe 'push event' do
+    it 'creates push event' do
+      expect_next_instance_of(EventCreateService) do |service|
+        expect(service).to receive(:push)
+      end
 
-    subject { TestService.new(project, user, change: { oldrev: oldrev, newrev: newrev, ref: ref }) }
+      subject.execute
+    end
 
-    context '#execute_hooks' do
+    context 'create_push_event is set to false' do
+      before do
+        params[:create_push_event] = false
+      end
+
+      it 'does not create push event' do
+        expect(EventCreateService).not_to receive(:new)
+
+        subject.execute
+      end
+    end
+  end
+
+  describe 'project hooks and services' do
+    context 'hooks' do
       before do
         expect(project).to receive(:has_active_hooks?).and_return(active)
       end
@@ -56,7 +87,7 @@ def commits
       end
     end
 
-    context '#execute_services' do
+    context 'services' do
       before do
         expect(project).to receive(:has_active_services?).and_return(active)
       end
@@ -83,5 +114,21 @@ def commits
         end
       end
     end
+
+    context 'execute_project_hooks param set to false' do
+      before do
+        params[:execute_project_hooks] = false
+
+        allow(project).to receive(:has_active_hooks?).and_return(true)
+        allow(project).to receive(:has_active_services?).and_return(true)
+      end
+
+      it 'does not execute hooks and services' do
+        expect(project).not_to receive(:execute_hooks)
+        expect(project).not_to receive(:execute_services)
+
+        subject.execute
+      end
+    end
   end
 end
diff --git a/spec/services/git/process_ref_changes_service_spec.rb b/spec/services/git/process_ref_changes_service_spec.rb
index 4d394a29867aecef426323e3c7443d0169c21094..35ddf95b5f6db70f17bbbf3fc446ba4c1ccdab4c 100644
--- a/spec/services/git/process_ref_changes_service_spec.rb
+++ b/spec/services/git/process_ref_changes_service_spec.rb
@@ -13,6 +13,12 @@
     let(:service) { double(execute: true) }
     let(:git_changes) { double(branch_changes: [], tag_changes: []) }
 
+    def multiple_changes(change, count)
+      Array.new(count).map.with_index do |n, index|
+        { index: index, oldrev: change[:oldrev], newrev: change[:newrev], ref: "#{change[:ref]}#{n}" }
+      end
+    end
+
     let(:changes) do
       [
         { index: 0, oldrev: Gitlab::Git::BLANK_SHA, newrev: '789012', ref: "#{ref_prefix}/create" },
@@ -28,12 +34,94 @@
     it "calls #{push_service_class}" do
       expect(push_service_class)
         .to receive(:new)
+        .with(project, project.owner, hash_including(execute_project_hooks: true, create_push_event: true))
         .exactly(changes.count).times
         .and_return(service)
 
       subject.execute
     end
 
+    context 'changes exceed push_event_hooks_limit' do
+      let(:push_event_hooks_limit) { 3 }
+
+      let(:changes) do
+        multiple_changes(
+          { oldrev: '123456', newrev: '789012', ref: "#{ref_prefix}/test" },
+          push_event_hooks_limit + 1
+        )
+      end
+
+      before do
+        stub_application_setting(push_event_hooks_limit: push_event_hooks_limit)
+      end
+
+      context 'git_push_execute_all_project_hooks is disabled' do
+        before do
+          stub_feature_flags(git_push_execute_all_project_hooks: false)
+        end
+
+        it "calls #{push_service_class} with execute_project_hooks set to false" do
+          expect(push_service_class)
+            .to receive(:new)
+            .with(project, project.owner, hash_including(execute_project_hooks: false))
+            .exactly(changes.count).times
+            .and_return(service)
+
+          subject.execute
+        end
+      end
+
+      context 'git_push_execute_all_project_hooks is enabled' do
+        before do
+          stub_feature_flags(git_push_execute_all_project_hooks: true)
+        end
+
+        it "calls #{push_service_class} with execute_project_hooks set to true" do
+          expect(push_service_class)
+            .to receive(:new)
+            .with(project, project.owner, hash_including(execute_project_hooks: true))
+            .exactly(changes.count).times
+            .and_return(service)
+
+          subject.execute
+        end
+      end
+    end
+
+    context 'changes exceed push_event_activities_limit per action' do
+      let(:push_event_activities_limit) { 3 }
+
+      let(:changes) do
+        [
+          { oldrev: Gitlab::Git::BLANK_SHA, newrev: '789012', ref: "#{ref_prefix}/create" },
+          { oldrev: '123456', newrev: '789012', ref: "#{ref_prefix}/update" },
+          { oldrev: '123456', newrev: Gitlab::Git::BLANK_SHA, ref: "#{ref_prefix}/delete" }
+        ].map do |change|
+          multiple_changes(change, push_event_activities_limit + 1)
+        end.flatten
+      end
+
+      before do
+        stub_application_setting(push_event_activities_limit: push_event_activities_limit)
+      end
+
+      it "calls #{push_service_class} with create_push_event set to false" do
+        expect(push_service_class)
+          .to receive(:new)
+          .with(project, project.owner, hash_including(create_push_event: false))
+          .exactly(changes.count).times
+          .and_return(service)
+
+        subject.execute
+      end
+
+      it 'creates events per action' do
+        allow(push_service_class).to receive(:new).and_return(service)
+
+        expect { subject.execute }.to change { Event.count }.by(3)
+      end
+    end
+
     context 'pipeline creation' do
       context 'with valid .gitlab-ci.yml' do
         before do
diff --git a/spec/services/git/tag_hooks_service_spec.rb b/spec/services/git/tag_hooks_service_spec.rb
index c97d4d38b1c860eb150df197dc4ce77b8d0157ae..abb5b9b130b880b665bb49489980bd485a384fb3 100644
--- a/spec/services/git/tag_hooks_service_spec.rb
+++ b/spec/services/git/tag_hooks_service_spec.rb
@@ -58,6 +58,7 @@
   describe 'Push data' do
     shared_examples_for 'tag push data expectations' do
       subject(:push_data) { service.send(:push_data) }
+
       it 'has expected push data attributes' do
         is_expected.to match a_hash_including(
           object_kind: 'tag_push',
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
index 0cbb3122bb042b5a60d122dd690b1dad0f0d2d1e..5ef1fb1932fdb9ca7f8567cf84ab31047c5ce22f 100644
--- a/spec/services/groups/transfer_service_spec.rb
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -426,5 +426,22 @@
         end
       end
     end
+
+    context 'when a project in group has container images' do
+      let(:group) { create(:group, :public, :nested) }
+      let!(:project) { create(:project, :repository, :public, namespace: group) }
+
+      before do
+        stub_container_registry_tags(repository: /image/, tags: %w[rc1])
+        create(:container_repository, project: project, name: :image)
+        create(:group_member, :owner, group: new_parent_group, user: user)
+      end
+
+      it 'does not allow group to be transferred' do
+        transfer_service.execute(new_parent_group)
+
+        expect(transfer_service.error).to match(/Docker images in their Container Registry/)
+      end
+    end
   end
 end
diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb
index 12e9c2b2f3a855a252b89f459193705fb86db713..ca8eaf4c9702704cffde1217101a1f2d9ac25bc5 100644
--- a/spec/services/groups/update_service_spec.rb
+++ b/spec/services/groups/update_service_spec.rb
@@ -148,6 +148,30 @@
     end
   end
 
+  context 'projects in group have container images' do
+    let(:service) { described_class.new(public_group, user, path: SecureRandom.hex) }
+    let(:project) { create(:project, :internal, group: public_group) }
+
+    before do
+      stub_container_registry_tags(repository: /image/, tags: %w[rc1])
+      create(:container_repository, project: project, name: :image)
+    end
+
+    it 'does not allow path to be changed' do
+      result = described_class.new(public_group, user, path: 'new-path').execute
+
+      expect(result).to eq false
+      expect(public_group.errors[:base].first).to match(/Docker images in their Container Registry/)
+    end
+
+    it 'allows other settings to be changed' do
+      result = described_class.new(public_group, user, name: 'new-name').execute
+
+      expect(result).to eq true
+      expect(public_group.reload.name).to eq('new-name')
+    end
+  end
+
   context 'for a subgroup' do
     let(:subgroup) { create(:group, :private, parent: private_group) }
 
diff --git a/spec/services/issues/zoom_link_service_spec.rb b/spec/services/issues/zoom_link_service_spec.rb
index f0762ba971ab7c82a23f6bcc53f9c2f3ddd43b4d..a0a5818b9c308cd26b0d6f6c81afa0a2d4b87761 100644
--- a/spec/services/issues/zoom_link_service_spec.rb
+++ b/spec/services/issues/zoom_link_service_spec.rb
@@ -40,6 +40,12 @@
         expect(result.payload[:zoom_meetings].map(&:url))
           .to include(zoom_link)
       end
+
+      it 'tracks the add event' do
+        expect(Gitlab::Tracking).to receive(:event)
+          .with('IncidentManagement::ZoomIntegration', 'add_zoom_meeting', label: 'Issue ID', value: issue.id)
+        result
+      end
     end
 
     shared_examples 'cannot add meeting' do
@@ -118,6 +124,13 @@
         include_examples 'can remove meeting'
       end
 
+      it 'tracks the remove event' do
+        expect(Gitlab::Tracking).to receive(:event)
+          .with('IncidentManagement::ZoomIntegration', 'remove_zoom_meeting', label: 'Issue ID', value: issue.id)
+
+        result
+      end
+
       context 'with insufficient permissions' do
         include_context 'insufficient permissions'
         include_examples 'cannot remove meeting'
diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb
index f56c31e51f6e7ec533dd2881ece8241de68e4a6d..5bbceac3dd0c074845d62b5f0bcf52d27bfa00f1 100644
--- a/spec/services/members/approve_access_request_service_spec.rb
+++ b/spec/services/members/approve_access_request_service_spec.rb
@@ -3,8 +3,8 @@
 require 'spec_helper'
 
 describe Members::ApproveAccessRequestService do
-  let(:project) { create(:project, :public, :access_requestable) }
-  let(:group) { create(:group, :public, :access_requestable) }
+  let(:project) { create(:project, :public) }
+  let(:group) { create(:group, :public) }
   let(:current_user) { create(:user) }
   let(:access_requester_user) { create(:user) }
   let(:access_requester) { source.requesters.find_by!(user_id: access_requester_user.id) }
diff --git a/spec/services/members/request_access_service_spec.rb b/spec/services/members/request_access_service_spec.rb
index 2e5275eb3f26e4e208d55e5db47fa7bfc8523ba0..a0f7ae91bdb98908d6a9ecfbd2edacd54011db94 100644
--- a/spec/services/members/request_access_service_spec.rb
+++ b/spec/services/members/request_access_service_spec.rb
@@ -41,7 +41,7 @@
   context 'when access requests are disabled' do
     %i[project group].each do |source_type|
       it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
-        let(:source) { create(source_type, :public) }
+        let(:source) { create(source_type, :public, :request_access_disabled) }
       end
     end
   end
@@ -49,7 +49,7 @@
   context 'when current user can request access to the project' do
     %i[project group].each do |source_type|
       it_behaves_like 'a service creating a access request' do
-        let(:source) { create(source_type, :public, :access_requestable) }
+        let(:source) { create(source_type, :public) }
       end
     end
   end
diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb
index 07e0218e1dfd5f6bbb8750d5570a3fcf1b28a317..51a5c51f6c3fa1981f64b6b1754dc16d21034908 100644
--- a/spec/services/merge_requests/create_from_issue_service_spec.rb
+++ b/spec/services/merge_requests/create_from_issue_service_spec.rb
@@ -13,6 +13,7 @@
   let(:custom_source_branch) { 'custom-source-branch' }
 
   subject(:service) { described_class.new(project, user, service_params) }
+
   subject(:service_with_custom_source_branch) { described_class.new(project, user, branch_name: custom_source_branch, **service_params) }
 
   before do
diff --git a/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb b/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f200c636aacf67c4443c854686da6e48fb2b77d1
--- /dev/null
+++ b/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb
@@ -0,0 +1,177 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Metrics::Dashboard::GrafanaMetricEmbedService do
+  include MetricsDashboardHelpers
+  include ReactiveCachingHelpers
+  include GrafanaApiHelpers
+
+  let_it_be(:project) { build(:project) }
+  let_it_be(:user) { create(:user) }
+  let_it_be(:grafana_integration) { create(:grafana_integration, project: project) }
+
+  let(:grafana_url) do
+    valid_grafana_dashboard_link(grafana_integration.grafana_url)
+  end
+
+  before do
+    project.add_maintainer(user)
+  end
+
+  describe '.valid_params?' do
+    let(:valid_params) { { embedded: true, grafana_url: grafana_url } }
+
+    subject { described_class.valid_params?(params) }
+
+    let(:params) { valid_params }
+
+    it { is_expected.to be_truthy }
+
+    context 'not embedded' do
+      let(:params) { valid_params.except(:embedded) }
+
+      it { is_expected.to be_falsey }
+    end
+
+    context 'undefined grafana_url' do
+      let(:params) { valid_params.except(:grafana_url) }
+
+      it { is_expected.to be_falsey }
+    end
+  end
+
+  describe '.from_cache' do
+    let(:params) { [project.id, user.id, grafana_url] }
+
+    subject { described_class.from_cache(*params) }
+
+    it 'initializes an instance of GrafanaMetricEmbedService' do
+      expect(subject).to be_an_instance_of(described_class)
+      expect(subject.project).to eq(project)
+      expect(subject.current_user).to eq(user)
+      expect(subject.params[:grafana_url]).to eq(grafana_url)
+    end
+  end
+
+  describe '#get_dashboard', :use_clean_rails_memory_store_caching do
+    let(:service_params) do
+      [
+        project,
+        user,
+        {
+          embedded: true,
+          grafana_url: grafana_url
+        }
+      ]
+    end
+
+    let(:service) { described_class.new(*service_params) }
+    let(:service_call) { service.get_dashboard }
+
+    context 'without caching' do
+      before do
+        synchronous_reactive_cache(service)
+      end
+
+      it_behaves_like 'raises error for users with insufficient permissions'
+
+      context 'without a grafana integration' do
+        before do
+          allow(project).to receive(:grafana_integration).and_return(nil)
+        end
+
+        it_behaves_like 'misconfigured dashboard service response', :bad_request
+      end
+
+      context 'when grafana cannot be reached' do
+        before do
+          allow(grafana_integration.client).to receive(:get_dashboard).and_raise(::Grafana::Client::Error)
+        end
+
+        it_behaves_like 'misconfigured dashboard service response', :service_unavailable
+      end
+
+      context 'when panelId is missing' do
+        let(:grafana_url) do
+          grafana_integration.grafana_url +
+            '/d/XDaNK6amz/gitlab-omnibus-redis' \
+            '?from=1570397739557&to=1570484139557'
+        end
+
+        before do
+          stub_dashboard_request(grafana_integration.grafana_url)
+        end
+
+        it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+      end
+
+      context 'when uid is missing' do
+        let(:grafana_url) { grafana_integration.grafana_url + '/d/' }
+
+        before do
+          stub_dashboard_request(grafana_integration.grafana_url)
+        end
+
+        it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+      end
+
+      context 'when the dashboard response contains misconfigured json' do
+        before do
+          stub_dashboard_request(grafana_integration.grafana_url, body: '')
+        end
+
+        it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+      end
+
+      context 'when the datasource response contains misconfigured json' do
+        before do
+          stub_dashboard_request(grafana_integration.grafana_url)
+          stub_datasource_request(grafana_integration.grafana_url, body: '')
+        end
+
+        it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+      end
+
+      context 'when the embed was created successfully' do
+        before do
+          stub_dashboard_request(grafana_integration.grafana_url)
+          stub_datasource_request(grafana_integration.grafana_url)
+        end
+
+        it_behaves_like 'valid embedded dashboard service response'
+      end
+    end
+
+    context 'with caching', :use_clean_rails_memory_store_caching do
+      let(:cache_params) { [project.id, user.id, grafana_url] }
+
+      context 'when value not present in cache' do
+        it 'returns nil' do
+          expect(ReactiveCachingWorker)
+            .to receive(:perform_async)
+            .with(service.class, service.id, *cache_params)
+
+          expect(service_call).to eq(nil)
+        end
+      end
+
+      context 'when value present in cache' do
+        let(:return_value) { { 'http_status' => :ok, 'dashboard' => '{}' } }
+
+        before do
+          stub_reactive_cache(service, return_value, cache_params)
+        end
+
+        it 'returns cached value' do
+          expect(ReactiveCachingWorker)
+            .not_to receive(:perform_async)
+            .with(service.class, service.id, *cache_params)
+
+          expect(service_call[:http_status]).to eq(return_value[:http_status])
+          expect(service_call[:dashboard]).to eq(return_value[:dashboard])
+        end
+      end
+    end
+  end
+end
diff --git a/spec/services/note_summary_spec.rb b/spec/services/note_summary_spec.rb
index e59731207a50eba81cbf0cb03f02699e1a37fe86..aa4e41f4d8c634b574faf89414627eadf75a7fde 100644
--- a/spec/services/note_summary_spec.rb
+++ b/spec/services/note_summary_spec.rb
@@ -46,5 +46,17 @@ def create_note_summary
     it 'returns metadata hash' do
       expect(create_note_summary.metadata).to eq(action: 'icon', commit_count: 5)
     end
+
+    context 'description action and noteable has saved_description_version' do
+      before do
+        noteable.saved_description_version = 1
+      end
+
+      subject { described_class.new(noteable, project, user, 'note', action: 'description') }
+
+      it 'sets the description_version metadata' do
+        expect(subject.metadata).to include(description_version: 1)
+      end
+    end
   end
 end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index bd6734634cb167a9a957d652ebf9a58cc6019ca0..aa67b87a6458585ee442f5560a4f53554ad4c8f0 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -678,6 +678,27 @@
     end
   end
 
+  describe '#send_new_release_notifications' do
+    context 'when recipients for a new release exist' do
+      let(:release) { create(:release) }
+
+      it 'calls new_release_email for each relevant recipient' do
+        user_1 = create(:user)
+        user_2 = create(:user)
+        user_3 = create(:user)
+        recipient_1 = NotificationRecipient.new(user_1, :custom, custom_action: :new_release)
+        recipient_2 = NotificationRecipient.new(user_2, :custom, custom_action: :new_release)
+        allow(NotificationRecipientService).to receive(:build_new_release_recipients).and_return([recipient_1, recipient_2])
+
+        release
+
+        should_email(user_1)
+        should_email(user_2)
+        should_not_email(user_3)
+      end
+    end
+  end
+
   describe 'Participating project notification settings have priority over group and global settings if available' do
     let!(:group) { create(:group) }
     let!(:maintainer) { group.add_owner(create(:user, username: 'maintainer')).user }
@@ -1942,7 +1963,7 @@
         let(:developer) { create(:user) }
 
         let!(:group) do
-          create(:group, :public, :access_requestable) do |group|
+          create(:group, :public) do |group|
             group.add_owner(owner)
             group.add_maintainer(maintainer)
             group.add_developer(developer)
@@ -1968,7 +1989,7 @@
       end
 
       it_behaves_like 'sends notification only to a maximum of ten, most recently active group owners' do
-        let(:group) { create(:group, :public, :access_requestable) }
+        let(:group) { create(:group, :public) }
         let(:notification_trigger) { group.request_access(added_user) }
       end
     end
@@ -2029,7 +2050,7 @@
           let(:maintainer) { create(:user) }
 
           let!(:project) do
-            create(:project, :public, :access_requestable) do |project|
+            create(:project, :public) do |project|
               project.add_developer(developer)
               project.add_maintainer(maintainer)
             end
@@ -2053,7 +2074,7 @@
         end
 
         it_behaves_like 'sends notification only to a maximum of ten, most recently active project maintainers' do
-          let(:project) { create(:project, :public, :access_requestable) }
+          let(:project) { create(:project, :public) }
           let(:notification_trigger) { project.request_access(added_user) }
         end
       end
@@ -2064,7 +2085,7 @@
 
         context 'when the project has no maintainers' do
           context 'when the group has at least one owner' do
-            let!(:project) { create(:project, :public, :access_requestable, namespace: group) }
+            let!(:project) { create(:project, :public, namespace: group) }
 
             before do
               reset_delivered_emails!
@@ -2079,14 +2100,14 @@
             end
 
             it_behaves_like 'sends notification only to a maximum of ten, most recently active group owners' do
-              let(:group) { create(:group, :public, :access_requestable) }
+              let(:group) { create(:group, :public) }
               let(:notification_trigger) { project.request_access(added_user) }
             end
           end
 
           context 'when the group does not have any owners' do
             let(:group) { create(:group) }
-            let!(:project) { create(:project, :public, :access_requestable, namespace: group) }
+            let!(:project) { create(:project, :public, namespace: group) }
 
             context 'recipients' do
               before do
@@ -2107,7 +2128,7 @@
           let(:developer) { create(:user) }
 
           let!(:project) do
-            create(:project, :public, :access_requestable, namespace: group) do |project|
+            create(:project, :public, namespace: group) do |project|
               project.add_maintainer(maintainer)
               project.add_developer(developer)
             end
@@ -2128,7 +2149,7 @@
           end
 
           it_behaves_like 'sends notification only to a maximum of ten, most recently active project maintainers' do
-            let(:project) { create(:project, :public, :access_requestable, namespace: group) }
+            let(:project) { create(:project, :public, namespace: group) }
             let(:notification_trigger) { project.request_access(added_user) }
           end
         end
diff --git a/spec/services/projects/container_repository/cleanup_tags_service_spec.rb b/spec/services/projects/container_repository/cleanup_tags_service_spec.rb
index 14247f1c71e4b5f4efc650f2227b8903342952b8..14772d172e81c3574f9d3b1311a2bdb3d2e01ea4 100644
--- a/spec/services/projects/container_repository/cleanup_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/cleanup_tags_service_spec.rb
@@ -157,6 +157,6 @@ def stub_digest_config(digest, created_at)
   def expect_delete(digest)
     expect_any_instance_of(ContainerRegistry::Client)
       .to receive(:delete_repository_tag)
-      .with(repository.path, digest)
+      .with(repository.path, digest) { true }
   end
 end
diff --git a/spec/services/projects/container_repository/delete_tags_service_spec.rb b/spec/services/projects/container_repository/delete_tags_service_spec.rb
index 2ec5850c69e47e7308d0eecb669f508886d254a6..f296ef3a7769e7836586c7bb4d56d0f36f391d0b 100644
--- a/spec/services/projects/container_repository/delete_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/delete_tags_service_spec.rb
@@ -87,6 +87,21 @@
 
           is_expected.to include(status: :success)
         end
+
+        it 'succedes when tag delete returns 404' do
+          stub_upload("{\n  \"config\": {\n  }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3')
+
+          stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/A")
+            .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' })
+
+          stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/Ba")
+            .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' })
+
+          stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/manifests/sha256:dummy")
+            .to_return(status: 404, body: "", headers: {})
+
+          is_expected.to include(status: :success)
+        end
       end
     end
   end
diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb
index f651db70cbd746c311a53f48a5923af90ccdab07..c99054d9fd5b08dbda690b2d4e7ddf561195f773 100644
--- a/spec/services/projects/housekeeping_service_spec.rb
+++ b/spec/services/projects/housekeeping_service_spec.rb
@@ -4,6 +4,7 @@
 
 describe Projects::HousekeepingService do
   subject { described_class.new(project) }
+
   set(:project) { create(:project, :repository) }
 
   before do
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index ec68e1a8cf97e039373d55bdfd1c8657742f1d20..788f83cc2334a75f65d2979f03f189655d5b7281 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -1545,12 +1545,20 @@
     end
 
     it 'limits to commands passed ' do
-      content = "/shrug\n/close"
+      content = "/shrug test\n/close"
 
       text, commands = service.execute(content, issue, only: [:shrug])
 
       expect(commands).to be_empty
-      expect(text).to eq("#{described_class::SHRUG}\n/close")
+      expect(text).to eq("test #{described_class::SHRUG}\n/close")
+    end
+
+    it 'preserves leading whitespace ' do
+      content = " - list\n\n/close\n\ntest\n\n"
+
+      text, _ = service.execute(content, issue)
+
+      expect(text).to eq(" - list\n\ntest")
     end
 
     context '/create_merge_request command' do
diff --git a/spec/services/system_notes/issuables_service_spec.rb b/spec/services/system_notes/issuables_service_spec.rb
index a914b34cb23539e7cd11f3fe394d4b02c5c6def0..5023abad4cd425c65291a9d24ea22b4c842d62c6 100644
--- a/spec/services/system_notes/issuables_service_spec.rb
+++ b/spec/services/system_notes/issuables_service_spec.rb
@@ -212,6 +212,15 @@ def build_note(old_assignees, new_assignees)
       it 'sets the note text' do
         expect(subject.note).to eq('changed the description')
       end
+
+      it 'associates the related description version' do
+        noteable.update!(description: 'New description')
+
+        description_version_id = subject.system_note_metadata.description_version_id
+
+        expect(description_version_id).not_to be_nil
+        expect(description_version_id).to eq(noteable.saved_description_version.id)
+      end
     end
   end
 
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 948e5e6250b20596618331d9319528520f6d33a0..7a5e570558ea414327ea751978627d1e07068d42 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -88,6 +88,7 @@
   config.include FixtureHelpers
   config.include GitlabRoutingHelper
   config.include StubFeatureFlags
+  config.include StubExperiments
   config.include StubGitlabCalls
   config.include StubGitlabData
   config.include NextInstanceOf
@@ -154,6 +155,17 @@
       .with(:force_autodevops_on_by_default, anything)
       .and_return(false)
 
+    # The following can be removed once Vue Issuable Sidebar
+    # is feature-complete and can be made default in place
+    # of older sidebar.
+    # See https://gitlab.com/groups/gitlab-org/-/epics/1863
+    allow(Feature).to receive(:enabled?)
+      .with(:vue_issuable_sidebar, anything)
+      .and_return(false)
+    allow(Feature).to receive(:enabled?)
+      .with(:vue_issuable_epic_sidebar, anything)
+      .and_return(false)
+
     # Stub these calls due to being expensive operations
     # It can be reenabled for specific tests via:
     #
@@ -367,3 +379,6 @@
 
 # Prevent Rugged from picking up local developer gitconfig.
 Rugged::Settings['search_path_global'] = Rails.root.join('tmp/tests').to_s
+
+# Disable timestamp checks for invisible_captcha
+InvisibleCaptcha.timestamp_enabled = false
diff --git a/spec/support/api/boards_shared_examples.rb b/spec/support/api/boards_shared_examples.rb
index b7aff32460d5bf3135102849d16b2950736f9a7b..d41490f33e473da4ca626d08c51de655cbd4ee26 100644
--- a/spec/support/api/boards_shared_examples.rb
+++ b/spec/support/api/boards_shared_examples.rb
@@ -171,7 +171,7 @@ def expect_schema_match_for(response, schema_file, ee)
         if board_parent.try(:namespace)
           board_parent.update(namespace: owner.namespace)
         else
-          board.parent.add_owner(owner)
+          board.resource_parent.add_owner(owner)
         end
       end
 
diff --git a/spec/support/api/milestones_shared_examples.rb b/spec/support/api/milestones_shared_examples.rb
index d6439f77408ebce5eda63752a472685d1edd207c..ce8c2140e99f5490448426b5d4fc87460aacc0ab 100644
--- a/spec/support/api/milestones_shared_examples.rb
+++ b/spec/support/api/milestones_shared_examples.rb
@@ -205,7 +205,7 @@
   describe "DELETE #{route_definition}/:milestone_id" do
     it "rejects a member with reporter access from deleting a milestone" do
       reporter = create(:user)
-      milestone.parent.add_reporter(reporter)
+      milestone.resource_parent.add_reporter(reporter)
 
       delete api(resource_route, reporter)
 
diff --git a/spec/support/features/rss_shared_examples.rb b/spec/support/features/rss_shared_examples.rb
index c97eeba87dbc8b6e1363720451110c0e1e2c61d3..bbe793a81bcec080c9614748027037140a212585 100644
--- a/spec/support/features/rss_shared_examples.rb
+++ b/spec/support/features/rss_shared_examples.rb
@@ -8,7 +8,9 @@
 
 shared_examples "it has an RSS button with current_user's feed token" do
   it "shows the RSS button with current_user's feed token" do
-    expect(page).to have_css("a:has(.fa-rss)[href*='feed_token=#{user.feed_token}']")
+    expect(page)
+      .to have_css("a:has(.fa-rss)[href*='feed_token=#{user.feed_token}']")
+      .or have_css("a.js-rss-button[href*='feed_token=#{user.feed_token}']")
   end
 end
 
@@ -20,6 +22,8 @@
 
 shared_examples "it has an RSS button without a feed token" do
   it "shows the RSS button without a feed token" do
-    expect(page).to have_css("a:has(.fa-rss):not([href*='feed_token'])")
+    expect(page)
+      .to have_css("a:has(.fa-rss):not([href*='feed_token'])")
+      .or have_css("a.js-rss-button:not([href*='feed_token'])")
   end
 end
diff --git a/spec/support/helpers/grafana_api_helpers.rb b/spec/support/helpers/grafana_api_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b212cbf2943b4f772f9d16b1ded3e52f03a68c81
--- /dev/null
+++ b/spec/support/helpers/grafana_api_helpers.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module GrafanaApiHelpers
+  def valid_grafana_dashboard_link(base_url)
+    base_url +
+      '/d/XDaNK6amz/gitlab-omnibus-redis' \
+      '?from=1570397739557&to=1570484139557' \
+      '&var-instance=localhost:9121&panelId=8'
+  end
+
+  def stub_dashboard_request(base_url, path: '/api/dashboards/uid/XDaNK6amz', body: nil)
+    body ||= fixture_file('grafana/dashboard_response.json')
+
+    stub_request(:get, "#{base_url}#{path}")
+      .to_return(
+        status: 200,
+        body: body,
+        headers: { 'Content-Type' => 'application/json' }
+      )
+  end
+
+  def stub_datasource_request(base_url, path: '/api/datasources/name/GitLab%20Omnibus', body: nil)
+    body ||= fixture_file('grafana/datasource_response.json')
+
+    stub_request(:get, "#{base_url}#{path}")
+      .to_return(
+        status: 200,
+        body: body,
+        headers: { 'Content-Type' => 'application/json' }
+      )
+  end
+end
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index da743e586f5a98eb0ab9487dd057dead685d934e..e74dbca4f935e323779d2a3c845bd40accf3dbf3 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -319,10 +319,10 @@ def kube_knative_pods_body(name, namespace)
     }
   end
 
-  def kube_knative_services_body(**options)
+  def kube_knative_services_body(legacy_knative: false, **options)
     {
       "kind" => "List",
-      "items" => [kube_service(options)]
+      "items" => [legacy_knative ? knative_05_service(options) : kube_service(options)]
     }
   end
 
@@ -421,6 +421,27 @@ def kube_service(name: "kubetest", namespace: "default", domain: "example.com")
     }
   end
 
+  def knative_05_service(name: "kubetest", namespace: "default", domain: "example.com")
+    {
+      "metadata" => {
+        "creationTimestamp" => "2018-11-21T06:16:33Z",
+        "name" => name,
+        "namespace" => namespace,
+        "selfLink" => "/apis/serving.knative.dev/v1alpha1/namespaces/#{namespace}/services/#{name}"
+      },
+      "spec" => {
+        "generation" => 2
+      },
+      "status" => {
+        "domain" => "#{name}.#{namespace}.#{domain}",
+        "domainInternal" => "#{name}.#{namespace}.svc.cluster.local",
+        "latestCreatedRevisionName" => "#{name}-00002",
+        "latestReadyRevisionName" => "#{name}-00002",
+        "observedGeneration" => 2
+      }
+    }
+  end
+
   def kube_service_full(name: "kubetest", namespace: "kube-ns", domain: "example.com")
     {
       "metadata" => {
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index 7d5896e4eebfd77636e1eafe2f60098cfd2d6b11..1d42f26ad3eb35a18efd5696a88ec4cd37489b62 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -53,7 +53,7 @@ def gitlab_enable_admin_mode_sign_in(user)
 
     fill_in 'password', with: user.password
 
-    click_button 'Enter admin mode'
+    click_button 'Enter Admin Mode'
   end
 
   def gitlab_sign_in_via(provider, user, uid, saml_response = nil)
diff --git a/spec/support/helpers/stub_experiments.rb b/spec/support/helpers/stub_experiments.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ed868e22c6edde4fb5157f8340b0557e2995343d
--- /dev/null
+++ b/spec/support/helpers/stub_experiments.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module StubExperiments
+  # Stub Experiment with `key: true/false`
+  #
+  # @param [Hash] experiment where key is feature name and value is boolean whether enabled or not.
+  #
+  # Examples
+  # - `stub_experiment(signup_flow: false)` ... Disable `signup_flow` experiment globally.
+  def stub_experiment(experiments)
+    experiments.each do |experiment_key, enabled|
+      allow(Gitlab::Experimentation).to receive(:enabled?).with(experiment_key, any_args) { enabled }
+    end
+  end
+end
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 323c8d1baf28e523f15fe00d25f6f42513cb4cf9..a409dd2ef26ff56d2b63aa7b32033f09120984e2 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -100,7 +100,6 @@ def init(opts = {})
 
     clean_test_path
 
-    # Set up GitLab shell for test instance
     setup_gitlab_shell
 
     setup_gitaly
@@ -145,10 +144,7 @@ def clean_test_path
   end
 
   def setup_gitlab_shell
-    component_timed_setup('GitLab Shell',
-      install_dir: Gitlab.config.gitlab_shell.path,
-      version: Gitlab::Shell.version_required,
-      task: 'gitlab:shell:install')
+    FileUtils.mkdir_p(Gitlab.config.gitlab_shell.path)
   end
 
   def setup_gitaly
diff --git a/spec/support/helpers/workhorse_helpers.rb b/spec/support/helpers/workhorse_helpers.rb
index 40007a14b8552277e5acef0804fec11b1f6fb797..e0fba191deb68873ea6709e0855729a450448b86 100644
--- a/spec/support/helpers/workhorse_helpers.rb
+++ b/spec/support/helpers/workhorse_helpers.rb
@@ -22,16 +22,40 @@ def workhorse_internal_api_request_header
 
   # workhorse_post_with_file will transform file_key inside params as if it was disk accelerated by workhorse
   def workhorse_post_with_file(url, file_key:, params:)
+    workhorse_request_with_file(:post, url,
+                                file_key: file_key,
+                                params: params,
+                                env: { 'CONTENT_TYPE' => 'multipart/form-data' },
+                                send_rewritten_field: true
+    )
+  end
+
+  # workhorse_finalize will transform file_key inside params as if it was the finalize call of an inline object storage upload.
+  # note that based on the content of the params it can simulate a disc acceleration or an object storage upload
+  def workhorse_finalize(url, method: :post, file_key:, params:, headers: {})
+    workhorse_request_with_file(method, url,
+                                file_key: file_key,
+                                params: params,
+                                extra_headers: headers,
+                                send_rewritten_field: false
+    )
+  end
+
+  def workhorse_request_with_file(method, url, file_key:, params:, env: {}, extra_headers: {}, send_rewritten_field:)
     workhorse_params = params.dup
     file = workhorse_params.delete(file_key)
 
-    workhorse_params.merge!(workhorse_disk_accelerated_file_params(file_key, file))
+    workhorse_params = workhorse_disk_accelerated_file_params(file_key, file).merge(workhorse_params)
+
+    headers = if send_rewritten_field
+                workhorse_rewritten_fields_header(file_key => file.path)
+              else
+                {}
+              end
+
+    headers.merge!(extra_headers)
 
-    post(url,
-         params: workhorse_params,
-         headers: workhorse_rewritten_fields_header(file_key => file.path),
-         env: { 'CONTENT_TYPE' => 'multipart/form-data' }
-        )
+    process(method, url, params: workhorse_params, headers: headers, env: env)
   end
 
   private
@@ -45,9 +69,24 @@ def workhorse_rewritten_fields_header(fields)
   end
 
   def workhorse_disk_accelerated_file_params(key, file)
+    return {} unless file
+
     {
       "#{key}.name" => file.original_filename,
-      "#{key}.path" => file.path
-    }
+      "#{key}.size" => file.size
+    }.tap do |params|
+      params["#{key}.path"] = file.path if file.path
+      params["#{key}.remote_id"] = file.remote_id if file.respond_to?(:remote_id) && file.remote_id
+    end
+  end
+
+  def fog_to_uploaded_file(file)
+    filename = File.basename(file.key)
+
+    UploadedFile.new(nil,
+                     filename: filename,
+                     remote_id: filename,
+                     size: file.content_length
+                    )
   end
 end
diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb
index 4d48b4b5389b7f4a7f3cf61ad512ea9777cd5e00..d735c10f69826d665db73caa99bae51deba147c4 100644
--- a/spec/support/matchers/graphql_matchers.rb
+++ b/spec/support/matchers/graphql_matchers.rb
@@ -28,9 +28,15 @@ def expected_field_names
   end
 end
 
-RSpec::Matchers.define :have_graphql_field do |field_name|
+RSpec::Matchers.define :have_graphql_field do |field_name, args = {}|
   match do |kls|
-    expect(kls.fields.keys).to include(GraphqlHelpers.fieldnamerize(field_name))
+    field = kls.fields[GraphqlHelpers.fieldnamerize(field_name)]
+
+    expect(field).to be_present
+
+    args.each do |argument, value|
+      expect(field.send(argument)).to eq(value)
+    end
   end
 end
 
diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb
index 7e47cdae8664c25fe402f77852f1d7c572b5f35e..97a23f02b3e4f68722590631551808d0767899b5 100644
--- a/spec/support/redis/redis_shared_examples.rb
+++ b/spec/support/redis/redis_shared_examples.rb
@@ -90,6 +90,7 @@
 
   describe '._raw_config' do
     subject { described_class._raw_config }
+
     let(:config_file_name) { '/var/empty/doesnotexist' }
 
     it 'is frozen' do
diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
index 1aa40dcde3dbf2980063f09433aac7276955ab70..65398c13d90123acf40f9de39e8829c064d6be09 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -38,14 +38,14 @@
       update_commit_status create_build update_build create_pipeline
       update_pipeline create_merge_request_from create_wiki push_code
       resolve_note create_container_image update_container_image
-      create_environment create_deployment create_release update_release
+      create_environment create_deployment update_deployment create_release update_release
     ]
   end
 
   let(:base_maintainer_permissions) do
     %i[
       push_to_delete_protected_branch update_project_snippet update_environment
-      update_deployment admin_project_snippet admin_project_member admin_note admin_wiki admin_project
+      admin_project_snippet admin_project_member admin_note admin_wiki admin_project
       admin_commit_status admin_build admin_container_image
       admin_pipeline admin_environment admin_deployment destroy_release add_cluster
       daily_statistics
diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
index 0b4ab9941fc35d18a95e7deb06e6b7386a803903..c24418b2f90221a4b5391b665807464acb8df20b 100644
--- a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
@@ -338,7 +338,7 @@
 
             it_behaves_like 'a valid response' do
               it 'responds with status 200, location of uploads remote store and object details' do
-                expect(json_response['TempPath']).to eq(uploader_class.workhorse_local_upload_path)
+                expect(json_response).not_to have_key('TempPath')
                 expect(json_response['RemoteObject']).to have_key('ID')
                 expect(json_response['RemoteObject']).to have_key('GetURL')
                 expect(json_response['RemoteObject']).to have_key('StoreURL')
diff --git a/spec/support/shared_examples/evidence_updated_exposed_fields.rb b/spec/support/shared_examples/evidence_updated_exposed_fields.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2a02fdd7666db1f01dc025506c5304340dd415f0
--- /dev/null
+++ b/spec/support/shared_examples/evidence_updated_exposed_fields.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+shared_examples 'updated exposed field' do
+  it 'creates another Evidence object' do
+    model.send("#{updated_field}=", updated_value)
+
+    expect(model.evidence_summary_keys).to include(updated_field)
+    expect { model.save! }.to change(Evidence, :count).by(1)
+    expect(updated_json_field).to eq(updated_value)
+  end
+end
+
+shared_examples 'updated non-exposed field' do
+  it 'does not create any Evidence object' do
+    model.send("#{updated_field}=", updated_value)
+
+    expect(model.evidence_summary_keys).not_to include(updated_field)
+    expect { model.save! }.not_to change(Evidence, :count)
+  end
+end
+
+shared_examples 'updated field on non-linked entity' do
+  it 'does not create any Evidence object' do
+    model.send("#{updated_field}=", updated_value)
+
+    expect(model.evidence_summary_keys).to be_empty
+    expect { model.save! }.not_to change(Evidence, :count)
+  end
+end
diff --git a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb
index 44f66ff47f4aac4185859c35108d711a045e8192..b837ca87256c9b7496ee19be3f1bc5c113ca6374 100644
--- a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb
+++ b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb
@@ -47,10 +47,6 @@
     end
 
     describe 'internal id generation' do
-      before do
-        stub_feature_flags(iid_always_track: false)
-      end
-
       subject { instance.save! }
 
       it 'calls InternalId.generate_next and sets internal id attribute' do
diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
index 6f06d323a82b4ad613ec5c41af328812129e0eda..a6653f89377be2323c865f768c7cdb9cfce98583 100644
--- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
@@ -75,7 +75,7 @@
 
         subject.reload
 
-        expect(subject.version).to eq(subject.class.const_get(:VERSION))
+        expect(subject.version).to eq(subject.class.const_get(:VERSION, false))
       end
 
       context 'application is updating' do
@@ -104,13 +104,14 @@
 
           subject.reload
 
-          expect(subject.version).to eq(subject.class.const_get(:VERSION))
+          expect(subject.version).to eq(subject.class.const_get(:VERSION, false))
         end
       end
     end
 
     describe '#make_errored' do
       subject { create(application_name, :installing) }
+
       let(:reason) { 'some errors' }
 
       it 'is errored' do
diff --git a/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb
index 181b102e685dabf2b952722c5dac8d50f2d2ad62..ba02da41b53b9e93a511097e25968a6d939e196d 100644
--- a/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb
@@ -12,7 +12,7 @@
 
     context 'version is the same as VERSION' do
       let(:application) { build(application_name) }
-      let(:version) { application.class.const_get(:VERSION) }
+      let(:version) { application.class.const_get(:VERSION, false) }
 
       it { is_expected.to be_falsey }
     end
diff --git a/spec/support/shared_examples/models/clusters/providers/provider_status.rb b/spec/support/shared_examples/models/clusters/providers/provider_status.rb
index af758b07d9605f5d33ac8ed6993c0af93b3fd40f..63cb9a56f5b104c658e0b75cc0a17171b10f3bad 100644
--- a/spec/support/shared_examples/models/clusters/providers/provider_status.rb
+++ b/spec/support/shared_examples/models/clusters/providers/provider_status.rb
@@ -17,7 +17,7 @@
       let(:provider) { build(factory) }
       let(:operation_id) { 'operation-xxx' }
 
-      it 'calls #operation_id on the provider' do
+      it 'calls #assign_operation_id on the provider' do
         expect(provider).to receive(:assign_operation_id).with(operation_id).and_call_original
 
         provider.make_creating(operation_id)
diff --git a/spec/support/shared_examples/models/concern/issuable_shared_examples.rb b/spec/support/shared_examples/models/concern/issuable_shared_examples.rb
index 13ab29e2ca6df38b949657c7ffc562e3d12b528d..4ebb5e35e0ecfcd0ac22e7063801395e9a4c1ee8 100644
--- a/spec/support/shared_examples/models/concern/issuable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concern/issuable_shared_examples.rb
@@ -10,7 +10,7 @@
 end
 
 shared_examples_for 'validates description length with custom validation' do
-  let(:issuable) { build(:issue, description: 'x' * 16_001) }
+  let(:issuable) { build(:issue, description: 'x' * (::Issuable::DESCRIPTION_LENGTH_MAX + 1)) }
   let(:context) { :update }
 
   subject { issuable.validate(context) }
@@ -18,7 +18,7 @@
   context 'when Issuable is a new record' do
     it 'validates the maximum description length' do
       subject
-      expect(issuable.errors[:description]).to eq(["is too long (maximum is 16000 characters)"])
+      expect(issuable.errors[:description]).to eq(["is too long (maximum is #{::Issuable::DESCRIPTION_LENGTH_MAX} characters)"])
     end
 
     context 'on create' do
@@ -53,14 +53,14 @@
     allow(issuable).to receive(:importing?).and_return(true)
   end
 
-  let(:issuable) { build(:issue, description: 'x' * 16_001) }
+  let(:issuable) { build(:issue, description: 'x' * (::Issuable::DESCRIPTION_LENGTH_MAX + 1)) }
 
   subject { issuable.validate(:create) }
 
   it 'truncates the description to its allowed maximum length' do
     subject
 
-    expect(issuable.description).to eq('x' * 16_000)
+    expect(issuable.description).to eq('x' * ::Issuable::DESCRIPTION_LENGTH_MAX)
     expect(issuable.errors[:description]).to be_empty
   end
 end
diff --git a/spec/support/shared_examples/services/boards/boards_create_service.rb b/spec/support/shared_examples/services/boards/boards_create_service.rb
index 19818a6091be19326db834838d6d56ff610ca836..7fd69354c2d15a6f734be97736e3519745be951b 100644
--- a/spec/support/shared_examples/services/boards/boards_create_service.rb
+++ b/spec/support/shared_examples/services/boards/boards_create_service.rb
@@ -17,7 +17,7 @@
 
   context 'when parent has a board' do
     before do
-      create(:board, parent: parent)
+      create(:board, resource_parent: parent)
     end
 
     it 'does not create a new board' do
diff --git a/spec/support/shared_examples/services/boards/boards_list_service.rb b/spec/support/shared_examples/services/boards/boards_list_service.rb
index 566e5050f8e03f2ea54dfb763326a3b00a600446..25dc2e04942ca29ac73e2e289ca0dce3f374a21e 100644
--- a/spec/support/shared_examples/services/boards/boards_list_service.rb
+++ b/spec/support/shared_examples/services/boards/boards_list_service.rb
@@ -15,7 +15,7 @@
 
   context 'when parent has a board' do
     before do
-      create(:board, parent: parent)
+      create(:board, resource_parent: parent)
     end
 
     it 'does not create a new board' do
@@ -24,7 +24,7 @@
   end
 
   it 'returns parent boards' do
-    board = create(:board, parent: parent)
+    board = create(:board, resource_parent: parent)
 
     expect(service.execute).to eq [board]
   end
diff --git a/spec/support/shared_examples/versioned_description_shared_examples.rb b/spec/support/shared_examples/versioned_description_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..59124af19ec720dc5c295b703e33890524d26590
--- /dev/null
+++ b/spec/support/shared_examples/versioned_description_shared_examples.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'versioned description' do
+  describe 'associations' do
+    it { is_expected.to have_many(:description_versions) }
+  end
+
+  describe 'save_description_version' do
+    let(:factory_name) { described_class.name.underscore.to_sym }
+    let!(:model) { create(factory_name, description: 'Original description') }
+
+    context 'when feature is enabled' do
+      before do
+        stub_feature_flags(save_description_versions: true)
+      end
+
+      context 'when description was changed' do
+        before do
+          model.update!(description: 'New description')
+        end
+
+        it 'saves the old and new description for the first update' do
+          expect(model.description_versions.first.description).to eq('Original description')
+          expect(model.description_versions.last.description).to eq('New description')
+        end
+
+        it 'only saves the new description for subsequent updates' do
+          expect { model.update!(description: 'Another description') }.to change { model.description_versions.count }.by(1)
+
+          expect(model.description_versions.last.description).to eq('Another description')
+        end
+
+        it 'sets the new description version to `saved_description_version`' do
+          expect(model.saved_description_version).to eq(model.description_versions.last)
+        end
+
+        it 'clears `saved_description_version` after another save that does not change description' do
+          model.save!
+
+          expect(model.saved_description_version).to be_nil
+        end
+      end
+
+      context 'when description was not changed' do
+        it 'does not save any description version' do
+          expect { model.save! }.not_to change { model.description_versions.count }
+
+          expect(model.saved_description_version).to be_nil
+        end
+      end
+    end
+
+    context 'when feature is disabled' do
+      before do
+        stub_feature_flags(save_description_versions: false)
+      end
+
+      it 'does not save any description version' do
+        expect { model.update!(description: 'New description') }.not_to change { model.description_versions.count }
+
+        expect(model.saved_description_version).to be_nil
+      end
+    end
+  end
+end
diff --git a/spec/tasks/gitlab/check_rake_spec.rb b/spec/tasks/gitlab/check_rake_spec.rb
index 1469143d2ac637e62929314b039c4f3235d26f7f..b3c8ca03aec7ce6884e7573ca085238dafb80e6a 100644
--- a/spec/tasks/gitlab/check_rake_spec.rb
+++ b/spec/tasks/gitlab/check_rake_spec.rb
@@ -19,6 +19,7 @@
 
   describe 'gitlab:check rake task' do
     subject { run_rake_task('gitlab:check') }
+
     let(:name) { 'GitLab subtasks' }
 
     it_behaves_like 'system check rake task'
@@ -26,6 +27,7 @@
 
   describe 'gitlab:gitlab_shell:check rake task' do
     subject { run_rake_task('gitlab:gitlab_shell:check') }
+
     let(:name) { 'GitLab Shell' }
 
     it_behaves_like 'system check rake task'
@@ -33,6 +35,7 @@
 
   describe 'gitlab:gitaly:check rake task' do
     subject { run_rake_task('gitlab:gitaly:check') }
+
     let(:name) { 'Gitaly' }
 
     it_behaves_like 'system check rake task'
@@ -40,6 +43,7 @@
 
   describe 'gitlab:sidekiq:check rake task' do
     subject { run_rake_task('gitlab:sidekiq:check') }
+
     let(:name) { 'Sidekiq' }
 
     it_behaves_like 'system check rake task'
@@ -47,6 +51,7 @@
 
   describe 'gitlab:incoming_email:check rake task' do
     subject { run_rake_task('gitlab:incoming_email:check') }
+
     let(:name) { 'Incoming Email' }
 
     it_behaves_like 'system check rake task'
@@ -56,6 +61,7 @@
     include LdapHelpers
 
     subject { run_rake_task('gitlab:ldap:check') }
+
     let(:name) { 'LDAP' }
 
     it_behaves_like 'system check rake task'
diff --git a/spec/tasks/gitlab/shell_rake_spec.rb b/spec/tasks/gitlab/shell_rake_spec.rb
index e3b7967bd193aa953b59e4e5852304d2ec6d1282..08b3fea0c8002d16c00984f9a3498f503e50dbd7 100644
--- a/spec/tasks/gitlab/shell_rake_spec.rb
+++ b/spec/tasks/gitlab/shell_rake_spec.rb
@@ -14,8 +14,10 @@
       storages = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
         Gitlab.config.repositories.storages.values.map(&:legacy_disk_path)
       end
-      expect(Kernel).to receive(:system).with('bin/install', *storages).and_call_original
-      expect(Kernel).to receive(:system).with('bin/compile').and_call_original
+
+      expect_any_instance_of(Gitlab::TaskHelpers).to receive(:checkout_or_clone_version)
+      allow(Kernel).to receive(:system).with('bin/install', *storages).and_return(true)
+      allow(Kernel).to receive(:system).with('make', 'build').and_return(true)
 
       run_rake_task('gitlab:shell:install')
     end
diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb
index 3aaaabe4629e51cc53504319787eb52231b3c4f8..93fedde6e9665f38d735394a6bf7b34b054ea839 100644
--- a/spec/views/admin/dashboard/index.html.haml_spec.rb
+++ b/spec/views/admin/dashboard/index.html.haml_spec.rb
@@ -17,6 +17,7 @@
 
     allow(view).to receive(:admin?).and_return(true)
     allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
+    allow(view).to receive(:show_license_breakdown?).and_return(false)
   end
 
   it "shows version of GitLab Workhorse" do
diff --git a/spec/views/devise/shared/_signin_box.html.haml_spec.rb b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
index 6d6406863375089cd8528774ab3bbec1d12d4c92..f88674776035dc740ea8e5a440cc12267c78709a 100644
--- a/spec/views/devise/shared/_signin_box.html.haml_spec.rb
+++ b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
@@ -10,6 +10,7 @@
       allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
       allow(view).to receive(:captcha_enabled?).and_return(false)
       allow(view).to receive(:captcha_on_login_required?).and_return(false)
+      allow(view).to receive(:experiment_enabled?).and_return(false)
     end
 
     it 'is shown when Crowd is enabled' do
diff --git a/spec/views/events/event/_push.html.haml_spec.rb b/spec/views/events/event/_push.html.haml_spec.rb
index e43e37188a3a1b35434eff935293fbd79a176e09..d33a8aa86fc1d5a02d9b2c0ef0da22c14601f73b 100644
--- a/spec/views/events/event/_push.html.haml_spec.rb
+++ b/spec/views/events/event/_push.html.haml_spec.rb
@@ -28,6 +28,23 @@
         expect(rendered).not_to have_link(event.ref_name)
       end
     end
+
+    context 'ref_count is more than 1' do
+      let(:payload) do
+        build_stubbed(
+          :push_event_payload,
+          event: event,
+          ref_count: 4,
+          ref_type: :branch
+        )
+      end
+
+      it 'includes the count in the text' do
+        render partial: 'events/event/push', locals: { event: event }
+
+        expect(rendered).to include('4 branches')
+      end
+    end
   end
 
   context 'with a tag' do
@@ -53,5 +70,22 @@
         expect(rendered).not_to have_link(event.ref_name)
       end
     end
+
+    context 'ref_count is more than 1' do
+      let(:payload) do
+        build_stubbed(
+          :push_event_payload,
+          event: event,
+          ref_count: 4,
+          ref_type: :tag
+        )
+      end
+
+      it 'includes the count in the text' do
+        render partial: 'events/event/push', locals: { event: event }
+
+        expect(rendered).to include('4 tags')
+      end
+    end
   end
 end
diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb
index bea0b0edf4d10fad2dd42ff85f348dfb5dbb9aa3..e9b3334fffc012ab6bc96faf5f59bbc846e10293 100644
--- a/spec/views/layouts/_head.html.haml_spec.rb
+++ b/spec/views/layouts/_head.html.haml_spec.rb
@@ -7,6 +7,7 @@
 
   before do
     allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
+    allow(view).to receive(:experiment_enabled?).and_return(false)
   end
 
   it 'escapes HTML-safe strings in page_title' do
diff --git a/spec/views/projects/show.html.haml_spec.rb b/spec/views/projects/show.html.haml_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..820772b592f71dd7322cc347fa6a5d4a6e90557c
--- /dev/null
+++ b/spec/views/projects/show.html.haml_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'projects/show' do
+  include Devise::Test::ControllerHelpers
+
+  let(:user) { create(:admin) }
+  let(:project) { create(:project, :repository) }
+
+  before do
+    presented_project = project.present(current_user: user)
+
+    allow(presented_project).to receive(:default_view).and_return('customize_workflow')
+    allow(controller).to receive(:current_user).and_return(user)
+
+    assign(:project, presented_project)
+  end
+
+  context 'commit signatures' do
+    context 'with vue tree view disabled' do
+      before do
+        stub_feature_flags(vue_file_list: false)
+      end
+
+      it 'rendered via js-signature-container' do
+        render
+
+        expect(rendered).to have_css('.js-signature-container')
+      end
+    end
+
+    context 'with vue tree view enabled' do
+      it 'are not rendered via js-signature-container' do
+        render
+
+        expect(rendered).not_to have_css('.js-signature-container')
+      end
+    end
+  end
+end
diff --git a/spec/views/projects/tree/_tree_header.html.haml_spec.rb b/spec/views/projects/tree/_tree_header.html.haml_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4b71ea9ffe3acf64ea277e96183524943b553345
--- /dev/null
+++ b/spec/views/projects/tree/_tree_header.html.haml_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'projects/tree/_tree_header' do
+  let(:project) { create(:project, :repository) }
+  let(:current_user) { create(:user) }
+  let(:repository) { project.repository }
+
+  before do
+    assign(:project, project)
+    assign(:repository, repository)
+    assign(:id, File.join('master', ''))
+    assign(:ref, 'master')
+
+    allow(view).to receive(:current_user).and_return(current_user)
+    allow(view).to receive(:can_collaborate_with_project?) { true }
+  end
+
+  it 'does not render the WebIDE button when user cannot create fork or cannot open MR' do
+    allow(view).to receive(:can?) { false }
+
+    render
+
+    expect(rendered).not_to have_link('Web IDE')
+  end
+
+  it 'renders the WebIDE button when user can create fork and can open MR in project' do
+    allow(view).to receive(:can?) { true }
+
+    render
+
+    expect(rendered).to have_link('Web IDE')
+  end
+
+  it 'opens a popup confirming a fork if the user can create fork/MR but cannot collaborate with the project' do
+    allow(view).to receive(:can?) { true }
+    allow(view).to receive(:can_collaborate_with_project?) { false }
+
+    render
+
+    expect(rendered).to have_link('Web IDE', href: '#modal-confirm-fork')
+  end
+end
diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb
index 960cf42a793b1215a63c6731afa59738ddb0a436..4307d1b49c9ecca522bfa8be24bdd8bfe3e1e0cc 100644
--- a/spec/views/projects/tree/show.html.haml_spec.rb
+++ b/spec/views/projects/tree/show.html.haml_spec.rb
@@ -7,6 +7,10 @@
 
   let(:project) { create(:project, :repository) }
   let(:repository) { project.repository }
+  let(:ref) { 'master' }
+  let(:commit) { repository.commit(ref) }
+  let(:path) { '' }
+  let(:tree) { repository.tree(commit.id, path) }
 
   before do
     stub_feature_flags(vue_file_list: false)
@@ -19,26 +23,45 @@
     allow(view).to receive(:can_collaborate_with_project?).and_return(true)
     allow(view).to receive_message_chain('user_access.can_push_to_branch?').and_return(true)
     allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
+    allow(view).to receive(:current_user).and_return(project.creator)
+
+    assign(:id, File.join(ref, path))
+    assign(:ref, ref)
+    assign(:path, path)
+    assign(:last_commit, commit)
+    assign(:tree, tree)
   end
 
   context 'for branch names ending on .json' do
     let(:ref) { 'ends-with.json' }
-    let(:commit) { repository.commit(ref) }
-    let(:path) { '' }
-    let(:tree) { repository.tree(commit.id, path) }
-
-    before do
-      assign(:id, File.join(ref, path))
-      assign(:ref, ref)
-      assign(:path, path)
-      assign(:last_commit, commit)
-      assign(:tree, tree)
-    end
 
     it 'displays correctly' do
       render
+
       expect(rendered).to have_css('.js-project-refs-dropdown .dropdown-toggle-text', text: ref)
       expect(rendered).to have_css('.readme-holder')
     end
   end
+
+  context 'commit signatures' do
+    context 'with vue tree view disabled' do
+      it 'rendered via js-signature-container' do
+        render
+
+        expect(rendered).to have_css('.js-signature-container')
+      end
+    end
+
+    context 'with vue tree view enabled' do
+      before do
+        stub_feature_flags(vue_file_list: true)
+      end
+
+      it 'are not rendered via js-signature-container' do
+        render
+
+        expect(rendered).not_to have_css('.js-signature-container')
+      end
+    end
+  end
 end
diff --git a/spec/workers/create_evidence_worker_spec.rb b/spec/workers/create_evidence_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..364b209825101ed2d413490a28a3ec74b455b02c
--- /dev/null
+++ b/spec/workers/create_evidence_worker_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe CreateEvidenceWorker do
+  let!(:release) { create(:release) }
+
+  it 'creates a new Evidence' do
+    expect { described_class.new.perform(release.id) }.to change(Evidence, :count).by(1)
+  end
+end
diff --git a/spec/workers/deployments/success_worker_spec.rb b/spec/workers/deployments/success_worker_spec.rb
index 1c68922b03d89ee53710865449ff2f6514f0cc4e..7f2816d7535f922a0e24b1cd3d17058c70b67382 100644
--- a/spec/workers/deployments/success_worker_spec.rb
+++ b/spec/workers/deployments/success_worker_spec.rb
@@ -8,8 +8,8 @@
   context 'when successful deployment' do
     let(:deployment) { create(:deployment, :success) }
 
-    it 'executes UpdateDeploymentService' do
-      expect(UpdateDeploymentService)
+    it 'executes Deployments::AfterCreateService' do
+      expect(Deployments::AfterCreateService)
         .to receive(:new).with(deployment).and_call_original
 
       subject
@@ -19,8 +19,8 @@
   context 'when canceled deployment' do
     let(:deployment) { create(:deployment, :canceled) }
 
-    it 'does not execute UpdateDeploymentService' do
-      expect(UpdateDeploymentService).not_to receive(:new)
+    it 'does not execute Deployments::AfterCreateService' do
+      expect(Deployments::AfterCreateService).not_to receive(:new)
 
       subject
     end
@@ -29,8 +29,8 @@
   context 'when deploy record does not exist' do
     let(:deployment) { nil }
 
-    it 'does not execute UpdateDeploymentService' do
-      expect(UpdateDeploymentService).not_to receive(:new)
+    it 'does not execute Deployments::AfterCreateService' do
+      expect(Deployments::AfterCreateService).not_to receive(:new)
 
       subject
     end
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index 8fddd8540ef6dd67d95af9fb7eb8fbaef1b338b7..b7ba4d617234bfd57f109065fff76e7fe487667c 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -35,4 +35,32 @@
       expect(config_queues).to include(queue).or(include(queue_namespace))
     end
   end
+
+  describe "feature category declarations" do
+    let(:feature_categories) do
+      YAML.load_file(Rails.root.join('config', 'feature_categories.yml')).map(&:to_sym).to_set
+    end
+
+    # All Sidekiq worker classes should declare a valid `feature_category`
+    # or explicitely be excluded with the `feature_category_not_owned!` annotation.
+    # Please see doc/development/sidekiq_style_guide.md#Feature-Categorization for more details.
+    it 'has a feature_category or feature_category_not_owned! attribute', :aggregate_failures do
+      Gitlab::SidekiqConfig.workers.each do |worker|
+        expect(worker.get_feature_category).to be_a(Symbol), "expected #{worker.inspect} to declare a feature_category or feature_category_not_owned!"
+      end
+    end
+
+    # All Sidekiq worker classes should declare a valid `feature_category`.
+    # The category should match a value in `config/feature_categories.yml`.
+    # Please see doc/development/sidekiq_style_guide.md#Feature-Categorization for more details.
+    it 'has a feature_category that maps to a value in feature_categories.yml', :aggregate_failures do
+      workers_with_feature_categories = Gitlab::SidekiqConfig.workers
+                  .select(&:get_feature_category)
+                  .reject(&:feature_category_not_owned?)
+
+      workers_with_feature_categories.each do |worker|
+        expect(feature_categories).to include(worker.get_feature_category), "expected #{worker.inspect} to declare a valid feature_category, but got #{worker.get_feature_category}"
+      end
+    end
+  end
 end
diff --git a/spec/workers/hashed_storage/migrator_worker_spec.rb b/spec/workers/hashed_storage/migrator_worker_spec.rb
index a318cdd003e98ff11d1d2dc439b4d5fb5cacb43c..12c1a26104ec51c99f48824cf2488e103b304c22 100644
--- a/spec/workers/hashed_storage/migrator_worker_spec.rb
+++ b/spec/workers/hashed_storage/migrator_worker_spec.rb
@@ -4,6 +4,7 @@
 
 describe HashedStorage::MigratorWorker do
   subject(:worker) { described_class.new }
+
   let(:projects) { create_list(:project, 2, :legacy_storage, :empty_repo) }
   let(:ids) { projects.map(&:id) }
 
diff --git a/spec/workers/hashed_storage/rollbacker_worker_spec.rb b/spec/workers/hashed_storage/rollbacker_worker_spec.rb
index 4055f3809782b9756a658fe306997dd6462a32a0..5fcb1adf9aeec47fe3fa2f3ba40122b617c0861c 100644
--- a/spec/workers/hashed_storage/rollbacker_worker_spec.rb
+++ b/spec/workers/hashed_storage/rollbacker_worker_spec.rb
@@ -4,6 +4,7 @@
 
 describe HashedStorage::RollbackerWorker do
   subject(:worker) { described_class.new }
+
   let(:projects) { create_list(:project, 2, :empty_repo) }
   let(:ids) { projects.map(&:id) }
 
diff --git a/spec/workers/new_release_worker_spec.rb b/spec/workers/new_release_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9010c36f795cd303aabcc24c29ecf26cc2d39732
--- /dev/null
+++ b/spec/workers/new_release_worker_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe NewReleaseWorker do
+  let(:release) { create(:release) }
+
+  it 'sends a new release notification' do
+    expect_any_instance_of(NotificationService).to receive(:send_new_release_notifications).with(release)
+
+    described_class.new.perform(release.id)
+  end
+end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 6983fea021c788c6f20a97ad711028b2c5469bc3..34aaa9bb1e9833494867b34ffbf9b318a580efae 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -93,6 +93,8 @@ def perform(changes: base64_changes)
     end
 
     context 'with changes' do
+      let(:push_service) { double(execute: true) }
+
       before do
         allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner)
         allow(Gitlab::GlRepository).to receive(:parse).and_return([project, Gitlab::GlRepository::PROJECT])
diff --git a/yarn.lock b/yarn.lock
index 45375114d430d46498a74de825ac149a18a72291..3abd18d111173ecc8b11227ad15727ac73f03fb2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -995,10 +995,10 @@
   resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.78.0.tgz#469493bd6cdd254eb5d1271edeab22bbbee2f4c4"
   integrity sha512-dBgEB/Q4FRD0NapmNrD86DF1FsV0uSgTx0UOJloHnGE2DNR2P1HQrCmLW2fX+QgN4P9CDAzdi2buVHuholofWw==
 
-"@gitlab/ui@5.32.0":
-  version "5.32.0"
-  resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.32.0.tgz#21bb70b6c8b68bdcbb53ffebde80ff3cd93851c8"
-  integrity sha512-xTFz4/WbR1e6zj2xI2DULcAGicA6qidb9Reoa02V5snqWcQY+iHDup/XzgXmttTPCiBlqPIFo/CMhH4gSJWuPQ==
+"@gitlab/ui@5.36.0":
+  version "5.36.0"
+  resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.36.0.tgz#3087b23c138ad1c222f6b047e533f253371bc618"
+  integrity sha512-XXWUYZbRItKh9N92Vxql04BJ05uW5HlOuTCkD+lMbUgneqYTgVoKGH8d9kD++Jy7q8l5+AfzjboUn2n9sbQMZA==
   dependencies:
     "@babel/standalone" "^7.0.0"
     "@gitlab/vue-toasted" "^1.2.1"
@@ -1008,6 +1008,7 @@
     highlight.js "^9.13.1"
     js-beautify "^1.8.8"
     lodash "^4.17.14"
+    resize-observer-polyfill "^1.5.1"
     url-search-params-polyfill "^5.0.0"
     vue "^2.6.10"
     vue-loader "^15.4.2"
@@ -10531,6 +10532,11 @@ requizzle@~0.2.1:
   dependencies:
     underscore "~1.6.0"
 
+resize-observer-polyfill@^1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
+  integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
+
 resolve-cwd@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"