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() {
|
||||||
|
if (this.isLoggedIn) {
|
||||||
|
return [
|
||||||
|
s__('DesignManagement|Discussion'),
|
||||||
|
getIdFromGraphQLId(this.noteableId),
|
||||||
|
this.shortDiscussionId,
|
||||||
|
].join('/');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
},
|
},
|
||||||
mounted() {
|
|
||||||
this.focusInput();
|
|
||||||
},
|
},
|
||||||
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"
|
|
||||||
v-model.trim="noteText"
|
|
||||||
class="note-textarea js-gfm-input js-autosize markdown-area"
|
|
||||||
dir="auto"
|
|
||||||
data-supports-quick-actions="false"
|
|
||||||
data-testid="note-textarea"
|
|
||||||
:aria-label="__('Description')"
|
|
||||||
:placeholder="__('Write a comment…')"
|
|
||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
@keydown.meta.enter="submitForm"
|
@keydown.meta.enter="submitForm"
|
||||||
@keydown.ctrl.enter="submitForm"
|
@keydown.ctrl.enter="submitForm"
|
||||||
@keyup.esc.stop="cancelComment"
|
@keydown.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"
|
|
||||||
:markdown-preview-path="markdownPreviewPath"
|
|
||||||
:markdown-docs-path="markdownDocsPath"
|
|
||||||
:add-spacing-classes="false"
|
|
||||||
:textarea-value="formattedReleaseNotes"
|
|
||||||
>
|
|
||||||
<template #textarea>
|
|
||||||
<textarea
|
|
||||||
id="release-notes"
|
|
||||||
v-model="releaseNotes"
|
v-model="releaseNotes"
|
||||||
class="note-textarea js-gfm-input js-autosize markdown-area"
|
:render-markdown-path="markdownPreviewPath"
|
||||||
dir="auto"
|
:markdown-docs-path="markdownDocsPath"
|
||||||
data-supports-quick-actions="false"
|
:supports-quick-actions="false"
|
||||||
:aria-label="__('Release notes')"
|
:form-field-props="formFieldProps"
|
||||||
: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,25 +46,6 @@ 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
|
|
||||||
before do
|
|
||||||
stub_feature_flags(oidc_issuer_url: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
it 'has correct values for the standard JWT attributes' do
|
||||||
aggregate_failures do
|
aggregate_failures do
|
||||||
expect(payload[:iss]).to eq(Gitlab.config.gitlab.url)
|
expect(payload[:iss]).to eq(Gitlab.config.gitlab.url)
|
||||||
|
|
@ -72,7 +53,6 @@ RSpec.describe Gitlab::Ci::JwtV2, feature_category: :secrets_management do
|
||||||
expect(payload[:sub]).to eq("project_path:#{project.full_path}:ref_type:branch:ref:#{pipeline.source_ref}")
|
expect(payload[:sub]).to eq("project_path:#{project.full_path}:ref_type:branch:ref:#{pipeline.source_ref}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
context 'when given an aud' do
|
context 'when given an aud' do
|
||||||
let(:aud) { 'AWS' }
|
let(:aud) { 'AWS' }
|
||||||
|
|
|
||||||
|
|
@ -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