Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-02-06 18:08:07 +00:00
parent edde77d99a
commit e7f151b0c0
37 changed files with 458 additions and 269 deletions

View File

@ -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

View File

@ -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>

View File

@ -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"

View File

@ -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"

View File

@ -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">

View File

@ -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

View File

@ -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;

View File

@ -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">

View File

@ -242,3 +242,12 @@
height: 100px; height: 100px;
} }
} }
.board-cut-line {
&::before, &::after {
content: '';
height: 1px;
flex: 1;
border-top: 1px dashed $red-700;
}
}

View File

@ -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.

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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).

View File

@ -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 |

View File

@ -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

View File

@ -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:

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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">

View File

@ -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

View File

@ -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:

View File

@ -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.
![Auto-merge is ready](img/auto_merge_ready_v16_0.png) ![Auto-merge is ready](img/auto_merge_ready_v16_0.png)
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.

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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);
}); });
}); });
}); });

View File

@ -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`);
});
});
});

View File

@ -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));
}); });
}); });
}); });

View File

@ -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();
}); });
}); });
}); });

View File

@ -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 },

View File

@ -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');

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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