From 38e4bfea582e8c755dd21613bf21658b1771449b Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 29 Apr 2021 09:10:11 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .rubocop_manual_todo.yml | 2 - GITALY_SERVER_VERSION | 2 +- .../boards/components/board_list_header.vue | 2 +- .../boards/components/board_settings_sidebar.vue | 8 +- app/assets/javascripts/boards/constants.js | 7 ++ app/assets/javascripts/boards/stores/actions.js | 8 +- .../components/invite_member_modal.vue | 67 ------------ .../components/invite_member_trigger.vue | 43 -------- app/assets/javascripts/invite_member/constants.js | 2 - app/assets/javascripts/invite_member/event_hub.js | 3 - .../invite_member/init_invite_member_modal.js | 27 ----- .../invite_member/init_invite_member_trigger.js | 18 ---- .../javascripts/pages/projects/issues/show.js | 4 - .../merge_requests/init_merge_request_show.js | 4 - .../assignees/sidebar_assignees_widget.vue | 5 +- .../assignees/sidebar_invite_members.vue | 44 ++------ app/assets/javascripts/sidebar/mount_sidebar.js | 4 +- app/controllers/projects/issues_controller.rb | 2 - .../projects/merge_requests_controller.rb | 2 - app/helpers/invite_members_helper.rb | 14 --- app/helpers/issuables_helper.rb | 3 +- app/helpers/sidebars_helper.rb | 3 +- app/models/ci/pipeline.rb | 2 +- app/models/concerns/atomic_internal_id.rb | 4 +- app/models/member.rb | 3 +- app/models/project.rb | 2 +- .../layouts/nav/sidebar/_project_menus.html.haml | 46 --------- .../shared/issuable/_sidebar_assignees.html.haml | 23 ++--- ...vite_members_version_b-in-assignee-dropdown.yml | 5 + .../327405-retry-failed-background-migrations.yml | 5 + ...ite_members_version_b_experiment_percentage.yml | 8 -- ...7_add_index_to_batched_migration_jobs_status.rb | 17 +++ ...23057_backfill_version_author_and_created_at.rb | 2 +- db/schema_migrations/20210427062807 | 1 + db/structure.sql | 2 + doc/administration/pages/index.md | 29 ++++++ doc/ci/environments/index.md | 2 +- doc/ci/variables/README.md | 2 +- doc/development/migration_style_guide.md | 2 +- doc/user/project/pages/introduction.md | 4 + .../database/background_migration/batched_job.rb | 13 +++ .../background_migration/batched_migration.rb | 3 +- .../batched_migration_runner.rb | 22 ++-- .../batched_migration_wrapper.rb | 2 +- lib/gitlab/experimentation.rb | 4 - lib/gitlab/object_hierarchy.rb | 34 +++--- lib/sidebars/projects/menus/ci_cd_menu.rb | 114 +++++++++++++++++++++ lib/sidebars/projects/panel.rb | 1 + locale/gitlab.pot | 38 +++---- qa/qa/page/project/sub_menus/ci_cd.rb | 6 +- spec/deprecation_toolkit_env.rb | 1 - spec/features/issues/issue_sidebar_spec.rb | 25 +---- spec/frontend/boards/stores/actions_spec.js | 9 +- .../components/invite_member_modal_spec.js | 67 ------------ .../components/invite_member_trigger_mock_data.js | 7 -- .../components/invite_member_trigger_spec.js | 48 --------- .../assignees/sidebar_assignees_widget_spec.js | 12 +-- .../assignees/sidebar_invite_members_spec.js | 38 +------ spec/frontend/users_select/test_helper.js | 8 +- spec/helpers/invite_members_helper_spec.rb | 47 --------- .../background_migration/batched_job_spec.rb | 36 +++++++ .../batched_migration_runner_spec.rb | 101 +++++++++++++++--- .../batched_migration_wrapper_spec.rb | 36 +++++++ spec/lib/gitlab/experimentation_spec.rb | 1 - .../lib/sidebars/projects/menus/ci_cd_menu_spec.rb | 66 ++++++++++++ spec/models/concerns/integration_spec.rb | 33 ++++++ spec/models/integration_spec.rb | 33 ------ .../issuable_invite_members_shared_examples.rb | 27 +---- .../layouts/nav/sidebar/_project.html.haml_spec.rb | 100 ++++++++++-------- 69 files changed, 623 insertions(+), 742 deletions(-) delete mode 100644 app/assets/javascripts/invite_member/components/invite_member_modal.vue delete mode 100644 app/assets/javascripts/invite_member/components/invite_member_trigger.vue delete mode 100644 app/assets/javascripts/invite_member/constants.js delete mode 100644 app/assets/javascripts/invite_member/event_hub.js delete mode 100644 app/assets/javascripts/invite_member/init_invite_member_modal.js delete mode 100644 app/assets/javascripts/invite_member/init_invite_member_trigger.js create mode 100644 changelogs/unreleased/326251-experiment-cleanup-invite_members_version_b-in-assignee-dropdown.yml create mode 100644 changelogs/unreleased/327405-retry-failed-background-migrations.yml delete mode 100644 config/feature_flags/experiment/invite_members_version_b_experiment_percentage.yml create mode 100644 db/migrate/20210427062807_add_index_to_batched_migration_jobs_status.rb create mode 100644 db/schema_migrations/20210427062807 create mode 100644 lib/sidebars/projects/menus/ci_cd_menu.rb delete mode 100644 spec/frontend/invite_member/components/invite_member_modal_spec.js delete mode 100644 spec/frontend/invite_member/components/invite_member_trigger_mock_data.js delete mode 100644 spec/frontend/invite_member/components/invite_member_trigger_spec.js create mode 100644 spec/lib/sidebars/projects/menus/ci_cd_menu_spec.rb create mode 100644 spec/models/concerns/integration_spec.rb delete mode 100644 spec/models/integration_spec.rb diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml index deb3d8f3df4..a50a96e7cb8 100644 --- a/.rubocop_manual_todo.yml +++ b/.rubocop_manual_todo.yml @@ -385,8 +385,6 @@ RSpec/EmptyLineAfterFinalLetItBe: - ee/spec/controllers/subscriptions_controller_spec.rb - ee/spec/features/ci_shared_runner_warnings_spec.rb - ee/spec/features/integrations/jira/jira_issues_list_spec.rb - - ee/spec/features/issues/bulk_assignment_epic_spec.rb - - ee/spec/features/issues/user_uses_quick_actions_spec.rb - ee/spec/features/markdown/metrics_spec.rb - ee/spec/features/registrations/group_invites_during_signup_flow_spec.rb - ee/spec/features/subscriptions_spec.rb diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index eba0eb0eea3..a1f074cd973 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -76be441c65a48b106fdfc06aa35c0d15bdf13b9b +f6f340eff91d01a1e36e8c9c368d93c9bff5e4f5 diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue index aa574222fef..f94697172ac 100644 --- a/app/assets/javascripts/boards/components/board_list_header.vue +++ b/app/assets/javascripts/boards/components/board_list_header.vue @@ -84,7 +84,7 @@ export default { return this.list?.label?.description || this.list?.assignee?.name || this.list.title || ''; }, showListHeaderButton() { - return !this.disabled && this.listType !== ListType.closed && !this.isEpicBoard; + return !this.disabled && this.listType !== ListType.closed; }, showMilestoneListDetails() { return this.listType === ListType.milestone && this.list.milestone && this.showListDetails; diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue index 997655c346a..3d7f1f38a34 100644 --- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue @@ -29,17 +29,17 @@ export default { }; }, computed: { - ...mapGetters(['isSidebarOpen', 'shouldUseGraphQL']), + ...mapGetters(['isSidebarOpen', 'shouldUseGraphQL', 'isEpicBoard']), ...mapState(['activeId', 'sidebarType', 'boardLists']), isWipLimitsOn() { - return this.glFeatures.wipLimits; + return this.glFeatures.wipLimits && !this.isEpicBoard; }, activeList() { /* Warning: Though a computed property it is not reactive because we are referencing a List Model class. Reactivity only applies to plain JS objects */ - if (this.shouldUseGraphQL) { + if (this.shouldUseGraphQL || this.isEpicBoard) { return this.boardLists[this.activeId]; } return boardsStore.state.lists.find(({ id }) => id === this.activeId); @@ -71,7 +71,7 @@ export default { deleteBoard() { // eslint-disable-next-line no-alert if (window.confirm(__('Are you sure you want to remove this list?'))) { - if (this.shouldUseGraphQL) { + if (this.shouldUseGraphQL || this.isEpicBoard) { this.removeList(this.activeId); } else { this.activeList.destroy(); diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js index 76edb69c204..4519992ca94 100644 --- a/app/assets/javascripts/boards/constants.js +++ b/app/assets/javascripts/boards/constants.js @@ -2,6 +2,7 @@ import { __ } from '~/locale'; import updateEpicSubscriptionMutation from '~/sidebar/queries/update_epic_subscription.mutation.graphql'; import updateEpicTitleMutation from '~/sidebar/queries/update_epic_title.mutation.graphql'; import boardBlockingIssuesQuery from './graphql/board_blocking_issues.query.graphql'; +import destroyBoardListMutation from './graphql/board_list_destroy.mutation.graphql'; import updateBoardListMutation from './graphql/board_list_update.mutation.graphql'; import issueSetSubscriptionMutation from './graphql/issue_set_subscription.mutation.graphql'; import issueSetTitleMutation from './graphql/issue_set_title.mutation.graphql'; @@ -73,6 +74,12 @@ export const updateListQueries = { }, }; +export const deleteListQueries = { + [issuableTypes.issue]: { + mutation: destroyBoardListMutation, + }, +}; + export const titleQueries = { [issuableTypes.issue]: { mutation: issueSetTitleMutation, diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index 5f8db077586..de634e844dc 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -8,6 +8,7 @@ import { titleQueries, subscriptionQueries, SupportedFilters, + deleteListQueries, updateListQueries, } from 'ee_else_ce/boards/constants'; import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql'; @@ -31,7 +32,6 @@ import { getSupportedParams, } from '../boards_util'; import boardLabelsQuery from '../graphql/board_labels.query.graphql'; -import destroyBoardListMutation from '../graphql/board_list_destroy.mutation.graphql'; import groupProjectsQuery from '../graphql/group_projects.query.graphql'; import issueCreateMutation from '../graphql/issue_create.mutation.graphql'; import issueSetDueDateMutation from '../graphql/issue_set_due_date.mutation.graphql'; @@ -265,14 +265,14 @@ export default { commit(types.TOGGLE_LIST_COLLAPSED, { listId, collapsed }); }, - removeList: ({ state, commit }, listId) => { - const listsBackup = { ...state.boardLists }; + removeList: ({ state: { issuableType, boardLists }, commit }, listId) => { + const listsBackup = { ...boardLists }; commit(types.REMOVE_LIST, listId); return gqlClient .mutate({ - mutation: destroyBoardListMutation, + mutation: deleteListQueries[issuableType].mutation, variables: { listId, }, diff --git a/app/assets/javascripts/invite_member/components/invite_member_modal.vue b/app/assets/javascripts/invite_member/components/invite_member_modal.vue deleted file mode 100644 index ec77e49ae53..00000000000 --- a/app/assets/javascripts/invite_member/components/invite_member_modal.vue +++ /dev/null @@ -1,67 +0,0 @@ - - diff --git a/app/assets/javascripts/invite_member/components/invite_member_trigger.vue b/app/assets/javascripts/invite_member/components/invite_member_trigger.vue deleted file mode 100644 index ee89e0bbf71..00000000000 --- a/app/assets/javascripts/invite_member/components/invite_member_trigger.vue +++ /dev/null @@ -1,43 +0,0 @@ - - - diff --git a/app/assets/javascripts/invite_member/constants.js b/app/assets/javascripts/invite_member/constants.js deleted file mode 100644 index fee6e7a260a..00000000000 --- a/app/assets/javascripts/invite_member/constants.js +++ /dev/null @@ -1,2 +0,0 @@ -export const OPEN_MODAL = 'openModal'; -export const MODAL_ID = 'invite-member-modal'; diff --git a/app/assets/javascripts/invite_member/event_hub.js b/app/assets/javascripts/invite_member/event_hub.js deleted file mode 100644 index e31806ad199..00000000000 --- a/app/assets/javascripts/invite_member/event_hub.js +++ /dev/null @@ -1,3 +0,0 @@ -import createEventHub from '~/helpers/event_hub_factory'; - -export default createEventHub(); diff --git a/app/assets/javascripts/invite_member/init_invite_member_modal.js b/app/assets/javascripts/invite_member/init_invite_member_modal.js deleted file mode 100644 index a50d31c9e7a..00000000000 --- a/app/assets/javascripts/invite_member/init_invite_member_modal.js +++ /dev/null @@ -1,27 +0,0 @@ -import { GlToast } from '@gitlab/ui'; -import Vue from 'vue'; -import { isInIssuePage, isInDesignPage } from '~/lib/utils/common_utils'; -import InviteMemberModal from './components/invite_member_modal.vue'; - -Vue.use(GlToast); - -const isAssigneesWidgetShown = - (isInIssuePage() || isInDesignPage()) && gon.features.issueAssigneesWidget; - -export default function initInviteMembersModal() { - const el = document.querySelector('.js-invite-member-modal'); - - if (!el || isAssigneesWidgetShown) { - return false; - } - - const { membersPath } = el.dataset; - - return new Vue({ - el, - render: (createElement) => - createElement(InviteMemberModal, { - props: { membersPath }, - }), - }); -} diff --git a/app/assets/javascripts/invite_member/init_invite_member_trigger.js b/app/assets/javascripts/invite_member/init_invite_member_trigger.js deleted file mode 100644 index eb765ae83b0..00000000000 --- a/app/assets/javascripts/invite_member/init_invite_member_trigger.js +++ /dev/null @@ -1,18 +0,0 @@ -import Vue from 'vue'; -import InviteMemberTrigger from './components/invite_member_trigger.vue'; - -export default function initInviteMembersTrigger() { - const el = document.querySelector('.js-invite-member-trigger'); - - if (!el) { - return false; - } - - return new Vue({ - el, - render: (createElement) => - createElement(InviteMemberTrigger, { - props: { ...el.dataset }, - }), - }); -} diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js index 2b679a83eac..3143ff5adac 100644 --- a/app/assets/javascripts/pages/projects/issues/show.js +++ b/app/assets/javascripts/pages/projects/issues/show.js @@ -1,8 +1,6 @@ import loadAwardsHandler from '~/awards_handler'; import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; import initIssuableSidebar from '~/init_issuable_sidebar'; -import initInviteMemberModal from '~/invite_member/init_invite_member_modal'; -import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger'; import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger'; import { IssuableType } from '~/issuable_show/constants'; @@ -58,7 +56,5 @@ export default function initShowIssue() { } else { loadAwardsHandler(); } - initInviteMemberModal(); - initInviteMemberTrigger(); } } diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js index a5118e3529a..021122d7637 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js +++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js @@ -3,8 +3,6 @@ import loadAwardsHandler from '~/awards_handler'; import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; import initPipelines from '~/commit/pipelines/pipelines_bundle'; import initIssuableSidebar from '~/init_issuable_sidebar'; -import initInviteMemberModal from '~/invite_member/init_invite_member_modal'; -import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger'; import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger'; import { handleLocationHash } from '~/lib/utils/common_utils'; @@ -28,8 +26,6 @@ export default function initMergeRequestShow() { } else { loadAwardsHandler(); } - initInviteMemberModal(); - initInviteMemberTrigger(); initInviteMembersModal(); initInviteMembersTrigger(); diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue index 2fc25151d1c..cced8462955 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue @@ -47,9 +47,6 @@ export default { directlyInviteMembers: { default: false, }, - indirectlyInviteMembers: { - default: false, - }, }, props: { iid: { @@ -444,7 +441,7 @@ export default { diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue index 9952c6db582..5c32d03e0d4 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue @@ -1,51 +1,23 @@ diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 9115c3562d3..b1d35a1e6f4 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -86,7 +86,7 @@ function mountAssigneesComponent() { if (!el) return; - const { id, iid, fullPath, editable, projectMembersPath } = getSidebarOptions(); + const { id, iid, fullPath, editable } = getSidebarOptions(); // eslint-disable-next-line no-new new Vue({ el, @@ -96,9 +96,7 @@ function mountAssigneesComponent() { }, provide: { canUpdate: editable, - projectMembersPath, directlyInviteMembers: el.hasAttribute('data-directly-invite-members'), - indirectlyInviteMembers: el.hasAttribute('data-indirectly-invite-members'), }, render: (createElement) => createElement('sidebar-assignees-widget', { diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index cae5cc411bc..dd9b9071fd8 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -56,8 +56,6 @@ class Projects::IssuesController < Projects::ApplicationController push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml) push_frontend_feature_flag(:issue_assignees_widget, @project, default_enabled: :yaml) - record_experiment_user(:invite_members_version_b) - experiment(:invite_members_in_comment, namespace: @project.root_ancestor) do |experiment_instance| experiment_instance.exclude! unless helpers.can_import_members? diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index a31437288b9..bf534c5fb04 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -47,8 +47,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo push_frontend_feature_flag(:users_expanding_widgets_usage_data, @project, default_enabled: :yaml) push_frontend_feature_flag(:diff_settings_usage_data, default_enabled: :yaml) - record_experiment_user(:invite_members_version_b) - experiment(:invite_members_in_comment, namespace: @project.root_ancestor) do |experiment_instance| experiment_instance.exclude! unless helpers.can_import_members? diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb index 5cf47f4a990..2cad6f4745c 100644 --- a/app/helpers/invite_members_helper.rb +++ b/app/helpers/invite_members_helper.rb @@ -17,20 +17,6 @@ module InviteMembersHelper end end - def indirectly_invite_members? - strong_memoize(:indirectly_invite_members) do - experiment_enabled?(:invite_members_version_b) && !can_import_members? - end - end - - def show_invite_members_track_event - if directly_invite_members? - 'show_invite_members' - elsif indirectly_invite_members? - 'show_invite_members_version_b' - end - end - def invite_group_members?(group) experiment_enabled?(:invite_members_empty_group_version_a) && Ability.allowed?(current_user, :admin_group_member, group) end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index e5ea2920eaa..d149389b31f 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -390,8 +390,7 @@ module IssuablesHelper severity: issuable[:severity], timeTrackingLimitToHours: Gitlab::CurrentSettings.time_tracking_limit_to_hours, createNoteEmail: issuable[:create_note_email], - issuableType: issuable[:type], - projectMembersPath: project_project_members_path(@project, sort: :access_level_desc) + issuableType: issuable[:type] } end diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb index 0aa3aa20306..143919a68ab 100644 --- a/app/helpers/sidebars_helper.rb +++ b/app/helpers/sidebars_helper.rb @@ -40,7 +40,8 @@ module SidebarsHelper container: project, learn_gitlab_experiment_enabled: learn_gitlab_experiment_enabled?(project), current_ref: current_ref, - jira_issues_integration: project_jira_issues_integration? + jira_issues_integration: project_jira_issues_integration?, + can_view_pipeline_editor: can_view_pipeline_editor?(project) } end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index c630dff6fa8..c2dc9c5d859 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -908,7 +908,7 @@ module Ci def same_family_pipeline_ids ::Gitlab::Ci::PipelineObjectHierarchy.new( - self.class.where(id: root_ancestor), options: { same_project: true } + self.class.default_scoped.where(id: root_ancestor), options: { same_project: true } ).base_and_descendants.select(:id) end diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb index bbf9ecbcfe9..80cf6260b0b 100644 --- a/app/models/concerns/atomic_internal_id.rb +++ b/app/models/concerns/atomic_internal_id.rb @@ -214,9 +214,9 @@ module AtomicInternalId def self.project_init(klass, column_name = :iid) ->(instance, scope) do if instance - klass.where(project_id: instance.project_id).maximum(column_name) + klass.default_scoped.where(project_id: instance.project_id).maximum(column_name) elsif scope.present? - klass.where(**scope).maximum(column_name) + klass.default_scoped.where(**scope).maximum(column_name) end end end diff --git a/app/models/member.rb b/app/models/member.rb index e978552592d..ee9b2c8cef3 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -140,7 +140,8 @@ class Member < ApplicationRecord scope :distinct_on_user_with_max_access_level, -> do distinct_members = select('DISTINCT ON (user_id, invite_email) *') .order('user_id, invite_email, access_level DESC, expires_at DESC, created_at ASC') - Member.from(distinct_members, :members) + + from(distinct_members, :members) end scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) } diff --git a/app/models/project.rb b/app/models/project.rb index 2cddcc43c4b..b8b52c594c4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2528,7 +2528,7 @@ class Project < ApplicationRecord namespace.root_ancestor.all_projects .joins(:packages) .where.not(id: id) - .merge(Packages::Package.with_name(package_name)) + .merge(Packages::Package.default_scoped.with_name(package_name)) .exists? end diff --git a/app/views/layouts/nav/sidebar/_project_menus.html.haml b/app/views/layouts/nav/sidebar/_project_menus.html.haml index 631fc3f095d..4826aca9f0a 100644 --- a/app/views/layouts/nav/sidebar/_project_menus.html.haml +++ b/app/views/layouts/nav/sidebar/_project_menus.html.haml @@ -1,49 +1,3 @@ -- if project_nav_tab? :pipelines - = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], unless: -> { current_path?('projects/pipelines#charts') }) do - = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines qa-link-pipelines rspec-link-pipelines', data: { qa_selector: 'ci_cd_link' } do - .nav-icon-container - = sprite_icon('rocket') - %span.nav-item-name#js-onboarding-pipelines-link - = _('CI/CD') - - %ul.sidebar-sub-level-items - = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], html_options: { class: "fly-out-top-item" }) do - = link_to project_pipelines_path(@project) do - %strong.fly-out-top-item-name - = _('CI/CD') - %li.divider.fly-out-top-item - - if project_nav_tab? :pipelines - = nav_link(path: ['pipelines#index', 'pipelines#show']) do - = link_to project_pipelines_path(@project), title: _('Pipelines'), class: 'shortcuts-pipelines' do - %span - = _('Pipelines') - - - if can_view_pipeline_editor?(@project) - = nav_link(controller: :pipeline_editor, action: :show) do - = link_to project_ci_pipeline_editor_path(@project), title: s_('Pipelines|Editor') do - %span - = s_('Pipelines|Editor') - - - if project_nav_tab? :builds - = nav_link(controller: :jobs) do - = link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do - %span - = _('Jobs') - - - if Feature.enabled?(:artifacts_management_page, @project) - = nav_link(controller: :artifacts, action: :index) do - = link_to project_artifacts_path(@project), title: _('Artifacts'), class: 'shortcuts-builds' do - %span - = _('Artifacts') - - - if project_nav_tab?(:pipelines) - = nav_link(controller: :pipeline_schedules) do - = link_to pipeline_schedules_path(@project), title: _('Schedules'), class: 'shortcuts-builds' do - %span - = _('Schedules') - - = render_if_exists "layouts/nav/test_cases_link", project: @project - - if project_nav_tab? :security_and_compliance = render_if_exists 'layouts/nav/sidebar/project_security_link' # EE-specific diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml index 47e7ff0e4bc..86369b32e98 100644 --- a/app/views/shared/issuable/_sidebar_assignees.html.haml +++ b/app/views/shared/issuable/_sidebar_assignees.html.haml @@ -1,7 +1,10 @@ - issuable_type = issuable_sidebar[:type] - dropdown_options = assignees_dropdown_options(issuable_type) -#js-vue-sidebar-assignees{ data: { field: issuable_type, signed_in: signed_in, max_assignees: dropdown_options[:data][:"max-select"], directly_invite_members: directly_invite_members?, indirectly_invite_members: indirectly_invite_members? } } +#js-vue-sidebar-assignees{ data: { field: issuable_type, + signed_in: signed_in, + max_assignees: dropdown_options[:data][:"max-select"], + directly_invite_members: directly_invite_members? } } .title.hide-collapsed = _('Assignee') = loading_icon(css_class: 'gl-vertical-align-text-bottom') @@ -39,12 +42,12 @@ - data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select'] - options[:data].merge!(data) - - if directly_invite_members? || indirectly_invite_members? + - if directly_invite_members? - options[:dropdown_class] += ' dropdown-extended-height' - options[:footer_content] = true - options[:wrapper_class] = 'js-sidebar-assignee-dropdown' - options[:toggle_class] += ' js-invite-members-track' - - data['track-event'] = show_invite_members_track_event + - data['track-event'] = 'show_invite_members' - options[:data].merge!(data) - invite_text = _('Invite Members') - track_label = 'edit_assignee' @@ -52,15 +55,9 @@ = dropdown_tag(title, options: options) do %ul.dropdown-footer-list %li - - if directly_invite_members? - .js-invite-members-trigger{ data: { trigger_element: 'anchor', - display_text: invite_text, - event: 'click_invite_members', - label: track_label } } - - else - .js-invite-member-trigger{ data: { display_text: invite_text, event: 'click_invite_members_version_b', label: track_label } } + .js-invite-members-trigger{ data: { trigger_element: 'anchor', + display_text: invite_text, + event: 'click_invite_members', + label: track_label } } - else = dropdown_tag(title, options: options) - -- if indirectly_invite_members? - .js-invite-member-modal{ data: { members_path: project_project_members_path(@project, sort: :access_level_desc) } } diff --git a/changelogs/unreleased/326251-experiment-cleanup-invite_members_version_b-in-assignee-dropdown.yml b/changelogs/unreleased/326251-experiment-cleanup-invite_members_version_b-in-assignee-dropdown.yml new file mode 100644 index 00000000000..3b484497362 --- /dev/null +++ b/changelogs/unreleased/326251-experiment-cleanup-invite_members_version_b-in-assignee-dropdown.yml @@ -0,0 +1,5 @@ +--- +title: Remove invite_members_version_b experiment +merge_request: 60426 +author: +type: other diff --git a/changelogs/unreleased/327405-retry-failed-background-migrations.yml b/changelogs/unreleased/327405-retry-failed-background-migrations.yml new file mode 100644 index 00000000000..34c3e59850d --- /dev/null +++ b/changelogs/unreleased/327405-retry-failed-background-migrations.yml @@ -0,0 +1,5 @@ +--- +title: Add index to batched migration jobs status +merge_request: 60248 +author: +type: other diff --git a/config/feature_flags/experiment/invite_members_version_b_experiment_percentage.yml b/config/feature_flags/experiment/invite_members_version_b_experiment_percentage.yml deleted file mode 100644 index 069e740ba44..00000000000 --- a/config/feature_flags/experiment/invite_members_version_b_experiment_percentage.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: invite_members_version_b_experiment_percentage -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43900 -rollout_issue_url: https://gitlab.com/gitlab-org/growth/team-tasks/-/issues/214 -milestone: '13.5' -type: experiment -group: group::expansion -default_enabled: false diff --git a/db/migrate/20210427062807_add_index_to_batched_migration_jobs_status.rb b/db/migrate/20210427062807_add_index_to_batched_migration_jobs_status.rb new file mode 100644 index 00000000000..c429094762e --- /dev/null +++ b/db/migrate/20210427062807_add_index_to_batched_migration_jobs_status.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddIndexToBatchedMigrationJobsStatus < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + INDEX_NAME = 'index_batched_jobs_on_batched_migration_id_and_status' + + def up + add_concurrent_index :batched_background_migration_jobs, [:batched_background_migration_id, :status], name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :batched_background_migration_jobs, INDEX_NAME + end +end diff --git a/db/post_migrate/20191030223057_backfill_version_author_and_created_at.rb b/db/post_migrate/20191030223057_backfill_version_author_and_created_at.rb index 5fcec83bfc3..3ec6c59f166 100644 --- a/db/post_migrate/20191030223057_backfill_version_author_and_created_at.rb +++ b/db/post_migrate/20191030223057_backfill_version_author_and_created_at.rb @@ -29,7 +29,7 @@ class BackfillVersionAuthorAndCreatedAt < ActiveRecord::Migration[5.2] issues = Issue.arel_table projects = Project.arel_table - Version.select(versions[:issue_id]).where( + select(versions[:issue_id]).where( versions[:author_id].eq(nil).or( versions[:created_at].eq(nil) ).and( diff --git a/db/schema_migrations/20210427062807 b/db/schema_migrations/20210427062807 new file mode 100644 index 00000000000..c9d82dfa931 --- /dev/null +++ b/db/schema_migrations/20210427062807 @@ -0,0 +1 @@ +306bb2bc3bfd20a57f1ac473e32596e7b7e7b6c2ae41c3fe5a7f45c551ce9207 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 653b329a067..a78b9656f28 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -22115,6 +22115,8 @@ CREATE INDEX index_badges_on_project_id ON badges USING btree (project_id); CREATE INDEX index_batched_jobs_by_batched_migration_id_and_id ON batched_background_migration_jobs USING btree (batched_background_migration_id, id); +CREATE INDEX index_batched_jobs_on_batched_migration_id_and_status ON batched_background_migration_jobs USING btree (batched_background_migration_id, status); + CREATE INDEX index_batched_migrations_on_job_table_and_column_name ON batched_background_migrations USING btree (job_class_name, table_name, column_name); CREATE INDEX index_board_assignees_on_assignee_id ON board_assignees USING btree (assignee_id); diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index b1c19ef7526..fd5642b2c4b 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -1122,6 +1122,35 @@ to define the explicit address that the GitLab Pages daemon should listen on: gitlab_pages['listen_proxy'] = '127.0.0.1:8090' ``` +### Intermittent 502 errors or after a few days + +If you run Pages on a system that uses `systemd` and +[`tmpfiles.d`](https://www.freedesktop.org/software/systemd/man/tmpfiles.d.html), +you may encounter intermittent 502 errors trying to serve Pages with an error similar to: + +```plaintext +dial tcp: lookup gitlab.example.com on [::1]:53: dial udp [::1]:53: connect: no route to host" +``` + +GitLab Pages creates a [bind mount](https://man7.org/linux/man-pages/man8/mount.8.html) +inside `/tmp/gitlab-pages-*` that includes files like `/etc/hosts`. +However, `systemd` may clean the `/tmp/` directory on a regular basis so the DNS +configuration may be lost. + +To stop `systemd` from cleaning the Pages related content: + +1. Tell `tmpfiles.d` to not remove the Pages `/tmp` directory: + + ```shell + echo 'x /tmp/gitlab-pages-*' >> /etc/tmpfiles.d/gitlab-pages-jail.conf + ``` + +1. Restart GitLab Pages: + + ```shell + sudo gitlab-ctl restart gitlab-pages + ``` + ### 404 error after transferring the project to a different group or user, or changing project path If you encounter a `404 Not Found` error a Pages site after transferring a project to diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md index dae38d0aea1..a9ab03c3213 100644 --- a/doc/ci/environments/index.md +++ b/doc/ci/environments/index.md @@ -676,7 +676,7 @@ fetch = +refs/environments/*:refs/remotes/origin/environments/* > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2112) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.4. > - [Environment scoping for CI/CD variables was moved to all tiers](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/30779) in GitLab 12.2. -> - [Environment scoping for Group CI/CD variables](https://gitlab.com/gitlab-org/gitlab/-/issues/2874) added to GitLab Premium in 13.11 +> - [Environment scoping for Group CI/CD variables](https://gitlab.com/gitlab-org/gitlab/-/issues/2874) added to GitLab Premium in 13.11. You can limit the environment scope of a CI/CD variable by defining which environments it can be available for. diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 9abd21c4d15..5796a5c6b7d 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -181,7 +181,7 @@ To add a group variable: - **Key**: Must be one line, with no spaces, using only letters, numbers, or `_`. - **Value**: No limitations. - **Type**: [`File` or `Variable`](#cicd-variable-types). - - **Environment scope** (optional): `All`, or specific [environments](#limit-the-environment-scope-of-a-cicd-variable). **PREMIUM** + - **Environment scope** (optional): `All`, or specific [environments](#limit-the-environment-scope-of-a-cicd-variable). **(PREMIUM)** - **Protect variable** (Optional): If selected, the variable is only available in pipelines that run on protected branches or tags. - **Mask variable** (Optional): If selected, the variable's **Value** is masked diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 40457dbb533..491f65bd88a 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -33,7 +33,7 @@ compatible. For GitLab.com, please take into consideration that regular migrations (under `db/migrate`) are run before [Canary is deployed](https://gitlab.com/gitlab-com/gl-infra/readiness/-/tree/master/library/canary/#configuration-and-deployment), -and post-deployment migrations (`db/post_migrate`) are run after the deployment to production has finished. +and [post-deployment migrations](post_deployment_migrations.md) (`db/post_migrate`) are run after the deployment to production has finished. ## Schema Changes diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md index 80b0c3b1f29..da2e2e8dcc2 100644 --- a/doc/user/project/pages/introduction.md +++ b/doc/user/project/pages/introduction.md @@ -273,6 +273,10 @@ Sure. All you need to do is download the artifacts archive from the job page. Yes. GitLab Pages doesn't care whether you set your project's visibility level to private, internal or public. +### Can I create a personal or a group website + +Yes. See the documentation about [GitLab Pages domain names, URLs, and base URLs](getting_started_part_one.md). + ### Do I need to create a user/group website before creating a project website? No, you don't. You can create your project first and access it under diff --git a/lib/gitlab/database/background_migration/batched_job.rb b/lib/gitlab/database/background_migration/batched_job.rb index 8cedace0db3..869b97b8ac0 100644 --- a/lib/gitlab/database/background_migration/batched_job.rb +++ b/lib/gitlab/database/background_migration/batched_job.rb @@ -4,10 +4,23 @@ module Gitlab module Database module BackgroundMigration class BatchedJob < ActiveRecord::Base # rubocop:disable Rails/ApplicationRecord + include FromUnion + self.table_name = :batched_background_migration_jobs + MAX_ATTEMPTS = 3 + STUCK_JOBS_TIMEOUT = 1.hour.freeze + belongs_to :batched_migration, foreign_key: :batched_background_migration_id + scope :active, -> { where(status: [:pending, :running]) } + scope :stuck, -> { active.where('updated_at <= ?', STUCK_JOBS_TIMEOUT.ago) } + scope :retriable, -> { + failed_jobs = where(status: :failed).where('attempts < ?', MAX_ATTEMPTS) + + from_union([failed_jobs, self.stuck]) + } + enum status: { pending: 0, running: 1, diff --git a/lib/gitlab/database/background_migration/batched_migration.rb b/lib/gitlab/database/background_migration/batched_migration.rb index 1203efd06a7..e85162f355e 100644 --- a/lib/gitlab/database/background_migration/batched_migration.rb +++ b/lib/gitlab/database/background_migration/batched_migration.rb @@ -20,7 +20,8 @@ module Gitlab paused: 0, active: 1, aborted: 2, - finished: 3 + finished: 3, + failed: 4 } attribute :pause_ms, :integer, default: 100 diff --git a/lib/gitlab/database/background_migration/batched_migration_runner.rb b/lib/gitlab/database/background_migration/batched_migration_runner.rb index 4e125431122..67fe6c536e6 100644 --- a/lib/gitlab/database/background_migration/batched_migration_runner.rb +++ b/lib/gitlab/database/background_migration/batched_migration_runner.rb @@ -19,7 +19,7 @@ module Gitlab # # Note that this method is primarily intended to called by a scheduled worker. def run_migration_job(active_migration) - if next_batched_job = create_next_batched_job!(active_migration) + if next_batched_job = find_or_create_next_batched_job(active_migration) migration_wrapper.perform(next_batched_job) active_migration.optimize! @@ -48,12 +48,12 @@ module Gitlab attr_reader :migration_wrapper - def create_next_batched_job!(active_migration) - next_batch_range = find_next_batch_range(active_migration) - - return if next_batch_range.nil? - - active_migration.create_batched_job!(next_batch_range.min, next_batch_range.max) + def find_or_create_next_batched_job(active_migration) + if next_batch_range = find_next_batch_range(active_migration) + active_migration.create_batched_job!(next_batch_range.min, next_batch_range.max) + else + active_migration.batched_jobs.retriable.first + end end def find_next_batch_range(active_migration) @@ -82,7 +82,13 @@ module Gitlab end def finish_active_migration(active_migration) - active_migration.finished! + return if active_migration.batched_jobs.active.exists? + + if active_migration.batched_jobs.failed.exists? + active_migration.failed! + else + active_migration.finished! + end end end end diff --git a/lib/gitlab/database/background_migration/batched_migration_wrapper.rb b/lib/gitlab/database/background_migration/batched_migration_wrapper.rb index ede1fbca737..4851c611b5a 100644 --- a/lib/gitlab/database/background_migration/batched_migration_wrapper.rb +++ b/lib/gitlab/database/background_migration/batched_migration_wrapper.rb @@ -31,7 +31,7 @@ module Gitlab private def start_tracking_execution(tracking_record) - tracking_record.update!(attempts: tracking_record.attempts + 1, status: :running, started_at: Time.current) + tracking_record.update!(attempts: tracking_record.attempts + 1, status: :running, started_at: Time.current, finished_at: nil, metrics: {}) end def execute_batch(tracking_record) diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb index 145bb6d7b8f..259e53c2f7c 100644 --- a/lib/gitlab/experimentation.rb +++ b/lib/gitlab/experimentation.rb @@ -34,10 +34,6 @@ module Gitlab module Experimentation EXPERIMENTS = { - invite_members_version_b: { - tracking_category: 'Growth::Expansion::Experiment::InviteMembersVersionB', - use_backwards_compatible_subject_index: true - }, invite_members_empty_group_version_a: { tracking_category: 'Growth::Expansion::Experiment::InviteMembersEmptyGroupVersionA', use_backwards_compatible_subject_index: true diff --git a/lib/gitlab/object_hierarchy.rb b/lib/gitlab/object_hierarchy.rb index 9a74266693b..ee44740b58b 100644 --- a/lib/gitlab/object_hierarchy.rb +++ b/lib/gitlab/object_hierarchy.rb @@ -7,7 +7,7 @@ module Gitlab class ObjectHierarchy DEPTH_COLUMN = :depth - attr_reader :ancestors_base, :descendants_base, :model, :options + attr_reader :ancestors_base, :descendants_base, :model, :options, :unscoped_model # ancestors_base - An instance of ActiveRecord::Relation for which to # get parent objects. @@ -19,6 +19,7 @@ module Gitlab @ancestors_base = ancestors_base @descendants_base = descendants_base @model = ancestors_base.model + @unscoped_model = @model.unscoped @options = options end @@ -70,23 +71,23 @@ module Gitlab # if hierarchy_order is given, the calculated `depth` should be present in SELECT if expose_depth - recursive_query = base_and_ancestors_cte(upto, hierarchy_order).apply_to(model.all).distinct - read_only(model.from(Arel::Nodes::As.new(recursive_query.arel, objects_table)).order(depth: hierarchy_order)) + recursive_query = base_and_ancestors_cte(upto, hierarchy_order).apply_to(unscoped_model.all).distinct + read_only(unscoped_model.from(Arel::Nodes::As.new(recursive_query.arel, objects_table)).order(depth: hierarchy_order)) else - recursive_query = base_and_ancestors_cte(upto).apply_to(model.all) + recursive_query = base_and_ancestors_cte(upto).apply_to(unscoped_model.all) if skip_ordering? recursive_query = recursive_query.distinct else recursive_query = recursive_query.reselect(*recursive_query.arel.projections, 'ROW_NUMBER() OVER () as depth').distinct - recursive_query = model.from(Arel::Nodes::As.new(recursive_query.arel, objects_table)) + recursive_query = unscoped_model.from(Arel::Nodes::As.new(recursive_query.arel, objects_table)) recursive_query = remove_depth_and_maintain_order(recursive_query, hierarchy_order: hierarchy_order) end read_only(recursive_query) end else - recursive_query = base_and_ancestors_cte(upto, hierarchy_order).apply_to(model.all) + recursive_query = base_and_ancestors_cte(upto, hierarchy_order).apply_to(unscoped_model.all) recursive_query = recursive_query.order(depth: hierarchy_order) if hierarchy_order read_only(recursive_query) end @@ -103,23 +104,23 @@ module Gitlab if use_distinct? # Always calculate `depth`, remove it later if with_depth is false if with_depth - base_cte = base_and_descendants_cte(with_depth: true).apply_to(model.all).distinct - read_only(model.from(Arel::Nodes::As.new(base_cte.arel, objects_table)).order(depth: :asc)) + base_cte = base_and_descendants_cte(with_depth: true).apply_to(unscoped_model.all).distinct + read_only(unscoped_model.from(Arel::Nodes::As.new(base_cte.arel, objects_table)).order(depth: :asc)) else - base_cte = base_and_descendants_cte.apply_to(model.all) + base_cte = base_and_descendants_cte.apply_to(unscoped_model.all) if skip_ordering? base_cte = base_cte.distinct else base_cte = base_cte.reselect(*base_cte.arel.projections, 'ROW_NUMBER() OVER () as depth').distinct - base_cte = model.from(Arel::Nodes::As.new(base_cte.arel, objects_table)) + base_cte = unscoped_model.from(Arel::Nodes::As.new(base_cte.arel, objects_table)) base_cte = remove_depth_and_maintain_order(base_cte, hierarchy_order: :asc) end read_only(base_cte) end else - read_only(base_and_descendants_cte(with_depth: with_depth).apply_to(model.all)) + read_only(base_and_descendants_cte(with_depth: with_depth).apply_to(unscoped_model.all)) end end # rubocop: enable CodeReuse/ActiveRecord @@ -154,16 +155,15 @@ module Gitlab ancestors_table = ancestors.alias_to(objects_table) descendants_table = descendants.alias_to(objects_table) - ancestors_scope = model.unscoped.from(ancestors_table) - descendants_scope = model.unscoped.from(descendants_table) + ancestors_scope = unscoped_model.from(ancestors_table) + descendants_scope = unscoped_model.from(descendants_table) if use_distinct? ancestors_scope = ancestors_scope.distinct descendants_scope = descendants_scope.distinct end - relation = model - .unscoped + relation = unscoped_model .with .recursive(ancestors.to_arel, descendants.to_arel) .from_union([ @@ -215,7 +215,7 @@ module Gitlab cte << base_query # Recursively get all the ancestors of the base set. - parent_query = model + parent_query = unscoped_model .from(from_tables(cte)) .where(ancestor_conditions(cte)) .except(:order) @@ -248,7 +248,7 @@ module Gitlab cte << base_query # Recursively get all the descendants of the base set. - descendants_query = model + descendants_query = unscoped_model .from(from_tables(cte)) .where(descendant_conditions(cte)) .except(:order) diff --git a/lib/sidebars/projects/menus/ci_cd_menu.rb b/lib/sidebars/projects/menus/ci_cd_menu.rb new file mode 100644 index 00000000000..c0336d8dfbc --- /dev/null +++ b/lib/sidebars/projects/menus/ci_cd_menu.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +module Sidebars + module Projects + module Menus + class CiCdMenu < ::Sidebars::Menu + override :configure_menu_items + def configure_menu_items + return unless can?(context.current_user, :read_build, context.project) + + add_item(pipelines_menu_item) + add_item(pipelines_editor_menu_item) + add_item(jobs_menu_item) + add_item(artifacts_menu_item) + add_item(pipeline_schedules_menu_item) + end + + override :link + def link + project_pipelines_path(context.project) + end + + override :extra_container_html_options + def extra_container_html_options + { + class: 'shortcuts-pipelines rspec-link-pipelines' + } + end + + override :title + def title + _('CI/CD') + end + + override :title_html_options + def title_html_options + { + id: 'js-onboarding-pipelines-link' + } + end + + override :sprite_icon + def sprite_icon + 'rocket' + end + + private + + def pipelines_menu_item + ::Sidebars::MenuItem.new( + title: _('Pipelines'), + link: project_pipelines_path(context.project), + container_html_options: { class: 'shortcuts-pipelines' }, + active_routes: { path: pipelines_routes }, + item_id: :pipelines + ) + end + + def pipelines_routes + %w[ + pipelines#index + pipelines#show + pipelines#new + ] + end + + def pipelines_editor_menu_item + return unless context.can_view_pipeline_editor + + ::Sidebars::MenuItem.new( + title: s_('Pipelines|Editor'), + link: project_ci_pipeline_editor_path(context.project), + active_routes: { path: 'projects/ci/pipeline_editor#show' }, + item_id: :pipelines_editor + ) + end + + def jobs_menu_item + ::Sidebars::MenuItem.new( + title: _('Jobs'), + link: project_jobs_path(context.project), + container_html_options: { class: 'shortcuts-builds' }, + active_routes: { controller: :jobs }, + item_id: :jobs + ) + end + + def artifacts_menu_item + return unless Feature.enabled?(:artifacts_management_page, context.project) + + ::Sidebars::MenuItem.new( + title: _('Artifacts'), + link: project_artifacts_path(context.project), + container_html_options: { class: 'shortcuts-builds' }, + active_routes: { path: 'artifacts#index' }, + item_id: :artifacts + ) + end + + def pipeline_schedules_menu_item + ::Sidebars::MenuItem.new( + title: _('Schedules'), + link: pipeline_schedules_path(context.project), + container_html_options: { class: 'shortcuts-builds' }, + active_routes: { controller: :pipeline_schedules }, + item_id: :pipeline_schedules + ) + end + end + end + end +end + +Sidebars::Projects::Menus::CiCdMenu.prepend_if_ee('EE::Sidebars::Projects::Menus::CiCdMenu') diff --git a/lib/sidebars/projects/panel.rb b/lib/sidebars/projects/panel.rb index 1db4d55740a..4cafc530979 100644 --- a/lib/sidebars/projects/panel.rb +++ b/lib/sidebars/projects/panel.rb @@ -14,6 +14,7 @@ module Sidebars add_menu(Sidebars::Projects::Menus::ExternalIssueTrackerMenu.new(context)) add_menu(Sidebars::Projects::Menus::LabelsMenu.new(context)) add_menu(Sidebars::Projects::Menus::MergeRequestsMenu.new(context)) + add_menu(Sidebars::Projects::Menus::CiCdMenu.new(context)) end override :render_raw_menus_partial diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 20f631e5d97..00c749e85d5 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6032,9 +6032,6 @@ msgstr "" msgid "Changes to the title have not been saved" msgstr "" -msgid "Changes won't take place until the index is %{link_start}recreated%{link_end}." -msgstr "" - msgid "Changing group URL can have unintended side effects." msgstr "" @@ -16252,15 +16249,9 @@ msgstr "" msgid "How many days need to pass between marking entity for deletion and actual removing it." msgstr "" -msgid "How many replicas each Elasticsearch shard has." -msgstr "" - msgid "How many seconds an IP will be counted towards the limit" msgstr "" -msgid "How many shards to split the Elasticsearch index over." -msgstr "" - msgid "How many users will be evaluating the trial?" msgstr "" @@ -17257,6 +17248,9 @@ msgstr "" msgid "Indent" msgstr "" +msgid "Index" +msgstr "" + msgid "Index all projects" msgstr "" @@ -17871,24 +17865,12 @@ msgstr "" msgid "InviteMember|Invited users will be added with developer level permissions. %{linkStart}View the documentation%{linkEnd} to see how to change this later." msgstr "" -msgid "InviteMember|Oops, this feature isn't ready yet" -msgstr "" - -msgid "InviteMember|See who can invite members for you" -msgstr "" - msgid "InviteMember|Send invitations" msgstr "" msgid "InviteMember|Skip this for now" msgstr "" -msgid "InviteMember|Until then, ask an owner to invite new project members for you" -msgstr "" - -msgid "InviteMember|We're working to allow everyone to invite new members, making it easier for teams to get started with GitLab" -msgstr "" - msgid "InviteReminderEmail|%{inviter} is still waiting for you to join GitLab" msgstr "" @@ -19001,6 +18983,9 @@ msgstr "" msgid "Learn more about group-level project templates" msgstr "" +msgid "Learn more about shards and replicas in the %{configuration_link_start}Advanced search configuration%{configuration_link_end} documentation. Changes won't take place until the index is %{recreated_link_start}recreated%{recreated_link_end}." +msgstr "" + msgid "Learn more about signing commits" msgstr "" @@ -22316,10 +22301,7 @@ msgstr "" msgid "Number of %{itemTitle}" msgstr "" -msgid "Number of Elasticsearch replicas" -msgstr "" - -msgid "Number of Elasticsearch shards" +msgid "Number of Elasticsearch shards and replicas (per index)" msgstr "" msgid "Number of Git pushes after which 'git gc' is run." @@ -22358,6 +22340,12 @@ msgstr "" msgid "Number of files touched" msgstr "" +msgid "Number of replicas" +msgstr "" + +msgid "Number of shards" +msgstr "" + msgid "OK" msgstr "" diff --git a/qa/qa/page/project/sub_menus/ci_cd.rb b/qa/qa/page/project/sub_menus/ci_cd.rb index 398712c04d2..7cb2fd6c655 100644 --- a/qa/qa/page/project/sub_menus/ci_cd.rb +++ b/qa/qa/page/project/sub_menus/ci_cd.rb @@ -12,16 +12,12 @@ module QA base.class_eval do include QA::Page::Project::SubMenus::Common - - view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do - element :link_pipelines - end end end def click_ci_cd_pipelines within_sidebar do - click_element :link_pipelines + click_element(:sidebar_menu_link, menu_item: 'CI/CD') end end end diff --git a/spec/deprecation_toolkit_env.rb b/spec/deprecation_toolkit_env.rb index 4bd04eabe69..2359765fedd 100644 --- a/spec/deprecation_toolkit_env.rb +++ b/spec/deprecation_toolkit_env.rb @@ -57,7 +57,6 @@ module DeprecationToolkitEnv %w[ activerecord-6.0.3.6/lib/active_record/migration.rb activesupport-6.0.3.6/lib/active_support/cache.rb - carrierwave-1.3.1/lib/carrierwave/sanitized_file.rb activerecord-6.0.3.6/lib/active_record/relation.rb asciidoctor-2.0.12/lib/asciidoctor/extensions.rb ] diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index 04b4caa52fe..0566ce968d2 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -130,30 +130,7 @@ RSpec.describe 'Issue Sidebar' do end end - context 'when invite_members_version_b experiment is enabled' do - before do - stub_experiment_for_subject(invite_members_version_b: true) - end - - it 'shows a link for inviting members and follows through to modal' do - project.add_developer(user) - visit_issue(project, issue2) - - open_assignees_dropdown - - page.within '.dropdown-menu-user' do - expect(page).to have_link('Invite members', href: '#') - expect(page).to have_selector('[data-track-event="click_invite_members_version_b"]') - expect(page).to have_selector('[data-track-label="edit_assignee"]') - end - - click_link 'Invite members' - - expect(page).to have_content("Oops, this feature isn't ready yet") - end - end - - context 'when invite_members_version_b experiment is disabled' do + context 'when user cannot invite members in assignee dropdown' do it 'shows author in assignee dropdown and no invite link' do project.add_developer(user) visit_issue(project, issue2) diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index 8417ac9a41a..3758723c571 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -9,7 +9,7 @@ import { formatIssue, getMoveData, } from '~/boards/boards_util'; -import { inactiveId, ISSUABLE, ListType } from '~/boards/constants'; +import { inactiveId, ISSUABLE, ListType, issuableTypes } from '~/boards/constants'; import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql'; import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql'; import actions, { gqlClient } from '~/boards/stores/actions'; @@ -459,7 +459,7 @@ describe('updateList', () => { boardType: 'group', disabled: false, boardLists: [{ type: 'closed' }], - issuableType: 'issue', + issuableType: issuableTypes.issue, }; testAction( @@ -503,6 +503,7 @@ describe('removeList', () => { beforeEach(() => { state = { boardLists: mockListsById, + issuableType: issuableTypes.issue, }; }); @@ -1375,7 +1376,7 @@ describe('setActiveItemSubscribed', () => { [mockActiveIssue.id]: mockActiveIssue, }, fullPath: 'gitlab-org', - issuableType: 'issue', + issuableType: issuableTypes.issue, }; const getters = { activeBoardItem: mockActiveIssue, isEpicBoard: false }; const subscribedState = true; @@ -1483,7 +1484,7 @@ describe('setActiveIssueMilestone', () => { describe('setActiveItemTitle', () => { const state = { boardItems: { [mockIssue.id]: mockIssue }, - issuableType: 'issue', + issuableType: issuableTypes.issue, fullPath: 'path/f', }; const getters = { activeBoardItem: mockIssue, isEpicBoard: false }; diff --git a/spec/frontend/invite_member/components/invite_member_modal_spec.js b/spec/frontend/invite_member/components/invite_member_modal_spec.js deleted file mode 100644 index 03e3da2d5ef..00000000000 --- a/spec/frontend/invite_member/components/invite_member_modal_spec.js +++ /dev/null @@ -1,67 +0,0 @@ -import { GlLink, GlModal } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { stubComponent } from 'helpers/stub_component'; -import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper'; -import InviteMemberModal from '~/invite_member/components/invite_member_modal.vue'; - -const memberPath = 'member_path'; - -const GlEmoji = { template: '' }; -const createComponent = () => { - return shallowMount(InviteMemberModal, { - propsData: { - membersPath: memberPath, - }, - stubs: { - GlEmoji, - GlModal: stubComponent(GlModal, { - template: '
', - }), - }, - }); -}; - -describe('InviteMemberModal', () => { - let wrapper; - - beforeEach(() => { - wrapper = createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - const findLink = () => wrapper.find(GlLink); - - describe('rendering the modal', () => { - it('renders the modal with the correct title', () => { - expect(wrapper.text()).toContain("Oops, this feature isn't ready yet"); - }); - - describe('rendering the see who link', () => { - it('renders the correct link', () => { - expect(findLink().attributes('href')).toBe(memberPath); - }); - }); - }); - - describe('tracking', () => { - let trackingSpy; - - afterEach(() => { - unmockTracking(); - }); - - it('send an event when go to pipelines is clicked', () => { - trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn); - - triggerEvent(findLink().element); - - expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_who_can_invite_link', { - label: 'invite_members_message', - }); - }); - }); -}); diff --git a/spec/frontend/invite_member/components/invite_member_trigger_mock_data.js b/spec/frontend/invite_member/components/invite_member_trigger_mock_data.js deleted file mode 100644 index 9b34a8027e9..00000000000 --- a/spec/frontend/invite_member/components/invite_member_trigger_mock_data.js +++ /dev/null @@ -1,7 +0,0 @@ -const triggerProvides = { - displayText: 'Invite member', - event: 'click_invite_members_version_b', - label: 'edit_assignee', -}; - -export default triggerProvides; diff --git a/spec/frontend/invite_member/components/invite_member_trigger_spec.js b/spec/frontend/invite_member/components/invite_member_trigger_spec.js deleted file mode 100644 index 630e2dbfc16..00000000000 --- a/spec/frontend/invite_member/components/invite_member_trigger_spec.js +++ /dev/null @@ -1,48 +0,0 @@ -import { GlLink } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper'; -import InviteMemberTrigger from '~/invite_member/components/invite_member_trigger.vue'; -import triggerProvides from './invite_member_trigger_mock_data'; - -const createComponent = () => { - return shallowMount(InviteMemberTrigger, { propsData: triggerProvides }); -}; - -describe('InviteMemberTrigger', () => { - let wrapper; - - beforeEach(() => { - wrapper = createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - const findLink = () => wrapper.find(GlLink); - - describe('displayText', () => { - it('includes the correct displayText for the link', () => { - expect(findLink().text()).toBe(triggerProvides.displayText); - }); - }); - - describe('tracking', () => { - let trackingSpy; - - afterEach(() => { - unmockTracking(); - }); - - it('send an event when go to pipelines is clicked', () => { - trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn); - - triggerEvent(findLink().element); - - expect(trackingSpy).toHaveBeenCalledWith('_category_', triggerProvides.event, { - label: triggerProvides.label, - }); - }); - }); -}); diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js index 543bc1c128a..2973c25b936 100644 --- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js +++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js @@ -533,7 +533,7 @@ describe('Sidebar assignees widget', () => { expect(findInviteMembersLink().exists()).toBe(false); }); - it('does not render invite members link if `directlyInviteMembers` and `indirectlyInviteMembers` were not passed', async () => { + it('does not render invite members link if `directlyInviteMembers` was not passed', async () => { createComponent(); await waitForPromises(); expect(findInviteMembersLink().exists()).toBe(false); @@ -548,14 +548,4 @@ describe('Sidebar assignees widget', () => { await waitForPromises(); expect(findInviteMembersLink().exists()).toBe(true); }); - - it('renders invite members link if `indirectlyInviteMembers` is true', async () => { - createComponent({ - provide: { - indirectlyInviteMembers: true, - }, - }); - await waitForPromises(); - expect(findInviteMembersLink().exists()).toBe(true); - }); }); diff --git a/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js index 06f7da3d1ab..cfbe7227915 100644 --- a/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js +++ b/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js @@ -1,25 +1,14 @@ import { shallowMount } from '@vue/test-utils'; -import InviteMemberModal from '~/invite_member/components/invite_member_modal.vue'; -import InviteMemberTrigger from '~/invite_member/components/invite_member_trigger.vue'; import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; import SidebarInviteMembers from '~/sidebar/components/assignees/sidebar_invite_members.vue'; -const testProjectMembersPath = 'test-path'; - describe('Sidebar invite members component', () => { let wrapper; const findDirectInviteLink = () => wrapper.findComponent(InviteMembersTrigger); - const findIndirectInviteLink = () => wrapper.findComponent(InviteMemberTrigger); - const findInviteModal = () => wrapper.findComponent(InviteMemberModal); - const createComponent = ({ directlyInviteMembers = false } = {}) => { - wrapper = shallowMount(SidebarInviteMembers, { - provide: { - directlyInviteMembers, - projectMembersPath: testProjectMembersPath, - }, - }); + const createComponent = () => { + wrapper = shallowMount(SidebarInviteMembers); }; afterEach(() => { @@ -28,32 +17,11 @@ describe('Sidebar invite members component', () => { describe('when directly inviting members', () => { beforeEach(() => { - createComponent({ directlyInviteMembers: true }); + createComponent(); }); it('renders a direct link to project members path', () => { expect(findDirectInviteLink().exists()).toBe(true); }); - - it('does not render invite members trigger and modal components', () => { - expect(findIndirectInviteLink().exists()).toBe(false); - expect(findInviteModal().exists()).toBe(false); - }); - }); - - describe('when indirectly inviting members', () => { - beforeEach(() => { - createComponent(); - }); - - it('does not render a direct link to project members path', () => { - expect(findDirectInviteLink().exists()).toBe(false); - }); - - it('does not render invite members trigger and modal components', () => { - expect(findIndirectInviteLink().exists()).toBe(true); - expect(findInviteModal().exists()).toBe(true); - expect(findInviteModal().props('membersPath')).toBe(testProjectMembersPath); - }); }); }); diff --git a/spec/frontend/users_select/test_helper.js b/spec/frontend/users_select/test_helper.js index 89bbbba9913..c5adbe9bb09 100644 --- a/spec/frontend/users_select/test_helper.js +++ b/spec/frontend/users_select/test_helper.js @@ -1,7 +1,7 @@ -import { waitFor } from '@testing-library/dom'; import MockAdapter from 'axios-mock-adapter'; import { memoize, cloneDeep } from 'lodash'; import { getFixture, getJSONFixture } from 'helpers/fixtures'; +import waitForPromises from 'helpers/wait_for_promises'; import axios from '~/lib/utils/axios_utils'; import UsersSelect from '~/users_select'; @@ -103,8 +103,10 @@ export const setAssignees = (...users) => { ); }; export const toggleDropdown = () => findUserSearchButton().click(); -export const waitForDropdownItems = () => - waitFor(() => expect(findDropdownItem(getUsersFixtureAt(0))).not.toBeNull()); +export const waitForDropdownItems = async () => { + await axios.waitForAll(); + await waitForPromises(); +}; // assertion helpers --------------------------------------------------------- export const createUnassignedExpectation = () => { diff --git a/spec/helpers/invite_members_helper_spec.rb b/spec/helpers/invite_members_helper_spec.rb index 7ddf7d059e5..dbe4f970a99 100644 --- a/spec/helpers/invite_members_helper_spec.rb +++ b/spec/helpers/invite_members_helper_spec.rb @@ -12,21 +12,6 @@ RSpec.describe InviteMembersHelper do helper.extend(Gitlab::Experimentation::ControllerConcern) end - describe '#show_invite_members_track_event' do - it 'shows values when can directly invite members' do - allow(helper).to receive(:directly_invite_members?).and_return(true) - - expect(helper.show_invite_members_track_event).to eq 'show_invite_members' - end - - it 'shows values when can indirectly invite members' do - allow(helper).to receive(:directly_invite_members?).and_return(false) - allow(helper).to receive(:indirectly_invite_members?).and_return(true) - - expect(helper.show_invite_members_track_event).to eq 'show_invite_members_version_b' - end - end - context 'with project' do before do assign(:project, project) @@ -87,38 +72,6 @@ RSpec.describe InviteMembersHelper do end end end - - describe "#indirectly_invite_members?" do - context 'when a user is a developer' do - before do - allow(helper).to receive(:current_user) { developer } - end - - it 'returns false' do - allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_b) { false } - - expect(helper.indirectly_invite_members?).to eq false - end - - it 'returns true' do - allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_b) { true } - - expect(helper.indirectly_invite_members?).to eq true - end - end - - context 'when a user is an owner' do - before do - allow(helper).to receive(:current_user) { owner } - end - - it 'returns false' do - allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_b) { true } - - expect(helper.indirectly_invite_members?).to eq false - end - end - end end context 'with group' do diff --git a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb index abee1fec80a..78e0b7627e9 100644 --- a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb +++ b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb @@ -9,6 +9,42 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d it { is_expected.to belong_to(:batched_migration).with_foreign_key(:batched_background_migration_id) } end + describe 'scopes' do + let_it_be(:fixed_time) { Time.new(2021, 04, 27, 10, 00, 00, 00) } + + let_it_be(:pending_job) { create(:batched_background_migration_job, status: :pending, updated_at: fixed_time) } + let_it_be(:running_job) { create(:batched_background_migration_job, status: :running, updated_at: fixed_time) } + let_it_be(:stuck_job) { create(:batched_background_migration_job, status: :pending, updated_at: fixed_time - described_class::STUCK_JOBS_TIMEOUT) } + let_it_be(:failed_job) { create(:batched_background_migration_job, status: :failed, attempts: 1) } + + before_all do + create(:batched_background_migration_job, status: :failed, attempts: described_class::MAX_ATTEMPTS) + create(:batched_background_migration_job, status: :succeeded) + end + + before do + travel_to fixed_time + end + + describe '.active' do + it 'returns active jobs' do + expect(described_class.active).to contain_exactly(pending_job, running_job, stuck_job) + end + end + + describe '.stuck' do + it 'returns stuck jobs' do + expect(described_class.stuck).to contain_exactly(stuck_job) + end + end + + describe '.retriable' do + it 'returns retriable jobs' do + expect(described_class.retriable).to contain_exactly(failed_job, stuck_job) + end + end + end + describe 'delegated batched_migration attributes' do let(:batched_job) { build(:batched_background_migration_job) } let(:batched_migration) { batched_job.batched_migration } diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb index 79b21172dc6..9f0493ab0d7 100644 --- a/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb +++ b/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb @@ -17,9 +17,9 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do end it 'marks the migration as finished' do - relation = Gitlab::Database::BackgroundMigration::BatchedMigration.finished.where(id: migration.id) + runner.run_migration_job(migration) - expect { runner.run_migration_job(migration) }.to change { relation.count }.by(1) + expect(migration.reload).to be_finished end end @@ -92,7 +92,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do let!(:event3) { create(:event) } let!(:migration) do - create(:batched_background_migration, :active, batch_size: 2, min_value: event1.id, max_value: event3.id) + create(:batched_background_migration, :active, batch_size: 2, min_value: event1.id, max_value: event2.id) end let!(:previous_job) do @@ -101,14 +101,24 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do min_value: event1.id, max_value: event2.id, batch_size: 2, - sub_batch_size: 1) + sub_batch_size: 1, + status: :succeeded + ) end let(:job_relation) do Gitlab::Database::BackgroundMigration::BatchedJob.where(batched_background_migration_id: migration.id) end + context 'when the migration has no batches remaining' do + it_behaves_like 'it has completed the migration' + end + context 'when the migration has batches to process' do + before do + migration.update!(max_value: event3.id) + end + it 'runs the migration job for the next batch' do expect(migration_wrapper).to receive(:perform) do |job_record| expect(job_record).to eq(job_relation.last) @@ -132,17 +142,82 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do end end - context 'when the migration has no batches remaining' do + context 'when migration has failed jobs' do before do - create(:batched_background_migration_job, - batched_migration: migration, - min_value: event3.id, - max_value: event3.id, - batch_size: 2, - sub_batch_size: 1) + previous_job.update!(status: :failed) end - it_behaves_like 'it has completed the migration' + it 'retries the failed job' do + expect(migration_wrapper).to receive(:perform) do |job_record| + expect(job_record).to eq(previous_job) + end + + expect { runner.run_migration_job(migration) }.to change { job_relation.count }.by(0) + end + + context 'when failed job has reached the maximum number of attempts' do + before do + previous_job.update!(attempts: Gitlab::Database::BackgroundMigration::BatchedJob::MAX_ATTEMPTS) + end + + it 'marks the migration as failed' do + expect(migration_wrapper).not_to receive(:perform) + + expect { runner.run_migration_job(migration) }.to change { job_relation.count }.by(0) + + expect(migration).to be_failed + end + end + end + + context 'when migration has stuck jobs' do + before do + previous_job.update!(status: :running, updated_at: 1.hour.ago - Gitlab::Database::BackgroundMigration::BatchedJob::STUCK_JOBS_TIMEOUT) + end + + it 'retries the stuck job' do + expect(migration_wrapper).to receive(:perform) do |job_record| + expect(job_record).to eq(previous_job) + end + + expect { runner.run_migration_job(migration.reload) }.to change { job_relation.count }.by(0) + end + end + + context 'when migration has possible stuck jobs' do + before do + previous_job.update!(status: :running, updated_at: 1.hour.from_now - Gitlab::Database::BackgroundMigration::BatchedJob::STUCK_JOBS_TIMEOUT) + end + + it 'keeps the migration active' do + expect(migration_wrapper).not_to receive(:perform) + + expect { runner.run_migration_job(migration) }.to change { job_relation.count }.by(0) + + expect(migration.reload).to be_active + end + end + + context 'when the migration has batches to process and failed jobs' do + before do + migration.update!(max_value: event3.id) + previous_job.update!(status: :failed) + end + + it 'runs next batch then retries the failed job' do + expect(migration_wrapper).to receive(:perform) do |job_record| + expect(job_record).to eq(job_relation.last) + job_record.update!(status: :succeeded) + end + + expect { runner.run_migration_job(migration) }.to change { job_relation.count }.by(1) + + expect(migration_wrapper).to receive(:perform) do |job_record| + expect(job_record).to eq(previous_job) + end + + expect { runner.run_migration_job(migration.reload) }.to change { job_relation.count }.by(0) + end end end end @@ -189,10 +264,12 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do it 'runs all jobs inline until finishing the migration' do expect(migration_wrapper).to receive(:perform) do |job_record| expect(job_record).to eq(job_relation.first) + job_record.update!(status: :succeeded) end expect(migration_wrapper).to receive(:perform) do |job_record| expect(job_record).to eq(job_relation.last) + job_record.update!(status: :succeeded) end expect { runner.run_entire_migration(migration) }.to change { job_relation.count }.by(2) diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb index fdbc2286502..987f2c5a935 100644 --- a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb +++ b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb @@ -49,6 +49,42 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, ' end end + context 'when running a job that failed previously' do + let!(:job_record) do + create(:batched_background_migration_job, + batched_migration: active_migration, + pause_ms: pause_ms, + attempts: 1, + status: :failed, + finished_at: 1.hour.ago, + metrics: { 'my_metrics' => 'some_value' } + ) + end + + it 'increments attempts and updates other fields' do + updated_metrics = { 'updated_metrics' => 'some_value' } + + expect(job_instance).to receive(:perform) + expect(job_instance).to receive(:batch_metrics).and_return(updated_metrics) + + expect(job_record).to receive(:update!).with( + hash_including(attempts: 2, status: :running, finished_at: nil, metrics: {}) + ).and_call_original + + freeze_time do + subject + + job_record.reload + + expect(job_record).not_to be_failed + expect(job_record.attempts).to eq(2) + expect(job_record.started_at).to eq(Time.current) + expect(job_record.finished_at).to eq(Time.current) + expect(job_record.metrics).to eq(updated_metrics) + end + end + end + context 'reporting prometheus metrics' do let(:labels) { job_record.batched_migration.prometheus_labels } diff --git a/spec/lib/gitlab/experimentation_spec.rb b/spec/lib/gitlab/experimentation_spec.rb index 5fef14bd2a0..10bfa9e8d0e 100644 --- a/spec/lib/gitlab/experimentation_spec.rb +++ b/spec/lib/gitlab/experimentation_spec.rb @@ -7,7 +7,6 @@ require 'spec_helper' RSpec.describe Gitlab::Experimentation::EXPERIMENTS do it 'temporarily ensures we know what experiments exist for backwards compatibility' do expected_experiment_keys = [ - :invite_members_version_b, :invite_members_empty_group_version_a, :contact_sales_btn_in_app ] diff --git a/spec/lib/sidebars/projects/menus/ci_cd_menu_spec.rb b/spec/lib/sidebars/projects/menus/ci_cd_menu_spec.rb new file mode 100644 index 00000000000..89b03e1c918 --- /dev/null +++ b/spec/lib/sidebars/projects/menus/ci_cd_menu_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::Projects::Menus::CiCdMenu do + let(:project) { build(:project) } + let(:user) { project.owner } + let(:can_view_pipeline_editor) { true } + let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project, current_ref: 'master', can_view_pipeline_editor: can_view_pipeline_editor) } + + subject { described_class.new(context) } + + describe '#render?' do + context 'when user cannot read builds' do + let(:user) { nil } + + it 'returns false' do + expect(subject.render?).to eq false + end + end + + context 'when user can read builds' do + it 'returns true' do + expect(subject.render?).to eq true + end + end + end + + describe 'Pipelines Editor' do + subject { described_class.new(context).items.index { |e| e.item_id == :pipelines_editor } } + + context 'when user cannot view pipeline editor' do + let(:can_view_pipeline_editor) { false } + + it 'does not include pipeline editor menu item' do + is_expected.to be_nil + end + end + + context 'when user can view pipeline editor' do + it 'includes pipeline editor menu item' do + is_expected.not_to be_nil + end + end + end + + describe 'Artifacts' do + subject { described_class.new(context).items.index { |e| e.item_id == :artifacts } } + + context 'when feature flag :artifacts_management_page is disabled' do + it 'does not include artifacts menu item' do + stub_feature_flags(artifacts_management_page: false) + + is_expected.to be_nil + end + end + + context 'when feature flag :artifacts_management_page is enabled' do + it 'includes artifacts menu item' do + stub_feature_flags(artifacts_management_page: true) + + is_expected.not_to be_nil + end + end + end +end diff --git a/spec/models/concerns/integration_spec.rb b/spec/models/concerns/integration_spec.rb new file mode 100644 index 00000000000..781e2aece56 --- /dev/null +++ b/spec/models/concerns/integration_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Integration do + let_it_be(:project_1) { create(:project) } + let_it_be(:project_2) { create(:project) } + let_it_be(:project_3) { create(:project) } + let_it_be(:project_4) { create(:project) } + let_it_be(:instance_integration) { create(:jira_service, :instance) } + + before do + create(:jira_service, project: project_1, inherit_from_id: instance_integration.id) + create(:jira_service, project: project_2, inherit_from_id: nil) + create(:jira_service, group: create(:group), project: nil, inherit_from_id: nil) + create(:jira_service, project: project_3, inherit_from_id: nil) + create(:slack_service, project: project_4, inherit_from_id: nil) + end + + describe '.with_custom_integration_for' do + it 'returns projects with custom integrations' do + # We use pagination to verify that the group is excluded from the query + expect(Project.with_custom_integration_for(instance_integration, 0, 2)).to contain_exactly(project_2, project_3) + expect(Project.with_custom_integration_for(instance_integration)).to contain_exactly(project_2, project_3) + end + end + + describe '.without_integration' do + it 'returns projects without integration' do + expect(Project.without_integration(instance_integration)).to contain_exactly(project_4) + end + end +end diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb deleted file mode 100644 index 781e2aece56..00000000000 --- a/spec/models/integration_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Integration do - let_it_be(:project_1) { create(:project) } - let_it_be(:project_2) { create(:project) } - let_it_be(:project_3) { create(:project) } - let_it_be(:project_4) { create(:project) } - let_it_be(:instance_integration) { create(:jira_service, :instance) } - - before do - create(:jira_service, project: project_1, inherit_from_id: instance_integration.id) - create(:jira_service, project: project_2, inherit_from_id: nil) - create(:jira_service, group: create(:group), project: nil, inherit_from_id: nil) - create(:jira_service, project: project_3, inherit_from_id: nil) - create(:slack_service, project: project_4, inherit_from_id: nil) - end - - describe '.with_custom_integration_for' do - it 'returns projects with custom integrations' do - # We use pagination to verify that the group is excluded from the query - expect(Project.with_custom_integration_for(instance_integration, 0, 2)).to contain_exactly(project_2, project_3) - expect(Project.with_custom_integration_for(instance_integration)).to contain_exactly(project_2, project_3) - end - end - - describe '.without_integration' do - it 'returns projects without integration' do - expect(Project.without_integration(instance_integration)).to contain_exactly(project_4) - end - end -end diff --git a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb index 49c3674277d..736c353c2aa 100644 --- a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb +++ b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb @@ -22,32 +22,7 @@ RSpec.shared_examples 'issuable invite members experiments' do end end - context 'when invite_members_version_b experiment is enabled' do - before do - stub_experiment_for_subject(invite_members_version_b: true) - end - - it 'shows a link for inviting members and follows through to modal' do - project.add_developer(user) - visit issuable_path - - find('.block.assignee .edit-link').click - - wait_for_requests - - page.within '.dropdown-menu-user' do - expect(page).to have_link('Invite Members', href: '#') - expect(page).to have_selector('[data-track-event="click_invite_members_version_b"]') - expect(page).to have_selector('[data-track-label="edit_assignee"]') - end - - click_link 'Invite Members' - - expect(page).to have_content("Oops, this feature isn't ready yet") - end - end - - context 'when invite_members_version_b experiment is disabled' do + context 'when user cannot invite members in assignee dropdown' do it 'shows author in assignee dropdown and no invite link' do project.add_developer(user) visit issuable_path diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb index c501c418466..16362aed1cd 100644 --- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb +++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb @@ -257,6 +257,64 @@ RSpec.describe 'layouts/nav/sidebar/_project' do end end + describe 'CI/CD' do + it 'has a link to pipelines page' do + render + + expect(rendered).to have_link('CI/CD', href: project_pipelines_path(project)) + end + + describe 'Artifacts' do + it 'has a link to the artifacts page' do + render + + expect(rendered).to have_link('Artifacts', href: project_artifacts_path(project)) + end + end + + describe 'Jobs' do + it 'has a link to the jobs page' do + render + + expect(rendered).to have_link('Jobs', href: project_jobs_path(project)) + end + end + + describe 'Pipeline Schedules' do + it 'has a link to the pipeline schedules page' do + render + + expect(rendered).to have_link('Schedules', href: pipeline_schedules_path(project)) + end + end + + describe 'Pipelines' do + it 'has a link to the pipelines page' do + render + + expect(rendered).to have_link('Pipelines', href: project_pipelines_path(project)) + end + end + + describe 'Pipeline Editor' do + it 'has a link to the pipeline editor' do + render + + expect(rendered).to have_link('Editor', href: project_ci_pipeline_editor_path(project)) + end + + context 'when user cannot access pipeline editor' do + it 'does not has a link to the pipeline editor' do + allow(view).to receive(:can_view_pipeline_editor?).and_return(false) + + render + + expect(rendered).not_to have_link('Editor', href: project_ci_pipeline_editor_path(project)) + end + end + end + end + describe 'packages tab' do before do stub_container_registry_config(enabled: true) @@ -419,48 +477,6 @@ RSpec.describe 'layouts/nav/sidebar/_project' do end end - describe 'ci/cd settings tab' do - before do - project.update!(archived: project_archived) - end - - context 'when project is archived' do - let(:project_archived) { true } - - it 'does not show the ci/cd settings tab' do - render - - expect(rendered).not_to have_link('CI/CD', href: project_settings_ci_cd_path(project)) - end - end - - context 'when project is active' do - let(:project_archived) { false } - - it 'shows the ci/cd settings tab' do - render - - expect(rendered).to have_link('CI/CD', href: project_settings_ci_cd_path(project)) - end - end - end - - describe 'pipeline editor link' do - it 'shows the pipeline editor link' do - render - - expect(rendered).to have_link('Editor', href: project_ci_pipeline_editor_path(project)) - end - - it 'does not show the pipeline editor link' do - allow(view).to receive(:can_view_pipeline_editor?).and_return(false) - - render - - expect(rendered).not_to have_link('Editor', href: project_ci_pipeline_editor_path(project)) - end - end - describe 'operations settings tab' do describe 'archive projects' do before do -- cgit v1.2.3