Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									edde77d99a
								
							
						
					
					
						commit
						e7f151b0c0
					
				|  | @ -204,6 +204,7 @@ trigger-omnibus-env: | ||||||
|       SECURITY_SOURCES=$([[ ! "$CI_PROJECT_NAMESPACE" =~ ^gitlab-org\/security ]] || echo "true") |       SECURITY_SOURCES=$([[ ! "$CI_PROJECT_NAMESPACE" =~ ^gitlab-org\/security ]] || echo "true") | ||||||
|       echo "SECURITY_SOURCES=${SECURITY_SOURCES:-false}" > $BUILD_ENV |       echo "SECURITY_SOURCES=${SECURITY_SOURCES:-false}" > $BUILD_ENV | ||||||
|       echo "OMNIBUS_GITLAB_CACHE_UPDATE=${OMNIBUS_GITLAB_CACHE_UPDATE:-false}" >> $BUILD_ENV |       echo "OMNIBUS_GITLAB_CACHE_UPDATE=${OMNIBUS_GITLAB_CACHE_UPDATE:-false}" >> $BUILD_ENV | ||||||
|  |       echo "OMNIBUS_GITLAB_CACHE_EDITION=${OMNIBUS_GITLAB_CACHE_EDITION}" >> $BUILD_ENV | ||||||
|       for version_file in *_VERSION; do echo "$version_file=$(cat $version_file)" >> $BUILD_ENV; done |       for version_file in *_VERSION; do echo "$version_file=$(cat $version_file)" >> $BUILD_ENV; done | ||||||
|       echo "OMNIBUS_GITLAB_BUILD_ON_ALL_OS=${OMNIBUS_GITLAB_BUILD_ON_ALL_OS:-false}" >> $BUILD_ENV |       echo "OMNIBUS_GITLAB_BUILD_ON_ALL_OS=${OMNIBUS_GITLAB_BUILD_ON_ALL_OS:-false}" >> $BUILD_ENV | ||||||
|       ruby -e 'puts "FULL_RUBY_VERSION=#{RUBY_VERSION}"' >> $BUILD_ENV |       ruby -e 'puts "FULL_RUBY_VERSION=#{RUBY_VERSION}"' >> $BUILD_ENV | ||||||
|  |  | ||||||
|  | @ -0,0 +1,19 @@ | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   name: 'BoardCutLine', | ||||||
|  |   props: { | ||||||
|  |     cutLineText: { | ||||||
|  |       type: String, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <div class="board-cut-line gl-display-flex gl-mb-3 gl-text-red-700 gl-align-items-center"> | ||||||
|  |     <span class="gl-px-2 gl-font-sm gl-font-weight-bold" data-testid="cut-line-text">{{ | ||||||
|  |       cutLineText | ||||||
|  |     }}</span> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | @ -29,6 +29,7 @@ import { shouldCloneCard, moveItemVariables } from '../boards_util'; | ||||||
| import eventHub from '../eventhub'; | import eventHub from '../eventhub'; | ||||||
| import BoardCard from './board_card.vue'; | import BoardCard from './board_card.vue'; | ||||||
| import BoardNewIssue from './board_new_issue.vue'; | import BoardNewIssue from './board_new_issue.vue'; | ||||||
|  | import BoardCutLine from './board_cut_line.vue'; | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
|   draggableItemTypes: DraggableItemTypes, |   draggableItemTypes: DraggableItemTypes, | ||||||
|  | @ -42,6 +43,7 @@ export default { | ||||||
|   components: { |   components: { | ||||||
|     BoardCard, |     BoardCard, | ||||||
|     BoardNewIssue, |     BoardNewIssue, | ||||||
|  |     BoardCutLine, | ||||||
|     BoardNewEpic: () => import('ee_component/boards/components/board_new_epic.vue'), |     BoardNewEpic: () => import('ee_component/boards/components/board_new_epic.vue'), | ||||||
|     GlLoadingIcon, |     GlLoadingIcon, | ||||||
|     GlIntersectionObserver, |     GlIntersectionObserver, | ||||||
|  | @ -154,6 +156,16 @@ export default { | ||||||
|     boardListItems() { |     boardListItems() { | ||||||
|       return this.currentList?.[`${this.issuableType}s`].nodes || []; |       return this.currentList?.[`${this.issuableType}s`].nodes || []; | ||||||
|     }, |     }, | ||||||
|  |     beforeCutLine() { | ||||||
|  |       return this.boardItemsSizeExceedsMax | ||||||
|  |         ? this.boardListItems.slice(0, this.list.maxIssueCount) | ||||||
|  |         : this.boardListItems; | ||||||
|  |     }, | ||||||
|  |     afterCutLine() { | ||||||
|  |       return this.boardItemsSizeExceedsMax | ||||||
|  |         ? this.boardListItems.slice(this.list.maxIssueCount) | ||||||
|  |         : []; | ||||||
|  |     }, | ||||||
|     listQueryVariables() { |     listQueryVariables() { | ||||||
|       return { |       return { | ||||||
|         fullPath: this.fullPath, |         fullPath: this.fullPath, | ||||||
|  | @ -174,6 +186,11 @@ export default { | ||||||
|         issuableType: this.isEpicBoard ? 'epics' : 'issues', |         issuableType: this.isEpicBoard ? 'epics' : 'issues', | ||||||
|       }); |       }); | ||||||
|     }, |     }, | ||||||
|  |     wipLimitText() { | ||||||
|  |       return sprintf(__('Work in progress limit: %{wipLimit}'), { | ||||||
|  |         wipLimit: this.list.maxIssueCount, | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|     toggleFormEventPrefix() { |     toggleFormEventPrefix() { | ||||||
|       return this.isEpicBoard ? toggleFormEventPrefix.epic : toggleFormEventPrefix.issue; |       return this.isEpicBoard ? toggleFormEventPrefix.epic : toggleFormEventPrefix.issue; | ||||||
|     }, |     }, | ||||||
|  | @ -653,7 +670,7 @@ export default { | ||||||
|       :data-board="list.id" |       :data-board="list.id" | ||||||
|       :data-board-type="list.listType" |       :data-board-type="list.listType" | ||||||
|       :class="{ |       :class="{ | ||||||
|         'gl-bg-red-100 gl-rounded-bottom-left-base gl-rounded-bottom-right-base': boardItemsSizeExceedsMax, |         'gl-bg-red-50 gl-rounded-bottom-left-base gl-rounded-bottom-right-base': boardItemsSizeExceedsMax, | ||||||
|         'gl-overflow-hidden': disableScrollingWhenMutationInProgress, |         'gl-overflow-hidden': disableScrollingWhenMutationInProgress, | ||||||
|         'gl-overflow-y-auto': !disableScrollingWhenMutationInProgress, |         'gl-overflow-y-auto': !disableScrollingWhenMutationInProgress, | ||||||
|       }" |       }" | ||||||
|  | @ -664,7 +681,32 @@ export default { | ||||||
|       @end="handleDragOnEnd" |       @end="handleDragOnEnd" | ||||||
|     > |     > | ||||||
|       <board-card |       <board-card | ||||||
|         v-for="(item, index) in boardListItems" |         v-for="(item, index) in beforeCutLine" | ||||||
|  |         ref="issue" | ||||||
|  |         :key="item.id" | ||||||
|  |         :index="index" | ||||||
|  |         :list="list" | ||||||
|  |         :item="item" | ||||||
|  |         :data-draggable-item-type="$options.draggableItemTypes.card" | ||||||
|  |         :show-work-item-type-icon="!isEpicBoard" | ||||||
|  |       > | ||||||
|  |         <board-card-move-to-position | ||||||
|  |           v-if="showMoveToPosition" | ||||||
|  |           :item="item" | ||||||
|  |           :index="index" | ||||||
|  |           :list="list" | ||||||
|  |           :list-items-length="boardListItems.length" | ||||||
|  |           @moveToPosition="moveToPosition($event, index, item)" | ||||||
|  |         /> | ||||||
|  |         <gl-intersection-observer | ||||||
|  |           v-if="isObservableItem(index)" | ||||||
|  |           data-testid="board-card-gl-io" | ||||||
|  |           @appear="onReachingListBottom" | ||||||
|  |         /> | ||||||
|  |       </board-card> | ||||||
|  |       <board-cut-line v-if="boardItemsSizeExceedsMax" :cut-line-text="wipLimitText" /> | ||||||
|  |       <board-card | ||||||
|  |         v-for="(item, index) in afterCutLine" | ||||||
|         ref="issue" |         ref="issue" | ||||||
|         :key="item.id" |         :key="item.id" | ||||||
|         :index="index" |         :index="index" | ||||||
|  |  | ||||||
|  | @ -110,6 +110,9 @@ export default { | ||||||
|     itemsCount() { |     itemsCount() { | ||||||
|       return this.isEpicBoard ? this.list.metadata.epicsCount : this.boardList?.issuesCount; |       return this.isEpicBoard ? this.list.metadata.epicsCount : this.boardList?.issuesCount; | ||||||
|     }, |     }, | ||||||
|  |     boardItemsSizeExceedsMax() { | ||||||
|  |       return this.list.maxIssueCount > 0 && this.itemsCount > this.list.maxIssueCount; | ||||||
|  |     }, | ||||||
|     listAssignee() { |     listAssignee() { | ||||||
|       return this.list?.assignee?.username || ''; |       return this.list?.assignee?.username || ''; | ||||||
|     }, |     }, | ||||||
|  | @ -333,6 +336,7 @@ export default { | ||||||
|       'gl-h-full': list.collapsed, |       'gl-h-full': list.collapsed, | ||||||
|       'gl-bg-gray-50': isSwimlanesHeader, |       'gl-bg-gray-50': isSwimlanesHeader, | ||||||
|       'gl-border-t-solid gl-border-4 gl-rounded-top-left-base gl-rounded-top-right-base': isLabelList, |       'gl-border-t-solid gl-border-4 gl-rounded-top-left-base gl-rounded-top-right-base': isLabelList, | ||||||
|  |       'gl-bg-red-50 gl-rounded-top-left-base gl-rounded-top-right-base': boardItemsSizeExceedsMax, | ||||||
|     }" |     }" | ||||||
|     :style="headerStyle" |     :style="headerStyle" | ||||||
|     class="board-header gl-relative" |     class="board-header gl-relative" | ||||||
|  |  | ||||||
|  | @ -26,7 +26,7 @@ export default { | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <div class="item-count text-nowrap"> |   <div class="item-count text-nowrap"> | ||||||
|     <span :class="{ 'text-danger': issuesExceedMax }" data-testid="board-items-count"> |     <span :class="{ 'gl-text-red-700': issuesExceedMax }" data-testid="board-items-count"> | ||||||
|       {{ itemsSize }} |       {{ itemsSize }} | ||||||
|     </span> |     </span> | ||||||
|     <span v-if="isMaxLimitSet" class="max-issue-size"> |     <span v-if="isMaxLimitSet" class="max-issue-size"> | ||||||
|  |  | ||||||
|  | @ -1,11 +1,12 @@ | ||||||
| <script> | <script> | ||||||
| import { GlButton, GlAlert } from '@gitlab/ui'; | import { GlButton, GlAlert } from '@gitlab/ui'; | ||||||
| import { helpPagePath } from '~/helpers/help_page_helper'; | import { helpPagePath } from '~/helpers/help_page_helper'; | ||||||
| import { s__ } from '~/locale'; | import { s__, __ } from '~/locale'; | ||||||
| import Autosave from '~/autosave'; |  | ||||||
| import { isLoggedIn } from '~/lib/utils/common_utils'; | import { isLoggedIn } from '~/lib/utils/common_utils'; | ||||||
| import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils'; | import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils'; | ||||||
| import MarkdownField from '~/vue_shared/components/markdown/field.vue'; | import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue'; | ||||||
|  | import markdownEditorEventHub from '~/vue_shared/components/markdown/eventhub'; | ||||||
|  | import { CLEAR_AUTOSAVE_ENTRY_EVENT } from '~/vue_shared/constants'; | ||||||
| import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; | import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; | ||||||
| import { | import { | ||||||
|   ADD_DISCUSSION_COMMENT_ERROR, |   ADD_DISCUSSION_COMMENT_ERROR, | ||||||
|  | @ -27,7 +28,7 @@ export default { | ||||||
|   }, |   }, | ||||||
|   markdownDocsPath: helpPagePath('user/markdown'), |   markdownDocsPath: helpPagePath('user/markdown'), | ||||||
|   components: { |   components: { | ||||||
|     MarkdownField, |     MarkdownEditor, | ||||||
|     GlButton, |     GlButton, | ||||||
|     GlAlert, |     GlAlert, | ||||||
|   }, |   }, | ||||||
|  | @ -78,6 +79,14 @@ export default { | ||||||
|       noteUpdateDirty: false, |       noteUpdateDirty: false, | ||||||
|       isLoggedIn: isLoggedIn(), |       isLoggedIn: isLoggedIn(), | ||||||
|       errorMessage: '', |       errorMessage: '', | ||||||
|  |       formFieldProps: { | ||||||
|  |         id: 'design-reply', | ||||||
|  |         name: 'design-reply', | ||||||
|  |         'aria-label': __('Description'), | ||||||
|  |         placeholder: __('Write a comment…'), | ||||||
|  |         'data-testid': 'note-textarea', | ||||||
|  |         class: 'note-textarea js-gfm-input js-autosize markdown-area', | ||||||
|  |       }, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|  | @ -92,9 +101,16 @@ export default { | ||||||
|     shortDiscussionId() { |     shortDiscussionId() { | ||||||
|       return isGid(this.discussionId) ? getIdFromGraphQLId(this.discussionId) : this.discussionId; |       return isGid(this.discussionId) ? getIdFromGraphQLId(this.discussionId) : this.discussionId; | ||||||
|     }, |     }, | ||||||
|   }, |     autosaveKey() { | ||||||
|   mounted() { |       if (this.isLoggedIn) { | ||||||
|     this.focusInput(); |         return [ | ||||||
|  |           s__('DesignManagement|Discussion'), | ||||||
|  |           getIdFromGraphQLId(this.noteableId), | ||||||
|  |           this.shortDiscussionId, | ||||||
|  |         ].join('/'); | ||||||
|  |       } | ||||||
|  |       return ''; | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   beforeDestroy() { |   beforeDestroy() { | ||||||
|     /** |     /** | ||||||
|  | @ -104,9 +120,7 @@ export default { | ||||||
|      * so we're safe to clear autosave data here conditionally. |      * so we're safe to clear autosave data here conditionally. | ||||||
|      */ |      */ | ||||||
|     this.$nextTick(() => { |     this.$nextTick(() => { | ||||||
|       if (!this.noteUpdateDirty) { |       markdownEditorEventHub.$emit(CLEAR_AUTOSAVE_ENTRY_EVENT, this.autosaveKey); | ||||||
|         this.autosaveDiscussion?.reset(); |  | ||||||
|       } |  | ||||||
|     }); |     }); | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|  | @ -181,20 +195,7 @@ export default { | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       this.$emit('cancel-form'); |       this.$emit('cancel-form'); | ||||||
|       this.autosaveDiscussion.reset(); |       markdownEditorEventHub.$emit(CLEAR_AUTOSAVE_ENTRY_EVENT, this.autosaveKey); | ||||||
|     }, |  | ||||||
|     focusInput() { |  | ||||||
|       this.$refs.textarea.focus(); |  | ||||||
|       this.initAutosaveComment(); |  | ||||||
|     }, |  | ||||||
|     initAutosaveComment() { |  | ||||||
|       if (this.isLoggedIn) { |  | ||||||
|         this.autosaveDiscussion = new Autosave(this.$refs.textarea, [ |  | ||||||
|           s__('DesignManagement|Discussion'), |  | ||||||
|           getIdFromGraphQLId(this.noteableId), |  | ||||||
|           this.shortDiscussionId, |  | ||||||
|         ]); |  | ||||||
|       } |  | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|  | @ -207,31 +208,19 @@ export default { | ||||||
|         {{ errorMessage }} |         {{ errorMessage }} | ||||||
|       </gl-alert> |       </gl-alert> | ||||||
|     </div> |     </div> | ||||||
|     <markdown-field |     <markdown-editor | ||||||
|       :markdown-preview-path="markdownPreviewPath" |       v-model="noteText" | ||||||
|       :enable-autocomplete="true" |       autofocus | ||||||
|       :textarea-value="noteText" |  | ||||||
|       :markdown-docs-path="$options.markdownDocsPath" |       :markdown-docs-path="$options.markdownDocsPath" | ||||||
|       class="bordered-box" |       :render-markdown-path="markdownPreviewPath" | ||||||
|     > |       :enable-autocomplete="true" | ||||||
|       <template #textarea> |       :supports-quick-actions="false" | ||||||
|         <textarea |       :form-field-props="formFieldProps" | ||||||
|           ref="textarea" |       @input="handleInput" | ||||||
|           v-model.trim="noteText" |       @keydown.meta.enter="submitForm" | ||||||
|           class="note-textarea js-gfm-input js-autosize markdown-area" |       @keydown.ctrl.enter="submitForm" | ||||||
|           dir="auto" |       @keydown.esc.stop="cancelComment" | ||||||
|           data-supports-quick-actions="false" |     /> | ||||||
|           data-testid="note-textarea" |  | ||||||
|           :aria-label="__('Description')" |  | ||||||
|           :placeholder="__('Write a comment…')" |  | ||||||
|           @input="handleInput" |  | ||||||
|           @keydown.meta.enter="submitForm" |  | ||||||
|           @keydown.ctrl.enter="submitForm" |  | ||||||
|           @keyup.esc.stop="cancelComment" |  | ||||||
|         > |  | ||||||
|         </textarea> |  | ||||||
|       </template> |  | ||||||
|     </markdown-field> |  | ||||||
|     <slot name="resolve-checkbox"></slot> |     <slot name="resolve-checkbox"></slot> | ||||||
|     <div class="note-form-actions gl-display-flex gl-mt-4!"> |     <div class="note-form-actions gl-display-flex gl-mt-4!"> | ||||||
|       <gl-button |       <gl-button | ||||||
|  |  | ||||||
|  | @ -276,9 +276,6 @@ export default { | ||||||
|     }, |     }, | ||||||
|     openCommentForm(annotationCoordinates) { |     openCommentForm(annotationCoordinates) { | ||||||
|       this.annotationCoordinates = annotationCoordinates; |       this.annotationCoordinates = annotationCoordinates; | ||||||
|       if (this.$refs.newDiscussionForm) { |  | ||||||
|         this.$refs.newDiscussionForm.focusInput(); |  | ||||||
|       } |  | ||||||
|     }, |     }, | ||||||
|     closeCommentForm(data) { |     closeCommentForm(data) { | ||||||
|       this.annotationCoordinates = null; |       this.annotationCoordinates = null; | ||||||
|  |  | ||||||
|  | @ -15,7 +15,7 @@ import { __ } from '~/locale'; | ||||||
| import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue'; | import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue'; | ||||||
| import { BACK_URL_PARAM } from '~/releases/constants'; | import { BACK_URL_PARAM } from '~/releases/constants'; | ||||||
| import { putCreateReleaseNotification } from '~/releases/release_notification_service'; | import { putCreateReleaseNotification } from '~/releases/release_notification_service'; | ||||||
| import MarkdownField from '~/vue_shared/components/markdown/field.vue'; | import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue'; | ||||||
| import AssetLinksForm from './asset_links_form.vue'; | import AssetLinksForm from './asset_links_form.vue'; | ||||||
| import ConfirmDeleteModal from './confirm_delete_modal.vue'; | import ConfirmDeleteModal from './confirm_delete_modal.vue'; | ||||||
| import TagField from './tag_field.vue'; | import TagField from './tag_field.vue'; | ||||||
|  | @ -31,11 +31,22 @@ export default { | ||||||
|     GlLink, |     GlLink, | ||||||
|     GlSprintf, |     GlSprintf, | ||||||
|     ConfirmDeleteModal, |     ConfirmDeleteModal, | ||||||
|     MarkdownField, |     MarkdownEditor, | ||||||
|     AssetLinksForm, |     AssetLinksForm, | ||||||
|     MilestoneCombobox, |     MilestoneCombobox, | ||||||
|     TagField, |     TagField, | ||||||
|   }, |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       formFieldProps: { | ||||||
|  |         id: 'release-notes', | ||||||
|  |         name: 'release-notes', | ||||||
|  |         class: 'note-textarea js-gfm-input js-autosize markdown-area', | ||||||
|  |         'aria-label': __('Release notes'), | ||||||
|  |         placeholder: __('Write your release notes or drag your files here…'), | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     ...mapState('editNew', [ |     ...mapState('editNew', [ | ||||||
|       'isExistingRelease', |       'isExistingRelease', | ||||||
|  | @ -71,7 +82,7 @@ export default { | ||||||
|     }, |     }, | ||||||
|     releaseNotes: { |     releaseNotes: { | ||||||
|       get() { |       get() { | ||||||
|         return this.$store.state.editNew.release.description; |         return this.$store.state.editNew.release.description || this.formattedReleaseNotes; | ||||||
|       }, |       }, | ||||||
|       set(notes) { |       set(notes) { | ||||||
|         this.updateReleaseNotes(notes); |         this.updateReleaseNotes(notes); | ||||||
|  | @ -220,25 +231,13 @@ export default { | ||||||
|       </gl-form-group> |       </gl-form-group> | ||||||
|       <gl-form-group :label="__('Release notes')" data-testid="release-notes"> |       <gl-form-group :label="__('Release notes')" data-testid="release-notes"> | ||||||
|         <div class="common-note-form"> |         <div class="common-note-form"> | ||||||
|           <markdown-field |           <markdown-editor | ||||||
|             :can-attach-file="true" |             v-model="releaseNotes" | ||||||
|             :markdown-preview-path="markdownPreviewPath" |             :render-markdown-path="markdownPreviewPath" | ||||||
|             :markdown-docs-path="markdownDocsPath" |             :markdown-docs-path="markdownDocsPath" | ||||||
|             :add-spacing-classes="false" |             :supports-quick-actions="false" | ||||||
|             :textarea-value="formattedReleaseNotes" |             :form-field-props="formFieldProps" | ||||||
|           > |           /> | ||||||
|             <template #textarea> |  | ||||||
|               <textarea |  | ||||||
|                 id="release-notes" |  | ||||||
|                 v-model="releaseNotes" |  | ||||||
|                 class="note-textarea js-gfm-input js-autosize markdown-area" |  | ||||||
|                 dir="auto" |  | ||||||
|                 data-supports-quick-actions="false" |  | ||||||
|                 :aria-label="__('Release notes')" |  | ||||||
|                 :placeholder="__('Write your release notes or drag your files here…')" |  | ||||||
|               ></textarea> |  | ||||||
|             </template> |  | ||||||
|           </markdown-field> |  | ||||||
|         </div> |         </div> | ||||||
|       </gl-form-group> |       </gl-form-group> | ||||||
|       <gl-form-group v-if="!isExistingRelease"> |       <gl-form-group v-if="!isExistingRelease"> | ||||||
|  |  | ||||||
|  | @ -242,3 +242,12 @@ | ||||||
|     height: 100px; |     height: 100px; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | .board-cut-line { | ||||||
|  |   &::before, &::after { | ||||||
|  |     content: ''; | ||||||
|  |     height: 1px; | ||||||
|  |     flex: 1; | ||||||
|  |     border-top: 1px dashed $red-700; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -30,16 +30,16 @@ module CascadingNamespaceSettingAttribute | ||||||
|     # similar to Rails' `attr_accessor`, defines convenience methods such as |     # similar to Rails' `attr_accessor`, defines convenience methods such as | ||||||
|     # a reader, writer, and validators. |     # a reader, writer, and validators. | ||||||
|     # |     # | ||||||
|     # Example: `cascading_attr :delayed_project_removal` |     # Example: `cascading_attr :toggle_security_policy_custom_ci` | ||||||
|     # |     # | ||||||
|     # Public methods defined: |     # Public methods defined: | ||||||
|     # - `delayed_project_removal` |     # - `toggle_security_policy_custom_ci` | ||||||
|     # - `delayed_project_removal=` |     # - `toggle_security_policy_custom_ci=` | ||||||
|     # - `delayed_project_removal_locked?` |     # - `toggle_security_policy_custom_ci_locked?` | ||||||
|     # - `delayed_project_removal_locked_by_ancestor?` |     # - `toggle_security_policy_custom_ci_locked_by_ancestor?` | ||||||
|     # - `delayed_project_removal_locked_by_application_setting?` |     # - `toggle_security_policy_custom_ci_locked_by_application_setting?` | ||||||
|     # - `delayed_project_removal?` (only defined for boolean attributes) |     # - `toggle_security_policy_custom_ci?` (only defined for boolean attributes) | ||||||
|     # - `delayed_project_removal_locked_ancestor` - Returns locked namespace settings object (only namespace_id) |     # - `toggle_security_policy_custom_ci_locked_ancestor` - Returns locked namespace settings object (only namespace_id) | ||||||
|     # |     # | ||||||
|     # Defined validators ensure attribute value cannot be updated if locked by |     # Defined validators ensure attribute value cannot be updated if locked by | ||||||
|     # an ancestor or application settings. |     # an ancestor or application settings. | ||||||
|  |  | ||||||
|  | @ -8,9 +8,11 @@ module Integrations | ||||||
| 
 | 
 | ||||||
|     field :token, |     field :token, | ||||||
|       type: :password, |       type: :password, | ||||||
|  |       description: -> { _('The Slack token.') }, | ||||||
|       non_empty_password_title: -> { s_('ProjectService|Enter new token') }, |       non_empty_password_title: -> { s_('ProjectService|Enter new token') }, | ||||||
|       non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') }, |       non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') }, | ||||||
|       placeholder: '' |       placeholder: '', | ||||||
|  |       required: true | ||||||
| 
 | 
 | ||||||
|     def self.title |     def self.title | ||||||
|       'Slack slash commands' |       'Slack slash commands' | ||||||
|  |  | ||||||
|  | @ -8,8 +8,8 @@ class NamespaceSetting < ApplicationRecord | ||||||
| 
 | 
 | ||||||
|   ignore_column :project_import_level, remove_with: '16.10', remove_after: '2024-02-22' |   ignore_column :project_import_level, remove_with: '16.10', remove_after: '2024-02-22' | ||||||
|   ignore_column :third_party_ai_features_enabled, remove_with: '16.10', remove_after: '2024-02-22' |   ignore_column :third_party_ai_features_enabled, remove_with: '16.10', remove_after: '2024-02-22' | ||||||
|  |   ignore_column %i[delayed_project_removal lock_delayed_project_removal], remove_with: '16.10', remove_after: '2024-02-22' | ||||||
| 
 | 
 | ||||||
|   cascading_attr :delayed_project_removal |  | ||||||
|   cascading_attr :toggle_security_policy_custom_ci |   cascading_attr :toggle_security_policy_custom_ci | ||||||
|   cascading_attr :toggle_security_policies_policy_scope |   cascading_attr :toggle_security_policies_policy_scope | ||||||
| 
 | 
 | ||||||
|  | @ -40,8 +40,6 @@ class NamespaceSetting < ApplicationRecord | ||||||
| 
 | 
 | ||||||
|   NAMESPACE_SETTINGS_PARAMS = %i[ |   NAMESPACE_SETTINGS_PARAMS = %i[ | ||||||
|     default_branch_name |     default_branch_name | ||||||
|     delayed_project_removal |  | ||||||
|     lock_delayed_project_removal |  | ||||||
|     resource_access_token_creation_allowed |     resource_access_token_creation_allowed | ||||||
|     prevent_sharing_groups_outside_hierarchy |     prevent_sharing_groups_outside_hierarchy | ||||||
|     new_user_signups_cap |     new_user_signups_cap | ||||||
|  |  | ||||||
|  | @ -1,8 +0,0 @@ | ||||||
| --- |  | ||||||
| name: oidc_issuer_url |  | ||||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135049 |  | ||||||
| rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429855 |  | ||||||
| milestone: '16.6' |  | ||||||
| type: development |  | ||||||
| group: group::pipeline security |  | ||||||
| default_enabled: false |  | ||||||
|  | @ -0,0 +1,27 @@ | ||||||
|  | - title: "Deprecate `fmt` job in Terraform Module CI/CD template" | ||||||
|  |   # The milestones for the deprecation announcement, and the removal. | ||||||
|  |   removal_milestone: "17.0" | ||||||
|  |   announcement_milestone: "16.9" | ||||||
|  |   # Change breaking_change to false if needed. | ||||||
|  |   breaking_change: true | ||||||
|  |   # The stage and GitLab username of the person reporting the change, | ||||||
|  |   # and a link to the deprecation issue | ||||||
|  |   reporter: timofurrer | ||||||
|  |   stage: deploy | ||||||
|  |   issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/440249 | ||||||
|  |   body: |  # (required) Don't change this line. | ||||||
|  |     The `fmt` job in the Terraform Module CI/CD templates is deprecated and will be removed in GitLab 17.0. | ||||||
|  |     This affects the following templates: | ||||||
|  | 
 | ||||||
|  |     - `Terraform-Module.gitlab-ci.yml` | ||||||
|  |     - `Terraform/Module-Base.gitlab-ci.yml` | ||||||
|  | 
 | ||||||
|  |     You can manually add back a Terraform `fmt` job to your pipeline using: | ||||||
|  | 
 | ||||||
|  |     ```yaml | ||||||
|  |     fmt: | ||||||
|  |       image: hashicorp/terraform | ||||||
|  |       script: terraform fmt -chdir "$TF_ROOT" -check -diff -recursive | ||||||
|  |     ``` | ||||||
|  | 
 | ||||||
|  |     You can also use the `fmt` template from the [OpenTofu CI/CD component](https://gitlab.com/components/opentofu). | ||||||
|  | @ -18,7 +18,7 @@ and the following external authentication and authorization providers: | ||||||
|   and 389 Server. |   and 389 Server. | ||||||
|   - [Google Secure LDAP](ldap/google_secure_ldap.md) |   - [Google Secure LDAP](ldap/google_secure_ldap.md) | ||||||
| - [SAML for GitLab.com groups](../../user/group/saml_sso/index.md) | - [SAML for GitLab.com groups](../../user/group/saml_sso/index.md) | ||||||
| - [Smartcard](smartcard.md) | - [Smart card](smartcard.md) | ||||||
| 
 | 
 | ||||||
| NOTE: | NOTE: | ||||||
| UltraAuth has removed their software which supports OmniAuth integration. We have therefore removed all references to UltraAuth integration. | UltraAuth has removed their software which supports OmniAuth integration. We have therefore removed all references to UltraAuth integration. | ||||||
|  | @ -32,7 +32,7 @@ For more information, see the links shown on this page for each external provide | ||||||
| |-------------------------------------------------|-----------------------------------------|------------------------------------| | |-------------------------------------------------|-----------------------------------------|------------------------------------| | ||||||
| | **User Provisioning**                           | SCIM<br>SAML <sup>1</sup> | LDAP <sup>1</sup><br>SAML <sup>1</sup><br>[OmniAuth Providers](../../integration/omniauth.md#supported-providers) <sup>1</sup><br>SCIM  | | | **User Provisioning**                           | SCIM<br>SAML <sup>1</sup> | LDAP <sup>1</sup><br>SAML <sup>1</sup><br>[OmniAuth Providers](../../integration/omniauth.md#supported-providers) <sup>1</sup><br>SCIM  | | ||||||
| | **User Detail Updating** (not group management) | Not Available                           | LDAP Sync                          | | | **User Detail Updating** (not group management) | Not Available                           | LDAP Sync                          | | ||||||
| | **Authentication**                              | SAML at top-level group (1 provider)    | LDAP (multiple providers)<br>Generic OAuth 2.0<br>SAML (only 1 permitted per unique provider)<br>Kerberos<br>JWT<br>Smartcard<br>[OmniAuth Providers](../../integration/omniauth.md#supported-providers) (only 1 permitted per unique provider) | | | **Authentication**                              | SAML at top-level group (1 provider)    | LDAP (multiple providers)<br>Generic OAuth 2.0<br>SAML (only 1 permitted per unique provider)<br>Kerberos<br>JWT<br>Smart card<br>[OmniAuth Providers](../../integration/omniauth.md#supported-providers) (only 1 permitted per unique provider) | | ||||||
| | **Provider-to-GitLab Role Sync**                | SAML Group Sync                         | LDAP Group Sync<br>SAML Group Sync ([GitLab 15.1](https://gitlab.com/gitlab-org/gitlab/-/issues/285150) and later) | | | **Provider-to-GitLab Role Sync**                | SAML Group Sync                         | LDAP Group Sync<br>SAML Group Sync ([GitLab 15.1](https://gitlab.com/gitlab-org/gitlab/-/issues/285150) and later) | | ||||||
| | **User Removal**                                | SCIM (remove user from top-level group) | LDAP (remove user from groups and block from the instance)<br>SCIM | | | **User Removal**                                | SCIM (remove user from top-level group) | LDAP (remove user from groups and block from the instance)<br>SCIM | | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,22 +4,22 @@ group: Authentication | ||||||
| info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments | info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments | ||||||
| --- | --- | ||||||
| 
 | 
 | ||||||
| # Smartcard authentication | # Smart card authentication | ||||||
| 
 | 
 | ||||||
| DETAILS: | DETAILS: | ||||||
| **Tier:** Premium, Ultimate | **Tier:** Premium, Ultimate | ||||||
| **Offering:** Self-managed | **Offering:** Self-managed | ||||||
| 
 | 
 | ||||||
| GitLab supports authentication using smartcards. | GitLab supports authentication using smart cards. | ||||||
| 
 | 
 | ||||||
| ## Existing password authentication | ## Existing password authentication | ||||||
| 
 | 
 | ||||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33669) in GitLab 12.6. | > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33669) in GitLab 12.6. | ||||||
| 
 | 
 | ||||||
| By default, existing users can continue to sign in with a username and password when smartcard | By default, existing users can continue to sign in with a username and password when smart card | ||||||
| authentication is enabled. | authentication is enabled. | ||||||
| 
 | 
 | ||||||
| To force existing users to use only smartcard authentication, | To force existing users to use only smart card authentication, | ||||||
| [disable username and password authentication](../settings/sign_in_restrictions.md#password-authentication-enabled). | [disable username and password authentication](../settings/sign_in_restrictions.md#password-authentication-enabled). | ||||||
| 
 | 
 | ||||||
| ## Authentication methods | ## Authentication methods | ||||||
|  | @ -34,12 +34,11 @@ GitLab supports two authentication methods: | ||||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/726) in GitLab 11.6 as an experimental feature. | > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/726) in GitLab 11.6 as an experimental feature. | ||||||
| 
 | 
 | ||||||
| WARNING: | WARNING: | ||||||
| Smartcard authentication against local databases may change or be removed completely in future | Smart card authentication against local databases may change or be removed completely in future releases. | ||||||
| releases. |  | ||||||
| 
 | 
 | ||||||
| Smartcards with X.509 certificates can be used to authenticate with GitLab. | Smart cards with X.509 certificates can be used to authenticate with GitLab. | ||||||
| 
 | 
 | ||||||
| To use a smartcard with an X.509 certificate to authenticate against a local | To use a smart card with an X.509 certificate to authenticate against a local | ||||||
| database with GitLab, `CN` and `emailAddress` must be defined in the | database with GitLab, `CN` and `emailAddress` must be defined in the | ||||||
| certificate. For example: | certificate. For example: | ||||||
| 
 | 
 | ||||||
|  | @ -60,14 +59,14 @@ Certificate: | ||||||
| 
 | 
 | ||||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8605) in GitLab 12.3. | > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8605) in GitLab 12.3. | ||||||
| 
 | 
 | ||||||
| Smartcards with X.509 certificates using SAN extensions can be used to authenticate | Smart cards with X.509 certificates using SAN extensions can be used to authenticate | ||||||
| with GitLab. | with GitLab. | ||||||
| 
 | 
 | ||||||
| NOTE: | NOTE: | ||||||
| This is an experimental feature. Smartcard authentication against local databases may | This is an experimental feature. Smart card authentication against local databases may | ||||||
| change or be removed completely in future releases. | change or be removed completely in future releases. | ||||||
| 
 | 
 | ||||||
| To use a smartcard with an X.509 certificate to authenticate against a local | To use a smart card with an X.509 certificate to authenticate against a local | ||||||
| database with GitLab, in: | database with GitLab, in: | ||||||
| 
 | 
 | ||||||
| - GitLab 12.4 and later, at least one of the `subjectAltName` (SAN) extensions | - GitLab 12.4 and later, at least one of the `subjectAltName` (SAN) extensions | ||||||
|  | @ -101,7 +100,7 @@ Certificate: | ||||||
| 
 | 
 | ||||||
| ### Authentication against an LDAP server | ### Authentication against an LDAP server | ||||||
| 
 | 
 | ||||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7693) in GitLab 11.8 as an experimental feature. Smartcard authentication against an LDAP server may change or be removed completely in the future. | > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7693) in GitLab 11.8 as an experimental feature. Smart card authentication against an LDAP server may change or be removed completely in the future. | ||||||
| 
 | 
 | ||||||
| GitLab implements a standard way of certificate matching following | GitLab implements a standard way of certificate matching following | ||||||
| [RFC4523](https://www.rfc-editor.org/rfc/rfc4523). It uses the | [RFC4523](https://www.rfc-editor.org/rfc/rfc4523). It uses the | ||||||
|  | @ -116,14 +115,14 @@ Active Directory doesn't support the `certificateExactMatch` matching rule so | ||||||
| [it is not supported at this time](https://gitlab.com/gitlab-org/gitlab/-/issues/327491). For | [it is not supported at this time](https://gitlab.com/gitlab-org/gitlab/-/issues/327491). For | ||||||
| more information, see [the relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/328074). | more information, see [the relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/328074). | ||||||
| 
 | 
 | ||||||
| ## Configure GitLab for smartcard authentication | ## Configure GitLab for smart card authentication | ||||||
| 
 | 
 | ||||||
| For Linux package installations: | For Linux package installations: | ||||||
| 
 | 
 | ||||||
| 1. Edit `/etc/gitlab/gitlab.rb`: | 1. Edit `/etc/gitlab/gitlab.rb`: | ||||||
| 
 | 
 | ||||||
|    ```ruby |    ```ruby | ||||||
|    # Allow smartcard authentication |    # Allow smart card authentication | ||||||
|    gitlab_rails['smartcard_enabled'] = true |    gitlab_rails['smartcard_enabled'] = true | ||||||
| 
 | 
 | ||||||
|    # Path to a file containing a CA certificate |    # Path to a file containing a CA certificate | ||||||
|  | @ -215,9 +214,9 @@ For self-compiled installations: | ||||||
| 1. Edit `config/gitlab.yml`: | 1. Edit `config/gitlab.yml`: | ||||||
| 
 | 
 | ||||||
|    ```yaml |    ```yaml | ||||||
|    ## Smartcard authentication settings |    ## Smart card authentication settings | ||||||
|    smartcard: |    smartcard: | ||||||
|      # Allow smartcard authentication |      # Allow smart card authentication | ||||||
|      enabled: true |      enabled: true | ||||||
| 
 | 
 | ||||||
|      # Path to a file containing a CA certificate |      # Path to a file containing a CA certificate | ||||||
|  | @ -251,7 +250,7 @@ For Linux package installations: | ||||||
| 
 | 
 | ||||||
| For self-compiled installations: | For self-compiled installations: | ||||||
| 
 | 
 | ||||||
| 1. Add the `san_extensions` line to `config/gitlab.yml` within the smartcard section: | 1. Add the `san_extensions` line to `config/gitlab.yml` within the smart card section: | ||||||
| 
 | 
 | ||||||
|    ```yaml |    ```yaml | ||||||
|    smartcard: |    smartcard: | ||||||
|  | @ -276,7 +275,7 @@ For Linux package installations: | ||||||
|    gitlab_rails['ldap_servers'] = YAML.load <<-EOS |    gitlab_rails['ldap_servers'] = YAML.load <<-EOS | ||||||
|    main: |    main: | ||||||
|      # snip... |      # snip... | ||||||
|      # Enable smartcard authentication against the LDAP server. Valid values |      # Enable smart card authentication against the LDAP server. Valid values | ||||||
|      # are "false", "optional", and "required". |      # are "false", "optional", and "required". | ||||||
|      smartcard_auth: optional |      smartcard_auth: optional | ||||||
|    EOS |    EOS | ||||||
|  | @ -295,7 +294,7 @@ For self-compiled installations: | ||||||
|        servers: |        servers: | ||||||
|          main: |          main: | ||||||
|            # snip... |            # snip... | ||||||
|            # Enable smartcard authentication against the LDAP server. Valid values |            # Enable smart card authentication against the LDAP server. Valid values | ||||||
|            # are "false", "optional", and "required". |            # are "false", "optional", and "required". | ||||||
|            smartcard_auth: optional |            smartcard_auth: optional | ||||||
|    ``` |    ``` | ||||||
|  | @ -303,7 +302,7 @@ For self-compiled installations: | ||||||
| 1. Save the file and [restart](../restart_gitlab.md#self-compiled-installations) | 1. Save the file and [restart](../restart_gitlab.md#self-compiled-installations) | ||||||
|    GitLab for the changes to take effect. |    GitLab for the changes to take effect. | ||||||
| 
 | 
 | ||||||
| ### Require browser session with smartcard sign-in for Git access | ### Require browser session with smart card sign-in for Git access | ||||||
| 
 | 
 | ||||||
| For Linux package installations: | For Linux package installations: | ||||||
| 
 | 
 | ||||||
|  | @ -321,19 +320,19 @@ For self-compiled installations: | ||||||
| 1. Edit `config/gitlab.yml`: | 1. Edit `config/gitlab.yml`: | ||||||
| 
 | 
 | ||||||
|    ```yaml |    ```yaml | ||||||
|    ## Smartcard authentication settings |    ## Smart card authentication settings | ||||||
|    smartcard: |    smartcard: | ||||||
|      # snip... |      # snip... | ||||||
|      # Browser session with smartcard sign-in is required for Git access |      # Browser session with smart card sign-in is required for Git access | ||||||
|      required_for_git_access: true |      required_for_git_access: true | ||||||
|    ``` |    ``` | ||||||
| 
 | 
 | ||||||
| 1. Save the file and [restart](../restart_gitlab.md#self-compiled-installations) | 1. Save the file and [restart](../restart_gitlab.md#self-compiled-installations) | ||||||
|    GitLab for the changes to take effect. |    GitLab for the changes to take effect. | ||||||
| 
 | 
 | ||||||
| ## Passwords for users created via smartcard authentication | ## Passwords for users created via smart card authentication | ||||||
| 
 | 
 | ||||||
| The [Generated passwords for users created through integrated authentication](../../security/passwords_for_integrated_authentication_methods.md) guide provides an overview of how GitLab generates and sets passwords for users created via smartcard authentication. | The [Generated passwords for users created through integrated authentication](../../security/passwords_for_integrated_authentication_methods.md) guide provides an overview of how GitLab generates and sets passwords for users created via smart card authentication. | ||||||
| 
 | 
 | ||||||
| <!-- ## Troubleshooting | <!-- ## Troubleshooting | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -774,7 +774,7 @@ DRIs: | ||||||
| | Leadership                   | Mark Nuzzo             | | | Leadership                   | Mark Nuzzo             | | ||||||
| | Product                      | Dov Hershkovitch       | | | Product                      | Dov Hershkovitch       | | ||||||
| | Engineering                  | Fabio Pitino           | | | Engineering                  | Fabio Pitino           | | ||||||
| | UX                           | Kevin Comoli (interim), Sunjung Park          | | | UX                           | Sunjung Park           | | ||||||
| 
 | 
 | ||||||
| Domain experts: | Domain experts: | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -103,6 +103,9 @@ To find a domain expert: | ||||||
| NOTE: | NOTE: | ||||||
| Reviewer roulette is an internal tool for use on GitLab.com, and not available for use on customer installations. | Reviewer roulette is an internal tool for use on GitLab.com, and not available for use on customer installations. | ||||||
| 
 | 
 | ||||||
|  | NOTE: | ||||||
|  | Until %16.11, GitLab is running [an experiment](https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/issues/377) to remove hungriness and busy indicators. | ||||||
|  | 
 | ||||||
| The [Danger bot](dangerbot.md) randomly picks a reviewer and a maintainer for | The [Danger bot](dangerbot.md) randomly picks a reviewer and a maintainer for | ||||||
| each area of the codebase that your merge request seems to touch. It makes | each area of the codebase that your merge request seems to touch. It makes | ||||||
| **recommendations** for developer reviewers and you should override it if you think someone else is a better | **recommendations** for developer reviewers and you should override it if you think someone else is a better | ||||||
|  | @ -140,7 +143,7 @@ page, with these behaviors: | ||||||
|   not counted. These MRs are usually backports, and maintainers or reviewers usually |   not counted. These MRs are usually backports, and maintainers or reviewers usually | ||||||
|   do not need much time reviewing them. |   do not need much time reviewing them. | ||||||
| 
 | 
 | ||||||
| - Team members whose Slack or [GitLab status](../user/profile/index.md#set-your-current-status) emoji | - 'Hungriness' for reviews: Team members whose Slack or [GitLab status](../user/profile/index.md#set-your-current-status) emoji | ||||||
|   is 🔵 `:large_blue_circle:` are more likely to be picked. This applies to both reviewers and trainee maintainers. |   is 🔵 `:large_blue_circle:` are more likely to be picked. This applies to both reviewers and trainee maintainers. | ||||||
|   - Reviewers with 🔵 `:large_blue_circle:` are two times as likely to be picked as other reviewers. |   - Reviewers with 🔵 `:large_blue_circle:` are two times as likely to be picked as other reviewers. | ||||||
|   - [Trainee maintainers](https://handbook.gitlab.com/handbook/engineering/workflow/code-review/#trainee-maintainer) with 🔵 `:large_blue_circle:` are three times as likely to be picked as other reviewers. |   - [Trainee maintainers](https://handbook.gitlab.com/handbook/engineering/workflow/code-review/#trainee-maintainer) with 🔵 `:large_blue_circle:` are three times as likely to be picked as other reviewers. | ||||||
|  |  | ||||||
|  | @ -148,10 +148,12 @@ class Ci::PipelineCreatedEvent < Gitlab::EventStore::Event | ||||||
| end | end | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| The schema is validated immediately when we initialize the event object so we can ensure that | The schema, which must be a valid [JSON schema](https://json-schema.org/specification), is validated | ||||||
| publishers follow the contract with the subscribers. | by the [`JSONSchemer`](https://github.com/davishmcclurg/json_schemer) gem. The validation happens | ||||||
|  | immediately when you initialize the event object to ensure that publishers follow the contract | ||||||
|  | with the subscribers. | ||||||
| 
 | 
 | ||||||
| We recommend using optional properties as much as possible, which require fewer rollouts for schema changes. | You should use optional properties as much as possible, which require fewer rollouts for schema changes. | ||||||
| However, `required` properties could be used for unique identifiers of the event's subject. For example: | However, `required` properties could be used for unique identifiers of the event's subject. For example: | ||||||
| 
 | 
 | ||||||
| - `pipeline_id` can be a required property for a `Ci::PipelineCreatedEvent`. | - `pipeline_id` can be a required property for a `Ci::PipelineCreatedEvent`. | ||||||
|  | @ -375,6 +377,21 @@ it 'publishes a ProjectCreatedEvent with project id and namespace id' do | ||||||
| end | end | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
|  | When you publish multiple events, you can also check for non-published events. | ||||||
|  | 
 | ||||||
|  | ```ruby | ||||||
|  | it 'publishes a ProjectCreatedEvent with project id and namespace id' do | ||||||
|  |   # The project ID is generated when `create_project` | ||||||
|  |   # is called in the `expect` block. | ||||||
|  |   expected_data = { project_id: kind_of(Numeric), namespace_id: group_id } | ||||||
|  | 
 | ||||||
|  |   expect { create_project(user, name: 'Project', path: 'project', namespace_id: group_id) } | ||||||
|  |     .to publish_event(Projects::ProjectCreatedEvent) | ||||||
|  |     .with(expected_data) | ||||||
|  |     .and not_publish_event(Projects::ProjectDeletedEvent) | ||||||
|  | end | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
| ### Testing the subscriber | ### Testing the subscriber | ||||||
| 
 | 
 | ||||||
| The subscriber must ensure that a published event can be consumed correctly. For this purpose | The subscriber must ensure that a published event can be consumed correctly. For this purpose | ||||||
|  |  | ||||||
|  | @ -141,7 +141,7 @@ To help you migrate your data to GitLab Dedicated, you can choose from the follo | ||||||
| 
 | 
 | ||||||
| The following GitLab application features are not available: | The following GitLab application features are not available: | ||||||
| 
 | 
 | ||||||
| - LDAP, Smartcard, or Kerberos authentication | - LDAP, smart card, or Kerberos authentication | ||||||
| - Multiple login providers | - Multiple login providers | ||||||
| - GitLab Pages | - GitLab Pages | ||||||
| - FortiAuthenticator, or FortiToken 2FA | - FortiAuthenticator, or FortiToken 2FA | ||||||
|  |  | ||||||
|  | @ -578,6 +578,34 @@ These fields (`architectureName`, `ipAddress`, `platformName`, `revision`, `vers | ||||||
| 
 | 
 | ||||||
| <div class="deprecation breaking-change" data-milestone="17.0"> | <div class="deprecation breaking-change" data-milestone="17.0"> | ||||||
| 
 | 
 | ||||||
|  | ### Deprecate `fmt` job in Terraform Module CI/CD template | ||||||
|  | 
 | ||||||
|  | <div class="deprecation-notes"> | ||||||
|  | - Announced in GitLab <span class="milestone">16.9</span> | ||||||
|  | - Removal in GitLab <span class="milestone">17.0</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change)) | ||||||
|  | - To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/440249). | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | The `fmt` job in the Terraform Module CI/CD templates is deprecated and will be removed in GitLab 17.0. | ||||||
|  | This affects the following templates: | ||||||
|  | 
 | ||||||
|  | - `Terraform-Module.gitlab-ci.yml` | ||||||
|  | - `Terraform/Module-Base.gitlab-ci.yml` | ||||||
|  | 
 | ||||||
|  | You can manually add back a Terraform `fmt` job to your pipeline using: | ||||||
|  | 
 | ||||||
|  | ```yaml | ||||||
|  | fmt: | ||||||
|  |   image: hashicorp/terraform | ||||||
|  |   script: terraform fmt -chdir "$TF_ROOT" -check -diff -recursive | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | You can also use the `fmt` template from the [OpenTofu CI/CD component](https://gitlab.com/components/opentofu). | ||||||
|  | 
 | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <div class="deprecation breaking-change" data-milestone="17.0"> | ||||||
|  | 
 | ||||||
| ### Deprecate `message` field from Vulnerability Management features | ### Deprecate `message` field from Vulnerability Management features | ||||||
| 
 | 
 | ||||||
| <div class="deprecation-notes"> | <div class="deprecation-notes"> | ||||||
|  |  | ||||||
|  | @ -28,7 +28,7 @@ SAML Group Sync only manages a group if that group has one or more SAML group li | ||||||
| 
 | 
 | ||||||
| You must configure the SAML group links before you configure SAML Group Sync. | You must configure the SAML group links before you configure SAML Group Sync. | ||||||
| 
 | 
 | ||||||
| When SAML is enabled, users with the Maintainer or Owner role see a new menu | When SAML is enabled, users with the Owner role see a new menu | ||||||
| item in group **Settings > SAML Group Links**. | item in group **Settings > SAML Group Links**. | ||||||
| 
 | 
 | ||||||
| - You can configure one or more **SAML Group Links** to map a SAML identity | - You can configure one or more **SAML Group Links** to map a SAML identity | ||||||
|  |  | ||||||
|  | @ -438,7 +438,7 @@ DETAILS: | ||||||
| > - Moved to GitLab Premium in 13.9. | > - Moved to GitLab Premium in 13.9. | ||||||
| 
 | 
 | ||||||
| You can set a work in progress (WIP) limit for each issue list on an issue board. When a limit is | You can set a work in progress (WIP) limit for each issue list on an issue board. When a limit is | ||||||
| set, the list's header shows the number of issues in the list and the soft limit of issues. | set, the list's header shows the number of issues in the list and the soft limit of issues. A line in the list separates items within the limit from those in excess of the limit. | ||||||
| You cannot set a WIP limit on the default lists (**Open** and **Closed**). | You cannot set a WIP limit on the default lists (**Open** and **Closed**). | ||||||
| 
 | 
 | ||||||
| Examples: | Examples: | ||||||
|  | @ -446,7 +446,7 @@ Examples: | ||||||
| - When you have a list with four issues and a limit of five, the header shows **4/5**. | - When you have a list with four issues and a limit of five, the header shows **4/5**. | ||||||
|   If you exceed the limit, the current number of issues is shown in red. |   If you exceed the limit, the current number of issues is shown in red. | ||||||
| - You have a list with five issues with a limit of five. When you move another issue to that list, | - You have a list with five issues with a limit of five. When you move another issue to that list, | ||||||
|   the list's header displays **6/5**, with the six shown in red. |   the list's header displays **6/5**, with the six shown in red. The work in progress line is shown before the sixth issue. | ||||||
| 
 | 
 | ||||||
| Prerequisites: | Prerequisites: | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -11,25 +11,19 @@ DETAILS: | ||||||
| **Offering:** SaaS, self-managed | **Offering:** SaaS, self-managed | ||||||
| 
 | 
 | ||||||
| > - **Merge when pipeline succeeds** and **Add to merge train when pipeline succeeds** [renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/409530) to **Auto-merge** in GitLab 16.0 [with a flag](../../../administration/feature_flags.md) named `auto_merge_labels_mr_widget`. Enabled by default. | > - **Merge when pipeline succeeds** and **Add to merge train when pipeline succeeds** [renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/409530) to **Auto-merge** in GitLab 16.0 [with a flag](../../../administration/feature_flags.md) named `auto_merge_labels_mr_widget`. Enabled by default. | ||||||
|  | > - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120922) in GitLab 16.0. Feature flag `auto_merge_labels_mr_widget` removed. | ||||||
| 
 | 
 | ||||||
| If you review a merge request and it's ready to merge, but the pipeline hasn't | If the content of a merge request is ready to merge, use **Set to auto-merge** on | ||||||
| completed yet, you can set it to auto-merge. You don't | the merge request. You don't have to remember later to merge the work manually. If set, | ||||||
| have to remember later to merge the work manually: | a merge request auto-merges when all these conditions are met: | ||||||
|  | 
 | ||||||
|  | - The merge request pipeline must complete successfully. | ||||||
|  | - All required approvals must be given. | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
| 
 | 
 | ||||||
| NOTE: | The [merge when checks pass](#merge-when-checks-pass) feature, available in | ||||||
| [In GitLab 16.0 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/359057), **Merge when pipeline succeeds** and **Add to merge train when pipeline succeeds** are renamed **Set to auto-merge**. | GitLab 16.9 and later, adds more checks to the auto-merge process. | ||||||
| 
 |  | ||||||
| If the pipeline succeeds, the merge request is merged. If the pipeline fails, the |  | ||||||
| author can either retry any failed jobs, or push new commits to fix the failure: |  | ||||||
| 
 |  | ||||||
| - If a retried job succeeds on the second try, the merge request is merged. |  | ||||||
| - If new commits are added to the merge request, GitLab cancels the request |  | ||||||
|   to ensure the new changes are reviewed before merge. |  | ||||||
| - If new commits are added to the target branch of the merge request and |  | ||||||
|   fast-forward only merge request is configured, GitLab cancels the request |  | ||||||
|   to prevent merge conflicts. |  | ||||||
| 
 | 
 | ||||||
| ## Auto-merge a merge request | ## Auto-merge a merge request | ||||||
| 
 | 
 | ||||||
|  | @ -57,6 +51,42 @@ If a new comment is added to the merge request after you select **Auto-merge**, | ||||||
| but before the pipeline completes, GitLab blocks the merge until you | but before the pipeline completes, GitLab blocks the merge until you | ||||||
| resolve all existing threads. | resolve all existing threads. | ||||||
| 
 | 
 | ||||||
|  | ### Merge when pipeline succeeds | ||||||
|  | 
 | ||||||
|  | If the pipeline succeeds, the merge request is merged. If the pipeline fails, the | ||||||
|  | author can either retry any failed jobs, or push new commits to fix the failure: | ||||||
|  | 
 | ||||||
|  | - If a retried job succeeds on the second try, the merge request is merged. | ||||||
|  | - If new commits are added to the merge request, GitLab cancels the request | ||||||
|  |   to ensure the new changes are reviewed before merge. | ||||||
|  | - If new commits are added to the target branch of the merge request and | ||||||
|  |   fast-forward only merge request is configured, GitLab cancels the request | ||||||
|  |   to prevent merge conflicts. | ||||||
|  | 
 | ||||||
|  | ### Merge when checks pass | ||||||
|  | 
 | ||||||
|  | DETAILS: | ||||||
|  | **Tier:** Free, Premium, Ultimate | ||||||
|  | **Offering:** SaaS | ||||||
|  | 
 | ||||||
|  | > - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10874) in GitLab 16.5 [with two flags](../../../administration/feature_flags.md) named `merge_when_checks_pass` and `additional_merge_when_checks_ready`. Disabled by default. | ||||||
|  | > - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/412995) in GitLab 16.9. | ||||||
|  | 
 | ||||||
|  | FLAG: | ||||||
|  | On self-managed GitLab, by default this feature is not available. To enable the feature, | ||||||
|  | an administrator can [enable the feature flags](../../../administration/feature_flags.md) | ||||||
|  | named `merge_when_checks_pass` and `additional_merge_when_checks_ready`. | ||||||
|  | On GitLab.com, this feature is available. | ||||||
|  | 
 | ||||||
|  | In GitLab 16.9 and later, **Merge when checks pass** adds more checks to the auto-merge | ||||||
|  | process. When set to auto-merge, all of these checks must pass for a merge request to merge: | ||||||
|  | 
 | ||||||
|  | - The merge request pipeline must complete successfully. | ||||||
|  | - All required approvals must be given. | ||||||
|  | - The merge request must not be a **Draft**. | ||||||
|  | - All discussions must be resolved. | ||||||
|  | - All blocking merge requests must be merged or closed. | ||||||
|  | 
 | ||||||
| ## Cancel an auto-merge | ## Cancel an auto-merge | ||||||
| 
 | 
 | ||||||
| If a merge request is set to auto-merge, you can cancel it. | If a merge request is set to auto-merge, you can cancel it. | ||||||
|  | @ -110,8 +140,6 @@ despite a newer but failed branch pipeline. | ||||||
| 
 | 
 | ||||||
| ### Allow merge after skipped pipelines | ### Allow merge after skipped pipelines | ||||||
| 
 | 
 | ||||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211482) in GitLab 13.1. |  | ||||||
| 
 |  | ||||||
| When the **Pipelines must succeed** checkbox is checked, | When the **Pipelines must succeed** checkbox is checked, | ||||||
| [skipped pipelines](../../../ci/pipelines/index.md#skip-a-pipeline) prevent | [skipped pipelines](../../../ci/pipelines/index.md#skip-a-pipeline) prevent | ||||||
| merge requests from being merged. | merge requests from being merged. | ||||||
|  |  | ||||||
|  | @ -30,20 +30,8 @@ module API | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     SLASH_COMMAND_INTEGRATIONS = { |     SLASH_COMMAND_INTEGRATIONS = { | ||||||
|       'mattermost-slash-commands' => [ |       'mattermost-slash-commands' => ::Integrations::MattermostSlashCommands.api_fields, | ||||||
|         { |       'slack-slash-commands' => ::Integrations::SlackSlashCommands.api_fields | ||||||
|           name: :token, |  | ||||||
|           type: String, |  | ||||||
|           desc: 'The Mattermost token' |  | ||||||
|         } |  | ||||||
|       ], |  | ||||||
|       'slack-slash-commands' => [ |  | ||||||
|         { |  | ||||||
|           name: :token, |  | ||||||
|           type: String, |  | ||||||
|           desc: 'The Slack token' |  | ||||||
|         } |  | ||||||
|       ] |  | ||||||
|     }.freeze |     }.freeze | ||||||
| 
 | 
 | ||||||
|     helpers do |     helpers do | ||||||
|  |  | ||||||
|  | @ -26,7 +26,7 @@ module Gitlab | ||||||
| 
 | 
 | ||||||
|       def reserved_claims |       def reserved_claims | ||||||
|         super.merge({ |         super.merge({ | ||||||
|           iss: Feature.enabled?(:oidc_issuer_url) ? Gitlab.config.gitlab.url : Settings.gitlab.base_url, |           iss: Gitlab.config.gitlab.url, | ||||||
|           sub: "project_path:#{project.full_path}:ref_type:#{ref_type}:ref:#{source_ref}", |           sub: "project_path:#{project.full_path}:ref_type:#{ref_type}:ref:#{source_ref}", | ||||||
|           aud: aud, |           aud: aud, | ||||||
|           wlif: wlif |           wlif: wlif | ||||||
|  |  | ||||||
|  | @ -49471,6 +49471,9 @@ msgstr "" | ||||||
| msgid "The Slack notifications integration is deprecated and will be removed in a future release. To continue to receive notifications from Slack, use the GitLab for Slack app instead. %{learn_more_link_start}Learn more%{link_end}." | msgid "The Slack notifications integration is deprecated and will be removed in a future release. To continue to receive notifications from Slack, use the GitLab for Slack app instead. %{learn_more_link_start}Learn more%{link_end}." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "The Slack token." | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "The Snowplow cookie domain." | msgid "The Snowplow cookie domain." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -55944,6 +55947,9 @@ msgstr "" | ||||||
| msgid "Work in progress limit" | msgid "Work in progress limit" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "Work in progress limit: %{wipLimit}" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "Work item parent removed successfully" | msgid "Work item parent removed successfully" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ import waitForPromises from 'helpers/wait_for_promises'; | ||||||
| import createComponent from 'jest/boards/board_list_helper'; | import createComponent from 'jest/boards/board_list_helper'; | ||||||
| import { ESC_KEY_CODE } from '~/lib/utils/keycodes'; | import { ESC_KEY_CODE } from '~/lib/utils/keycodes'; | ||||||
| import BoardCard from '~/boards/components/board_card.vue'; | import BoardCard from '~/boards/components/board_card.vue'; | ||||||
|  | import BoardCutLine from '~/boards/components/board_cut_line.vue'; | ||||||
| import eventHub from '~/boards/eventhub'; | import eventHub from '~/boards/eventhub'; | ||||||
| import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue'; | import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue'; | ||||||
| import listIssuesQuery from '~/boards/graphql/lists_issues.query.graphql'; | import listIssuesQuery from '~/boards/graphql/lists_issues.query.graphql'; | ||||||
|  | @ -22,6 +23,8 @@ describe('Board list component', () => { | ||||||
|   const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver); |   const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver); | ||||||
|   const findBoardListCount = () => wrapper.find('.board-list-count'); |   const findBoardListCount = () => wrapper.find('.board-list-count'); | ||||||
| 
 | 
 | ||||||
|  |   const maxIssueCountWarningClass = '.gl-bg-red-50'; | ||||||
|  | 
 | ||||||
|   const triggerInfiniteScroll = () => findIntersectionObserver().vm.$emit('appear'); |   const triggerInfiniteScroll = () => findIntersectionObserver().vm.$emit('appear'); | ||||||
| 
 | 
 | ||||||
|   const startDrag = ( |   const startDrag = ( | ||||||
|  | @ -143,34 +146,48 @@ describe('Board list component', () => { | ||||||
| 
 | 
 | ||||||
|   describe('max issue count warning', () => { |   describe('max issue count warning', () => { | ||||||
|     describe('when issue count exceeds max issue count', () => { |     describe('when issue count exceeds max issue count', () => { | ||||||
|       it('sets background to gl-bg-red-100', async () => { |       beforeEach(async () => { | ||||||
|         wrapper = createComponent({ listProps: { issuesCount: 4, maxIssueCount: 3 } }); |         wrapper = createComponent({ listProps: { issuesCount: 4, maxIssueCount: 2 } }); | ||||||
| 
 |  | ||||||
|         await waitForPromises(); |         await waitForPromises(); | ||||||
|         const block = wrapper.find('.gl-bg-red-100'); |       }); | ||||||
|  |       it('sets background to warning color', () => { | ||||||
|  |         const block = wrapper.find(maxIssueCountWarningClass); | ||||||
| 
 | 
 | ||||||
|         expect(block.exists()).toBe(true); |         expect(block.exists()).toBe(true); | ||||||
|         expect(block.attributes('class')).toContain( |         expect(block.attributes('class')).toContain( | ||||||
|           'gl-rounded-bottom-left-base gl-rounded-bottom-right-base', |           'gl-rounded-bottom-left-base gl-rounded-bottom-right-base', | ||||||
|         ); |         ); | ||||||
|       }); |       }); | ||||||
|  |       it('shows cut line', () => { | ||||||
|  |         const cutline = wrapper.findComponent(BoardCutLine); | ||||||
|  |         expect(cutline.exists()).toBe(true); | ||||||
|  |         expect(cutline.props('cutLineText')).toEqual('Work in progress limit: 2'); | ||||||
|  |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     describe('when list issue count does NOT exceed list max issue count', () => { |     describe('when list issue count does NOT exceed list max issue count', () => { | ||||||
|       it('does not sets background to gl-bg-red-100', async () => { |       beforeEach(async () => { | ||||||
|         wrapper = createComponent({ list: { issuesCount: 2, maxIssueCount: 3 } }); |         wrapper = createComponent({ list: { issuesCount: 2, maxIssueCount: 3 } }); | ||||||
|         await waitForPromises(); |         await waitForPromises(); | ||||||
| 
 |       }); | ||||||
|         expect(wrapper.find('.gl-bg-red-100').exists()).toBe(false); |       it('does not sets background to warning color', () => { | ||||||
|  |         expect(wrapper.find(maxIssueCountWarningClass).exists()).toBe(false); | ||||||
|  |       }); | ||||||
|  |       it('does not show cut line', () => { | ||||||
|  |         expect(wrapper.findComponent(BoardCutLine).exists()).toBe(false); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     describe('when list max issue count is 0', () => { |     describe('when list max issue count is 0', () => { | ||||||
|       it('does not sets background to gl-bg-red-100', async () => { |       beforeEach(async () => { | ||||||
|         wrapper = createComponent({ list: { maxIssueCount: 0 } }); |         wrapper = createComponent({ list: { maxIssueCount: 0 } }); | ||||||
|         await waitForPromises(); |         await waitForPromises(); | ||||||
| 
 |       }); | ||||||
|         expect(wrapper.find('.gl-bg-red-100').exists()).toBe(false); |       it('does not sets background to warning color', () => { | ||||||
|  |         expect(wrapper.find(maxIssueCountWarningClass).exists()).toBe(false); | ||||||
|  |       }); | ||||||
|  |       it('does not show cut line', () => { | ||||||
|  |         expect(wrapper.findComponent(BoardCutLine).exists()).toBe(false); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  | @ -0,0 +1,27 @@ | ||||||
|  | import { shallowMount } from '@vue/test-utils'; | ||||||
|  | import BoardCutLine from '~/boards/components/board_cut_line.vue'; | ||||||
|  | 
 | ||||||
|  | describe('BoardCutLine', () => { | ||||||
|  |   let wrapper; | ||||||
|  |   const cutLineText = 'Work in progress limit: 3'; | ||||||
|  | 
 | ||||||
|  |   const createComponent = (props) => { | ||||||
|  |     wrapper = shallowMount(BoardCutLine, { propsData: props }); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   describe('when cut line is shown', () => { | ||||||
|  |     beforeEach(() => { | ||||||
|  |       createComponent({ cutLineText }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('contains cut line text in the template', () => { | ||||||
|  |       expect(wrapper.find('[data-testid="cut-line-text"]').text()).toContain( | ||||||
|  |         `Work in progress limit: 3`, | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('does not contain other text in the template', () => { | ||||||
|  |       expect(wrapper.find('[data-testid="cut-line-text"]').text()).not.toContain(`unexpected`); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | @ -2,19 +2,17 @@ import { shallowMount } from '@vue/test-utils'; | ||||||
| import IssueCount from '~/boards/components/item_count.vue'; | import IssueCount from '~/boards/components/item_count.vue'; | ||||||
| 
 | 
 | ||||||
| describe('IssueCount', () => { | describe('IssueCount', () => { | ||||||
|   let vm; |   let wrapper; | ||||||
|   let maxIssueCount; |   let maxIssueCount; | ||||||
|   let itemsSize; |   let itemsSize; | ||||||
| 
 | 
 | ||||||
|   const createComponent = (props) => { |   const createComponent = (props) => { | ||||||
|     vm = shallowMount(IssueCount, { propsData: props }); |     wrapper = shallowMount(IssueCount, { propsData: props }); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   afterEach(() => { |   afterEach(() => { | ||||||
|     maxIssueCount = 0; |     maxIssueCount = 0; | ||||||
|     itemsSize = 0; |     itemsSize = 0; | ||||||
| 
 |  | ||||||
|     if (vm) vm.destroy(); |  | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('when maxIssueCount is zero', () => { |   describe('when maxIssueCount is zero', () => { | ||||||
|  | @ -25,11 +23,11 @@ describe('IssueCount', () => { | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('contains issueSize in the template', () => { |     it('contains issueSize in the template', () => { | ||||||
|       expect(vm.find('[data-testid="board-items-count"]').text()).toEqual(String(itemsSize)); |       expect(wrapper.find('[data-testid="board-items-count"]').text()).toEqual(String(itemsSize)); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('does not contains maxIssueCount in the template', () => { |     it('does not contains maxIssueCount in the template', () => { | ||||||
|       expect(vm.find('.max-issue-size').exists()).toBe(false); |       expect(wrapper.find('.max-issue-size').exists()).toBe(false); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  | @ -42,15 +40,15 @@ describe('IssueCount', () => { | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('contains issueSize in the template', () => { |     it('contains issueSize in the template', () => { | ||||||
|       expect(vm.find('[data-testid="board-items-count"]').text()).toEqual(String(itemsSize)); |       expect(wrapper.find('[data-testid="board-items-count"]').text()).toEqual(String(itemsSize)); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('contains maxIssueCount in the template', () => { |     it('contains maxIssueCount in the template', () => { | ||||||
|       expect(vm.find('.max-issue-size').text()).toContain(String(maxIssueCount)); |       expect(wrapper.find('.max-issue-size').text()).toContain(String(maxIssueCount)); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('does not have text-danger class when issueSize is less than maxIssueCount', () => { |     it('does not have red text when issueSize is less than maxIssueCount', () => { | ||||||
|       expect(vm.classes('.text-danger')).toBe(false); |       expect(wrapper.classes('.gl-text-red-700')).toBe(false); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  | @ -63,15 +61,15 @@ describe('IssueCount', () => { | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('contains issueSize in the template', () => { |     it('contains issueSize in the template', () => { | ||||||
|       expect(vm.find('[data-testid="board-items-count"]').text()).toEqual(String(itemsSize)); |       expect(wrapper.find('[data-testid="board-items-count"]').text()).toEqual(String(itemsSize)); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('contains maxIssueCount in the template', () => { |     it('contains maxIssueCount in the template', () => { | ||||||
|       expect(vm.find('.max-issue-size').text()).toContain(String(maxIssueCount)); |       expect(wrapper.find('.max-issue-size').text()).toContain(String(maxIssueCount)); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('has text-danger class', () => { |     it('has red text', () => { | ||||||
|       expect(vm.find('.text-danger').text()).toEqual(String(itemsSize)); |       expect(wrapper.find('.gl-text-red-700').text()).toEqual(String(itemsSize)); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -2,7 +2,8 @@ import { GlAlert } from '@gitlab/ui'; | ||||||
| import { mount } from '@vue/test-utils'; | import { mount } from '@vue/test-utils'; | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import VueApollo from 'vue-apollo'; | import VueApollo from 'vue-apollo'; | ||||||
| import Autosave from '~/autosave'; | import markdownEditorEventHub from '~/vue_shared/components/markdown/eventhub'; | ||||||
|  | import { CLEAR_AUTOSAVE_ENTRY_EVENT } from '~/vue_shared/constants'; | ||||||
| import createMockApollo from 'helpers/mock_apollo_helper'; | import createMockApollo from 'helpers/mock_apollo_helper'; | ||||||
| import waitForPromises from 'helpers/wait_for_promises'; | import waitForPromises from 'helpers/wait_for_promises'; | ||||||
| import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; | import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; | ||||||
|  | @ -94,6 +95,12 @@ describe('Design reply form component', () => { | ||||||
|     expect(findTextarea().element).toEqual(document.activeElement); |     expect(findTextarea().element).toEqual(document.activeElement); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   it('allows switching to rich text', () => { | ||||||
|  |     createComponent(); | ||||||
|  | 
 | ||||||
|  |     expect(wrapper.text()).toContain('Switch to rich text editing'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|   it('renders "Attach a file or image" button in markdown toolbar', () => { |   it('renders "Attach a file or image" button in markdown toolbar', () => { | ||||||
|     createComponent(); |     createComponent(); | ||||||
| 
 | 
 | ||||||
|  | @ -118,23 +125,6 @@ describe('Design reply form component', () => { | ||||||
|     expect(findSubmitButton().html()).toMatchSnapshot(); |     expect(findSubmitButton().html()).toMatchSnapshot(); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   it.each` |  | ||||||
|     discussionId                         | shortDiscussionId |  | ||||||
|     ${undefined}                         | ${'new'} |  | ||||||
|     ${'gid://gitlab/DiffDiscussion/123'} | ${123} |  | ||||||
|   `(
 |  | ||||||
|     'initializes autosave support on discussion with proper key', |  | ||||||
|     ({ discussionId, shortDiscussionId }) => { |  | ||||||
|       createComponent({ props: { discussionId } }); |  | ||||||
| 
 |  | ||||||
|       expect(Autosave).toHaveBeenCalledWith(expect.any(Element), [ |  | ||||||
|         'Discussion', |  | ||||||
|         6, |  | ||||||
|         shortDiscussionId, |  | ||||||
|       ]); |  | ||||||
|     }, |  | ||||||
|   ); |  | ||||||
| 
 |  | ||||||
|   describe('when form has no text', () => { |   describe('when form has no text', () => { | ||||||
|     beforeEach(() => { |     beforeEach(() => { | ||||||
|       createComponent(); |       createComponent(); | ||||||
|  | @ -155,7 +145,7 @@ describe('Design reply form component', () => { | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('emits cancelForm event on pressing escape button on textarea', () => { |     it('emits cancelForm event on pressing escape button on textarea', () => { | ||||||
|       findTextarea().trigger('keyup.esc'); |       findTextarea().trigger('keydown.esc'); | ||||||
| 
 | 
 | ||||||
|       expect(wrapper.emitted('cancel-form')).toHaveLength(1); |       expect(wrapper.emitted('cancel-form')).toHaveLength(1); | ||||||
|     }); |     }); | ||||||
|  | @ -261,7 +251,7 @@ describe('Design reply form component', () => { | ||||||
|     it('emits cancelForm event on Escape key if text was not changed', () => { |     it('emits cancelForm event on Escape key if text was not changed', () => { | ||||||
|       createComponent(); |       createComponent(); | ||||||
| 
 | 
 | ||||||
|       findTextarea().trigger('keyup.esc'); |       findTextarea().trigger('keydown.esc'); | ||||||
| 
 | 
 | ||||||
|       expect(wrapper.emitted('cancel-form')).toHaveLength(1); |       expect(wrapper.emitted('cancel-form')).toHaveLength(1); | ||||||
|     }); |     }); | ||||||
|  | @ -271,7 +261,7 @@ describe('Design reply form component', () => { | ||||||
| 
 | 
 | ||||||
|       findTextarea().setValue(mockComment); |       findTextarea().setValue(mockComment); | ||||||
| 
 | 
 | ||||||
|       findTextarea().trigger('keyup.esc'); |       findTextarea().trigger('keydown.esc'); | ||||||
| 
 | 
 | ||||||
|       expect(confirmAction).toHaveBeenCalled(); |       expect(confirmAction).toHaveBeenCalled(); | ||||||
|     }); |     }); | ||||||
|  | @ -282,7 +272,7 @@ describe('Design reply form component', () => { | ||||||
|       createComponent({ props: { value: mockComment } }); |       createComponent({ props: { value: mockComment } }); | ||||||
|       findTextarea().setValue('Comment changed'); |       findTextarea().setValue('Comment changed'); | ||||||
| 
 | 
 | ||||||
|       findTextarea().trigger('keyup.esc'); |       findTextarea().trigger('keydown.esc'); | ||||||
| 
 | 
 | ||||||
|       expect(confirmAction).toHaveBeenCalled(); |       expect(confirmAction).toHaveBeenCalled(); | ||||||
| 
 | 
 | ||||||
|  | @ -296,7 +286,7 @@ describe('Design reply form component', () => { | ||||||
|       createComponent({ props: { value: mockComment } }); |       createComponent({ props: { value: mockComment } }); | ||||||
|       findTextarea().setValue('Comment changed'); |       findTextarea().setValue('Comment changed'); | ||||||
| 
 | 
 | ||||||
|       findTextarea().trigger('keyup.esc'); |       findTextarea().trigger('keydown.esc'); | ||||||
| 
 | 
 | ||||||
|       expect(confirmAction).toHaveBeenCalled(); |       expect(confirmAction).toHaveBeenCalled(); | ||||||
|       await waitForPromises(); |       await waitForPromises(); | ||||||
|  | @ -306,11 +296,12 @@ describe('Design reply form component', () => { | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('when component is destroyed', () => { |   describe('when component is destroyed', () => { | ||||||
|     it('calls autosave.reset', async () => { |     it('clears autosave entry', async () => { | ||||||
|       const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset'); |       const clearAutosaveSpy = jest.fn(); | ||||||
|  |       markdownEditorEventHub.$on(CLEAR_AUTOSAVE_ENTRY_EVENT, clearAutosaveSpy); | ||||||
|       createComponent(); |       createComponent(); | ||||||
|       await wrapper.destroy(); |       await wrapper.destroy(); | ||||||
|       expect(autosaveResetSpy).toHaveBeenCalled(); |       expect(clearAutosaveSpy).toHaveBeenCalled(); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -37,7 +37,6 @@ import { mockCreateImageNoteDiffResponse } from '../../mock_data/apollo_mock'; | ||||||
| jest.mock('~/alert'); | jest.mock('~/alert'); | ||||||
| jest.mock('~/api.js'); | jest.mock('~/api.js'); | ||||||
| 
 | 
 | ||||||
| const focusInput = jest.fn(); |  | ||||||
| const mockCacheObject = { | const mockCacheObject = { | ||||||
|   readQuery: jest.fn().mockReturnValue(mockProject), |   readQuery: jest.fn().mockReturnValue(mockProject), | ||||||
|   writeQuery: jest.fn(), |   writeQuery: jest.fn(), | ||||||
|  | @ -51,9 +50,6 @@ const mockPageLayoutElement = { | ||||||
| }; | }; | ||||||
| const DesignReplyForm = { | const DesignReplyForm = { | ||||||
|   template: '<div><textarea ref="textarea"></textarea></div>', |   template: '<div><textarea ref="textarea"></textarea></div>', | ||||||
|   methods: { |  | ||||||
|     focusInput, |  | ||||||
|   }, |  | ||||||
| }; | }; | ||||||
| const mockDesignNoDiscussions = { | const mockDesignNoDiscussions = { | ||||||
|   ...design, |   ...design, | ||||||
|  | @ -219,22 +215,6 @@ describe('Design management design index page', () => { | ||||||
|     expect(findDesignReplyForm().exists()).toBe(true); |     expect(findDesignReplyForm().exists()).toBe(true); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   it('keeps new discussion form focused', () => { |  | ||||||
|     createComponent( |  | ||||||
|       { loading: false }, |  | ||||||
|       { |  | ||||||
|         data: { |  | ||||||
|           design, |  | ||||||
|           annotationCoordinates, |  | ||||||
|         }, |  | ||||||
|       }, |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     findDesignPresentation().vm.$emit('openCommentForm', { x: 10, y: 10 }); |  | ||||||
| 
 |  | ||||||
|     expect(focusInput).toHaveBeenCalled(); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   it('sends a update and closes the form when mutation is completed', async () => { |   it('sends a update and closes the form when mutation is completed', async () => { | ||||||
|     createComponent( |     createComponent( | ||||||
|       { loading: false }, |       { loading: false }, | ||||||
|  |  | ||||||
|  | @ -16,7 +16,6 @@ import { putCreateReleaseNotification } from '~/releases/release_notification_se | ||||||
| import AssetLinksForm from '~/releases/components/asset_links_form.vue'; | import AssetLinksForm from '~/releases/components/asset_links_form.vue'; | ||||||
| import ConfirmDeleteModal from '~/releases/components/confirm_delete_modal.vue'; | import ConfirmDeleteModal from '~/releases/components/confirm_delete_modal.vue'; | ||||||
| import { BACK_URL_PARAM } from '~/releases/constants'; | import { BACK_URL_PARAM } from '~/releases/constants'; | ||||||
| import MarkdownField from '~/vue_shared/components/markdown/field.vue'; |  | ||||||
| import { ValidationResult } from '~/lib/utils/ref_validator'; | import { ValidationResult } from '~/lib/utils/ref_validator'; | ||||||
| 
 | 
 | ||||||
| const originalRelease = originalOneReleaseForEditingQueryResponse.data.project.release; | const originalRelease = originalOneReleaseForEditingQueryResponse.data.project.release; | ||||||
|  | @ -41,6 +40,7 @@ describe('Release edit/new component', () => { | ||||||
|       release, |       release, | ||||||
|       isExistingRelease: true, |       isExistingRelease: true, | ||||||
|       projectPath, |       projectPath, | ||||||
|  |       markdownPreviewPath: 'path/to/markdown/preview', | ||||||
|       markdownDocsPath: 'path/to/markdown/docs', |       markdownDocsPath: 'path/to/markdown/docs', | ||||||
|       releasesPagePath, |       releasesPagePath, | ||||||
|       projectId: '8', |       projectId: '8', | ||||||
|  | @ -54,6 +54,7 @@ describe('Release edit/new component', () => { | ||||||
|       saveRelease: jest.fn(), |       saveRelease: jest.fn(), | ||||||
|       addEmptyAssetLink: jest.fn(), |       addEmptyAssetLink: jest.fn(), | ||||||
|       deleteRelease: jest.fn(), |       deleteRelease: jest.fn(), | ||||||
|  |       updateReleaseNotes: jest.fn(), | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     getters = { |     getters = { | ||||||
|  | @ -173,15 +174,14 @@ describe('Release edit/new component', () => { | ||||||
|       expect(wrapper.find('#release-notes').element.value).toBe(release.description); |       expect(wrapper.find('#release-notes').element.value).toBe(release.description); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('sets the preview text to be the formatted release notes', () => { |  | ||||||
|       const notes = getters.formattedReleaseNotes(); |  | ||||||
|       expect(wrapper.findComponent(MarkdownField).props('textareaValue')).toBe(notes); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     it('renders the "Save changes" button as type="submit"', () => { |     it('renders the "Save changes" button as type="submit"', () => { | ||||||
|       expect(findSubmitButton().attributes('type')).toBe('submit'); |       expect(findSubmitButton().attributes('type')).toBe('submit'); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     it('allows switching to rich text editor', () => { | ||||||
|  |       expect(wrapper.html()).toContain('Switch to rich text editing'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     it('calls saveRelease when the form is submitted', () => { |     it('calls saveRelease when the form is submitted', () => { | ||||||
|       findForm().trigger('submit'); |       findForm().trigger('submit'); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -43,7 +43,7 @@ RSpec.describe NamespacesHelper, feature_category: :groups_and_projects do | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '#cascading_namespace_settings_popover_data' do |   describe '#cascading_namespace_settings_popover_data' do | ||||||
|     attribute = :delayed_project_removal |     attribute = :toggle_security_policy_custom_ci | ||||||
| 
 | 
 | ||||||
|     subject do |     subject do | ||||||
|       helper.cascading_namespace_settings_popover_data( |       helper.cascading_namespace_settings_popover_data( | ||||||
|  | @ -94,7 +94,7 @@ RSpec.describe NamespacesHelper, feature_category: :groups_and_projects do | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '#cascading_namespace_setting_locked?' do |   describe '#cascading_namespace_setting_locked?' do | ||||||
|     let(:attribute) { :delayed_project_removal } |     let(:attribute) { :toggle_security_policy_custom_ci } | ||||||
| 
 | 
 | ||||||
|     context 'when `group` argument is `nil`' do |     context 'when `group` argument is `nil`' do | ||||||
|       it 'returns `false`' do |       it 'returns `false`' do | ||||||
|  | @ -110,13 +110,13 @@ RSpec.describe NamespacesHelper, feature_category: :groups_and_projects do | ||||||
| 
 | 
 | ||||||
|     context 'when `*_locked?` method does exist' do |     context 'when `*_locked?` method does exist' do | ||||||
|       before do |       before do | ||||||
|         allow(admin_group.namespace_settings).to receive(:delayed_project_removal_locked?).and_return(true) |         allow(admin_group.namespace_settings).to receive(:toggle_security_policy_custom_ci_locked?).and_return(true) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'calls corresponding `*_locked?` method' do |       it 'calls corresponding `*_locked?` method' do | ||||||
|         helper.cascading_namespace_setting_locked?(attribute, admin_group, include_self: true) |         helper.cascading_namespace_setting_locked?(attribute, admin_group, include_self: true) | ||||||
| 
 | 
 | ||||||
|         expect(admin_group.namespace_settings).to have_received(:delayed_project_removal_locked?).with(include_self: true) |         expect(admin_group.namespace_settings).to have_received(:toggle_security_policy_custom_ci_locked?).with(include_self: true) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -0,0 +1,44 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require "spec_helper" | ||||||
|  | 
 | ||||||
|  | RSpec.describe API::Helpers::IntegrationsHelpers, feature_category: :integrations do | ||||||
|  |   let(:base_classes) { Integration::BASE_CLASSES.map(&:constantize) } | ||||||
|  |   let(:development_classes) { [Integrations::MockCi, Integrations::MockMonitoring] } | ||||||
|  |   let(:instance_level_classes) { [Integrations::BeyondIdentity] } | ||||||
|  | 
 | ||||||
|  |   describe '.chat_notification_flags' do | ||||||
|  |     it 'returns correct values' do | ||||||
|  |       expect(described_class.chat_notification_flags).to match_array( | ||||||
|  |         [ | ||||||
|  |           { | ||||||
|  |             required: false, | ||||||
|  |             name: :notify_only_broken_pipelines, | ||||||
|  |             type: ::Grape::API::Boolean, | ||||||
|  |             desc: 'Send notifications for broken pipelines' | ||||||
|  |           } | ||||||
|  |         ] | ||||||
|  |       ) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '.integrations' do | ||||||
|  |     it 'has correct integrations' do | ||||||
|  |       expect(described_class.integrations.keys.map(&:underscore)) | ||||||
|  |         .to match_array(described_class.integration_classes.map(&:to_param)) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '.integration_classes' do | ||||||
|  |     it 'returns correct integrations' do | ||||||
|  |       expect(described_class.integration_classes) | ||||||
|  |         .to match_array(Integration.descendants.without(base_classes, development_classes, instance_level_classes)) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '.development_integration_classes' do | ||||||
|  |     it 'returns correct integrations' do | ||||||
|  |       expect(described_class.development_integration_classes).to eq(development_classes) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -46,31 +46,11 @@ RSpec.describe Gitlab::Ci::JwtV2, feature_category: :secrets_management do | ||||||
|       expect(payload).not_to include(:user_identities) |       expect(payload).not_to include(:user_identities) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     context 'when oidc_issuer_url is disabled' do |     it 'has correct values for the standard JWT attributes' do | ||||||
|       before do |       aggregate_failures do | ||||||
|         stub_feature_flags(oidc_issuer_url: false) |         expect(payload[:iss]).to eq(Gitlab.config.gitlab.url) | ||||||
|       end |         expect(payload[:aud]).to eq(Settings.gitlab.base_url) | ||||||
| 
 |         expect(payload[:sub]).to eq("project_path:#{project.full_path}:ref_type:branch:ref:#{pipeline.source_ref}") | ||||||
|       it 'has correct values for the standard JWT attributes' do |  | ||||||
|         aggregate_failures do |  | ||||||
|           expect(payload[:iss]).to eq(Settings.gitlab.base_url) |  | ||||||
|           expect(payload[:aud]).to eq(Settings.gitlab.base_url) |  | ||||||
|           expect(payload[:sub]).to eq("project_path:#{project.full_path}:ref_type:branch:ref:#{pipeline.source_ref}") |  | ||||||
|         end |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     context 'when oidc_issuer_url is enabled' do |  | ||||||
|       before do |  | ||||||
|         stub_feature_flags(oidc_issuer_url: true) |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'has correct values for the standard JWT attributes' do |  | ||||||
|         aggregate_failures do |  | ||||||
|           expect(payload[:iss]).to eq(Gitlab.config.gitlab.url) |  | ||||||
|           expect(payload[:aud]).to eq(Settings.gitlab.base_url) |  | ||||||
|           expect(payload[:sub]).to eq("project_path:#{project.full_path}:ref_type:branch:ref:#{pipeline.source_ref}") |  | ||||||
|         end |  | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -448,8 +448,12 @@ RSpec.describe NamespaceSetting, feature_category: :groups_and_projects, type: : | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '#delayed_project_removal' do |   describe '#toggle_security_policy_custom_ci' do | ||||||
|     it_behaves_like 'a cascading namespace setting boolean attribute', settings_attribute_name: :delayed_project_removal |     it_behaves_like 'a cascading namespace setting boolean attribute', settings_attribute_name: :toggle_security_policy_custom_ci | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#toggle_security_policies_policy_scope' do | ||||||
|  |     it_behaves_like 'a cascading namespace setting boolean attribute', settings_attribute_name: :toggle_security_policies_policy_scope | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe 'default_branch_protection_defaults' do |   describe 'default_branch_protection_defaults' do | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue