diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index f3e23f5324df85341e16149e15fb60e48e8492b9..520cde9eceed18889de0ca9a0ad1649690d3c12e 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -168,22 +168,22 @@ def todos_filter_path(options = {})
 
   def todo_actions_options
     [
-      { id: '', text: 'Any Action' },
-      { id: Todo::ASSIGNED, text: 'Assigned' },
-      { id: Todo::REVIEW_REQUESTED, text: 'Review requested' },
-      { id: Todo::MENTIONED, text: 'Mentioned' },
-      { id: Todo::MARKED, text: 'Added' },
-      { id: Todo::BUILD_FAILED, text: 'Pipelines' }
+      { id: '', text: s_('Todos|Any Action') },
+      { id: Todo::ASSIGNED, text: s_('Todos|Assigned') },
+      { id: Todo::REVIEW_REQUESTED, text: s_('Todos|Review requested') },
+      { id: Todo::MENTIONED, text: s_('Todos|Mentioned') },
+      { id: Todo::MARKED, text: s_('Todos|Added') },
+      { id: Todo::BUILD_FAILED, text: s_('Todos|Pipelines') }
     ]
   end
 
   def todo_types_options
     [
-      { id: '', text: 'Any Type' },
-      { id: 'Issue', text: 'Issue' },
-      { id: 'MergeRequest', text: 'Merge request' },
-      { id: 'DesignManagement::Design', text: 'Design' },
-      { id: 'AlertManagement::Alert', text: 'Alert' }
+      { id: '', text: s_('Todos|Any Type') },
+      { id: 'Issue', text: s_('Todos|Issue') },
+      { id: 'MergeRequest', text: s_('Todos|Merge request') },
+      { id: 'DesignManagement::Design', text: s_('Todos|Design') },
+      { id: 'AlertManagement::Alert', text: s_('Todos|Alert') }
     ]
   end
 
diff --git a/app/models/environment.rb b/app/models/environment.rb
index a601b3a88a8d517f52bfeefc87f2618c2e9376d9..2d3f342953f715bd9193e80e94daed402439098e 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -544,7 +544,7 @@ def guess_tier
       self.class.tiers[:development]
     when /(test|tst|int|ac(ce|)pt|qa|qc|control|quality)/i
       self.class.tiers[:testing]
-    when /(st(a|)g|mod(e|)l|pre|demo)/i
+    when /(st(a|)g|mod(e|)l|pre|demo|non)/i
       self.class.tiers[:staging]
     when /(pr(o|)d|live)/i
       self.class.tiers[:production]
diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb
index b7df201824ae5c5e46009704c0e83b08e3f3e314..01dd6323d94903c086c8280abb2caff142486a38 100644
--- a/app/services/releases/create_service.rb
+++ b/app/services/releases/create_service.rb
@@ -3,10 +3,10 @@
 module Releases
   class CreateService < Releases::BaseService
     def execute
-      return error('Access Denied', 403) unless allowed?
-      return error('You are not allowed to create this tag as it is protected.', 403) unless can_create_tag?
-      return error('Release already exists', 409) if release
-      return error("Milestone(s) not found: #{inexistent_milestones.join(', ')}", 400) if inexistent_milestones.any?
+      return error(_('Access Denied'), 403) unless allowed?
+      return error(_('You are not allowed to create this tag as it is protected.'), 403) unless can_create_tag?
+      return error(_('Release already exists'), 409) if release
+      return error(format(_("Milestone(s) not found: %{milestones}"), milestones: inexistent_milestones.join(', ')), 400) if inexistent_milestones.any? # rubocop:disable Layout/LineLength
 
       # should be found before the creation of new tag
       # because tag creation can spawn new pipeline
