diff options
56 files changed, 518 insertions, 176 deletions
diff --git a/.rubocop_todo/layout/hash_alignment.yml b/.rubocop_todo/layout/hash_alignment.yml index 96bd299cb48..6f88f121a88 100644 --- a/.rubocop_todo/layout/hash_alignment.yml +++ b/.rubocop_todo/layout/hash_alignment.yml @@ -164,21 +164,6 @@ Layout/HashAlignment: - 'app/models/user_status.rb' - 'app/models/wiki.rb' - 'app/models/work_items/type.rb' - - 'db/migrate/20210601080039_group_protected_environments_add_index_and_constraint.rb' - - 'db/migrate/20210804150320_create_base_work_item_types.rb' - - 'db/migrate/20210831203408_upsert_base_work_item_types.rb' - - 'db/migrate/20210901065504_add_index_on_name_and_id_to_public_groups.rb' - - 'db/post_migrate/20210311120156_backfill_push_event_payload_event_id_for_bigint_conversion.rb' - - 'db/post_migrate/20210622045705_finalize_events_bigint_conversion.rb' - - 'db/post_migrate/20210701141346_finalize_ci_builds_stage_id_bigint_conversion.rb' - - 'db/post_migrate/20210707210916_finalize_ci_stages_bigint_conversion.rb' - - 'db/post_migrate/20210708011426_finalize_ci_builds_metadata_bigint_conversion.rb' - - 'db/post_migrate/20210802043253_finalize_push_event_payloads_bigint_conversion_3.rb' - - 'db/post_migrate/20210804151444_prepare_indexes_for_ci_job_artifact_bigint_conversion.rb' - - 'db/post_migrate/20210804153307_prepare_indexes_for_tagging_bigint_conversion.rb' - - 'db/post_migrate/20210804154407_prepare_indexes_for_ci_stage_bigint_conversion.rb' - - 'db/post_migrate/20210817024335_prepare_indexes_for_events_bigint_conversion.rb' - - 'db/post_migrate/20210824174615_prepare_ci_builds_metadata_and_ci_build_async_indexes.rb' - 'ee/app/controllers/ee/search_controller.rb' - 'ee/app/controllers/projects/integrations/zentao/issues_controller.rb' - 'ee/app/graphql/mutations/iterations/cadences/create.rb' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index d89e6180f1a..abcb6861189 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -157e6b6ad8fd7aa0ebdd43727f00b81f34b100a1 +0a03f045e065b9e7a157322fbe486e1f02fd8617 diff --git a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue index b151e1605da..b2e554bc913 100644 --- a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue +++ b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue @@ -285,7 +285,7 @@ export default { :shape="$options.AVATAR_SHAPE_OPTION_RECT" /> <div> - <div data-testid="project-name">{{ project.name }}</div> + <div data-testid="project-name" data-qa-selector="project_name">{{ project.name }}</div> <div class="gl-text-gray-500" data-testid="project-full-path"> {{ project.fullPath }} </div> diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 0e33c712470..01d218438cf 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -7,6 +7,7 @@ import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js'; import { s__, __, sprintf } from '~/locale'; import { isUserBusy } from '~/set_status_modal/utils'; import SidebarMediator from '~/sidebar/sidebar_mediator'; +import { state } from '~/sidebar/components/reviewers/sidebar_reviewers.vue'; import AjaxCache from './lib/utils/ajax_cache'; import { spriteIcon } from './lib/utils/common_utils'; import { parsePikadayDate } from './lib/utils/datetime_utility'; @@ -352,8 +353,7 @@ class GfmAutoComplete { // Cache assignees & reviewers list for easier filtering later assignees = SidebarMediator.singleton?.store?.assignees?.map(createMemberSearchString) || []; - reviewers = - SidebarMediator.singleton?.store?.reviewers?.map(createMemberSearchString) || []; + reviewers = state.issuable?.reviewers?.nodes?.map(createMemberSearchString) || []; const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers); return match && match.length ? match[1] : null; diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue index 8a8c50c7138..34c7ed69515 100644 --- a/app/assets/javascripts/issues/show/components/description.vue +++ b/app/assets/javascripts/issues/show/components/description.vue @@ -133,7 +133,7 @@ export default { }, computed: { workItemsEnabled() { - return this.glFeatures.workItems; + return this.glFeatures.workItemsCreateFromMarkdown; }, taskWorkItemType() { return this.workItemTypes.find((type) => type.name === TASK_TYPE_NAME)?.id; diff --git a/app/assets/javascripts/ref/components/ref_selector.vue b/app/assets/javascripts/ref/components/ref_selector.vue index d02526160fd..1343ad8246c 100644 --- a/app/assets/javascripts/ref/components/ref_selector.vue +++ b/app/assets/javascripts/ref/components/ref_selector.vue @@ -256,6 +256,7 @@ export default { :error-message="i18n.branchesErrorMessage" :show-header="showSectionHeaders" data-testid="branches-section" + data-qa-selector="branches_section" @selected="selectRef($event)" /> diff --git a/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue index 9502b2e78b3..6f82178b6fd 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue @@ -33,7 +33,7 @@ export default { return this.users.length > 2; }, allReviewersCanMerge() { - return this.users.every((user) => user.can_merge); + return this.users.every((user) => user.mergeRequestInteraction?.canMerge); }, sidebarAvatarCounter() { if (this.users.length > DEFAULT_MAX_COUNTER) { @@ -48,7 +48,7 @@ export default { return this.users.slice(0, collapsedLength); }, tooltipTitleMergeStatus() { - const mergeLength = this.users.filter((u) => u.can_merge).length; + const mergeLength = this.users.filter((u) => u.mergeRequestInteraction?.canMerge).length; if (mergeLength === this.users.length) { return ''; diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue index 7961b7cd679..a7db3b3d09f 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue @@ -23,10 +23,10 @@ export default { return sprintf(__("%{userName}'s avatar"), { userName: this.user.name }); }, avatarUrl() { - return this.user.avatar || this.user.avatar_url || gon.default_avatar_url; + return this.user.avatarUrl || this.user.avatar_url || gon.default_avatar_url; }, hasMergeIcon() { - return !this.user.can_merge; + return !this.user.mergeRequestInteraction?.canMerge; }, }, }; diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue index c9b0a4ae2b3..f69c027e201 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue @@ -40,7 +40,7 @@ export default { }, computed: { cannotMerge() { - return this.issuableType === 'merge_request' && !this.user.can_merge; + return this.issuableType === 'merge_request' && !this.user.mergeRequestInteraction?.canMerge; }, tooltipTitle() { if (this.cannotMerge && this.tooltipHasName) { @@ -59,7 +59,7 @@ export default { }; }, reviewerUrl() { - return this.user.web_url; + return this.user.webUrl; }, }, }; diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue index 5729b958b5d..5e1172ad835 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue @@ -36,8 +36,8 @@ export default { return !this.users.length; }, sortedReviewers() { - const canMergeUsers = this.users.filter((user) => user.can_merge); - const canNotMergeUsers = this.users.filter((user) => !user.can_merge); + const canMergeUsers = this.users.filter((user) => user.mergeRequestInteraction?.canMerge); + const canNotMergeUsers = this.users.filter((user) => !user.mergeRequestInteraction?.canMerge); return [...canMergeUsers, ...canNotMergeUsers]; }, diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue index e414aaf719b..b0d820ddd15 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue @@ -1,15 +1,23 @@ <script> // NOTE! For the first iteration, we are simply copying the implementation of Assignees // It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736 +import Vue from 'vue'; import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import createFlash from '~/flash'; import { __ } from '~/locale'; import eventHub from '~/sidebar/event_hub'; import Store from '~/sidebar/stores/sidebar_store'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import getMergeRequestReviewersQuery from '~/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql'; import ReviewerTitle from './reviewer_title.vue'; import Reviewers from './reviewers.vue'; +export const state = Vue.observable({ + issuable: {}, + loading: false, + initialLoading: true, +}); + export default { name: 'SidebarReviewers', components: { @@ -40,18 +48,49 @@ export default { required: true, }, }, + apollo: { + issuable: { + query: getMergeRequestReviewersQuery, + variables() { + return { + iid: this.issuableIid, + fullPath: this.projectPath, + }; + }, + update(data) { + return data.workspace?.issuable; + }, + result() { + this.initialLoading = false; + }, + error() { + createFlash({ message: __('An error occurred while fetching reviewers.') }); + }, + }, + }, data() { - return { - store: new Store(), - loading: false, - }; + return state; }, computed: { relativeUrlRoot() { return gon.relative_url_root ?? ''; }, + reviewers() { + return this.issuable.reviewers?.nodes || []; + }, + graphqlFetching() { + return this.$apollo.queries.issuable.loading; + }, + isLoading() { + return this.loading || this.$apollo.queries.issuable.loading; + }, + canUpdate() { + return this.issuable.userPermissions?.updateMergeRequest || false; + }, }, created() { + this.store = new Store(); + this.removeReviewer = this.store.removeReviewer.bind(this.store); this.addReviewer = this.store.addReviewer.bind(this.store); this.removeAllReviewers = this.store.removeAllReviewers.bind(this.store); @@ -77,6 +116,7 @@ export default { .then(() => { this.loading = false; refreshUserMergeRequestCounts(); + this.$apollo.queries.issuable.refetch(); }) .catch(() => { this.loading = false; @@ -95,15 +135,15 @@ export default { <template> <div> <reviewer-title - :number-of-reviewers="store.reviewers.length" - :loading="loading || store.isFetching.reviewers" - :editable="store.editable" + :number-of-reviewers="reviewers.length" + :loading="isLoading" + :editable="canUpdate" /> <reviewers - v-if="!store.isFetching.reviewers" + v-if="!initialLoading" :root-path="relativeUrlRoot" - :users="store.reviewers" - :editable="store.editable" + :users="reviewers" + :editable="canUpdate" :issuable-type="issuableType" @request-review="requestReview" /> diff --git a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue index 3aeb49eb422..217ca2e2548 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue @@ -105,7 +105,7 @@ export default { </div> </reviewer-avatar-link> <gl-icon - v-if="user.approved" + v-if="user.mergeRequestInteraction.approved" v-gl-tooltip.left :size="16" :title="approvedByTooltipTitle(user)" @@ -121,7 +121,7 @@ export default { data-testid="re-request-success" /> <gl-button - v-else-if="user.can_update_merge_request && user.reviewed" + v-else-if="user.mergeRequestInteraction.canUpdate && user.mergeRequestInteraction.reviewed" v-gl-tooltip.left :title="$options.i18n.reRequestReview" :aria-label="$options.i18n.reRequestReview" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql new file mode 100644 index 00000000000..3ee622ae3f2 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql @@ -0,0 +1,28 @@ +#import "~/graphql_shared/fragments/user.fragment.graphql" +#import "~/graphql_shared/fragments/user_availability.fragment.graphql" + +query mergeRequestReviewers($fullPath: ID!, $iid: String!) { + workspace: project(fullPath: $fullPath) { + __typename + id + issuable: mergeRequest(iid: $iid) { + __typename + id + reviewers { + nodes { + ...User + ...UserAvailability + mergeRequestInteraction { + canMerge + canUpdate + approved + reviewed + } + } + } + userPermissions { + updateMergeRequest + } + } + } +} diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue index e65f0cfb1f2..6a43fdb541f 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue @@ -95,8 +95,14 @@ export default { toggle() { this.isOpen = !this.isOpen; }, - toggleAddForm() { - this.isShownAddForm = !this.isShownAddForm; + showAddForm() { + this.isShownAddForm = true; + this.$nextTick(() => { + this.$refs.wiLinksForm.$refs.wiTitleInput?.$el.focus(); + }); + }, + hideAddForm() { + this.isShownAddForm = false; }, addChild(child) { this.children = [child, ...this.children]; @@ -122,10 +128,10 @@ export default { > <h5 class="gl-m-0 gl-line-height-32 gl-flex-grow-1">{{ $options.i18n.title }}</h5> <gl-button - v-if="!isShownAddForm && canUpdate" + v-if="canUpdate" category="secondary" data-testid="toggle-add-form" - @click="toggleAddForm" + @click="showAddForm" > {{ $options.i18n.addChildButtonLabel }} </gl-button> @@ -154,10 +160,11 @@ export default { </div> <work-item-links-form v-if="isShownAddForm" + ref="wiLinksForm" data-testid="add-links-form" :issuable-gid="issuableGid" :children-ids="childrenIds" - @cancel="toggleAddForm" + @cancel="hideAddForm" @addWorkItemChild="addChild" /> <div diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue index fadba0753db..d01d59050e6 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue @@ -1,9 +1,11 @@ <script> -import { GlAlert, GlForm, GlFormCombobox, GlButton } from '@gitlab/ui'; +import { GlAlert, GlFormGroup, GlForm, GlFormCombobox, GlButton, GlFormInput } from '@gitlab/ui'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { __, s__ } from '~/locale'; import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql'; import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql'; +import createWorkItemMutation from '../../graphql/create_work_item.mutation.graphql'; +import { WORK_ITEM_TYPE_IDS } from '../../constants'; export default { components: { @@ -11,6 +13,8 @@ export default { GlForm, GlFormCombobox, GlButton, + GlFormGroup, + GlFormInput, }, inject: ['projectPath'], props: { @@ -28,6 +32,7 @@ export default { apollo: { availableWorkItems: { query: projectWorkItemsQuery, + debounce: 200, variables() { return { projectPath: this.projectPath, @@ -50,8 +55,29 @@ export default { availableWorkItems: [], search: '', error: null, + childToCreateTitle: null, }; }, + computed: { + actionsList() { + return [ + { + label: this.$options.i18n.createChildOptionLabel, + fn: () => { + this.childToCreateTitle = this.search?.title || this.search; + }, + }, + ]; + }, + addOrCreateButtonLabel() { + return this.childToCreateTitle + ? this.$options.i18n.createChildOptionLabel + : this.$options.i18n.addTaskButtonLabel; + }, + addOrCreateMethod() { + return this.childToCreateTitle ? this.createChild : this.addChild; + }, + }, methods: { getIdFromGraphQLId, unsetError() { @@ -79,35 +105,77 @@ export default { } }) .catch(() => { - this.error = this.$options.i18n.errorMessage; + this.error = this.$options.i18n.addChildErrorMessage; }) .finally(() => { this.search = ''; }); }, + createChild() { + this.$apollo + .mutate({ + mutation: createWorkItemMutation, + variables: { + input: { + title: this.search?.title || this.search, + projectPath: this.projectPath, + workItemTypeId: WORK_ITEM_TYPE_IDS.TASK, + hierarchyWidget: { + parentId: this.issuableGid, + }, + }, + }, + }) + .then(({ data }) => { + if (data.workItemCreate?.errors?.length) { + [this.error] = data.workItemCreate.errors; + } else { + this.unsetError(); + this.$emit('addWorkItemChild', data.workItemCreate.workItem); + } + }) + .catch(() => { + this.error = this.$options.i18n.createChildErrorMessage; + }) + .finally(() => { + this.search = ''; + this.childToCreateTitle = null; + }); + }, }, i18n: { - inputLabel: __('Children'), - errorMessage: s__( + inputLabel: __('Title'), + addTaskButtonLabel: s__('WorkItem|Add task'), + addChildErrorMessage: s__( 'WorkItem|Something went wrong when trying to add a child. Please try again.', ), + createChildOptionLabel: s__('WorkItem|Create task'), + createChildErrorMessage: s__( + 'WorkItem|Something went wrong when trying to create a child. Please try again.', + ), + placeholder: s__('WorkItem|Add a title'), + fieldValidationMessage: __('Maximum of 255 characters'), }, }; </script> <template> <gl-form - class="gl-mb-3 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base" + class="gl-bg-white gl-mb-3 gl-p-4 gl-border gl-border-gray-100 gl-rounded-base" + @submit.prevent="createChild" > <gl-alert v-if="error" variant="danger" class="gl-mb-3" @dismiss="unsetError"> {{ error }} </gl-alert> + <!-- Follow up issue to turn this functionality back on https://gitlab.com/gitlab-org/gitlab/-/issues/368757 --> <gl-form-combobox + v-if="false" v-model="search" :token-list="availableWorkItems" match-value-to-attr="title" class="gl-mb-4" :label-text="$options.i18n.inputLabel" + :action-list="actionsList" label-sr-only autofocus > @@ -117,11 +185,34 @@ export default { <div>{{ item.title }}</div> </div> </template> + <template #action="{ item }"> + <span class="gl-text-blue-500">{{ item.label }}</span> + </template> </gl-form-combobox> - <gl-button category="secondary" data-testid="add-child-button" @click="addChild"> - {{ s__('WorkItem|Add task') }} + <gl-form-group + :label="$options.i18n.inputLabel" + :description="$options.i18n.fieldValidationMessage" + > + <gl-form-input + ref="wiTitleInput" + v-model="search" + :placeholder="$options.i18n.placeholder" + maxlength="255" + class="gl-mb-3" + autofocus + /> + </gl-form-group> + <gl-button + category="primary" + variant="confirm" + size="small" + type="submit" + :disabled="search.length === 0" + data-testid="add-child-button" + > + {{ $options.i18n.createChildOptionLabel }} </gl-button> - <gl-button category="tertiary" @click="$emit('cancel')"> + <gl-button category="secondary" size="small" @click="$emit('cancel')"> {{ s__('WorkItem|Cancel') }} </gl-button> </gl-form> diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index 2b44642877b..00a5f050b77 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -35,3 +35,7 @@ export const WORK_ITEM_STATUS_TEXT = { CLOSED: s__('WorkItem|Closed'), OPEN: s__('WorkItem|Open'), }; + +export const WORK_ITEM_TYPE_IDS = { + TASK: 'gid://gitlab/WorkItems::Type/5', +}; diff --git a/app/assets/javascripts/work_items/graphql/create_work_item.mutation.graphql b/app/assets/javascripts/work_items/graphql/create_work_item.mutation.graphql index 0a406187a09..4cc23fa0071 100644 --- a/app/assets/javascripts/work_items/graphql/create_work_item.mutation.graphql +++ b/app/assets/javascripts/work_items/graphql/create_work_item.mutation.graphql @@ -5,5 +5,6 @@ mutation createWorkItem($input: WorkItemCreateInput!) { workItem { ...WorkItem } + errors } } diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 9cafd442913..537c08c9ebb 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -53,6 +53,7 @@ class Projects::IssuesController < Projects::ApplicationController push_frontend_feature_flag(:realtime_labels, project) push_force_frontend_feature_flag(:work_items_mvc_2, project&.work_items_mvc_2_feature_flag_enabled?) push_frontend_feature_flag(:work_items_hierarchy, project) + push_force_frontend_feature_flag(:work_items_create_from_markdown, project&.work_items_create_from_markdown_feature_flag_enabled?) end around_action :allow_gitaly_ref_name_caching, only: [:discussions] diff --git a/app/models/group.rb b/app/models/group.rb index 07badd18965..4bf91474405 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -859,6 +859,10 @@ class Group < Namespace feature_flag_enabled_for_self_or_ancestor?(:work_items_mvc_2) end + def work_items_create_from_markdown_feature_flag_enabled? + feature_flag_enabled_for_self_or_ancestor?(:work_items_create_from_markdown) + end + # Check for enabled features, similar to `Project#feature_available?` # NOTE: We still want to keep this after removing `Namespace#feature_available?`. override :feature_available? diff --git a/app/models/project.rb b/app/models/project.rb index cfa726ede41..1a30a2fe4ec 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2995,6 +2995,10 @@ class Project < ApplicationRecord group&.work_items_mvc_2_feature_flag_enabled? || Feature.enabled?(:work_items_mvc_2) end + def work_items_create_from_markdown_feature_flag_enabled? + work_items_feature_flag_enabled? && (group&.work_items_create_from_markdown_feature_flag_enabled? || Feature.enabled?(:work_items_create_from_markdown)) + end + def enqueue_record_project_target_platforms return unless Gitlab.com? return unless Feature.enabled?(:record_projects_target_platforms, self) diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb index c73b1ad6073..6363d6276a7 100644 --- a/app/serializers/environment_serializer.rb +++ b/app/serializers/environment_serializer.rb @@ -34,8 +34,8 @@ class EnvironmentSerializer < BaseSerializer # rubocop: disable CodeReuse/ActiveRecord def itemize(resource) items = resource.order('folder ASC') - .group('COALESCE(environment_type, name)') - .select('COALESCE(environment_type, name) AS folder', + .group('COALESCE(environment_type, id::text)', 'COALESCE(environment_type, name)') + .select('COALESCE(environment_type, id::text), COALESCE(environment_type, name) AS folder', 'COUNT(*) AS size', 'MAX(id) AS last_id') # It makes a difference when you call `paginate` method, because diff --git a/config/feature_flags/development/work_items_create_from_markdown.yml b/config/feature_flags/development/work_items_create_from_markdown.yml new file mode 100644 index 00000000000..9a954001f2b --- /dev/null +++ b/config/feature_flags/development/work_items_create_from_markdown.yml @@ -0,0 +1,8 @@ +--- +name: work_items_create_from_markdown +introduced_by_url: +rollout_issue_url: +milestone: '15.3' +type: development +group: group::project management +default_enabled: false diff --git a/db/migrate/20210601080039_group_protected_environments_add_index_and_constraint.rb b/db/migrate/20210601080039_group_protected_environments_add_index_and_constraint.rb index 611619e496c..1109d814ee9 100644 --- a/db/migrate/20210601080039_group_protected_environments_add_index_and_constraint.rb +++ b/db/migrate/20210601080039_group_protected_environments_add_index_and_constraint.rb @@ -9,7 +9,7 @@ class GroupProtectedEnvironmentsAddIndexAndConstraint < ActiveRecord::Migration[ def up add_concurrent_index :protected_environments, [:group_id, :name], unique: true, - name: INDEX_NAME, where: 'group_id IS NOT NULL' + name: INDEX_NAME, where: 'group_id IS NOT NULL' add_concurrent_foreign_key :protected_environments, :namespaces, column: :group_id, on_delete: :cascade add_check_constraint :protected_environments, diff --git a/db/migrate/20210804150320_create_base_work_item_types.rb b/db/migrate/20210804150320_create_base_work_item_types.rb index b7a44eaabe0..836264aae6e 100644 --- a/db/migrate/20210804150320_create_base_work_item_types.rb +++ b/db/migrate/20210804150320_create_base_work_item_types.rb @@ -8,9 +8,9 @@ class CreateBaseWorkItemTypes < ActiveRecord::Migration[6.1] self.table_name = 'work_item_types' enum base_type: { - issue: 0, - incident: 1, - test_case: 2, + issue: 0, + incident: 1, + test_case: 2, requirement: 3 } diff --git a/db/migrate/20210831203408_upsert_base_work_item_types.rb b/db/migrate/20210831203408_upsert_base_work_item_types.rb index 314412d8d3d..9a556bd6c2c 100644 --- a/db/migrate/20210831203408_upsert_base_work_item_types.rb +++ b/db/migrate/20210831203408_upsert_base_work_item_types.rb @@ -6,9 +6,9 @@ class UpsertBaseWorkItemTypes < ActiveRecord::Migration[6.1] self.table_name = 'work_item_types' enum base_type: { - issue: 0, - incident: 1, - test_case: 2, + issue: 0, + incident: 1, + test_case: 2, requirement: 3 } end diff --git a/db/migrate/20210901065504_add_index_on_name_and_id_to_public_groups.rb b/db/migrate/20210901065504_add_index_on_name_and_id_to_public_groups.rb index 77b9e5297a7..e0c9d932b38 100644 --- a/db/migrate/20210901065504_add_index_on_name_and_id_to_public_groups.rb +++ b/db/migrate/20210901065504_add_index_on_name_and_id_to_public_groups.rb @@ -7,8 +7,9 @@ class AddIndexOnNameAndIdToPublicGroups < Gitlab::Database::Migration[1.0] disable_ddl_transaction! def up - add_concurrent_index :namespaces, [:name, :id], name: INDEX_NAME, - where: "type = 'Group' AND visibility_level = #{PUBLIC_VISIBILITY_LEVEL}" + add_concurrent_index :namespaces, [:name, :id], + name: INDEX_NAME, + where: "type = 'Group' AND visibility_level = #{PUBLIC_VISIBILITY_LEVEL}" end def down diff --git a/db/post_migrate/20210311120156_backfill_push_event_payload_event_id_for_bigint_conversion.rb b/db/post_migrate/20210311120156_backfill_push_event_payload_event_id_for_bigint_conversion.rb index bb444f5b407..0d610f1dde1 100644 --- a/db/post_migrate/20210311120156_backfill_push_event_payload_event_id_for_bigint_conversion.rb +++ b/db/post_migrate/20210311120156_backfill_push_event_payload_event_id_for_bigint_conversion.rb @@ -11,7 +11,7 @@ class BackfillPushEventPayloadEventIdForBigintConversion < ActiveRecord::Migrati return unless should_run? backfill_conversion_of_integer_to_bigint :push_event_payloads, :event_id, primary_key: :event_id, - batch_size: 15000, sub_batch_size: 100 + batch_size: 15000, sub_batch_size: 100 end def down diff --git a/db/post_migrate/20210622045705_finalize_events_bigint_conversion.rb b/db/post_migrate/20210622045705_finalize_events_bigint_conversion.rb index 715bc392c68..1f0bdfc5012 100644 --- a/db/post_migrate/20210622045705_finalize_events_bigint_conversion.rb +++ b/db/post_migrate/20210622045705_finalize_events_bigint_conversion.rb @@ -31,7 +31,7 @@ class FinalizeEventsBigintConversion < ActiveRecord::Migration[6.1] add_concurrent_index TABLE_NAME, [:project_id, :id_convert_to_bigint], name: 'index_events_on_project_id_and_id_convert_to_bigint' # This is to replace the existing "index_events_on_project_id_and_id_desc_on_merged_action" btree (project_id, id DESC) WHERE action = 7 add_concurrent_index TABLE_NAME, [:project_id, :id_convert_to_bigint], order: { id_convert_to_bigint: :desc }, - where: "action = 7", name: 'index_events_on_project_id_and_id_bigint_desc_on_merged_action' + where: "action = 7", name: 'index_events_on_project_id_and_id_bigint_desc_on_merged_action' # Add a FK on `push_event_payloads(event_id)` to `id_convert_to_bigint`, the old FK (fk_36c74129da) # will be removed when events_pkey constraint is droppped. diff --git a/db/post_migrate/20210701141346_finalize_ci_builds_stage_id_bigint_conversion.rb b/db/post_migrate/20210701141346_finalize_ci_builds_stage_id_bigint_conversion.rb index 161366590be..776f1ad545d 100644 --- a/db/post_migrate/20210701141346_finalize_ci_builds_stage_id_bigint_conversion.rb +++ b/db/post_migrate/20210701141346_finalize_ci_builds_stage_id_bigint_conversion.rb @@ -30,7 +30,7 @@ class FinalizeCiBuildsStageIdBigintConversion < ActiveRecord::Migration[6.1] # Create a copy of the original column's FK on the new column add_concurrent_foreign_key TABLE_NAME, :ci_stages, column: :stage_id_convert_to_bigint, on_delete: :cascade, - reverse_lock_order: true + reverse_lock_order: true with_lock_retries(raise_on_exhaustion: true) do quoted_table_name = quote_table_name(TABLE_NAME) diff --git a/db/post_migrate/20210707210916_finalize_ci_stages_bigint_conversion.rb b/db/post_migrate/20210707210916_finalize_ci_stages_bigint_conversion.rb index dbefbeb26cb..33a54b298f0 100644 --- a/db/post_migrate/20210707210916_finalize_ci_stages_bigint_conversion.rb +++ b/db/post_migrate/20210707210916_finalize_ci_stages_bigint_conversion.rb @@ -37,10 +37,10 @@ class FinalizeCiStagesBigintConversion < ActiveRecord::Migration[6.1] fk_stage_id = concurrent_foreign_key_name(:ci_builds, :stage_id) fk_stage_id_tmp = "#{fk_stage_id}_tmp" add_concurrent_foreign_key :ci_builds, :ci_stages, column: :stage_id, - target_column: :id_convert_to_bigint, - name: fk_stage_id_tmp, - on_delete: :cascade, - reverse_lock_order: true + target_column: :id_convert_to_bigint, + name: fk_stage_id_tmp, + on_delete: :cascade, + reverse_lock_order: true # Now it's time to do things in a transaction with_lock_retries(raise_on_exhaustion: true) do diff --git a/db/post_migrate/20210708011426_finalize_ci_builds_metadata_bigint_conversion.rb b/db/post_migrate/20210708011426_finalize_ci_builds_metadata_bigint_conversion.rb index c2444ccbc6c..3879c3c368a 100644 --- a/db/post_migrate/20210708011426_finalize_ci_builds_metadata_bigint_conversion.rb +++ b/db/post_migrate/20210708011426_finalize_ci_builds_metadata_bigint_conversion.rb @@ -40,7 +40,7 @@ class FinalizeCiBuildsMetadataBigintConversion < Gitlab::Database::Migration[1.0 # rubocop:enable Migration/PreventIndexCreation add_concurrent_foreign_key TABLE_NAME, :ci_builds, column: :build_id_convert_to_bigint, on_delete: :cascade, - reverse_lock_order: true + reverse_lock_order: true with_lock_retries(raise_on_exhaustion: true) do execute "LOCK TABLE ci_builds, #{TABLE_NAME} IN ACCESS EXCLUSIVE MODE" diff --git a/db/post_migrate/20210802043253_finalize_push_event_payloads_bigint_conversion_3.rb b/db/post_migrate/20210802043253_finalize_push_event_payloads_bigint_conversion_3.rb index 6dab29e10d4..2a8c63eba71 100644 --- a/db/post_migrate/20210802043253_finalize_push_event_payloads_bigint_conversion_3.rb +++ b/db/post_migrate/20210802043253_finalize_push_event_payloads_bigint_conversion_3.rb @@ -40,7 +40,7 @@ class FinalizePushEventPayloadsBigintConversion3 < ActiveRecord::Migration[6.1] # Add a foreign key on `event_id_convert_to_bigint` before we swap the columns and drop the old FK (fk_36c74129da) add_concurrent_foreign_key TABLE_NAME, :events, column: :event_id_convert_to_bigint, - on_delete: :cascade, reverse_lock_order: true + on_delete: :cascade, reverse_lock_order: true with_lock_retries(raise_on_exhaustion: true) do # We'll need ACCESS EXCLUSIVE lock on the related tables, diff --git a/db/post_migrate/20210804151444_prepare_indexes_for_ci_job_artifact_bigint_conversion.rb b/db/post_migrate/20210804151444_prepare_indexes_for_ci_job_artifact_bigint_conversion.rb index 8115465e311..81e73effe7b 100644 --- a/db/post_migrate/20210804151444_prepare_indexes_for_ci_job_artifact_bigint_conversion.rb +++ b/db/post_migrate/20210804151444_prepare_indexes_for_ci_job_artifact_bigint_conversion.rb @@ -5,19 +5,23 @@ class PrepareIndexesForCiJobArtifactBigintConversion < ActiveRecord::Migration[6 def up prepare_async_index :ci_job_artifacts, :id_convert_to_bigint, unique: true, - name: :index_ci_job_artifact_on_id_convert_to_bigint + name: :index_ci_job_artifact_on_id_convert_to_bigint - prepare_async_index :ci_job_artifacts, [:project_id, :id_convert_to_bigint], where: 'file_type = 18', - name: :index_ci_job_artifacts_for_terraform_reports_bigint + prepare_async_index :ci_job_artifacts, + [:project_id, :id_convert_to_bigint], + where: 'file_type = 18', name: :index_ci_job_artifacts_for_terraform_reports_bigint - prepare_async_index :ci_job_artifacts, :id_convert_to_bigint, where: 'file_type = 18', - name: :index_ci_job_artifacts_id_for_terraform_reports_bigint + prepare_async_index :ci_job_artifacts, :id_convert_to_bigint, + where: 'file_type = 18', + name: :index_ci_job_artifacts_id_for_terraform_reports_bigint - prepare_async_index :ci_job_artifacts, [:expire_at, :job_id_convert_to_bigint], - name: :index_ci_job_artifacts_on_expire_at_and_job_id_bigint + prepare_async_index :ci_job_artifacts, + [:expire_at, :job_id_convert_to_bigint], + name: :index_ci_job_artifacts_on_expire_at_and_job_id_bigint - prepare_async_index :ci_job_artifacts, [:job_id_convert_to_bigint, :file_type], unique: true, - name: :index_ci_job_artifacts_on_job_id_and_file_type_bigint + prepare_async_index :ci_job_artifacts, + [:job_id_convert_to_bigint, :file_type], + unique: true, name: :index_ci_job_artifacts_on_job_id_and_file_type_bigint end def down diff --git a/db/post_migrate/20210804153307_prepare_indexes_for_tagging_bigint_conversion.rb b/db/post_migrate/20210804153307_prepare_indexes_for_tagging_bigint_conversion.rb index 98f90bafce3..ab6691dea1f 100644 --- a/db/post_migrate/20210804153307_prepare_indexes_for_tagging_bigint_conversion.rb +++ b/db/post_migrate/20210804153307_prepare_indexes_for_tagging_bigint_conversion.rb @@ -5,7 +5,7 @@ class PrepareIndexesForTaggingBigintConversion < ActiveRecord::Migration[6.1] def up prepare_async_index :taggings, :id_convert_to_bigint, unique: true, - name: :index_taggings_on_id_convert_to_bigint + name: :index_taggings_on_id_convert_to_bigint prepare_async_index :taggings, [:taggable_id_convert_to_bigint, :taggable_type], name: :i_taggings_on_taggable_id_convert_to_bigint_and_taggable_type diff --git a/db/post_migrate/20210804154407_prepare_indexes_for_ci_stage_bigint_conversion.rb b/db/post_migrate/20210804154407_prepare_indexes_for_ci_stage_bigint_conversion.rb index 82af595b2d3..16f3e87000c 100644 --- a/db/post_migrate/20210804154407_prepare_indexes_for_ci_stage_bigint_conversion.rb +++ b/db/post_migrate/20210804154407_prepare_indexes_for_ci_stage_bigint_conversion.rb @@ -5,14 +5,16 @@ class PrepareIndexesForCiStageBigintConversion < ActiveRecord::Migration[6.1] def up prepare_async_index :ci_stages, :id_convert_to_bigint, unique: true, - name: :index_ci_stages_on_id_convert_to_bigint + name: :index_ci_stages_on_id_convert_to_bigint - prepare_async_index :ci_stages, [:pipeline_id, :id_convert_to_bigint], where: 'status in (0, 1, 2, 8, 9, 10)', - name: :index_ci_stages_on_pipeline_id_and_id_convert_to_bigint + prepare_async_index :ci_stages, [:pipeline_id, :id_convert_to_bigint], + where: 'status in (0, 1, 2, 8, 9, 10)', + name: :index_ci_stages_on_pipeline_id_and_id_convert_to_bigint end def down - unprepare_async_index_by_name :ci_stages, :index_ci_stages_on_pipeline_id_and_id_convert_to_bigint + unprepare_async_index_by_name :ci_stages, + :index_ci_stages_on_pipeline_id_and_id_convert_to_bigint unprepare_async_index_by_name :ci_stages, :index_ci_stages_on_id_convert_to_bigint end diff --git a/db/post_migrate/20210817024335_prepare_indexes_for_events_bigint_conversion.rb b/db/post_migrate/20210817024335_prepare_indexes_for_events_bigint_conversion.rb index 1d102d6216c..e1ed28f6826 100644 --- a/db/post_migrate/20210817024335_prepare_indexes_for_events_bigint_conversion.rb +++ b/db/post_migrate/20210817024335_prepare_indexes_for_events_bigint_conversion.rb @@ -7,13 +7,14 @@ class PrepareIndexesForEventsBigintConversion < ActiveRecord::Migration[6.1] def up prepare_async_index TABLE_NAME, :id_convert_to_bigint, unique: true, - name: :index_events_on_id_convert_to_bigint + name: :index_events_on_id_convert_to_bigint prepare_async_index TABLE_NAME, [:project_id, :id_convert_to_bigint], name: :index_events_on_project_id_and_id_convert_to_bigint - prepare_async_index TABLE_NAME, [:project_id, :id_convert_to_bigint], order: { id_convert_to_bigint: :desc }, - where: 'action = 7', name: :index_events_on_project_id_and_id_bigint_desc_on_merged_action + prepare_async_index TABLE_NAME, [:project_id, :id_convert_to_bigint], + order: { id_convert_to_bigint: :desc }, + where: 'action = 7', name: :index_events_on_project_id_and_id_bigint_desc_on_merged_action end def down diff --git a/db/post_migrate/20210824174615_prepare_ci_builds_metadata_and_ci_build_async_indexes.rb b/db/post_migrate/20210824174615_prepare_ci_builds_metadata_and_ci_build_async_indexes.rb index f63645b4ffa..bf95aa98295 100644 --- a/db/post_migrate/20210824174615_prepare_ci_builds_metadata_and_ci_build_async_indexes.rb +++ b/db/post_migrate/20210824174615_prepare_ci_builds_metadata_and_ci_build_async_indexes.rb @@ -5,13 +5,13 @@ class PrepareCiBuildsMetadataAndCiBuildAsyncIndexes < ActiveRecord::Migration[6. def up prepare_async_index :ci_builds_metadata, :id_convert_to_bigint, unique: true, - name: :index_ci_builds_metadata_on_id_convert_to_bigint + name: :index_ci_builds_metadata_on_id_convert_to_bigint prepare_async_index :ci_builds_metadata, :build_id_convert_to_bigint, unique: true, - name: :index_ci_builds_metadata_on_build_id_convert_to_bigint + name: :index_ci_builds_metadata_on_build_id_convert_to_bigint prepare_async_index :ci_builds_metadata, :build_id_convert_to_bigint, where: 'has_exposed_artifacts IS TRUE', - name: :index_ci_builds_metadata_on_build_id_int8_and_exposed_artifacts + name: :index_ci_builds_metadata_on_build_id_int8_and_exposed_artifacts prepare_async_index_from_sql(:ci_builds_metadata, :index_ci_builds_metadata_on_build_id_int8_where_interruptible, <<~SQL.squish) CREATE INDEX CONCURRENTLY "index_ci_builds_metadata_on_build_id_int8_where_interruptible" @@ -20,7 +20,7 @@ class PrepareCiBuildsMetadataAndCiBuildAsyncIndexes < ActiveRecord::Migration[6. SQL prepare_async_index :ci_builds, :id_convert_to_bigint, unique: true, - name: :index_ci_builds_on_converted_id + name: :index_ci_builds_on_converted_id end def down diff --git a/doc/operations/incident_management/incidents.md b/doc/operations/incident_management/incidents.md index b8ca0e5b899..c1a4c1eb93e 100644 --- a/doc/operations/incident_management/incidents.md +++ b/doc/operations/incident_management/incidents.md @@ -205,10 +205,10 @@ field populated. ### Timeline events -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/344059) in GitLab 15.2 [with a flag](../../administration/feature_flags.md) named `incident_timeline`. Enabled on GitLab.com. Disabled on self-managed. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/344059) in GitLab 15.2 [with a flag](../../administration/feature_flags.md) named `incident_timeline`. Enabled by default. FLAG: -On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `incident_timeline`. +On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `incident_timeline`. On GitLab.com, this feature is available. Incident timelines are an important part of record keeping for incidents. diff --git a/lib/gitlab/import_export/decompressed_archive_size_validator.rb b/lib/gitlab/import_export/decompressed_archive_size_validator.rb index a185eb4df1c..c98dcf7b848 100644 --- a/lib/gitlab/import_export/decompressed_archive_size_validator.rb +++ b/lib/gitlab/import_export/decompressed_archive_size_validator.rb @@ -28,25 +28,26 @@ module Gitlab private def validate - pgrp = nil + pgrps = nil valid_archive = true validate_archive_path Timeout.timeout(TIMEOUT_LIMIT) do - stdin, stdout, stderr, wait_thr = Open3.popen3(command, pgroup: true) - stdin.close + stderr_r, stderr_w = IO.pipe + stdout, wait_threads = Open3.pipeline_r(*command, pgroup: true, err: stderr_w ) # When validation is performed on a small archive (e.g. 100 bytes) # `wait_thr` finishes before we can get process group id. Do not # raise exception in this scenario. - pgrp = begin + pgrps = wait_threads.map do |wait_thr| Process.getpgid(wait_thr[:pid]) rescue Errno::ESRCH nil end + pgrps.compact! - status = wait_thr.value + status = wait_threads.last.value if status.success? result = stdout.readline @@ -64,20 +65,21 @@ module Gitlab ensure stdout.close - stderr.close + stderr_w.close + stderr_r.close end valid_archive rescue Timeout::Error log_error('Timeout reached during archive decompression') - Process.kill(-1, pgrp) if pgrp + pgrps.each { |pgrp| Process.kill(-1, pgrp) } if pgrps false rescue StandardError => e log_error(e.message) - Process.kill(-1, pgrp) if pgrp + pgrps.each { |pgrp| Process.kill(-1, pgrp) } if pgrps false end @@ -91,7 +93,7 @@ module Gitlab end def command - "gzip -dc #{@archive_path} | wc -c" + [['gzip', '-dc', @archive_path], ['wc', '-c']] end def log_error(error) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 9b7ae62b370..8f0e86b144b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4101,6 +4101,9 @@ msgstr "" msgid "An error occurred while fetching reference" msgstr "" +msgid "An error occurred while fetching reviewers." +msgstr "" + msgid "An error occurred while fetching tags. Retry the search." msgstr "" @@ -7972,9 +7975,6 @@ msgstr "" msgid "Child issues and epics" msgstr "" -msgid "Children" -msgstr "" - msgid "Chinese language support using" msgstr "" @@ -27438,6 +27438,9 @@ msgstr "" msgid "OperationsDashboard|Operations Dashboard" msgstr "" +msgid "OperationsDashboard|The Operations and Environments dashboards share the same list of projects. When you add or remove a project from one, GitLab adds or removes the project from the other. %{linkStart}More information%{linkEnd}" +msgstr "" + msgid "OperationsDashboard|The operations dashboard provides a summary of each project's operational health, including pipeline and alert statuses." msgstr "" @@ -44165,6 +44168,9 @@ msgstr "" msgid "WorkItem|Add a task" msgstr "" +msgid "WorkItem|Add a title" +msgstr "" + msgid "WorkItem|Add assignee" msgstr "" @@ -44251,6 +44257,9 @@ msgstr "" msgid "WorkItem|Something went wrong when trying to add a child. Please try again." msgstr "" +msgid "WorkItem|Something went wrong when trying to create a child. Please try again." +msgstr "" + msgid "WorkItem|Something went wrong while updating the work item. Please try again." msgstr "" diff --git a/qa/qa/ce/strategy.rb b/qa/qa/ce/strategy.rb index bf08f887c7d..981b60d1920 100644 --- a/qa/qa/ce/strategy.rb +++ b/qa/qa/ce/strategy.rb @@ -6,6 +6,16 @@ module QA extend self def perform_before_hooks + if QA::Runtime::Env.admin_personal_access_token.present? + QA::Resource::PersonalAccessTokenCache.set_token_for_username(QA::Runtime::User.admin_username, + QA::Runtime::Env.admin_personal_access_token) + end + + if QA::Runtime::Env.personal_access_token.present? && QA::Runtime::Env.user_username.present? + QA::Resource::PersonalAccessTokenCache.set_token_for_username(QA::Runtime::Env.user_username, + QA::Runtime::Env.personal_access_token) + end + # The login page could take some time to load the first time it is visited. # We visit the login page and wait for it to properly load only once before the tests. QA::Runtime::Logger.info("Performing sanity check for environment!") diff --git a/qa/qa/resource/personal_access_token.rb b/qa/qa/resource/personal_access_token.rb index a12c210ea0e..ad0f183c603 100644 --- a/qa/qa/resource/personal_access_token.rb +++ b/qa/qa/resource/personal_access_token.rb @@ -17,20 +17,17 @@ module QA # If Runtime::Env.admin_personal_access_token is provided, fabricate via the API, # else, fabricate via the browser. def fabricate_via_api! - QA::Resource::PersonalAccessTokenCache.get_token_for_username(user.username).tap do |cached_token| - @token = cached_token if cached_token - end - return if @token + return if find_and_set_value resource = if Runtime::Env.admin_personal_access_token && !@user.nil? self.api_client = Runtime::API::Client.as_admin - super else fabricate! end - QA::Resource::PersonalAccessTokenCache.set_token_for_username(user.username, token) if @user + self.token = api_response[:token] unless api_response.nil? + cache_token resource end @@ -60,7 +57,17 @@ module QA # this particular resource does not expose a web_url property end + def find_and_set_value + @token ||= QA::Resource::PersonalAccessTokenCache.get_token_for_username(user.username) + end + + def cache_token + QA::Resource::PersonalAccessTokenCache.set_token_for_username(user.username, self.token) if @user && self.token + end + def fabricate! + return if find_and_set_value + Flow::Login.sign_in_unless_signed_in(user: user) Page::Main::Menu.perform(&:click_edit_profile_link) @@ -74,6 +81,10 @@ module QA token_page.click_create_token_button self.token = Page::Profile::PersonalAccessTokens.perform(&:created_access_token) + + cache_token + + self.token end end end diff --git a/qa/qa/resource/personal_access_token_cache.rb b/qa/qa/resource/personal_access_token_cache.rb index 3e9dc3fd7df..0874618201a 100644 --- a/qa/qa/resource/personal_access_token_cache.rb +++ b/qa/qa/resource/personal_access_token_cache.rb @@ -6,7 +6,17 @@ module QA @personal_access_tokens = {} def self.get_token_for_username(username) - @personal_access_tokens[username] + token = @personal_access_tokens[username] + + log_message = if token + %Q[Retrieved cached token for username: #{username}, last six chars of token:#{token[-6..]}] + else + %Q[No cached token found for username: #{username}] + end + + QA::Runtime::Logger.info(log_message) + + token end def self.set_token_for_username(username, token) diff --git a/scripts/static-analysis b/scripts/static-analysis index 14d6a3deccb..144d40d36c2 100755 --- a/scripts/static-analysis +++ b/scripts/static-analysis @@ -11,7 +11,9 @@ class StaticAnalysis # https://github.com/browserslist/browserslist/blob/d0ec62eb48c41c218478cd3ac28684df051cc865/node.js#L329 # warns if caniuse-lite package is older than 6 months. Ignore this # warning message so that GitLab backports don't fail. - "Browserslist: caniuse-lite is outdated. Please run next command `yarn upgrade`" + "Browserslist: caniuse-lite is outdated. Please run next command `yarn upgrade`", + # https://github.com/mime-types/mime-types-data/pull/50#issuecomment-1060908930 + "Type application/netcdf is already registered as a variant of application/netcdf" ].freeze Task = Struct.new(:command, :duration) do diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 97ddbf21b99..0751f4aa670 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -504,6 +504,20 @@ FactoryBot.define do artifacts_expire_at { 1.minute.ago } end + trait :with_artifacts_paths do + options do + { + artifacts: { + name: 'artifacts_file', + untracked: false, + paths: ['out/'], + when: 'always', + expire_in: '7d' + } + } + end + end + trait :with_commit do after(:build) do |build| commit = build(:commit, :without_author) diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js index 8ee57f97754..823121c24d8 100644 --- a/spec/frontend/issues/show/components/description_spec.js +++ b/spec/frontend/issues/show/components/description_spec.js @@ -266,7 +266,7 @@ describe('Description component', () => { }); }); - describe('with work items feature flag is enabled', () => { + describe('with work_items_create_from_markdown feature flag enabled', () => { describe('empty description', () => { beforeEach(() => { createComponent({ @@ -275,7 +275,7 @@ describe('Description component', () => { }, provide: { glFeatures: { - workItems: true, + workItemsCreateFromMarkdown: true, }, }, }); @@ -295,7 +295,7 @@ describe('Description component', () => { }, provide: { glFeatures: { - workItems: true, + workItemsCreateFromMarkdown: true, }, }, }); @@ -344,7 +344,7 @@ describe('Description component', () => { descriptionHtml: descriptionHtmlWithTask, }, provide: { - glFeatures: { workItems: true }, + glFeatures: { workItemsCreateFromMarkdown: true }, }, }); return nextTick(); @@ -406,7 +406,7 @@ describe('Description component', () => { createComponent({ props: { descriptionHtml: descriptionHtmlWithTask }, - provide: { glFeatures: { workItems: true } }, + provide: { glFeatures: { workItemsCreateFromMarkdown: true } }, }); expect(showDetailsModal).toHaveBeenCalledTimes(modalOpened); @@ -422,7 +422,7 @@ describe('Description component', () => { descriptionHtml: descriptionHtmlWithTask, }, provide: { - glFeatures: { workItems: true }, + glFeatures: { workItemsCreateFromMarkdown: true }, }, }); return nextTick(); diff --git a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js index 8ac85d4da81..2c24df2436a 100644 --- a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js +++ b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js @@ -2,7 +2,21 @@ import { shallowMount } from '@vue/test-utils'; import { TEST_HOST } from 'helpers/test_constants'; import ReviewerAvatarLink from '~/sidebar/components/reviewers/reviewer_avatar_link.vue'; import UncollapsedReviewerList from '~/sidebar/components/reviewers/uncollapsed_reviewer_list.vue'; -import userDataMock from '../../user_data_mock'; + +const userDataMock = () => ({ + id: 1, + name: 'Root', + state: 'active', + username: 'root', + webUrl: `${TEST_HOST}/root`, + avatarUrl: `${TEST_HOST}/avatar/root.png`, + mergeRequestInteraction: { + canMerge: true, + canUpdate: true, + reviewed: true, + approved: false, + }, +}); describe('UncollapsedReviewerList component', () => { let wrapper; @@ -69,7 +83,10 @@ describe('UncollapsedReviewerList component', () => { id: 2, name: 'nonrooty-nonrootersen', username: 'hello-world', - approved: true, + mergeRequestInteraction: { + ...user.mergeRequestInteraction, + approved: true, + }, }; beforeEach(() => { diff --git a/spec/frontend/sidebar/reviewers_spec.js b/spec/frontend/sidebar/reviewers_spec.js index 351dfc9a6ed..88bacc9b7f7 100644 --- a/spec/frontend/sidebar/reviewers_spec.js +++ b/spec/frontend/sidebar/reviewers_spec.js @@ -1,9 +1,23 @@ import { GlIcon } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { trimText } from 'helpers/text_helper'; -import UsersMockHelper from 'helpers/user_mock_data_helper'; +import { TEST_HOST } from 'helpers/test_constants'; import Reviewer from '~/sidebar/components/reviewers/reviewers.vue'; -import UsersMock from './mock_data'; + +const usersMock = (id = 1) => ({ + id, + name: 'Root', + state: 'active', + username: 'root', + webUrl: `${TEST_HOST}/root`, + avatarUrl: `${TEST_HOST}/avatar/root.png`, + mergeRequestInteraction: { + canMerge: true, + canUpdate: true, + reviewed: true, + approved: false, + }, +}); describe('Reviewer component', () => { const getDefaultProps = () => ({ @@ -42,23 +56,23 @@ describe('Reviewer component', () => { it('displays one reviewer icon when collapsed', () => { createWrapper({ ...getDefaultProps(), - users: [UsersMock.user], + users: [usersMock()], }); const collapsedChildren = findCollapsedChildren(); const reviewer = collapsedChildren.at(0); expect(collapsedChildren.length).toBe(1); - expect(reviewer.find('.avatar').attributes('src')).toBe(UsersMock.user.avatar); - expect(reviewer.find('.avatar').attributes('alt')).toBe(`${UsersMock.user.name}'s avatar`); + expect(reviewer.find('.avatar').attributes('src')).toContain('avatar/root.png'); + expect(reviewer.find('.avatar').attributes('alt')).toBe(`Root's avatar`); - expect(trimText(reviewer.find('.author').text())).toBe(UsersMock.user.name); + expect(trimText(reviewer.find('.author').text())).toBe('Root'); }); }); describe('Two or more reviewers/users', () => { it('displays two reviewer icons when collapsed', () => { - const users = UsersMockHelper.createNumberRandomUsers(2); + const users = [usersMock(), usersMock(2)]; createWrapper({ ...getDefaultProps(), users, @@ -70,21 +84,21 @@ describe('Reviewer component', () => { const first = collapsedChildren.at(0); - expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar_url); + expect(first.find('.avatar').attributes('src')).toBe(users[0].avatarUrl); expect(first.find('.avatar').attributes('alt')).toBe(`${users[0].name}'s avatar`); expect(trimText(first.find('.author').text())).toBe(users[0].name); const second = collapsedChildren.at(1); - expect(second.find('.avatar').attributes('src')).toBe(users[1].avatar_url); + expect(second.find('.avatar').attributes('src')).toBe(users[1].avatarUrl); expect(second.find('.avatar').attributes('alt')).toBe(`${users[1].name}'s avatar`); expect(trimText(second.find('.author').text())).toBe(users[1].name); }); it('displays one reviewer icon and counter when collapsed', () => { - const users = UsersMockHelper.createNumberRandomUsers(3); + const users = [usersMock(), usersMock(2), usersMock(3)]; createWrapper({ ...getDefaultProps(), users, @@ -96,7 +110,7 @@ describe('Reviewer component', () => { const first = collapsedChildren.at(0); - expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar_url); + expect(first.find('.avatar').attributes('src')).toBe(users[0].avatarUrl); expect(first.find('.avatar').attributes('alt')).toBe(`${users[0].name}'s avatar`); expect(trimText(first.find('.author').text())).toBe(users[0].name); @@ -107,7 +121,7 @@ describe('Reviewer component', () => { }); it('Shows two reviewers', () => { - const users = UsersMockHelper.createNumberRandomUsers(2); + const users = [usersMock(), usersMock(2)]; createWrapper({ ...getDefaultProps(), users, @@ -118,10 +132,10 @@ describe('Reviewer component', () => { }); it('shows sorted reviewer where "can merge" users are sorted first', () => { - const users = UsersMockHelper.createNumberRandomUsers(3); - users[0].can_merge = false; - users[1].can_merge = false; - users[2].can_merge = true; + const users = [usersMock(), usersMock(2), usersMock(3)]; + users[0].mergeRequestInteraction.canMerge = false; + users[1].mergeRequestInteraction.canMerge = false; + users[2].mergeRequestInteraction.canMerge = true; createWrapper({ ...getDefaultProps(), @@ -129,14 +143,14 @@ describe('Reviewer component', () => { editable: true, }); - expect(wrapper.vm.sortedReviewers[0].can_merge).toBe(true); + expect(wrapper.vm.sortedReviewers[0].mergeRequestInteraction.canMerge).toBe(true); }); it('passes the sorted reviewers to the uncollapsed-reviewer-list', () => { - const users = UsersMockHelper.createNumberRandomUsers(3); - users[0].can_merge = false; - users[1].can_merge = false; - users[2].can_merge = true; + const users = [usersMock(), usersMock(2), usersMock(3)]; + users[0].mergeRequestInteraction.canMerge = false; + users[1].mergeRequestInteraction.canMerge = false; + users[2].mergeRequestInteraction.canMerge = true; createWrapper({ ...getDefaultProps(), @@ -149,10 +163,10 @@ describe('Reviewer component', () => { }); it('passes the sorted reviewers to the collapsed-reviewer-list', () => { - const users = UsersMockHelper.createNumberRandomUsers(3); - users[0].can_merge = false; - users[1].can_merge = false; - users[2].can_merge = true; + const users = [usersMock(), usersMock(2), usersMock(3)]; + users[0].mergeRequestInteraction.canMerge = false; + users[1].mergeRequestInteraction.canMerge = false; + users[2].mergeRequestInteraction.canMerge = true; createWrapper({ ...getDefaultProps(), diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js index 93bf7286aa7..56c1d8054fd 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js @@ -1,13 +1,18 @@ import Vue from 'vue'; -import { GlForm, GlFormCombobox } from '@gitlab/ui'; +import { GlForm, GlFormInput, GlFormCombobox } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue'; import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql'; +import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; -import { availableWorkItemsResponse, updateWorkItemMutationResponse } from '../../mock_data'; +import { + availableWorkItemsResponse, + createWorkItemMutationResponse, + updateWorkItemMutationResponse, +} from '../../mock_data'; Vue.use(VueApollo); @@ -15,12 +20,14 @@ describe('WorkItemLinksForm', () => { let wrapper; const updateMutationResolver = jest.fn().mockResolvedValue(updateWorkItemMutationResponse); + const createMutationResolver = jest.fn().mockResolvedValue(createWorkItemMutationResponse); const createComponent = async ({ listResponse = availableWorkItemsResponse } = {}) => { wrapper = shallowMountExtended(WorkItemLinksForm, { apolloProvider: createMockApollo([ [projectWorkItemsQuery, jest.fn().mockResolvedValue(listResponse)], [updateWorkItemMutation, updateMutationResolver], + [createWorkItemMutation, createMutationResolver], ]), propsData: { issuableGid: 'gid://gitlab/WorkItem/1' }, provide: { @@ -33,6 +40,7 @@ describe('WorkItemLinksForm', () => { const findForm = () => wrapper.findComponent(GlForm); const findCombobox = () => wrapper.findComponent(GlFormCombobox); + const findInput = () => wrapper.findComponent(GlFormInput); const findAddChildButton = () => wrapper.findByTestId('add-child-button'); beforeEach(async () => { @@ -47,19 +55,41 @@ describe('WorkItemLinksForm', () => { expect(findForm().exists()).toBe(true); }); - it('passes available work items as prop when typing in combobox', async () => { - findCombobox().vm.$emit('input', 'Task'); - await waitForPromises(); + it('creates child task', async () => { + findInput().vm.$emit('input', 'Create task test'); - expect(findCombobox().exists()).toBe(true); - expect(findCombobox().props('tokenList').length).toBe(2); + findForm().vm.$emit('submit', { + preventDefault: jest.fn(), + }); + await waitForPromises(); + expect(createMutationResolver).toHaveBeenCalled(); }); - it('selects and add child', async () => { + // Follow up issue to turn this functionality back on https://gitlab.com/gitlab-org/gitlab/-/issues/368757 + // eslint-disable-next-line jest/no-disabled-tests + it.skip('selects and add child', async () => { findCombobox().vm.$emit('input', availableWorkItemsResponse.data.workspace.workItems.edges[0]); findAddChildButton().vm.$emit('click'); await waitForPromises(); expect(updateMutationResolver).toHaveBeenCalled(); }); + + // eslint-disable-next-line jest/no-disabled-tests + describe.skip('when typing in combobox', () => { + beforeEach(async () => { + findCombobox().vm.$emit('input', 'Task'); + await waitForPromises(); + await jest.runOnlyPendingTimers(); + }); + + it('passes available work items as prop', () => { + expect(findCombobox().exists()).toBe(true); + expect(findCombobox().props('tokenList').length).toBe(2); + }); + + it('passes action to create task', () => { + expect(findCombobox().props('actionList').length).toBe(1); + }); + }); }); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index 7232d588add..05ca66d2523 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -219,6 +219,7 @@ export const createWorkItemMutationResponse = { }, widgets: [], }, + errors: [], }, }, }; diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb index 97cd4777b4d..feaa1f6595c 100644 --- a/spec/lib/gitlab/git/branch_spec.rb +++ b/spec/lib/gitlab/git/branch_spec.rb @@ -2,8 +2,9 @@ require "spec_helper" -RSpec.describe Gitlab::Git::Branch, :seed_helper do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } +RSpec.describe Gitlab::Git::Branch do + let(:project) { create(:project, :repository) } + let(:repository) { project.repository.raw } subject { repository.branches } @@ -54,14 +55,14 @@ RSpec.describe Gitlab::Git::Branch, :seed_helper do describe '#size' do subject { super().size } - it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) } + it { is_expected.to eq(TestEnv::BRANCH_SHA.size) } end describe 'first branch' do let(:branch) { repository.branches.first } - it { expect(branch.name).to eq(SeedRepo::Repo::BRANCHES.first) } - it { expect(branch.dereferenced_target.sha).to eq("0b4bc9a49b562e85de7cc9e834518ea6828729b9") } + it { expect(branch.name).to eq(TestEnv::BRANCH_SHA.keys.min) } + it { expect(branch.dereferenced_target.sha).to start_with(TestEnv::BRANCH_SHA[TestEnv::BRANCH_SHA.keys.min]) } end describe 'master branch' do @@ -69,14 +70,10 @@ RSpec.describe Gitlab::Git::Branch, :seed_helper do repository.branches.find { |branch| branch.name == 'master' } end - it { expect(branch.dereferenced_target.sha).to eq(SeedRepo::LastCommit::ID) } + it { expect(branch.dereferenced_target.sha).to start_with(TestEnv::BRANCH_SHA['master']) } end context 'with active, stale and future branches' do - let(:repository) do - Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '', 'group/project') - end - let(:user) { create(:user) } let(:stale_sha) { travel_to(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago - 5.days) { create_commit } } let(:active_sha) { travel_to(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago + 5.days) { create_commit } } @@ -88,10 +85,6 @@ RSpec.describe Gitlab::Git::Branch, :seed_helper do repository.create_branch('future-1', future_sha) end - after do - ensure_seeds - end - describe 'examine if the branch is active or stale' do let(:stale_branch) { repository.find_branch('stale-1') } let(:active_branch) { repository.find_branch('active-1') } @@ -117,8 +110,6 @@ RSpec.describe Gitlab::Git::Branch, :seed_helper do end end - it { expect(repository.branches.size).to eq(SeedRepo::Repo::BRANCHES.size) } - def create_commit repository.multi_action( user, diff --git a/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb b/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb index dea584e5019..9af72cc0dea 100644 --- a/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb +++ b/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb @@ -51,10 +51,11 @@ RSpec.describe Gitlab::ImportExport::DecompressedArchiveSizeValidator do shared_examples 'logs raised exception and terminates validator process group' do let(:std) { double(:std, close: nil, value: nil) } let(:wait_thr) { double } + let(:wait_threads) { [wait_thr, wait_thr] } before do allow(Process).to receive(:getpgid).and_return(2) - allow(Open3).to receive(:popen3).and_return([std, std, std, wait_thr]) + allow(Open3).to receive(:pipeline_r).and_return([std, wait_threads]) allow(wait_thr).to receive(:[]).with(:pid).and_return(1) allow(wait_thr).to receive(:value).and_raise(exception) end @@ -67,7 +68,7 @@ RSpec.describe Gitlab::ImportExport::DecompressedArchiveSizeValidator do import_upload_archive_size: File.size(filepath), message: error_message ) - expect(Process).to receive(:kill).with(-1, 2) + expect(Process).to receive(:kill).with(-1, 2).twice expect(subject.valid?).to eq(false) end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 4531ca713f0..83ec4a5585d 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -3390,6 +3390,13 @@ RSpec.describe Group do end end + describe '#work_items_create_from_markdown_feature_flag_enabled?' do + it_behaves_like 'checks self and root ancestor feature flag' do + let(:feature_flag) { :work_items_create_from_markdown } + let(:feature_flag_method) { :work_items_create_from_markdown_feature_flag_enabled? } + end + end + describe 'group shares' do let!(:sub_group) { create(:group, parent: group) } let!(:sub_sub_group) { create(:group, parent: sub_group) } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index fb6fc01ed88..7870326c29a 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -8281,6 +8281,16 @@ RSpec.describe Project, factory_default: :keep do end end + describe '#work_items_create_from_markdown_feature_flag_enabled?' do + let_it_be(:group_project) { create(:project, :in_subgroup) } + + it_behaves_like 'checks parent group feature flag' do + let(:feature_flag_method) { :work_items_create_from_markdown_feature_flag_enabled? } + let(:feature_flag) { :work_items_create_from_markdown } + let(:subject_project) { group_project } + end + end + describe 'serialization' do let(:object) { build(:project) } diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb index 05644dad151..01d1e47b5bb 100644 --- a/spec/serializers/environment_serializer_spec.rb +++ b/spec/serializers/environment_serializer_spec.rb @@ -101,6 +101,37 @@ RSpec.describe EnvironmentSerializer do expect(subject.third[:latest][:environment_type]).to be_nil end end + + context 'when folders and standalone environments share the same name' do + before do + create(:environment, project: project, name: 'staging/my-review-1') + create(:environment, project: project, name: 'staging/my-review-2') + create(:environment, project: project, name: 'production/my-review-3') + create(:environment, project: project, name: 'staging') + create(:environment, project: project, name: 'testing') + end + + it 'does not group standalone environments with folders that have the same name' do + expect(subject.count).to eq 4 + + expect(subject.first[:name]).to eq 'production' + expect(subject.first[:size]).to eq 1 + expect(subject.first[:latest][:name]).to eq 'production/my-review-3' + expect(subject.first[:latest][:environment_type]).to eq 'production' + expect(subject.second[:name]).to eq 'staging' + expect(subject.second[:size]).to eq 1 + expect(subject.second[:latest][:name]).to eq 'staging' + expect(subject.second[:latest][:environment_type]).to be_nil + expect(subject.third[:name]).to eq 'staging' + expect(subject.third[:size]).to eq 2 + expect(subject.third[:latest][:name]).to eq 'staging/my-review-2' + expect(subject.third[:latest][:environment_type]).to eq 'staging' + expect(subject.fourth[:name]).to eq 'testing' + expect(subject.fourth[:size]).to eq 1 + expect(subject.fourth[:latest][:name]).to eq 'testing' + expect(subject.fourth[:latest][:environment_type]).to be_nil + end + end end context 'when used with pagination' do diff --git a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb index 255c4e6f882..ca2fe8a6c54 100644 --- a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb +++ b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb @@ -66,7 +66,7 @@ Integration.available_integration_names.each do |integration| hash.merge!(k => 'foo@bar.com') elsif (integration == 'slack' || integration == 'mattermost') && k == :labels_to_be_notified_behavior hash.merge!(k => "match_any") - elsif integration == 'campfire' && k = :room + elsif integration == 'campfire' && k == :room hash.merge!(k => '1234') else hash.merge!(k => "someword") |