+ {{
+ s__(
+ 'Hierarchy|Is there a framework or type of work item you wish you had access to in GitLab? Give us your feedback and help us build the experiences valuable to you.',
+ )
+ }}
+
+
+
{{ s__('Hierarchy|Planning hierarchy') }}
+
+ {{
+ s__(
+ 'Hierarchy|Deliver value more efficiently by breaking down necessary work into a hierarchical structure. This structure helps teams understand scope, priorities, and how work cascades up toward larger goals.',
+ )
+ }}
+
+
+
{{ s__('Hierarchy|Current structure') }}
+
{{ s__('Hierarchy|You can start using these items now.') }}
+
+
+
+ {{ s__('Hierarchy|Unavailable structure') }}
+
+
+ {{ s__('Hierarchy|These items are unavailable in the current structure.') }}
+
+
diff --git a/app/assets/javascripts/work_items_hierarchy/constants.js b/app/assets/javascripts/work_items_hierarchy/constants.js
new file mode 100644
index 00000000000..f001f556c0e
--- /dev/null
+++ b/app/assets/javascripts/work_items_hierarchy/constants.js
@@ -0,0 +1,12 @@
+export const WORK_ITEMS_SURVEY_COOKIE_NAME = 'hide_work_items_hierarchy_survey';
+
+/**
+ * Hard-coded strings since we're rendering hierarchy
+ * items from mock responses. Remove this when we
+ * have a real hierarchy endpoint.
+ */
+export const LICENSE_PLAN = {
+ FREE: 'free',
+ PREMIUM: 'premium',
+ ULTIMATE: 'ultimate',
+};
diff --git a/app/assets/javascripts/work_items_hierarchy/hierarchy_util.js b/app/assets/javascripts/work_items_hierarchy/hierarchy_util.js
new file mode 100644
index 00000000000..61d93acdb91
--- /dev/null
+++ b/app/assets/javascripts/work_items_hierarchy/hierarchy_util.js
@@ -0,0 +1,10 @@
+import { LICENSE_PLAN } from './constants';
+
+export function inferLicensePlan({ hasSubEpics, hasEpics }) {
+ if (hasSubEpics) {
+ return LICENSE_PLAN.ULTIMATE;
+ } else if (hasEpics) {
+ return LICENSE_PLAN.PREMIUM;
+ }
+ return LICENSE_PLAN.FREE;
+}
diff --git a/app/assets/javascripts/work_items_hierarchy/static_response.js b/app/assets/javascripts/work_items_hierarchy/static_response.js
new file mode 100644
index 00000000000..d1e2e486082
--- /dev/null
+++ b/app/assets/javascripts/work_items_hierarchy/static_response.js
@@ -0,0 +1,142 @@
+const FREE_TIER = 'free';
+const ULTIMATE_TIER = 'ultimate';
+const PREMIUM_TIER = 'premium';
+
+const RESPONSE = {
+ [FREE_TIER]: [
+ {
+ id: '1',
+ type: 'ISSUE',
+ available: true,
+ license: null,
+ nestedTypes: null,
+ },
+ {
+ id: '2',
+ type: 'TASK',
+ available: true,
+ license: null,
+ nestedTypes: null,
+ },
+ {
+ id: '3',
+ type: 'INCIDENT',
+ available: true,
+ license: null,
+ nestedTypes: null,
+ },
+ {
+ id: '4',
+ type: 'EPIC',
+ available: false,
+ license: 'Premium', // eslint-disable-line @gitlab/require-i18n-strings
+ nestedTypes: null,
+ },
+ {
+ id: '5',
+ type: 'SUB_EPIC',
+ available: false,
+ license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
+ nestedTypes: null,
+ },
+ {
+ id: '6',
+ type: 'REQUIREMENT',
+ available: false,
+ license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
+ nestedTypes: null,
+ },
+ {
+ id: '7',
+ type: 'TEST_CASE',
+ available: false,
+ license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
+ nestedTypes: null,
+ },
+ ],
+
+ [PREMIUM_TIER]: [
+ {
+ id: '1',
+ type: 'EPIC',
+ available: true,
+ license: null,
+ nestedTypes: ['ISSUE'],
+ },
+ {
+ id: '2',
+ type: 'TASK',
+ available: true,
+ license: null,
+ nestedTypes: null,
+ },
+ {
+ id: '3',
+ type: 'INCIDENT',
+ available: true,
+ license: null,
+ nestedTypes: null,
+ },
+ {
+ id: '5',
+ type: 'SUB_EPIC',
+ available: false,
+ license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
+ nestedTypes: null,
+ },
+ {
+ id: '6',
+ type: 'REQUIREMENT',
+ available: false,
+ license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
+ nestedTypes: null,
+ },
+ {
+ id: '7',
+ type: 'TEST_CASE',
+ available: false,
+ license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
+ nestedTypes: null,
+ },
+ ],
+
+ [ULTIMATE_TIER]: [
+ {
+ id: '1',
+ type: 'EPIC',
+ available: true,
+ license: null,
+ nestedTypes: ['SUB_EPIC', 'ISSUE'],
+ },
+ {
+ id: '2',
+ type: 'TASK',
+ available: true,
+ license: null,
+ nestedTypes: null,
+ },
+ {
+ id: '3',
+ type: 'INCIDENT',
+ available: true,
+ license: null,
+ nestedTypes: null,
+ },
+ {
+ id: '6',
+ type: 'REQUIREMENT',
+ available: true,
+ license: null,
+ nestedTypes: null,
+ },
+ {
+ id: '7',
+ type: 'TEST_CASE',
+ available: true,
+ license: null,
+ nestedTypes: null,
+ },
+ ],
+};
+
+export default RESPONSE;
diff --git a/app/assets/javascripts/work_items_hierarchy/work_items_hierarchy_bundle.js b/app/assets/javascripts/work_items_hierarchy/work_items_hierarchy_bundle.js
new file mode 100644
index 00000000000..2258c725301
--- /dev/null
+++ b/app/assets/javascripts/work_items_hierarchy/work_items_hierarchy_bundle.js
@@ -0,0 +1,26 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import App from './components/app.vue';
+import { inferLicensePlan } from './hierarchy_util';
+
+export const initWorkItemsHierarchy = () => {
+ const el = document.querySelector('#js-work-items-hierarchy');
+
+ const { illustrationPath, hasEpics, hasSubEpics } = el.dataset;
+
+ const licensePlan = inferLicensePlan({
+ hasEpics: parseBoolean(hasEpics),
+ hasSubEpics: parseBoolean(hasSubEpics),
+ });
+
+ return new Vue({
+ el,
+ provide: {
+ illustrationPath,
+ licensePlan,
+ },
+ render(createElement) {
+ return createElement(App);
+ },
+ });
+};
diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss
index 8f3b5b3b7cc..072b78305a9 100644
--- a/app/assets/stylesheets/_page_specific_files.scss
+++ b/app/assets/stylesheets/_page_specific_files.scss
@@ -32,3 +32,4 @@
@import './pages/storage_quota';
@import './pages/tree';
@import './pages/users';
+@import './pages/hierarchy';
diff --git a/app/assets/stylesheets/pages/hierarchy.scss b/app/assets/stylesheets/pages/hierarchy.scss
new file mode 100644
index 00000000000..0812e4cc41e
--- /dev/null
+++ b/app/assets/stylesheets/pages/hierarchy.scss
@@ -0,0 +1,15 @@
+.hierarchy-rounded-arrow-tail {
+ position: absolute;
+ top: 4px;
+ left: 5px;
+ height: calc(100% - 20px);
+}
+
+.hierarchy-icon-wrapper {
+ height: $default-icon-size;
+ width: $default-icon-size;
+}
+
+.hierarchy-rounded-arrow {
+ transform: scale(1, -1) rotate(90deg);
+}
diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss
index e616996bd14..3ed257caf60 100644
--- a/app/assets/stylesheets/startup/startup-signin.scss
+++ b/app/assets/stylesheets/startup/startup-signin.scss
@@ -772,6 +772,9 @@ svg {
.gl-mt-2 {
margin-top: 0.25rem;
}
+.gl-mt-5 {
+ margin-top: 1rem;
+}
.gl-mb-3 {
margin-bottom: 0.5rem;
}
diff --git a/app/controllers/concerns/work_items_hierarchy.rb b/app/controllers/concerns/work_items_hierarchy.rb
new file mode 100644
index 00000000000..6008256408c
--- /dev/null
+++ b/app/controllers/concerns/work_items_hierarchy.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module WorkItemsHierarchy
+ extend ActiveSupport::Concern
+
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def planning_hierarchy
+ return render_404 unless Feature.enabled?(:work_items_hierarchy, @project, default_enabled: :yaml)
+
+ render 'shared/planning_hierarchy'
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+end
+
+WorkItemsHierarchy.prepend_mod_with('WorkItemsHierarchy')
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 04dde5ef7b2..27483d455b1 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -9,6 +9,7 @@ class ProjectsController < Projects::ApplicationController
include RecordUserLastActivity
include ImportUrlParams
include FiltersEvents
+ include WorkItemsHierarchy
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
@@ -52,6 +53,7 @@ class ProjectsController < Projects::ApplicationController
feature_category :team_planning, [:preview_markdown, :new_issuable_address]
feature_category :importers, [:export, :remove_export, :generate_new_export, :download_export]
feature_category :code_review, [:unfoldered_environment_names]
+ feature_category :portfolio_management, [:planning_hierarchy]
urgency :low, [:refs]
urgency :high, [:unfoldered_environment_names]
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index c988a232a87..f4067552f55 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -27,7 +27,6 @@ module Types
field :description, GraphQL::Types::String, null: true,
description: 'Short description of the project.'
- markdown_field :description_html, null: true
field :tag_list, GraphQL::Types::String, null: true,
deprecated: { reason: 'Use `topics`', milestone: '13.12' },
@@ -75,21 +74,6 @@ module Types
field :avatar_url, GraphQL::Types::String, null: true, calls_gitaly: true,
description: 'URL to avatar image file of the project.'
- {
- issues: "Issues are",
- merge_requests: "Merge Requests are",
- wiki: 'Wikis are',
- snippets: 'Snippets are',
- container_registry: 'Container Registry is'
- }.each do |feature, name_string|
- field "#{feature}_enabled", GraphQL::Types::Boolean, null: true,
- description: "Indicates if #{name_string} enabled for the current user"
-
- define_method "#{feature}_enabled" do
- object.feature_available?(feature, context[:current_user])
- end
- end
-
field :jobs_enabled, GraphQL::Types::Boolean, null: true,
description: 'Indicates if CI/CD pipeline jobs are enabled for the current user.'
@@ -391,15 +375,6 @@ module Types
null: true,
description: 'Template used to create squash commit message in merge requests.'
- def label(title:)
- BatchLoader::GraphQL.for(title).batch(key: project) do |titles, loader, args|
- LabelsFinder
- .new(current_user, project: args[:key], title: titles)
- .execute
- .each { |label| loader.call(label.title, label) }
- end
- end
-
field :labels,
Types::LabelType.connection_type,
null: true,
@@ -411,6 +386,32 @@ module Types
description: 'Work item types available to the project.',
feature_flag: :work_items
+ def label(title:)
+ BatchLoader::GraphQL.for(title).batch(key: project) do |titles, loader, args|
+ LabelsFinder
+ .new(current_user, project: args[:key], title: titles)
+ .execute
+ .each { |label| loader.call(label.title, label) }
+ end
+ end
+
+ {
+ issues: "Issues are",
+ merge_requests: "Merge Requests are",
+ wiki: 'Wikis are',
+ snippets: 'Snippets are',
+ container_registry: 'Container Registry is'
+ }.each do |feature, name_string|
+ field "#{feature}_enabled", GraphQL::Types::Boolean, null: true,
+ description: "Indicates if #{name_string} enabled for the current user"
+
+ define_method "#{feature}_enabled" do
+ object.feature_available?(feature, context[:current_user])
+ end
+ end
+
+ markdown_field :description_html, null: true
+
def avatar_url
object.avatar_url(only_path: false)
end
diff --git a/app/graphql/types/projects/topic_type.rb b/app/graphql/types/projects/topic_type.rb
index 79ab69e794b..c579f2f2b9d 100644
--- a/app/graphql/types/projects/topic_type.rb
+++ b/app/graphql/types/projects/topic_type.rb
@@ -14,11 +14,12 @@ module Types
field :description, GraphQL::Types::String, null: true,
description: 'Description of the topic.'
- markdown_field :description_html, null: true
field :avatar_url, GraphQL::Types::String, null: true,
description: 'URL to avatar image file of the topic.'
+ markdown_field :description_html, null: true
+
def avatar_url
object.avatar_url(only_path: false)
end
diff --git a/app/graphql/types/release_type.rb b/app/graphql/types/release_type.rb
index fcc9ec49252..fbc3779ea9b 100644
--- a/app/graphql/types/release_type.rb
+++ b/app/graphql/types/release_type.rb
@@ -20,7 +20,6 @@ module Types
authorize: :download_code
field :description, GraphQL::Types::String, null: true,
description: 'Description (also known as "release notes") of the release.'
- markdown_field :description_html, null: true
field :name, GraphQL::Types::String, null: true,
description: 'Name of the release.'
field :created_at, Types::TimeType, null: true,
@@ -42,14 +41,16 @@ module Types
field :author, Types::UserType, null: true,
description: 'User that created the release.'
- def author
- Gitlab::Graphql::Loaders::BatchModelLoader.new(User, release.author_id).find
- end
-
field :commit, Types::CommitType, null: true,
complexity: 10, calls_gitaly: true,
description: 'Commit associated with the release.'
+ markdown_field :description_html, null: true
+
+ def author
+ Gitlab::Graphql::Loaders::BatchModelLoader.new(User, release.author_id).find
+ end
+
def commit
return if release.sha.nil?
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index de696c2ec87..084c962d34c 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -672,18 +672,16 @@ module ProjectsHelper
html_escape(message) % { strong_start: strong_start, strong_end: strong_end, project_name: project.name, group_name: project.group ? project.group.name : nil }
end
- def visibility_confirm_modal_data(project, remove_form_id = nil)
+ def visibility_confirm_modal_data(project, target_form_id = nil)
{
- remove_form_id: remove_form_id,
- qa_selector: 'visibility_features_permissions_save_button',
- button_text: _('Save changes'),
+ target_form_id: target_form_id,
button_testid: 'reduce-project-visibility-button',
- button_variant: 'confirm',
confirm_button_text: _('Reduce project visibility'),
confirm_danger_message: confirm_reduce_visibility_message(project),
phrase: project.full_path,
additional_information: _('Note: current forks will keep their visibility level.'),
- html_confirmation_message: true
+ html_confirmation_message: true.to_s,
+ show_visibility_confirm_modal: show_visibility_confirm_modal?(project).to_s
}
end
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
index c0c2ea42d46..c8a0b247b59 100644
--- a/app/models/project_ci_cd_setting.rb
+++ b/app/models/project_ci_cd_setting.rb
@@ -3,7 +3,7 @@
class ProjectCiCdSetting < ApplicationRecord
belongs_to :project, inverse_of: :ci_cd_settings
- DEFAULT_GIT_DEPTH = 50
+ DEFAULT_GIT_DEPTH = 20
before_create :set_default_git_depth
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 55f43cd9f7b..d89a449bdd5 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -240,6 +240,7 @@ class ProjectPolicy < BasePolicy
enable :read_wiki
enable :read_issue
enable :read_label
+ enable :read_work_items_hierarchy
enable :read_milestone
enable :read_snippet
enable :read_project_member
@@ -572,6 +573,7 @@ class ProjectPolicy < BasePolicy
enable :read_issue_board_list
enable :read_wiki
enable :read_label
+ enable :read_work_items_hierarchy
enable :read_milestone
enable :read_snippet
enable :read_project_member
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 15143684b8b..982171b9e34 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -62,7 +62,7 @@
%div
- if show_recaptcha_sign_up?
= recaptcha_tags nonce: content_security_policy_nonce
- .submit-container
+ .submit-container.gl-mt-5
= f.submit button_text, class: 'btn gl-button btn-confirm gl-display-block gl-w-full', data: { qa_selector: 'new_user_register_button' }
= render 'devise/shared/terms_of_service_notice', button_text: button_text
- if show_omniauth_providers && omniauth_providers_placement == :bottom
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index a8275576327..887d07f7a20 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -32,64 +32,5 @@
type_plural: type_plural,
active_tokens: @active_personal_access_tokens,
revoke_route_helper: ->(token) { revoke_profile_personal_access_token_path(token) }
-- if Feature.enabled?(:hide_access_tokens, default_enabled: :yaml)
- #js-tokens-app{ data: { tokens_data: tokens_app_data } }
-- else
- - unless Gitlab::CurrentSettings.disable_feed_token
- .col-lg-12
- %hr
- .row.gl-mt-3.js-search-settings-section
- .col-lg-4.profile-settings-sidebar
- %h4.gl-mt-0
- = s_('AccessTokens|Feed token')
- %p
- = s_('AccessTokens|Your feed token authenticates you when your RSS reader loads a personalized RSS feed or when your calendar application loads a personalized calendar. It is visible in those feed URLs.')
- %p
- = s_('AccessTokens|It cannot be used to access any other data.')
- .col-lg-8.feed-token-reset
- = label_tag :feed_token, s_('AccessTokens|Feed token'), class: 'label-bold'
- = text_field_tag :feed_token, current_user.feed_token, class: 'form-control gl-form-input js-select-on-focus', readonly: true
- %p.form-text.text-muted
- - reset_link = link_to s_('AccessTokens|reset this token'), [:reset, :feed_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any RSS or calendar URLs currently in use will stop working.'), testid: :reset_feed_token_link }
- - reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you. If that happens, %{link_reset_it}.') % { link_reset_it: reset_link }
- = reset_message.html_safe
- - if incoming_email_token_enabled?
- .col-lg-12
- %hr
- .row.gl-mt-3.js-search-settings-section
- .col-lg-4.profile-settings-sidebar
- %h4.gl-mt-0
- = s_('AccessTokens|Incoming email token')
- %p
- = s_('AccessTokens|Your incoming email token authenticates you when you create a new issue by email, and is included in your personal project-specific email addresses.')
- %p
- = s_('AccessTokens|It cannot be used to access any other data.')
- .col-lg-8.incoming-email-token-reset
- = label_tag :incoming_email_token, s_('AccessTokens|Incoming email token'), class: 'label-bold'
- = text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control gl-form-input js-select-on-focus', readonly: true
- %p.form-text.text-muted
- - reset_link = link_to s_('AccessTokens|reset this token'), [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any issue email addresses currently in use will stop working.'), testid: :reset_email_token_link }
- - reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can create issues as if they were you. If that happens, %{link_reset_it}.') % { link_reset_it: reset_link }
- = reset_message.html_safe
-
- - if static_objects_external_storage_enabled?
- .col-lg-12
- %hr
- .row.gl-mt-3.js-search-settings-section
- .col-lg-4
- %h4.gl-mt-0
- = s_('AccessTokens|Static object token')
- %p
- = s_('AccessTokens|Your static object token authenticates you when repository static objects (such as archives or blobs) are served from an external storage.')
- %p
- = s_('AccessTokens|It cannot be used to access any other data.')
- .col-lg-8
- = label_tag :static_object_token, s_('AccessTokens|Static object token'), class: "label-bold"
- = text_field_tag :static_object_token, current_user.static_object_token, class: 'form-control gl-form-input', readonly: true, onclick: 'this.select()'
- %p.form-text.text-muted
- - reset_link = url_for [:reset, :static_object_token, :profile]
- - reset_link_start = ''.html_safe % { confirm: s_('AccessTokens|Are you sure?'), url: reset_link }
- - reset_link_end = ''.html_safe
- - reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can access repository static objects as if they were you. If that ever happens, %{reset_link_start}reset this token%{reset_link_end}.') % { reset_link_start: reset_link_start, reset_link_end: reset_link_end }
- = reset_message.html_safe
+#js-tokens-app{ data: { tokens_data: tokens_app_data } }
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 1cb935307af..aa9a3ea61f7 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -2,7 +2,7 @@
- page_title _("General")
- @content_class = "limit-container-width" unless fluid_layout
- expanded = expanded_by_default?
-- remove_visibility_form_id = 'reduce-visibility-form'
+- reduce_visibility_form_id = 'reduce-visibility-form'
%section.settings.general-settings.no-animate.expanded#js-general-settings
.settings-header
@@ -18,11 +18,10 @@
%p= _('Choose visibility level, enable/disable project features and their permissions, disable email notifications, and show default award emoji.')
.settings-content
- = form_for @project, html: { multipart: true, class: "sharing-permissions-form", id: remove_visibility_form_id }, authenticity_token: true do |f|
+ = form_for @project, html: { multipart: true, class: "sharing-permissions-form", id: reduce_visibility_form_id }, authenticity_token: true do |f|
%input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' }
%template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project).to_json.html_safe
- .js-project-permissions-form
- = f.submit _('Save changes'), class: "btn gl-button btn-confirm #{('js-confirm-danger' if show_visibility_confirm_modal?(@project))}", data: visibility_confirm_modal_data(@project, remove_visibility_form_id)
+ .js-project-permissions-form{ data: visibility_confirm_modal_data(@project, reduce_visibility_form_id) }
%section.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)], data: { qa_selector: 'merge_request_settings_content' } }
.settings-header
diff --git a/app/views/shared/planning_hierarchy.html.haml b/app/views/shared/planning_hierarchy.html.haml
new file mode 100644
index 00000000000..d67ecc6ee48
--- /dev/null
+++ b/app/views/shared/planning_hierarchy.html.haml
@@ -0,0 +1,5 @@
+- page_title _("Planning hierarchy")
+- has_sub_epics = Gitlab.ee? && @project&.feature_available?(:subepics)
+- has_epics = Gitlab.ee? && @project&.feature_available?(:epics)
+
+#js-work-items-hierarchy{ data: { has_sub_epics: has_sub_epics.to_s, has_epics: has_epics.to_s, illustration_path: image_path('illustrations/rocket-launch-md.svg') } }
diff --git a/app/views/shared/web_hooks/_hook.html.haml b/app/views/shared/web_hooks/_hook.html.haml
index 45baa7e2184..c5a03ef4dc1 100644
--- a/app/views/shared/web_hooks/_hook.html.haml
+++ b/app/views/shared/web_hooks/_hook.html.haml
@@ -1,22 +1,23 @@
+- sslStatus = hook.enable_ssl_verification ? _('enabled') : _('disabled')
+- sslBadgeText = _('SSL Verification:') + ' ' + sslStatus
+
%li
.row
.col-md-8.col-lg-7
%strong.light-header
= hook.url
- if hook.rate_limited?
- %span.gl-badge.badge-danger.badge-pill.sm= _('Disabled')
+ = gl_badge_tag(_('Disabled'), variant: :danger, size: :sm)
- elsif hook.permanently_disabled?
- %span.gl-badge.badge-danger.badge-pill.sm= s_('Webhooks|Failed to connect')
+ = gl_badge_tag(s_('Webhooks|Failed to connect'), variant: :danger, size: :sm)
- elsif hook.temporarily_disabled?
- %span.gl-badge.badge-warning.badge-pill.sm= s_('Webhooks|Fails to connect')
+ = gl_badge_tag(s_('Webhooks|Fails to connect'), variant: :warning, size: :sm)
%div
- hook.class.triggers.each_value do |trigger|
- if hook.public_send(trigger)
- %span.gl-badge.badge-muted.badge-pill.sm.gl-mt-2.deploy-project-label= trigger.to_s.titleize
- %span.gl-badge.badge-muted.badge-pill.sm.gl-mt-2
- = _('SSL Verification:')
- = hook.enable_ssl_verification ? _('enabled') : _('disabled')
+ = gl_badge_tag(trigger.to_s.titleize, size: :sm)
+ = gl_badge_tag(sslBadgeText, size: :sm)
.col-md-4.col-lg-5.text-right-md.gl-mt-2
%span>= render 'shared/web_hooks/test_button', hook: hook, button_class: 'btn-sm btn-default gl-mr-3'
diff --git a/config/feature_flags/development/hide_access_tokens.yml b/config/feature_flags/development/work_items_hierarchy.yml
similarity index 55%
rename from config/feature_flags/development/hide_access_tokens.yml
rename to config/feature_flags/development/work_items_hierarchy.yml
index 1607780a0d0..8699511ea63 100644
--- a/config/feature_flags/development/hide_access_tokens.yml
+++ b/config/feature_flags/development/work_items_hierarchy.yml
@@ -1,8 +1,8 @@
---
-name: hide_access_tokens
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76280
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/347490
-milestone: '14.6'
+name: work_items_hierarchy
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76720
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350451
+milestone: '14.7'
type: development
-group: group::access
-default_enabled: true
+group: group::product planning
+default_enabled: false
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 702ef64a2ca..004206e0ae2 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -641,6 +641,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
post :generate_new_export
get :download_export
get :activity
+ get :planning_hierarchy
get :refs
put :new_issuable_address
get :unfoldered_environment_names
diff --git a/data/deprecations/14-7-pseudonymizer.yml b/data/deprecations/14-7-pseudonymizer.yml
index 34aecb55bd7..bd8cb215496 100644
--- a/data/deprecations/14-7-pseudonymizer.yml
+++ b/data/deprecations/14-7-pseudonymizer.yml
@@ -1,8 +1,8 @@
- name: "Pseudonymizer" # The name of the feature to be deprecated
announcement_milestone: "14.7" # The milestone when this feature was first announced as deprecated.
- announcement_date: "2021-01-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
+ announcement_date: "2022-01-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_milestone: "15.0" # The milestone when this feature is planned to be removed
- removal_date: "2021-05-22" # This should almost always be the 22nd of a month (YYYY-MM-22), the date of the milestone release when this feature is planned to be removed.
+ removal_date: "2022-05-22" # This should almost always be the 22nd of a month (YYYY-MM-22), the date of the milestone release when this feature is planned to be removed.
body: | # Do not modify this line, instead modify the lines below.
The Pseudonymizer feature is generally unused,
can cause production issues with large databases,
diff --git a/data/deprecations/14-7-sidekiq-metrics-health-check-donfig.yml b/data/deprecations/14-7-sidekiq-metrics-health-check-donfig.yml
new file mode 100644
index 00000000000..e81c85a820f
--- /dev/null
+++ b/data/deprecations/14-7-sidekiq-metrics-health-check-donfig.yml
@@ -0,0 +1,30 @@
+- name: "Sidekiq metrics and health checks configuration"
+ announcement_milestone: "14.7"
+ announcement_date: "2021-01-22"
+ removal_milestone: "15.0"
+ removal_date: "2022-05-22"
+ breaking_change: true
+ body: | # Do not modify this line, instead modify the lines below.
+ Exporting Sidekiq metrics and health checks using a single process and port is deprecated.
+ Support will be removed in 15.0.
+
+ We have updated Sidekiq to export [metrics and health checks from two separate processes](https://gitlab.com/groups/gitlab-org/-/epics/6409)
+ to improve stability and availability and prevent data loss in edge cases.
+ As those are two separate servers, a configuration change will be required in 15.0
+ to explicitly set separate ports for metrics and health-checks.
+ The newly introduced settings for `sidekiq['health_checks_*']`
+ should always be set in `gitlab.rb`.
+ For more information, check the documentation for [configuring Sidekiq](https://docs.gitlab.com/ee/administration/sidekiq.html).
+
+ These changes also require updates in either Prometheus to scrape the new endpoint or k8s health-checks to target the new
+ health-check port to work properly, otherwise either metrics or health-checks will disappear.
+
+ For the deprecation period those settings are optional
+ and GitLab will default the Sidekiq health-checks port to the same port as `sidekiq_exporter`
+ and only run one server (not changing the current behaviour).
+ Only if they are both set and a different port is provided, a separate metrics server will spin up
+ to serve the Sidekiq metrics, similar to the way Sidekiq will behave in 15.0.
+ stage: Enablement
+ tiers: [Free, Premium, Ultimate]
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/347509
+ documentation_url: https://docs.gitlab.com/ee/administration/sidekiq.html
diff --git a/db/post_migrate/20211206161271_add_indexes_for_primary_email_cleanup_migration.rb b/db/post_migrate/20211206161271_add_indexes_for_primary_email_cleanup_migration.rb
new file mode 100644
index 00000000000..0e370dfc5f8
--- /dev/null
+++ b/db/post_migrate/20211206161271_add_indexes_for_primary_email_cleanup_migration.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class AddIndexesForPrimaryEmailCleanupMigration < Gitlab::Database::Migration[1.0]
+ USERS_INDEX = :index_users_on_id_for_primary_email_migration
+ EMAIL_INDEX = :index_emails_on_email_user_id
+
+ disable_ddl_transaction!
+
+ def up
+ unless index_exists_by_name?(:users, USERS_INDEX)
+
+ disable_statement_timeout do
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY #{USERS_INDEX}
+ ON users (id) INCLUDE (email, confirmed_at)
+ WHERE confirmed_at IS NOT NULL
+ SQL
+ end
+ end
+
+ add_concurrent_index :emails, [:email, :user_id], name: EMAIL_INDEX
+ end
+
+ def down
+ remove_concurrent_index_by_name :users, USERS_INDEX
+ remove_concurrent_index_by_name :emails, EMAIL_INDEX
+ end
+end
diff --git a/db/post_migrate/20211206162601_cleanup_after_add_primary_email_to_emails_if_user_confirmed.rb b/db/post_migrate/20211206162601_cleanup_after_add_primary_email_to_emails_if_user_confirmed.rb
new file mode 100644
index 00000000000..14f6c751e4d
--- /dev/null
+++ b/db/post_migrate/20211206162601_cleanup_after_add_primary_email_to_emails_if_user_confirmed.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+class CleanupAfterAddPrimaryEmailToEmailsIfUserConfirmed < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ MIGRATION_NAME = 'AddPrimaryEmailToEmailsIfUserConfirmed'
+ BATCH_SIZE = 10_000
+
+ # Stubbed class to access the User table
+ class User < ActiveRecord::Base
+ include ::EachBatch
+
+ self.table_name = 'users'
+ self.inheritance_column = :_type_disabled
+
+ scope :confirmed, -> { where.not(confirmed_at: nil) }
+
+ has_many :emails
+ end
+
+ # Stubbed class to access the Emails table
+ class Email < ActiveRecord::Base
+ self.table_name = 'emails'
+ self.inheritance_column = :_type_disabled
+
+ belongs_to :user
+ end
+
+ def up
+ finalize_background_migration(MIGRATION_NAME)
+
+ # Select confirmed users that do not have their primary email in the emails table,
+ # and create the email record. There should be none if the background migration
+ # completed, but in case there is any leftover, we deal with it synchronously.
+ not_exists_condition = 'NOT EXISTS (SELECT 1 FROM emails WHERE emails.email = users.email AND emails.user_id = users.id)'
+
+ User.confirmed.each_batch(of: BATCH_SIZE) do |user_batch|
+ user_batch.select(:id, :email, :confirmed_at).where(not_exists_condition).each do |user|
+ current_time = Time.now.utc
+
+ begin
+ Email.create(
+ user_id: user.id,
+ email: user.email,
+ confirmed_at: user.confirmed_at,
+ created_at: current_time,
+ updated_at: current_time
+ )
+ rescue StandardError => error
+ Gitlab::AppLogger.error("Could not add primary email #{user.email} to emails for user with ID #{user.id} due to #{error}")
+ end
+ end
+ end
+ end
+
+ def down
+ # Intentionally left blank
+ end
+end
diff --git a/db/post_migrate/20211220064757_drop_temporary_indexes_for_primary_email_migration.rb b/db/post_migrate/20211220064757_drop_temporary_indexes_for_primary_email_migration.rb
new file mode 100644
index 00000000000..1d61aec401e
--- /dev/null
+++ b/db/post_migrate/20211220064757_drop_temporary_indexes_for_primary_email_migration.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class DropTemporaryIndexesForPrimaryEmailMigration < Gitlab::Database::Migration[1.0]
+ USERS_INDEX = :index_users_on_id_for_primary_email_migration
+ EMAIL_INDEX = :index_emails_on_email_user_id
+
+ disable_ddl_transaction!
+
+ def up
+ remove_concurrent_index_by_name :users, USERS_INDEX
+ remove_concurrent_index_by_name :emails, EMAIL_INDEX
+ end
+
+ def down
+ unless index_exists_by_name?(:users, USERS_INDEX)
+
+ disable_statement_timeout do
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY #{USERS_INDEX}
+ ON users (id) INCLUDE (email, confirmed_at)
+ WHERE confirmed_at IS NOT NULL
+ SQL
+ end
+ end
+
+ add_concurrent_index :emails, [:email, :user_id], name: EMAIL_INDEX
+ end
+end
diff --git a/db/schema_migrations/20211206161271 b/db/schema_migrations/20211206161271
new file mode 100644
index 00000000000..8a2561eec24
--- /dev/null
+++ b/db/schema_migrations/20211206161271
@@ -0,0 +1 @@
+fa4a39c3bea70d31e8144f8830ef0353f22a7a663a891d9043e79f362058fbde
\ No newline at end of file
diff --git a/db/schema_migrations/20211206162601 b/db/schema_migrations/20211206162601
new file mode 100644
index 00000000000..5e19e21507d
--- /dev/null
+++ b/db/schema_migrations/20211206162601
@@ -0,0 +1 @@
+529c7ea38bbaa0c29491c2dfdb654a4a6adba93122d9bc23d6632526ff7fdb05
\ No newline at end of file
diff --git a/db/schema_migrations/20211220064757 b/db/schema_migrations/20211220064757
new file mode 100644
index 00000000000..675596f1ce6
--- /dev/null
+++ b/db/schema_migrations/20211220064757
@@ -0,0 +1 @@
+34bfe07fff59a415540ca2c5c96b33dc9030c15b2ffbb30cb7deedeb939ae132
\ No newline at end of file
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index 92167953441..0d7635405d6 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -28,14 +28,14 @@ A logger emits a log message only if its log level is equal to or above the mini
The following log levels are supported:
-| Level | Name |
-|-------|---------|
-| 0 | DEBUG |
-| 1 | INFO |
-| 2 | WARN |
-| 3 | ERROR |
-| 4 | FATAL |
-| 5 | UNKNOWN |
+| Level | Name |
+|:------|:----------|
+| 0 | `DEBUG` |
+| 1 | `INFO` |
+| 2 | `WARN` |
+| 3 | `ERROR` |
+| 4 | `FATAL` |
+| 5 | `UNKNOWN` |
GitLab loggers emit all log messages because they are set to `DEBUG` by default.
@@ -53,8 +53,8 @@ GITLAB_LOG_LEVEL=info
For some services, other log levels are in place that are not affected by this setting.
Some of these services have their own environment variables to override the log level. For example:
-| Service | Log Level | Environment variable |
-|----------------------|-----------|----------------------|
+| Service | Log level | Environment variable |
+|:---------------------|:----------|:---------------------|
| GitLab API | `INFO` | |
| GitLab Cleanup | `INFO` | `DEBUG` |
| GitLab Doctor | `INFO` | `VERBOSE` |
@@ -84,26 +84,26 @@ are written to a file called `current`. The `logrotate` service built into GitLa
[manages all logs](https://docs.gitlab.com/omnibus/settings/logs.html#logrotate)
except those captured by `runit`.
-| Log type | Managed by logrotate | Managed by svlogd/runit |
-|-------------------------------------------------|------------------------|-------------------------|
-| [Alertmanager Logs](#alertmanager-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
-| [Crond Logs](#crond-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
-| [Gitaly](#gitaly-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
-| [GitLab Exporter for Omnibus](#gitlab-exporter) | **{dotted-circle}** No | **{check-circle}** Yes |
-| [GitLab Pages Logs](#pages-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
-| GitLab Rails | **{check-circle}** Yes | **{dotted-circle}** No |
-| [GitLab Shell Logs](#gitlab-shelllog) | **{check-circle}** Yes | **{dotted-circle}** No |
-| [Grafana Logs](#grafana-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
-| [LogRotate Logs](#logrotate-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
-| [Mailroom](#mail_room_jsonlog-default) | **{check-circle}** Yes | **{check-circle}** Yes |
-| [NGINX](#nginx-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
-| [PostgreSQL Logs](#postgresql-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
-| [Praefect Logs](#praefect-logs) | **{dotted-circle}** Yes| **{check-circle}** Yes |
-| [Prometheus Logs](#prometheus-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
-| [Puma](#puma-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
-| [Redis Logs](#redis-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
-| [Registry Logs](#registry-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
-| [Workhorse Logs](#workhorse-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
+| Log type | Managed by logrotate | Managed by svlogd/runit |
+|:------------------------------------------------|:------------------------|:------------------------|
+| [Alertmanager Logs](#alertmanager-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
+| [Crond Logs](#crond-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
+| [Gitaly](#gitaly-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
+| [GitLab Exporter for Omnibus](#gitlab-exporter) | **{dotted-circle}** No | **{check-circle}** Yes |
+| [GitLab Pages Logs](#pages-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
+| GitLab Rails | **{check-circle}** Yes | **{dotted-circle}** No |
+| [GitLab Shell Logs](#gitlab-shelllog) | **{check-circle}** Yes | **{dotted-circle}** No |
+| [Grafana Logs](#grafana-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
+| [LogRotate Logs](#logrotate-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
+| [Mailroom](#mail_room_jsonlog-default) | **{check-circle}** Yes | **{check-circle}** Yes |
+| [NGINX](#nginx-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
+| [PostgreSQL Logs](#postgresql-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
+| [Praefect Logs](#praefect-logs) | **{dotted-circle}** Yes | **{check-circle}** Yes |
+| [Prometheus Logs](#prometheus-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
+| [Puma](#puma-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
+| [Redis Logs](#redis-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
+| [Registry Logs](#registry-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
+| [Workhorse Logs](#workhorse-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
## `production_json.log`
diff --git a/doc/administration/operations/fast_ssh_key_lookup.md b/doc/administration/operations/fast_ssh_key_lookup.md
index c99f94589d7..ddedb3fe76a 100644
--- a/doc/administration/operations/fast_ssh_key_lookup.md
+++ b/doc/administration/operations/fast_ssh_key_lookup.md
@@ -27,13 +27,13 @@ lookup of authorized SSH keys.
WARNING:
OpenSSH version 6.9+ is required because `AuthorizedKeysCommand` must be
-able to accept a fingerprint. Check the version of OpenSSH on your server.
+able to accept a fingerprint. Check the version of OpenSSH on your server with `sshd -V`.
## Fast lookup is required for Geo **(PREMIUM)**
By default, GitLab manages an `authorized_keys` file that is located in the
`git` user's home directory. For most installations, this will be located under
-`/var/opt/gitlab/.ssh/authorized_keys`, but you can use the following command to locate the `authorized_keys` on your system.:
+`/var/opt/gitlab/.ssh/authorized_keys`, but you can use the following command to locate the `authorized_keys` on your system:
```shell
getent passwd git | cut -d: -f6 | awk '{print $1"/.ssh/authorized_keys"}'
@@ -77,9 +77,13 @@ sudo service sshd reload
```
Confirm that SSH is working by commenting out your user's key in the `authorized_keys`
-file (start the line with a `#` to comment it), and attempting to pull a repository.
+file (start the line with a `#` to comment it), and from your local machine, attempt to pull a repository or run:
-A successful pull would mean that GitLab was able to find the key in the database,
+```shell
+ssh -T git@gitlab.example.com
+```
+
+A successful pull or [welcome message](../../ssh/index.md#verify-that-you-can-connect) would mean that GitLab was able to find the key in the database,
since it is not present in the file anymore.
NOTE:
@@ -114,7 +118,7 @@ adding a new one, and attempting to pull a repository.
Then you can backup and delete your `authorized_keys` file for best performance.
The current users' keys are already present in the database, so there is no need for migration
-or for asking users to re-add their keys.
+or for users to re-add their keys.
## How to go back to using the `authorized_keys` file
diff --git a/doc/ci/pipelines/settings.md b/doc/ci/pipelines/settings.md
index cb225a306c9..85824dfb7c7 100644
--- a/doc/ci/pipelines/settings.md
+++ b/doc/ci/pipelines/settings.md
@@ -159,7 +159,8 @@ in the `.gitlab-ci.yml` file.
## Limit the number of changes fetched during clone
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/28919) in GitLab 12.0.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/28919) in GitLab 12.0.
+> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77576) `git depth` value in GitLab 14.7.
You can limit the number of changes that GitLab CI/CD fetches when it clones
a repository.
@@ -171,8 +172,8 @@ a repository.
The maximum value is `1000`. To disable shallow clone and make GitLab CI/CD
fetch all branches and tags each time, keep the value empty or set to `0`.
-In GitLab 12.0 and later, newly created projects automatically have a default
-`git depth` value of `50`.
+In GitLab versions 14.7 and later, newly created projects have a default `git depth`
+value of `20`. GitLab versions 14.6 and earlier have a default `git depth` value of `50`.
This value can be overridden by the [`GIT_DEPTH` variable](../large_repositories/index.md#shallow-cloning)
in the `.gitlab-ci.yml` file.
diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md
index c1e00c2f57b..00675fe5cd4 100644
--- a/doc/update/deprecations.md
+++ b/doc/update/deprecations.md
@@ -335,7 +335,7 @@ can cause production issues with large databases,
and can interfere with object storage development.
It is now considered deprecated, and will be removed in GitLab 15.0.
-Planned removal milestone: 15.0 (2021-05-22)
+Planned removal milestone: 15.0 (2022-05-22)
### Removal of Static Site Editor
@@ -352,6 +352,30 @@ only supported report file in 15.0, but this is the first step towards GitLab su
Planned removal milestone: 15.0 (2022-05-22)
+### Sidekiq metrics and health checks configuration
+
+Exporting Sidekiq metrics and health checks using a single process and port is deprecated.
+Support will be removed in 15.0.
+
+We have updated Sidekiq to export [metrics and health checks from two separate processes](https://gitlab.com/groups/gitlab-org/-/epics/6409)
+to improve stability and availability and prevent data loss in edge cases.
+As those are two separate servers, a configuration change will be required in 15.0
+to explicitly set separate ports for metrics and health-checks.
+The newly introduced settings for `sidekiq['health_checks_*']`
+should always be set in `gitlab.rb`.
+For more information, check the documentation for [configuring Sidekiq](https://docs.gitlab.com/ee/administration/sidekiq.html).
+
+These changes also require updates in either Prometheus to scrape the new endpoint or k8s health-checks to target the new
+health-check port to work properly, otherwise either metrics or health-checks will disappear.
+
+For the deprecation period those settings are optional
+and GitLab will default the Sidekiq health-checks port to the same port as `sidekiq_exporter`
+and only run one server (not changing the current behaviour).
+Only if they are both set and a different port is provided, a separate metrics server will spin up
+to serve the Sidekiq metrics, similar to the way Sidekiq will behave in 15.0.
+
+Planned removal milestone: 15.0 (2022-05-22)
+
### Tracing in GitLab
Tracing in GitLab is an integration with Jaeger, an open-source end-to-end distributed tracing system. GitLab users can navigate to their Jaeger instance to gain insight into the performance of a deployed application, tracking each function or microservice that handles a given request. Tracing in GitLab is deprecated in GitLab 14.7, and scheduled for removal in 15.0. To track work on a possible replacement, see the issue for [Opstrace integration with GitLab](https://gitlab.com/groups/gitlab-org/-/epics/6976).
diff --git a/doc/update/index.md b/doc/update/index.md
index 35f320841c7..3a17d3c01d7 100644
--- a/doc/update/index.md
+++ b/doc/update/index.md
@@ -537,6 +537,31 @@ See [Maintenance mode issue in GitLab 13.9 to 14.4](#maintenance-mode-issue-in-g
- See [Maintenance mode issue in GitLab 13.9 to 14.4](#maintenance-mode-issue-in-gitlab-139-to-144).
+- For GitLab Enterprise Edition customers, we noticed an issue when [subscription expiration is upcoming, and you create new subgroups and projects](https://gitlab.com/gitlab-org/gitlab/-/issues/322546). If you fall under that category and get 500 errors, you can work around this issue:
+
+ 1. SSH into you GitLab server, and open a Rails console:
+
+ ```shell
+ sudo gitlab-rails console
+ ```
+
+ 1. Disable the following features:
+
+ ```ruby
+ Feature.disable(:subscribable_subscription_banner)
+ Feature.disable(:subscribable_license_banner)
+ ```
+
+ 1. Restart Puma or Unicorn:
+
+ ```shell
+ #For installations using Puma
+ sudo gitlab-ctl restart puma
+
+ #For installations using Unicorn
+ sudo gitlab-ctl restart unicorn
+ ```
+
### 13.8.8
GitLab 13.8 includes a background migration to address [an issue with duplicate service records](https://gitlab.com/gitlab-org/gitlab/-/issues/290008). If duplicate services are present, this background migration must complete before a unique index is applied to the services table, which was [introduced in GitLab 13.9](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52563). Upgrades from GitLab 13.8 and earlier to later versions must include an intermediate upgrade to GitLab 13.8.8 and [must wait until the background migrations complete](#checking-for-background-migrations-before-upgrading) before proceeding.
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index 4ef400aae98..a8cc33d5545 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -63,6 +63,9 @@ vulnerability.
## Requirements
+Dependency Scanning runs in the `test` stage, which is available by default. If you redefine the
+stages in the `.gitlab-ci.yml` file, the `test` stage is required.
+
To run dependency scanning jobs, by default, you need GitLab Runner with the
[`docker`](https://docs.gitlab.com/runner/executors/docker.html) or
[`kubernetes`](https://docs.gitlab.com/runner/install/kubernetes.html) executor.
diff --git a/doc/user/application_security/iac_scanning/index.md b/doc/user/application_security/iac_scanning/index.md
index f49c6bf47d4..4d5c1da3c47 100644
--- a/doc/user/application_security/iac_scanning/index.md
+++ b/doc/user/application_security/iac_scanning/index.md
@@ -14,6 +14,8 @@ Currently, IaC scanning supports configuration files for Terraform, Ansible, AWS
## Requirements
+IaC Scanning runs in the `test` stage, which is available by default. If you redefine the stages in the `.gitlab-ci.yml` file, the `test` stage is required.
+
To run IaC scanning jobs, by default, you need GitLab Runner with the
[`docker`](https://docs.gitlab.com/runner/executors/docker.html) or
[`kubernetes`](https://docs.gitlab.com/runner/install/kubernetes.html) executor.
diff --git a/doc/user/application_security/secret_detection/index.md b/doc/user/application_security/secret_detection/index.md
index f897d9c6e5b..c5761a5743f 100644
--- a/doc/user/application_security/secret_detection/index.md
+++ b/doc/user/application_security/secret_detection/index.md
@@ -370,10 +370,10 @@ For information on this, see the [general Application Security troubleshooting s
### Error: `Couldn't run the gitleaks command: exit status 2`
-If a pipeline is triggered from a Merge Request containing 60 commits while the `GIT_DEPTH` variable
-is set to 50 (a [project default](../../../ci/pipelines/settings.md#limit-the-number-of-changes-fetched-during-clone)),
-the Secret Detection job fails as the clone is not deep enough to contain all of the
-relevant commits.
+If a pipeline is triggered from a Merge Request containing 60 commits while the `GIT_DEPTH` variable's
+value is less than that, the Secret Detection job fails as the clone is not deep enough to contain all of the
+relevant commits. For information on the current default value, see the
+[pipeline configuration documentation](../../../ci/pipelines/settings.md#limit-the-number-of-changes-fetched-during-clone).
To confirm this as the cause of the error, set the
[logging level](../../application_security/secret_detection/index.md#logging-level) to `debug`, then
diff --git a/doc/user/group/planning_hierarchy/img/view-project-work-item-hierarchy_v14_7.png b/doc/user/group/planning_hierarchy/img/view-project-work-item-hierarchy_v14_7.png
new file mode 100644
index 00000000000..2ddd551ee46
Binary files /dev/null and b/doc/user/group/planning_hierarchy/img/view-project-work-item-hierarchy_v14_7.png differ
diff --git a/doc/user/group/planning_hierarchy/index.md b/doc/user/group/planning_hierarchy/index.md
index 5887328abe4..0525246b812 100644
--- a/doc/user/group/planning_hierarchy/index.md
+++ b/doc/user/group/planning_hierarchy/index.md
@@ -20,6 +20,20 @@ To learn about hierarchies in general, common frameworks, and using GitLab for
portfolio management, see
[How to use GitLab for Agile portfolio planning and project management](https://about.gitlab.com/blog/2020/11/11/gitlab-for-agile-portfolio-planning-project-management/).
+## View planning hierarchies
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340844/) in GitLab 14.7 and is behind the feature flag `work_items_hierarchy`.
+
+To view the planning hierarchy in a project:
+
+1. On the top bar, select **Menu > Projects** and find your project.
+1. On the left sidebar, select **Project information > Planning hierarchy**.
+
+Under **Current structure**, you can see a hierarchy diagram that matches your current planning hierarchy.
+The work items outside your subscription plan show up below **Unavailable structure**.
+
+
+
## Hierarchies with epics
With epics, you can achieve the following hierarchy:
diff --git a/lib/gitlab/ci/templates/Jobs/Secret-Detection.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Secret-Detection.gitlab-ci.yml
index 07d54ae2239..42487cc0c67 100644
--- a/lib/gitlab/ci/templates/Jobs/Secret-Detection.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Secret-Detection.gitlab-ci.yml
@@ -14,6 +14,8 @@ variables:
image: "$SECURE_ANALYZERS_PREFIX/secrets:$SECRETS_ANALYZER_VERSION"
services: []
allow_failure: true
+ variables:
+ GIT_DEPTH: "50"
# `rules` must be overridden explicitly by each child job
# see https://gitlab.com/gitlab-org/gitlab/-/issues/218444
artifacts:
diff --git a/lib/sidebars/concerns/work_item_hierarchy.rb b/lib/sidebars/concerns/work_item_hierarchy.rb
new file mode 100644
index 00000000000..f88e24b205d
--- /dev/null
+++ b/lib/sidebars/concerns/work_item_hierarchy.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+# This module has the necessary methods to render
+# work items hierarchy menu
+module Sidebars
+ module Concerns
+ module WorkItemHierarchy
+ def hierarchy_menu_item(container, url, path)
+ unless show_hierarachy_menu_item?(container)
+ return ::Sidebars::NilMenuItem.new(item_id: :hierarchy)
+ end
+
+ ::Sidebars::MenuItem.new(
+ title: _('Planning hierarchy'),
+ link: url,
+ active_routes: { path: path },
+ item_id: :hierarchy
+ )
+ end
+
+ def show_hierarachy_menu_item?(container)
+ Feature.enabled?(:work_items_hierarchy, container, default_enabled: :yaml) &&
+ can?(context.current_user, :read_work_items_hierarchy, container)
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/projects/menus/project_information_menu.rb b/lib/sidebars/projects/menus/project_information_menu.rb
index 44b94ee3522..ad11a383757 100644
--- a/lib/sidebars/projects/menus/project_information_menu.rb
+++ b/lib/sidebars/projects/menus/project_information_menu.rb
@@ -4,10 +4,13 @@ module Sidebars
module Projects
module Menus
class ProjectInformationMenu < ::Sidebars::Menu
+ include ::Sidebars::Concerns::WorkItemHierarchy
+
override :configure_menu_items
def configure_menu_items
add_item(activity_menu_item)
add_item(labels_menu_item)
+ add_item(hierarchy_menu_item(context.project, planning_hierarchy_project_path(context.project), 'projects#planning_hierarchy'))
add_item(members_menu_item)
true
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c3d67ca8f8f..d2fcecb165b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1835,21 +1835,12 @@ msgstr ""
msgid "AccessTokens|Keep this token secret. Anyone who has it can access repository static objects as if they were you. If that ever happens, %{linkStart}reset this token%{linkEnd}."
msgstr ""
-msgid "AccessTokens|Keep this token secret. Anyone who has it can access repository static objects as if they were you. If that ever happens, %{reset_link_start}reset this token%{reset_link_end}."
-msgstr ""
-
msgid "AccessTokens|Keep this token secret. Anyone who has it can create issues as if they were you. If that happens, %{linkStart}reset this token%{linkEnd}."
msgstr ""
-msgid "AccessTokens|Keep this token secret. Anyone who has it can create issues as if they were you. If that happens, %{link_reset_it}."
-msgstr ""
-
msgid "AccessTokens|Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you. If that happens, %{linkStart}reset this token%{linkEnd}."
msgstr ""
-msgid "AccessTokens|Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you. If that happens, %{link_reset_it}."
-msgstr ""
-
msgid "AccessTokens|Personal Access Tokens"
msgstr ""
@@ -1874,9 +1865,6 @@ msgstr ""
msgid "AccessTokens|Your static object token authenticates you when repository static objects (such as archives or blobs) are served from an external storage."
msgstr ""
-msgid "AccessTokens|reset this token"
-msgstr ""
-
msgid "AccessibilityReport|Learn more"
msgstr ""
@@ -7083,6 +7071,9 @@ msgstr ""
msgid "Child"
msgstr ""
+msgid "Child epic"
+msgstr ""
+
msgid "Child epic does not exist."
msgstr ""
@@ -17628,6 +17619,33 @@ msgstr[1] ""
msgid "Hide values"
msgstr ""
+msgid "Hierarchy|Current structure"
+msgstr ""
+
+msgid "Hierarchy|Deliver value more efficiently by breaking down necessary work into a hierarchical structure. This structure helps teams understand scope, priorities, and how work cascades up toward larger goals."
+msgstr ""
+
+msgid "Hierarchy|Help us improve work items in GitLab!"
+msgstr ""
+
+msgid "Hierarchy|Is there a framework or type of work item you wish you had access to in GitLab? Give us your feedback and help us build the experiences valuable to you."
+msgstr ""
+
+msgid "Hierarchy|Planning hierarchy"
+msgstr ""
+
+msgid "Hierarchy|Take the work items survey"
+msgstr ""
+
+msgid "Hierarchy|These items are unavailable in the current structure."
+msgstr ""
+
+msgid "Hierarchy|Unavailable structure"
+msgstr ""
+
+msgid "Hierarchy|You can start using these items now."
+msgstr ""
+
msgid "High or unknown vulnerabilities present"
msgstr ""
@@ -26512,6 +26530,9 @@ msgstr ""
msgid "Plan:"
msgstr ""
+msgid "Planning hierarchy"
+msgstr ""
+
msgid "PlantUML"
msgstr ""
@@ -30235,6 +30256,9 @@ msgstr ""
msgid "Required only if you are not using role instance credentials."
msgstr ""
+msgid "Requirement"
+msgstr ""
+
msgid "Requirement %{reference} has been added"
msgstr ""
@@ -34918,6 +34942,9 @@ msgstr ""
msgid "Target-Branch"
msgstr ""
+msgid "Task"
+msgstr ""
+
msgid "Task ID: %{elastic_task}"
msgstr ""
@@ -35163,6 +35190,9 @@ msgstr ""
msgid "Test Cases"
msgstr ""
+msgid "Test case"
+msgstr ""
+
msgid "Test coverage parsing"
msgstr ""
diff --git a/qa/qa/page/project/settings/visibility_features_permissions.rb b/qa/qa/page/project/settings/visibility_features_permissions.rb
index 5415595803f..60cea6de7f5 100644
--- a/qa/qa/page/project/settings/visibility_features_permissions.rb
+++ b/qa/qa/page/project/settings/visibility_features_permissions.rb
@@ -5,12 +5,9 @@ module QA
module Project
module Settings
class VisibilityFeaturesPermissions < Page::Base
- view 'app/helpers/projects_helper.rb' do
- element :visibility_features_permissions_save_button
- end
-
view 'app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue' do
element :project_visibility_dropdown
+ element :visibility_features_permissions_save_button
end
def set_project_visibility(visibility)
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb
index d198d79c5fe..b0c6d01e8ca 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb
@@ -2,7 +2,11 @@
module QA
RSpec.describe 'Create' do
- describe 'Merge request creation from fork' do
+ describe 'Merge request creation from fork', quarantine: {
+ only: { subdomain: %i[canary production] },
+ issue: "https://gitlab.com/gitlab-org/gitlab/-/issues/343801",
+ type: :investigation
+ } do
let(:merge_request) do
Resource::MergeRequestFromFork.fabricate_via_browser_ui! do |merge_request|
merge_request.fork_branch = 'feature-branch'
diff --git a/spec/controllers/concerns/work_items_hierarchy_spec.rb b/spec/controllers/concerns/work_items_hierarchy_spec.rb
new file mode 100644
index 00000000000..270e30cb5f9
--- /dev/null
+++ b/spec/controllers/concerns/work_items_hierarchy_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItemsHierarchy do
+ controller(ApplicationController) do
+ include WorkItemsHierarchy
+ end
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ render_views
+
+ before do
+ sign_in user
+ routes.draw { get :planning_hierarchy, to: "anonymous#planning_hierarchy" }
+ controller.instance_variable_set(:@project, project)
+ end
+
+ it 'renders hierarchy' do
+ stub_feature_flags(work_items_hierarchy: true)
+
+ get :planning_hierarchy
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to match(/id="js-work-items-hierarchy"/)
+ end
+
+ it 'renders 404' do
+ stub_feature_flags(work_items_hierarchy: false)
+
+ get :planning_hierarchy
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(response.body).not_to match(/id="js-work-items-hierarchy"/)
+ end
+end
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index eeb9bf476c3..34eb07d78f1 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -62,66 +62,33 @@ RSpec.describe 'Profile account page', :js do
end
end
- describe 'when I reset feed token' do
- it 'resets feed token with `hide_access_tokens` feature flag enabled' do
- visit profile_personal_access_tokens_path
+ it 'allows resetting of feed token' do
+ visit profile_personal_access_tokens_path
- within('[data-testid="feed-token-container"]') do
- previous_token = find_field('Feed token').value
+ within('[data-testid="feed-token-container"]') do
+ previous_token = find_field('Feed token').value
- accept_confirm { click_link('reset this token') }
+ accept_confirm { click_link('reset this token') }
- click_button('Click to reveal')
+ click_button('Click to reveal')
- expect(find_field('Feed token').value).not_to eq(previous_token)
- end
- end
-
- it 'resets feed token with `hide_access_tokens` feature flag disabled' do
- stub_feature_flags(hide_access_tokens: false)
- visit profile_personal_access_tokens_path
-
- within('.feed-token-reset') do
- previous_token = find("#feed_token").value
-
- accept_confirm { find('[data-testid="reset_feed_token_link"]').click }
-
- expect(find('#feed_token').value).not_to eq(previous_token)
- end
+ expect(find_field('Feed token').value).not_to eq(previous_token)
end
end
- describe 'when I reset incoming email token' do
- before do
- allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
- stub_feature_flags(bootstrap_confirmation_modals: false)
- end
+ it 'allows resetting of incoming email token' do
+ allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
- it 'resets incoming email token with `hide_access_tokens` feature flag enabled' do
- visit profile_personal_access_tokens_path
+ visit profile_personal_access_tokens_path
- within('[data-testid="incoming-email-token-container"]') do
- previous_token = find_field('Incoming email token').value
+ within('[data-testid="incoming-email-token-container"]') do
+ previous_token = find_field('Incoming email token').value
- accept_confirm { click_link('reset this token') }
+ accept_confirm { click_link('reset this token') }
- click_button('Click to reveal')
+ click_button('Click to reveal')
- expect(find_field('Incoming email token').value).not_to eq(previous_token)
- end
- end
-
- it 'resets incoming email token with `hide_access_tokens` feature flag disabled' do
- stub_feature_flags(hide_access_tokens: false)
- visit profile_personal_access_tokens_path
-
- within('.incoming-email-token-reset') do
- previous_token = find('#incoming_email_token').value
-
- accept_confirm { find('[data-testid="reset_email_token_link"]').click }
-
- expect(find('#incoming_email_token').value).not_to eq(previous_token)
- end
+ expect(find_field('Incoming email token').value).not_to eq(previous_token)
end
end
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
index 135a940807e..f1e5658cd7b 100644
--- a/spec/features/profiles/personal_access_tokens_spec.rb
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -132,7 +132,7 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
describe "feed token" do
context "when enabled" do
- it "displays feed token with `hide_access_tokens` feature flag enabled" do
+ it "displays feed token" do
allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(false)
visit profile_personal_access_tokens_path
@@ -143,15 +143,6 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
expect(page).to have_content(feed_token_description)
end
end
-
- it "displays feed token with `hide_access_tokens` feature flag disabled" do
- stub_feature_flags(hide_access_tokens: false)
- allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(false)
- visit profile_personal_access_tokens_path
-
- expect(page).to have_field('Feed token', with: user.feed_token)
- expect(page).to have_content(feed_token_description)
- end
end
context "when disabled" do
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index a7e773dda2d..23fcc1fe444 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe 'Edit Project Settings' do
# disable by clicking toggle
toggle_feature_off("project[project_feature_attributes][#{tool_name}_access_level]")
page.within('.sharing-permissions') do
- find('input[value="Save changes"]').click
+ find('[data-testid="project-features-save-button"]').click
end
wait_for_requests
expect(page).not_to have_selector(".shortcuts-#{shortcut_name}")
@@ -32,7 +32,7 @@ RSpec.describe 'Edit Project Settings' do
# re-enable by clicking toggle again
toggle_feature_on("project[project_feature_attributes][#{tool_name}_access_level]")
page.within('.sharing-permissions') do
- find('input[value="Save changes"]').click
+ find('[data-testid="project-features-save-button"]').click
end
wait_for_requests
expect(page).to have_selector(".shortcuts-#{shortcut_name}")
diff --git a/spec/features/projects/settings/project_settings_spec.rb b/spec/features/projects/settings/project_settings_spec.rb
index 71b319d192c..b67caa5a5f9 100644
--- a/spec/features/projects/settings/project_settings_spec.rb
+++ b/spec/features/projects/settings/project_settings_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe 'Projects settings' do
# disable by clicking toggle
forking_enabled_button.click
page.within('.sharing-permissions') do
- find('input[value="Save changes"]').click
+ find('[data-testid="project-features-save-button"]').click
end
wait_for_requests
@@ -77,7 +77,7 @@ RSpec.describe 'Projects settings' do
expect(default_award_emojis_input.value).to eq('false')
page.within('.sharing-permissions') do
- find('input[value="Save changes"]').click
+ find('[data-testid="project-features-save-button"]').click
end
wait_for_requests
diff --git a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
index 862bae45fc6..77be351f3d8 100644
--- a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
@@ -54,7 +54,7 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click
- find('input[value="Save changes"]').send_keys(:return)
+ find('[data-testid="project-features-save-button"]').send_keys(:return)
end
expect(page).not_to have_content 'Pipelines must succeed'
@@ -74,7 +74,7 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .gl-toggle').click
- find('input[value="Save changes"]').send_keys(:return)
+ find('[data-testid="project-features-save-button"]').send_keys(:return)
end
expect(page).to have_content 'Pipelines must succeed'
@@ -95,7 +95,7 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click
- find('input[value="Save changes"]').send_keys(:return)
+ find('[data-testid="project-features-save-button"]').send_keys(:return)
end
expect(page).to have_content 'Pipelines must succeed'
diff --git a/spec/features/projects/user_changes_project_visibility_spec.rb b/spec/features/projects/user_changes_project_visibility_spec.rb
index 10b9cf84256..68fed9b8a74 100644
--- a/spec/features/projects/user_changes_project_visibility_spec.rb
+++ b/spec/features/projects/user_changes_project_visibility_spec.rb
@@ -5,14 +5,6 @@ require 'spec_helper'
RSpec.describe 'User changes public project visibility', :js do
include ProjectForksHelper
- before do
- fork_project(project, project.owner)
-
- sign_in(project.owner)
-
- visit edit_project_path(project)
- end
-
shared_examples 'changing visibility to private' do
it 'requires confirmation' do
visibility_select = first('.project-feature-controls .select-control')
@@ -34,15 +26,85 @@ RSpec.describe 'User changes public project visibility', :js do
end
end
- context 'when a project is public' do
+ shared_examples 'does not require confirmation' do
+ it 'saves without confirmation' do
+ visibility_select = first('.project-feature-controls .select-control')
+ visibility_select.select('Private')
+
+ page.within('#js-shared-permissions') do
+ click_button 'Save changes'
+ end
+
+ wait_for_requests
+
+ expect(project.reload).to be_private
+ end
+ end
+
+ context 'when the project has forks' do
+ before do
+ fork_project(project, project.owner)
+
+ sign_in(project.owner)
+
+ visit edit_project_path(project)
+ end
+
+ context 'when a project is public' do
+ let(:project) { create(:project, :empty_repo, :public) }
+
+ it_behaves_like 'changing visibility to private'
+ end
+
+ context 'when the project is internal' do
+ let(:project) { create(:project, :empty_repo, :internal) }
+
+ it_behaves_like 'changing visibility to private'
+ end
+
+ context 'when the visibility level is untouched' do
+ let(:project) { create(:project, :empty_repo, :public) }
+
+ it 'saves without confirmation' do
+ expect(page).to have_selector('.js-emails-disabled', visible: true)
+ find('.js-emails-disabled input[type="checkbox"]').click
+
+ page.within('#js-shared-permissions') do
+ click_button 'Save changes'
+ end
+
+ wait_for_requests
+
+ expect(project.reload).to be_public
+ end
+ end
+ end
+
+ context 'when the project is not forked' do
let(:project) { create(:project, :empty_repo, :public) }
- it_behaves_like 'changing visibility to private'
+ before do
+ sign_in(project.owner)
+
+ visit edit_project_path(project)
+ end
+
+ it_behaves_like 'does not require confirmation'
end
- context 'when the project is internal' do
- let(:project) { create(:project, :empty_repo, :internal) }
+ context 'with unlink_fork_network_upon_visibility_decrease = false' do
+ let(:project) { create(:project, :empty_repo, :public) }
- it_behaves_like 'changing visibility to private'
+ before do
+ stub_feature_flags(unlink_fork_network_upon_visibility_decrease: false)
+
+ fork_project(project, project.owner)
+
+ sign_in(project.owner)
+
+ visit edit_project_path(project)
+ end
+
+ it_behaves_like 'does not require confirmation'
end
end
diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
index 0020269e4e7..8a9bb025d55 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
@@ -7,6 +7,7 @@ import {
visibilityLevelDescriptions,
visibilityOptions,
} from '~/pages/projects/shared/permissions/constants';
+import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
const defaultProps = {
currentSettings: {
@@ -47,6 +48,8 @@ const defaultProps = {
packagesAvailable: false,
packagesHelpPath: '/help/user/packages/index',
requestCveAvailable: true,
+ confirmationPhrase: 'my-fake-project',
+ showVisibilityConfirmModal: false,
};
describe('Settings Panel', () => {
@@ -104,6 +107,7 @@ describe('Settings Panel', () => {
);
const findMetricsVisibilitySettings = () => wrapper.find({ ref: 'metrics-visibility-settings' });
const findOperationsSettings = () => wrapper.find({ ref: 'operations-settings' });
+ const findConfirmDangerButton = () => wrapper.findComponent(ConfirmDanger);
afterEach(() => {
wrapper.destroy();
@@ -177,6 +181,44 @@ describe('Settings Panel', () => {
expect(findRequestAccessEnabledInput().exists()).toBe(false);
});
+
+ it('does not require confirmation if the visibility is reduced', async () => {
+ wrapper = mountComponent({
+ currentSettings: { visibilityLevel: visibilityOptions.INTERNAL },
+ });
+
+ expect(findConfirmDangerButton().exists()).toBe(false);
+
+ await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE);
+
+ expect(findConfirmDangerButton().exists()).toBe(false);
+ });
+
+ describe('showVisibilityConfirmModal=true', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ currentSettings: { visibilityLevel: visibilityOptions.INTERNAL },
+ showVisibilityConfirmModal: true,
+ });
+ });
+
+ it('will render the confirmation dialog if the visibility is reduced', async () => {
+ expect(findConfirmDangerButton().exists()).toBe(false);
+
+ await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE);
+
+ expect(findConfirmDangerButton().exists()).toBe(true);
+ });
+
+ it('emits the `confirm` event when the reduce visibility warning is confirmed', async () => {
+ expect(wrapper.emitted('confirm')).toBeUndefined();
+
+ await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE);
+ await findConfirmDangerButton().vm.$emit('confirm');
+
+ expect(wrapper.emitted('confirm')).toHaveLength(1);
+ });
+ });
});
describe('Issues settings', () => {
diff --git a/spec/frontend/work_items_hierarchy/components/__snapshots__/app_spec.js.snap b/spec/frontend/work_items_hierarchy/components/__snapshots__/app_spec.js.snap
new file mode 100644
index 00000000000..f4979e4e707
--- /dev/null
+++ b/spec/frontend/work_items_hierarchy/components/__snapshots__/app_spec.js.snap
@@ -0,0 +1,1197 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`WorkItemsHierarchy App when licensePlan is free matches the snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+ Help us improve work items in GitLab!
+
+
+
+
+ Is there a framework or type of work item you wish you had access to in GitLab? Give us your feedback and help us build the experiences valuable to you.
+
+
+
+ Deliver value more efficiently by breaking down necessary work into a hierarchical structure. This structure helps teams understand scope, priorities, and how work cascades up toward larger goals.
+
+
+
+
+ Current structure
+
+
+
+ You can start using these items now.
+
+
+
+
+
+
+
+
+
+
+ Issue
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Task
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Incident
+
+
+
+
+
+
+
+
+
+
+
+ Unavailable structure
+
+
+
+
+
+ These items are unavailable in the current structure.
+
+
+
+
+
+
+
+
+ Test case
+
+
+
+
+
+ Ultimate
+
+
+
+
+
+
+`;
+
+exports[`WorkItemsHierarchy App when licensePlan is premium matches the snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+ Help us improve work items in GitLab!
+
+
+
+
+ Is there a framework or type of work item you wish you had access to in GitLab? Give us your feedback and help us build the experiences valuable to you.
+
+
+
+ Deliver value more efficiently by breaking down necessary work into a hierarchical structure. This structure helps teams understand scope, priorities, and how work cascades up toward larger goals.
+
+
+
+
+ Current structure
+
+
+
+ You can start using these items now.
+
+
+
+
+
+
+
+
+
+
+ Epic
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Issue
+
+
+
+
+
+
+
+
+
+
+
+ Task
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Incident
+
+
+
+
+
+
+
+
+
+
+
+ Unavailable structure
+
+
+
+
+
+ These items are unavailable in the current structure.
+
+
+
+
+
+
+
+
+ Test case
+
+
+
+
+
+ Ultimate
+
+
+
+
+
+
+`;
+
+exports[`WorkItemsHierarchy App when licensePlan is ultimate matches the snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+ Help us improve work items in GitLab!
+
+
+
+
+ Is there a framework or type of work item you wish you had access to in GitLab? Give us your feedback and help us build the experiences valuable to you.
+
+
+
+ Deliver value more efficiently by breaking down necessary work into a hierarchical structure. This structure helps teams understand scope, priorities, and how work cascades up toward larger goals.
+
+
+
+
+ Current structure
+
+
+
+ You can start using these items now.
+
+
+
+
+
+
+
+
+
+
+ Epic
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Child epic
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Issue
+
+
+
+
+
+
+
+
+
+
+
+ Task
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Incident
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Requirement
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Test case
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/spec/frontend/work_items_hierarchy/components/app_spec.js b/spec/frontend/work_items_hierarchy/components/app_spec.js
new file mode 100644
index 00000000000..c22c0fcb21c
--- /dev/null
+++ b/spec/frontend/work_items_hierarchy/components/app_spec.js
@@ -0,0 +1,78 @@
+import { nextTick } from 'vue';
+import { createLocalVue, mount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import { GlBanner } from '@gitlab/ui';
+import App from '~/work_items_hierarchy/components/app.vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('WorkItemsHierarchy App', () => {
+ let wrapper;
+ const createComponent = (props = {}, data = {}) => {
+ wrapper = extendedWrapper(
+ mount(App, {
+ localVue,
+ provide: {
+ illustrationPath: '/foo.svg',
+ licensePlan: 'free',
+ ...props,
+ },
+ data() {
+ return data;
+ },
+ }),
+ );
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe.each`
+ licensePlan
+ ${'free'}
+ ${'premium'}
+ ${'ultimate'}
+ `('when licensePlan is $licensePlan', ({ licensePlan }) => {
+ beforeEach(() => {
+ createComponent({ licensePlan });
+ });
+
+ it('matches the snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('survey banner', () => {
+ it('shows when the banner is visible', () => {
+ createComponent({}, { bannerVisible: true });
+
+ expect(wrapper.find(GlBanner).exists()).toBe(true);
+ });
+
+ it('hide when close is called', async () => {
+ createComponent({}, { bannerVisible: true });
+
+ wrapper.findByTestId('close-icon').trigger('click');
+
+ await nextTick();
+
+ expect(wrapper.find(GlBanner).exists()).toBe(false);
+ });
+ });
+
+ describe('Unavailable structure', () => {
+ it.each`
+ licensePlan | visible
+ ${'free'} | ${true}
+ ${'premium'} | ${true}
+ ${'ultimate'} | ${false}
+ `('visibility is $visible when plan is $licensePlan', ({ licensePlan, visible }) => {
+ createComponent({ licensePlan });
+
+ expect(wrapper.findByTestId('unavailable-structure').exists()).toBe(visible);
+ });
+ });
+});
diff --git a/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
new file mode 100644
index 00000000000..14c15fb5cbe
--- /dev/null
+++ b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
@@ -0,0 +1,118 @@
+import { createLocalVue, mount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import { GlBadge } from '@gitlab/ui';
+import Hierarchy from '~/work_items_hierarchy/components/hierarchy.vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import RESPONSE from '~/work_items_hierarchy/static_response';
+import { workItemTypes } from '~/work_items/constants';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('WorkItemsHierarchy Hierarchy', () => {
+ let wrapper;
+
+ const workItemsFromResponse = (response) => {
+ return response.reduce(
+ (itemTypes, item) => {
+ const key = item.available ? 'available' : 'unavailable';
+ itemTypes[key].push({
+ ...item,
+ ...workItemTypes[item.type],
+ nestedTypes: item.nestedTypes
+ ? item.nestedTypes.map((type) => workItemTypes[type])
+ : null,
+ });
+ return itemTypes;
+ },
+ { available: [], unavailable: [] },
+ );
+ };
+
+ const createComponent = (props = {}) => {
+ wrapper = extendedWrapper(
+ mount(Hierarchy, {
+ localVue,
+ propsData: {
+ workItemTypes: props.workItemTypes,
+ ...props,
+ },
+ }),
+ );
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('available structure', () => {
+ let items = [];
+
+ beforeEach(() => {
+ items = workItemsFromResponse(RESPONSE.ultimate).available;
+ createComponent({ workItemTypes: items });
+ });
+
+ it('renders all work items', () => {
+ expect(wrapper.findAllByTestId('work-item-wrapper')).toHaveLength(items.length);
+ });
+
+ it('does not render badges', () => {
+ expect(wrapper.find(GlBadge).exists()).toBe(false);
+ });
+ });
+
+ describe('unavailable structure', () => {
+ let items = [];
+
+ beforeEach(() => {
+ items = workItemsFromResponse(RESPONSE.premium).unavailable;
+ createComponent({ workItemTypes: items });
+ });
+
+ it('renders all work items', () => {
+ expect(wrapper.findAllByTestId('work-item-wrapper')).toHaveLength(items.length);
+ });
+
+ it('renders license badges for all work items', () => {
+ expect(wrapper.findAll(GlBadge)).toHaveLength(items.length);
+ });
+
+ it('does not render svg icon for linking', () => {
+ expect(wrapper.findByTestId('hierarchy-rounded-arrow-tail').exists()).toBe(false);
+ expect(wrapper.findByTestId('level-up-icon').exists()).toBe(false);
+ });
+ });
+
+ describe('nested work items', () => {
+ describe.each`
+ licensePlan | arrowTailVisible | levelUpIconVisible | arrowDownIconVisible
+ ${'ultimate'} | ${true} | ${true} | ${true}
+ ${'premium'} | ${false} | ${false} | ${true}
+ ${'free'} | ${false} | ${false} | ${false}
+ `(
+ 'when $licensePlan license',
+ ({ licensePlan, arrowTailVisible, levelUpIconVisible, arrowDownIconVisible }) => {
+ let items = [];
+ beforeEach(() => {
+ items = workItemsFromResponse(RESPONSE[licensePlan]).available;
+ createComponent({ workItemTypes: items });
+ });
+
+ it(`${arrowTailVisible ? 'render' : 'does not render'} arrow tail svg`, () => {
+ expect(wrapper.findByTestId('hierarchy-rounded-arrow-tail').exists()).toBe(
+ arrowTailVisible,
+ );
+ });
+
+ it(`${levelUpIconVisible ? 'render' : 'does not render'} arrow tail svg`, () => {
+ expect(wrapper.findByTestId('level-up-icon').exists()).toBe(levelUpIconVisible);
+ });
+
+ it(`${arrowDownIconVisible ? 'render' : 'does not render'} arrow tail svg`, () => {
+ expect(wrapper.findByTestId('arrow-down-icon').exists()).toBe(arrowDownIconVisible);
+ });
+ },
+ );
+ });
+});
diff --git a/spec/frontend/work_items_hierarchy/hierarchy_util_spec.js b/spec/frontend/work_items_hierarchy/hierarchy_util_spec.js
new file mode 100644
index 00000000000..9042fa27d16
--- /dev/null
+++ b/spec/frontend/work_items_hierarchy/hierarchy_util_spec.js
@@ -0,0 +1,16 @@
+import { inferLicensePlan } from '~/work_items_hierarchy/hierarchy_util';
+import { LICENSE_PLAN } from '~/work_items_hierarchy/constants';
+
+describe('inferLicensePlan', () => {
+ it.each`
+ epics | subEpics | licensePlan
+ ${true} | ${true} | ${LICENSE_PLAN.ULTIMATE}
+ ${true} | ${false} | ${LICENSE_PLAN.PREMIUM}
+ ${false} | ${false} | ${LICENSE_PLAN.FREE}
+ `(
+ 'returns $licensePlan when epic is $epics and sub-epic is $subEpics',
+ ({ epics, subEpics, licensePlan }) => {
+ expect(inferLicensePlan({ hasEpics: epics, hasSubEpics: subEpics })).toBe(licensePlan);
+ },
+ );
+});
diff --git a/spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb b/spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb
new file mode 100644
index 00000000000..f0a5e032764
--- /dev/null
+++ b/spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Concerns::WorkItemHierarchy do
+ shared_examples 'hierarchy menu' do
+ let(:item_id) { :hierarchy }
+
+ context 'when the feature is disabled does not render' do
+ before do
+ stub_feature_flags(work_items_hierarchy: false)
+ end
+
+ specify { is_expected.to be_nil }
+ end
+
+ context 'when the feature is enabled does render' do
+ before do
+ stub_feature_flags(work_items_hierarchy: true)
+ end
+
+ specify { is_expected.not_to be_nil }
+ end
+ end
+
+ describe 'Project hierarchy menu item' do
+ let_it_be_with_reload(:project) { create(:project, :repository) }
+
+ let(:user) { project.owner }
+ let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
+
+ subject { Sidebars::Projects::Menus::ProjectInformationMenu.new(context).renderable_items.index { |e| e.item_id == item_id } }
+
+ it_behaves_like 'hierarchy menu'
+ end
+end
diff --git a/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb b/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb
index 7e8d0ab0518..5cbf0a13b66 100644
--- a/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb
@@ -59,5 +59,25 @@ RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do
specify { is_expected.to be_nil }
end
end
+
+ describe 'Hierarchy' do
+ let(:item_id) { :hierarchy }
+
+ context 'when the feature is disabled' do
+ before do
+ stub_feature_flags(work_items_hierarchy: false)
+ end
+
+ specify { is_expected.to be_nil }
+ end
+
+ context 'when the feature is enabled' do
+ before do
+ stub_feature_flags(work_items_hierarchy: true)
+ end
+
+ specify { is_expected.not_to be_nil }
+ end
+ end
end
end
diff --git a/spec/migrations/cleanup_after_add_primary_email_to_emails_if_user_confirmed_spec.rb b/spec/migrations/cleanup_after_add_primary_email_to_emails_if_user_confirmed_spec.rb
new file mode 100644
index 00000000000..abff7c6aba1
--- /dev/null
+++ b/spec/migrations/cleanup_after_add_primary_email_to_emails_if_user_confirmed_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe CleanupAfterAddPrimaryEmailToEmailsIfUserConfirmed, :sidekiq do
+ let(:migration) { described_class.new }
+ let(:users) { table(:users) }
+ let(:emails) { table(:emails) }
+
+ let!(:user_1) { users.create!(name: 'confirmed-user-1', email: 'confirmed-1@example.com', confirmed_at: 3.days.ago, projects_limit: 100) }
+ let!(:user_2) { users.create!(name: 'confirmed-user-2', email: 'confirmed-2@example.com', confirmed_at: 1.day.ago, projects_limit: 100) }
+ let!(:user_3) { users.create!(name: 'confirmed-user-3', email: 'confirmed-3@example.com', confirmed_at: 1.day.ago, projects_limit: 100) }
+ let!(:user_4) { users.create!(name: 'unconfirmed-user', email: 'unconfirmed@example.com', confirmed_at: nil, projects_limit: 100) }
+
+ let!(:email_1) { emails.create!(email: 'confirmed-1@example.com', user_id: user_1.id, confirmed_at: 1.day.ago) }
+ let!(:email_2) { emails.create!(email: 'other_2@example.com', user_id: user_2.id, confirmed_at: 1.day.ago) }
+
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+ end
+
+ it 'consume any pending background migration job' do
+ expect_next_instance_of(Gitlab::BackgroundMigration::JobCoordinator) do |coordinator|
+ expect(coordinator).to receive(:steal).with('AddPrimaryEmailToEmailsIfUserConfirmed').twice
+ end
+
+ migration.up
+ end
+
+ it 'adds the primary email to emails for leftover confirmed users that do not have their primary email in the emails table', :aggregate_failures do
+ original_email_1_confirmed_at = email_1.reload.confirmed_at
+
+ expect { migration.up }.to change { emails.count }.by(2)
+
+ expect(emails.find_by(user_id: user_2.id, email: 'confirmed-2@example.com').confirmed_at).to eq(user_2.reload.confirmed_at)
+ expect(emails.find_by(user_id: user_3.id, email: 'confirmed-3@example.com').confirmed_at).to eq(user_3.reload.confirmed_at)
+ expect(email_1.reload.confirmed_at).to eq(original_email_1_confirmed_at)
+
+ expect(emails.exists?(user_id: user_4.id)).to be(false)
+ end
+
+ it 'continues in case of errors with one email' do
+ allow(Email).to receive(:create) { raise 'boom!' }
+
+ expect { migration.up }.not_to raise_error
+ end
+end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index 27967850389..576a8aa44fa 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -22,6 +22,7 @@ RSpec.shared_context 'project navbar structure' do
nav_sub_items: [
_('Activity'),
_('Labels'),
+ _('Planning hierarchy'),
_('Members')
]
},
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 c39252cef13..aabd6454b88 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -17,7 +17,7 @@ RSpec.shared_context 'ProjectPolicy context' do
%i[
award_emoji create_issue create_merge_request_in create_note
create_project read_issue_board read_issue read_issue_iid read_issue_link
- read_label read_issue_board_list read_milestone read_note read_project
+ read_label read_work_items_hierarchy read_issue_board_list read_milestone read_note read_project
read_project_for_iids read_project_member read_release read_snippet
read_wiki upload_file
]
diff --git a/tooling/bin/find_changes b/tooling/bin/find_changes
index c6b8bafbd85..8ad5011459b 100755
--- a/tooling/bin/find_changes
+++ b/tooling/bin/find_changes
@@ -48,7 +48,7 @@ class FindChanges # rubocop:disable Gitlab/NamespacedClass
mr_changes = Gitlab.merge_request_changes(mr_project_path, mr_iid)
- mr_changes.changes.map { |change| change['new_path'] }
+ mr_changes.changes.map { |change| change['new_path'] unless change['deleted_file'] }.compact
end
end