diff --git a/app/services/releases/destroy_service.rb b/app/services/releases/destroy_service.rb
index 8abf93086895fe6a5e39a2975fe4f3ea895a743d..ff2b3a7bd1863bd95f2252c65abe2522d9d45491 100644
--- a/app/services/releases/destroy_service.rb
+++ b/app/services/releases/destroy_service.rb
@@ -3,8 +3,8 @@
 module Releases
   class DestroyService < Releases::BaseService
     def execute
-      return error('Release does not exist', 404) unless release
-      return error('Access Denied', 403) unless allowed?
+      return error(_('Release does not exist'), 404) unless release
+      return error(_('Access Denied'), 403) unless allowed?
 
       if release.destroy
         success(tag: existing_tag, release: release)
diff --git a/app/services/releases/update_service.rb b/app/services/releases/update_service.rb
index 2e0a2f8488a153cde15ce33249dd8f19c45d7892..b9b2aba9805cd40aebb0e996c93945157aedf349 100644
--- a/app/services/releases/update_service.rb
+++ b/app/services/releases/update_service.rb
@@ -31,11 +31,11 @@ def execute
     private
 
     def validate
-      return error('Tag does not exist', 404) unless existing_tag
-      return error('Release does not exist', 404) unless release
-      return error('Access Denied', 403) unless allowed?
-      return error('params is empty', 400) if empty_params?
-      return error("Milestone(s) not found: #{inexistent_milestones.join(', ')}", 400) if inexistent_milestones.any?
+      return error(_('Tag does not exist'), 404) unless existing_tag
+      return error(_('Release does not exist'), 404) unless release
+      return error(_('Access Denied'), 403) unless allowed?
+      return error(_('params is empty'), 400) if empty_params?
+      return error(format(_("Milestone(s) not found: %{milestones}"), milestones: inexistent_milestones.join(', ')), 400) if inexistent_milestones.any? # rubocop:disable Layout/LineLength
     end
 
     def allowed?
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index abaf250fa698aa819361a576bf086fbe6f958125..48ae1f7eb1d9431388fa003e605bfb8b6f0bd224 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -9,7 +9,7 @@
         %span.js-clone-dropdown-label
           = default_clone_protocol.upcase
         = sprite_icon('chevron-down', css_class: 'gl-icon')
-      %ul.dropdown-menu.dropdown-menu-selectable{ data: { qa_selector: 'clone_dropdown_content' } }
+      %ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown{ data: { qa_selector: 'clone_dropdown_content' } }
         %li
           = ssh_clone_button(container)
         %li
diff --git a/config/feature_flags/development/ci_limit_complete_hierarchy_size.yml b/config/feature_flags/development/ci_limit_complete_hierarchy_size.yml
index ad0dd85a25aa13b480c309b3d691b93626efb910..d6cc87873331aa539031b17410d8d0d162214f5f 100644
--- a/config/feature_flags/development/ci_limit_complete_hierarchy_size.yml
+++ b/config/feature_flags/development/ci_limit_complete_hierarchy_size.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/373719
 milestone: '15.4'
 type: development
 group: group::pipeline execution
-default_enabled: false
+default_enabled: true
diff --git a/doc/administration/gitaly/configure_gitaly.md b/doc/administration/gitaly/configure_gitaly.md
index f6bf563b8cd6efae8a3d7c065d8e1c431e476f32..e4aef2db9a8292cab85889d6a245f9ce27c58ca9 100644
--- a/doc/administration/gitaly/configure_gitaly.md
+++ b/doc/administration/gitaly/configure_gitaly.md
@@ -555,12 +555,15 @@ Additionally, the certificate (or its certificate authority) must be installed o
 - Gitaly servers.
 - Gitaly clients that communicate with it.
 
-Note the following:
+### Certificate requirements
 
 - The certificate must specify the address you use to access the Gitaly server. You must add the hostname or IP address as a Subject Alternative Name to the certificate.
 - You can configure Gitaly servers with both an unencrypted listening address `listen_addr` and an
   encrypted listening address `tls_listen_addr` at the same time. This allows you to gradually
   transition from unencrypted to encrypted traffic if necessary.
+- The certificate's Common Name field is ignored.
+
+### Configure Gitaly with TLS
 
 To configure Gitaly with TLS:
 
diff --git a/doc/administration/gitaly/troubleshooting.md b/doc/administration/gitaly/troubleshooting.md
index b53534cdcebae8ab3158a121b0fb83adcb19ca5d..1c5f0d438646cd8fb15281ac77c77a5fb22e742d 100644
--- a/doc/administration/gitaly/troubleshooting.md
+++ b/doc/administration/gitaly/troubleshooting.md
@@ -67,6 +67,13 @@ remote: GitLab: 401 Unauthorized
 You need to sync your `gitlab-secrets.json` file with your GitLab
 application nodes.
 
+### 500 and `fetching folder content` errors on repository pages
+
+`Fetching folder content`, and in some cases `500`, errors indicate
+connectivity problems between GitLab and Gitaly.
+Consult the [client-side gRPC logs](#client-side-grpc-logs)
+for details.
+
 ### Client side gRPC logs
 
 Gitaly uses the [gRPC](https://grpc.io/) RPC framework. The Ruby gRPC
@@ -81,6 +88,19 @@ You can run a gRPC trace with:
 sudo GRPC_TRACE=all GRPC_VERBOSITY=DEBUG gitlab-rake gitlab:gitaly:check
 ```
 
+If this command fails with a `failed to connect to all addresses` error,
+check for an SSL or TLS problem:
+
+```shell
+/opt/gitlab/embedded/bin/openssl s_client -connect <gitaly-ipaddress>:<port> -verify_return_error
+```
+
+Check whether `Verify return code` field indicates a
+[known Omnibus GitLab configuration problem](https://docs.gitlab.com/omnibus/settings/ssl.html).
+
+If `openssl` succeeds but `gitlab-rake gitlab:gitaly:check` fails,
+check [certificate requirements](configure_gitaly.md#certificate-requirements) for Gitaly.
+
 ### Server side gRPC logs
 
 gRPC tracing can also be enabled in Gitaly itself with the `GODEBUG=http2debug`
diff --git a/doc/update/zero_downtime.md b/doc/update/zero_downtime.md
index 9ed09e17d01d116a33dae3180fce221f83657161..aebd27f84a9729aaf6701f096fb87112bd7b4362 100644
--- a/doc/update/zero_downtime.md
+++ b/doc/update/zero_downtime.md
@@ -313,7 +313,7 @@ node throughout the process.
 
 - If you're using PgBouncer:
 
-  You must bypass PgBouncer and connect directly to the database leader
+  You must [bypass PgBouncer](../administration/postgresql/pgbouncer.md#procedure-for-bypassing-pgbouncer) and connect directly to the database leader
   before running migrations.
 
   Rails uses an advisory lock when attempting to run a migration to prevent
@@ -699,7 +699,7 @@ sudo touch /etc/gitlab/skip-auto-reconfigure
 
 1. If you're using PgBouncer:
 
-   You must bypass PgBouncer and connect directly to the database leader
+   You must [bypass PgBouncer](../administration/postgresql/pgbouncer.md#procedure-for-bypassing-pgbouncer) and connect directly to the database leader
    before running migrations.
 
    Rails uses an advisory lock when attempting to run a migration to prevent
diff --git a/ee/app/helpers/ee/todos_helper.rb b/ee/app/helpers/ee/todos_helper.rb
index d0a0acadb7fd62d25c86509b375f39e219178a2a..0b06a17ca4c6b7c39ca6b8274bd52f6b6c8ce8f6 100644
--- a/ee/app/helpers/ee/todos_helper.rb
+++ b/ee/app/helpers/ee/todos_helper.rb
@@ -6,7 +6,7 @@ module TodosHelper
 
     override :todo_types_options
     def todo_types_options
-      super + [{ id: 'Epic', text: 'Epic' }]
+      super + [{ id: 'Epic', text: s_('Todos|Epic') }]
     end
 
     override :todo_author_display?
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6e155e9bc7b96282573eccc5ecca2df6154e14c3..a4ac7b42231a7e6c773c34274dbd9a10ea8b22f6 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1933,6 +1933,9 @@ msgstr ""
 msgid "Acceptable for use in this project"
 msgstr ""
 
+msgid "Access Denied"
+msgstr ""
+
 msgid "Access Git repositories or the API."
 msgstr ""
 
@@ -25994,6 +25997,9 @@ msgstr ""
 msgid "Milestone lists not available with your current license"
 msgstr ""
 
+msgid "Milestone(s) not found: %{milestones}"
+msgstr ""
+
 msgid "MilestoneCombobox|An error occurred while searching for milestones"
 msgstr ""
 
@@ -33441,6 +33447,9 @@ msgstr[1] ""
 msgid "Release %{deletedRelease} has been successfully deleted."
 msgstr ""
 
+msgid "Release already exists"
+msgstr ""
+
 msgid "Release assets"
 msgstr ""
 
@@ -33450,6 +33459,9 @@ msgstr ""
 msgid "Release date"
 msgstr ""
 
+msgid "Release does not exist"
+msgstr ""
+
 msgid "Release does not have the same project as the milestone"
 msgstr ""
 
@@ -39576,6 +39588,9 @@ msgstr ""
 msgid "Tag"
 msgstr ""
 
+msgid "Tag does not exist"
+msgstr ""
+
 msgid "Tag list:"
 msgstr ""
 
@@ -42222,9 +42237,30 @@ msgstr ""
 msgid "Todos count"
 msgstr ""
 
+msgid "Todos|Added"
+msgstr ""
+
+msgid "Todos|Alert"
+msgstr ""
+
+msgid "Todos|Any Action"
+msgstr ""
+
+msgid "Todos|Any Type"
+msgstr ""
+
 msgid "Todos|Are you looking for things to do? Take a look at %{strongStart}%{openIssuesLinkStart}open issues%{openIssuesLinkEnd}%{strongEnd}, contribute to %{strongStart}%{mergeRequestLinkStart}a merge request%{mergeRequestLinkEnd}%{mergeRequestLinkEnd}%{strongEnd}, or mention someone in a comment to automatically assign them a new to-do item."
 msgstr ""
 
+msgid "Todos|Assigned"
+msgstr ""
+
+msgid "Todos|Design"
+msgstr ""
+
+msgid "Todos|Epic"
+msgstr ""
+
 msgid "Todos|Filter by author"
 msgstr ""
 
@@ -42246,18 +42282,33 @@ msgstr ""
 msgid "Todos|Isn't an empty To-Do List beautiful?"
 msgstr ""
 
+msgid "Todos|Issue"
+msgstr ""
+
 msgid "Todos|It's how you always know what to work on next."
 msgstr ""
 
 msgid "Todos|Mark all as done"
 msgstr ""
 
+msgid "Todos|Mentioned"
+msgstr ""
+
+msgid "Todos|Merge request"
+msgstr ""
+
 msgid "Todos|Nothing is on your to-do list. Nice work!"
 msgstr ""
 
 msgid "Todos|Nothing left to do. High five!"
 msgstr ""
 
+msgid "Todos|Pipelines"
+msgstr ""
+
+msgid "Todos|Review requested"
+msgstr ""
+
 msgid "Todos|Undo mark all as done"
 msgstr ""
 
@@ -46020,6 +46071,9 @@ msgstr ""
 msgid "You are not allowed to approve a user"
 msgstr ""
 
+msgid "You are not allowed to create this tag as it is protected."
+msgstr ""
+
 msgid "You are not allowed to log in using password"
 msgstr ""
 
@@ -48589,6 +48643,9 @@ msgstr ""
 msgid "pages"
 msgstr ""
 
+msgid "params is empty"
+msgstr ""
+
 msgid "parent"
 msgid_plural "parents"
 msgstr[0] ""
diff --git a/qa/Gemfile b/qa/Gemfile
index 4dd02c7b3d849d6219e48ba0c917b7ab97723d20..61d4b2d5059a8d62b939e12214cb79ab13d8923b 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -4,7 +4,7 @@ source 'https://rubygems.org'
 
 gem 'gitlab-qa', '~> 8', require: 'gitlab/qa'
 gem 'activesupport', '~> 6.1.4.7' # This should stay in sync with the root's Gemfile
-gem 'allure-rspec', '~> 2.16.0'
+gem 'allure-rspec', '~> 2.18.0'
 gem 'capybara', '~> 3.35.0'
 gem 'capybara-screenshot', '~> 1.0.26'
 gem 'rake', '~> 13'
@@ -14,7 +14,7 @@ gem 'airborne', '~> 0.3.7', require: false # airborne is messing with rspec sand
 gem 'rest-client', '~> 2.1.0'
 gem 'rspec-retry', '~> 0.6.1', require: 'rspec/retry'
 gem 'rspec_junit_formatter', '~> 0.6.0'
-gem 'faker', '~> 2.19', '>= 2.19.0'
+gem 'faker', '~> 2.23'
 gem 'knapsack', '~> 4.0'
 gem 'parallel_tests', '~> 2.32'
 gem 'rotp', '~> 6.2.0'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 280d0a72c29ca017bb15132758eef5dd83c82928..837e7e8bdb0bf1f24b7dbc50e37af83aa3baea04 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -15,10 +15,10 @@ GEM
       rack-test (>= 1.1.0, < 2.0)
       rest-client (>= 2.0.2, < 3.0)
       rspec (~> 3.8)
-    allure-rspec (2.16.1)
-      allure-ruby-commons (= 2.16.1)
+    allure-rspec (2.18.0)
+      allure-ruby-commons (= 2.18.0)
       rspec-core (>= 3.8, < 4)
-    allure-ruby-commons (2.16.1)
+    allure-ruby-commons (2.18.0)
       mime-types (>= 3.3, < 4)
       oj (>= 3.10, < 4)
       require_all (>= 2, < 4)
@@ -60,8 +60,8 @@ GEM
     domain_name (0.5.20190701)
       unf (>= 0.0.5, < 1.0.0)
     excon (0.92.4)
-    faker (2.19.0)
-      i18n (>= 1.6, < 2)
+    faker (2.23.0)
+      i18n (>= 1.8.11, < 2)
     faraday (2.5.2)
       faraday-net_http (>= 2.0, < 3.1)
       ruby2_keywords (>= 0.0.4)
@@ -182,7 +182,7 @@ GEM
     octokit (5.6.1)
       faraday (>= 1, < 3)
       sawyer (~> 0.9)
-    oj (3.13.11)
+    oj (3.13.21)
     os (1.1.4)
     parallel (1.19.2)
     parallel_tests (2.32.0)
@@ -299,14 +299,14 @@ PLATFORMS
 DEPENDENCIES
   activesupport (~> 6.1.4.7)
   airborne (~> 0.3.7)
-  allure-rspec (~> 2.16.0)
+  allure-rspec (~> 2.18.0)
   capybara (~> 3.35.0)
   capybara-screenshot (~> 1.0.26)
   chemlab (~> 0.10)
   chemlab-library-www-gitlab-com (~> 0.1)
   confiner (~> 0.3)
   deprecation_toolkit (~> 2.0.0)
-  faker (~> 2.19, >= 2.19.0)
+  faker (~> 2.23)
   faraday-retry (~> 2.0)
   fog-core (= 2.1.0)
   fog-google (~> 1.19)
@@ -336,4 +336,4 @@ DEPENDENCIES
   zeitwerk (~> 2.4)
 
 BUNDLED WITH
-   2.3.23
+   2.3.24
diff --git a/qa/qa/resource/project_imported_from_github.rb b/qa/qa/resource/project_imported_from_github.rb
index b9dbd2a6131d08274a80a6a542b87189b6d0d1e4..9ba9723f0cca6d8f27140f3f78768faa23e806d4 100644
--- a/qa/qa/resource/project_imported_from_github.rb
+++ b/qa/qa/resource/project_imported_from_github.rb
@@ -3,6 +3,8 @@
 module QA
   module Resource
     class ProjectImportedFromGithub < Resource::Project
+      attr_accessor :issue_events_import, :full_notes_import, :attachments_import
+
       attribute :github_repo_id do
         github_client.repository(github_repository_path).id
       end
@@ -51,7 +53,12 @@ def api_post_body
           new_name: name,
           target_namespace: @personal_namespace || group.full_path,
           personal_access_token: github_personal_access_token,
-          ci_cd_only: false
+          ci_cd_only: false,
+          optional_stages: {
+            single_endpoint_issue_events_import: issue_events_import,
+            single_endpoint_notes_import: full_notes_import,
+            attachments_import: attachments_import
+          }
         }
       end
 
diff --git a/qa/qa/specs/features/api/1_manage/import/import_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import/import_github_repo_spec.rb
index 85eb28e901c6f758e26ed992150032da0c009973..c3e41e9298b7015e03d00efbc8e5f44e1cbdbabf 100644
--- a/qa/qa/specs/features/api/1_manage/import/import_github_repo_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/import/import_github_repo_spec.rb
@@ -21,6 +21,8 @@ module QA
           project.github_personal_access_token = Runtime::Env.github_access_token
           project.github_repository_path = 'gitlab-qa-github/import-test'
           project.api_client = Runtime::API::Client.new(user: user)
+          project.issue_events_import = true
+          project.full_notes_import = true
         end
       end
 
diff --git a/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb
index 9728eee386958ed127f7034e93814584f2fe77f0..5acf15dd2b4a9e21d9bed42d0d9b6c7a319a0e6f 100644
--- a/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb
@@ -199,13 +199,11 @@ module QA
           project.github_repository_path = github_repo
           project.personal_namespace = user.username
           project.api_client = Runtime::API::Client.new(user: user)
+          project.issue_events_import = true
+          project.full_notes_import = true
         end
       end
 
-      before do
-        Runtime::Feature.enable(:github_importer_single_endpoint_issue_events_import)
-      end
-
       after do |example|
         next unless defined?(@import_time)
 
diff --git a/rubocop/cop_todo.rb b/rubocop/cop_todo.rb
index a36afc08673a3378a396e4cf9c2498bbcebce3c5..943f33754617ce5711d5ce0efa672f21d6d03a6e 100644
--- a/rubocop/cop_todo.rb
+++ b/rubocop/cop_todo.rb
@@ -26,10 +26,14 @@ def autocorrectable?
       @cop_class&.support_autocorrect?
     end
 
+    def generate?
+      previously_disabled || grace_period || files.any?
+    end
+
     def to_yaml
       yaml = []
       yaml << '---'
-      yaml << '# Cop supports --auto-correct.' if autocorrectable?
+      yaml << '# Cop supports --autocorrect.' if autocorrectable?
       yaml << "#{cop_name}:"
 
       if previously_disabled
@@ -39,8 +43,12 @@ def to_yaml
       end
 
       yaml << "  #{RuboCop::Formatter::GracefulFormatter.grace_period_key_value}" if grace_period
-      yaml << '  Exclude:'
-      yaml.concat files.sort.map { |file| "    - '#{file}'" }
+
+      if files.any?
+        yaml << '  Exclude:'
+        yaml.concat files.sort.map { |file| "    - '#{file}'" }
+      end
+
       yaml << ''
 
       yaml.join("\n")
diff --git a/rubocop/formatter/todo_formatter.rb b/rubocop/formatter/todo_formatter.rb
index b1c6d1c1688b220e6a43688f80324d1982640e2d..5e49e2dc082411d3815bf6a74c1e096826f250fc 100644
--- a/rubocop/formatter/todo_formatter.rb
+++ b/rubocop/formatter/todo_formatter.rb
@@ -31,6 +31,7 @@ def initialize(output, _options = {})
         @config_inspect_todo_dir = load_config_inspect_todo_dir
         @config_old_todo_yml = load_config_old_todo_yml
         check_multiple_configurations!
+        create_empty_todos(@config_inspect_todo_dir)
 
         super
       end
@@ -47,11 +48,9 @@ def file_finished(file, offenses)
 
       def finished(_inspected_files)
         @todos.values.sort_by(&:cop_name).each do |todo|
-          todo.previously_disabled = previously_disabled?(todo)
-          todo.grace_period = grace_period?(todo)
-          validate_todo!(todo)
-          path = @todo_dir.write(todo.cop_name, todo.to_yaml)
+          next unless configure_and_validate_todo(todo)
 
+          path = @todo_dir.write(todo.cop_name, todo.to_yaml)
           output.puts "Written to #{relative_path(path)}\n"
         end
       end
@@ -82,6 +81,14 @@ def check_multiple_configurations!
         raise "Multiple configurations found for cops:\n#{list}\n"
       end
 
+      # For each inspected cop TODO config create a TODO object to make sure
+      # the cop TODO config will be written even without any offenses.
+      def create_empty_todos(inspected_cop_config)
+        inspected_cop_config.each_key do |cop_name|
+          @todos[cop_name]
+        end
+      end
+
       def config_for(todo)
         cop_name = todo.cop_name
 
@@ -101,10 +108,15 @@ def grace_period?(todo)
         GracefulFormatter.grace_period?(todo.cop_name, config)
       end
 
-      def validate_todo!(todo)
-        return unless todo.previously_disabled && todo.grace_period
+      def configure_and_validate_todo(todo)
+        todo.previously_disabled = previously_disabled?(todo)
+        todo.grace_period = grace_period?(todo)
+
+        if todo.previously_disabled && todo.grace_period
+          raise "#{todo.cop_name}: Cop must be enabled to use `#{GracefulFormatter.grace_period_key_value}`."
+        end
 
-        raise "#{todo.cop_name}: Cop must be enabled to use `#{GracefulFormatter.grace_period_key_value}`."
+        todo.generate?
       end
 
       def load_config_inspect_todo_dir
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index a5806910b23f653ead7222dc4625ce7899af06f7..a442856d993704df7cacd69c8c67d3157f4b47cd 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -390,7 +390,10 @@
       'staging'            | described_class.tiers[:staging]
       'pre-prod'           | described_class.tiers[:staging]
       'blue-kit-stage'     | described_class.tiers[:staging]
-      'pre-prod'           | described_class.tiers[:staging]
+      'nonprod'            | described_class.tiers[:staging]
+      'nonlive'            | described_class.tiers[:staging]
+      'non-prod'           | described_class.tiers[:staging]
+      'non-live'           | described_class.tiers[:staging]
       'gprd'               | described_class.tiers[:production]
       'gprd-cny'           | described_class.tiers[:production]
       'production'         | described_class.tiers[:production]
diff --git a/spec/rubocop/cop_todo_spec.rb b/spec/rubocop/cop_todo_spec.rb
index 3f9c378b3036e2f71c0c8feff604c50287f86a01..c641001789fc29d3a2c2d94371f3e19cfee28fa9 100644
--- a/spec/rubocop/cop_todo_spec.rb
+++ b/spec/rubocop/cop_todo_spec.rb
@@ -66,6 +66,38 @@
     end
   end
 
+  describe '#generate?' do
+    subject { cop_todo.generate? }
+
+    context 'when empty todo' do
+      it { is_expected.to eq(false) }
+    end
+
+    context 'when previously disabled' do
+      before do
+        cop_todo.previously_disabled = true
+      end
+
+      it { is_expected.to eq(true) }
+    end
+
+    context 'when in grace period' do
+      before do
+        cop_todo.grace_period = true
+      end
+
+      it { is_expected.to eq(true) }
+    end
+
+    context 'with offenses recorded' do
+      before do
+        cop_todo.record('a.rb', 1)
+      end
+
+      it { is_expected.to eq(true) }
+    end
+  end
+
   describe '#to_yaml' do
     subject(:yaml) { cop_todo.to_yaml }
 
@@ -77,9 +109,8 @@
       specify do
         expect(yaml).to eq(<<~YAML)
           ---
-          # Cop supports --auto-correct.
+          # Cop supports --autocorrect.
           #{cop_name}:
-            Exclude:
         YAML
       end
     end
diff --git a/spec/rubocop/formatter/todo_formatter_spec.rb b/spec/rubocop/formatter/todo_formatter_spec.rb
index edd846324096a6da1d5ded21b591756b7fdafc40..5494d51860572005a48aa2a1a366d598ed056c3d 100644
--- a/spec/rubocop/formatter/todo_formatter_spec.rb
+++ b/spec/rubocop/formatter/todo_formatter_spec.rb
@@ -82,7 +82,7 @@ def run_formatter
 
       expect(todo_yml('B/AutoCorrect')).to eq(<<~YAML)
         ---
-        # Cop supports --auto-correct.
+        # Cop supports --autocorrect.
         B/AutoCorrect:
           Exclude:
             - 'd.rb'
@@ -309,18 +309,78 @@ def run_formatter
 
   context 'without offenses detected' do
     before do
+      todo_dir.write('A/Cop', yaml) if yaml
+      todo_dir.inspect_all
+
       formatter.started(%w[a.rb b.rb])
       formatter.file_finished('a.rb', [])
       formatter.file_finished('b.rb', [])
       formatter.finished(%w[a.rb b.rb])
+
+      todo_dir.delete_inspected
     end
 
-    it 'does not output anything' do
-      expect(stdout.string).to eq('')
+    context 'without existing TODOs' do
+      let(:yaml) { nil }
+
+      it 'does not output anything' do
+        expect(stdout.string).to eq('')
+      end
+
+      it 'does not write any YAML files' do
+        expect(rubocop_todo_dir_listing).to be_empty
+      end
     end
 
-    it 'does not write any YAML files' do
-      expect(rubocop_todo_dir_listing).to be_empty
+    context 'with existing TODOs' do
+      context 'when existing offenses only' do
+        let(:yaml) do
+          <<~YAML
+            ---
+            A/Cop:
+              Exclude:
+                - x.rb
+          YAML
+        end
+
+        it 'does not output anything' do
+          expect(stdout.string).to eq('')
+        end
+
+        it 'does not write any YAML files' do
+          expect(rubocop_todo_dir_listing).to be_empty
+        end
+      end
+
+      context 'when in grace period' do
+        let(:yaml) do
+          <<~YAML
+            ---
+            A/Cop:
+              Details: grace period
+              Exclude:
+                - x.rb
+          YAML
+        end
+
+        it 'outputs its actions' do
+          expect(stdout.string).to eq(<<~OUTPUT)
+            Written to .rubocop_todo/a/cop.yml
+          OUTPUT
+        end
+
+        it 'creates YAML file with Details only', :aggregate_failures do
+          expect(rubocop_todo_dir_listing).to contain_exactly(
+            'a/cop.yml'
+          )
+
+          expect(todo_yml('A/Cop')).to eq(<<~YAML)
+            ---
+            A/Cop:
+              Details: grace period
+          YAML
+        end
+      end
     end
   end