Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-07-07 21:10:07 +00:00
parent 4d4c33870f
commit 90ffe7528b
98 changed files with 1017 additions and 928 deletions

View File

@ -18,7 +18,7 @@ variables:
# Helm chart ref used by test-on-cng pipeline
GITLAB_HELM_CHART_REF: "6cdb0e1cd4ceb7c9fd01ffa2f62c4a7a4c77a23b"
# Specific ref for cng-mirror project to trigger builds for
GITLAB_CNG_MIRROR_REF: "272b2069fc386348d2474eac26060e781f2a0dd2"
GITLAB_CNG_MIRROR_REF: "51637e0b7ab573aa0817945df517c7c0d0250d79"
# Makes sure some of the common scripts from pipeline-common use bundler to execute commands
RUN_WITH_BUNDLE: "true"
# Makes sure reporting script defined in .gitlab-qa-report from pipeline-common is executed from correct folder

View File

@ -1 +1 @@
6093f9c42ad8d328c37423e3067f3c9e7cd1ddcf
c995db1d0baa5ffa66832d48adeac8c2eabe2811

View File

@ -364,7 +364,7 @@
{"name":"kubeclient","version":"4.11.0","platform":"ruby","checksum":"4985fcd749fb8c364a668a8350a49821647f03aa52d9ee6cbc582beb8e883fcc"},
{"name":"language_server-protocol","version":"3.17.0.3","platform":"ruby","checksum":"3d5c58c02f44a20d972957a9febe386d7e7468ab3900ce6bd2b563dd910c6b3f"},
{"name":"launchy","version":"2.5.2","platform":"ruby","checksum":"8aa0441655aec5514008e1d04892c2de3ba57bd337afb984568da091121a241b"},
{"name":"lefthook","version":"1.11.14","platform":"ruby","checksum":"c11c55f5096f5d38068b66be8a33143899b7095f28a8145c9adf0b3eb611c098"},
{"name":"lefthook","version":"1.11.16","platform":"ruby","checksum":"1a273f1963b4a899c56f7cf14ccea90b17af239bf7acf09cbc9e4ca6298ee685"},
{"name":"letter_opener","version":"1.10.0","platform":"ruby","checksum":"2ff33f2e3b5c3c26d1959be54b395c086ca6d44826e8bf41a14ff96fdf1bdbb2"},
{"name":"letter_opener_web","version":"3.0.0","platform":"ruby","checksum":"3f391efe0e8b9b24becfab5537dfb17a5cf5eb532038f947daab58cb4b749860"},
{"name":"libyajl2","version":"2.1.0","platform":"ruby","checksum":"aa5df6c725776fc050c8418450de0f7c129cb7200b811907c4c0b3b5c0aea0ef"},

View File

@ -1105,7 +1105,7 @@ GEM
language_server-protocol (3.17.0.3)
launchy (2.5.2)
addressable (~> 2.8)
lefthook (1.11.14)
lefthook (1.11.16)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
letter_opener_web (3.0.0)

View File

@ -364,7 +364,7 @@
{"name":"kubeclient","version":"4.11.0","platform":"ruby","checksum":"4985fcd749fb8c364a668a8350a49821647f03aa52d9ee6cbc582beb8e883fcc"},
{"name":"language_server-protocol","version":"3.17.0.3","platform":"ruby","checksum":"3d5c58c02f44a20d972957a9febe386d7e7468ab3900ce6bd2b563dd910c6b3f"},
{"name":"launchy","version":"2.5.2","platform":"ruby","checksum":"8aa0441655aec5514008e1d04892c2de3ba57bd337afb984568da091121a241b"},
{"name":"lefthook","version":"1.11.14","platform":"ruby","checksum":"c11c55f5096f5d38068b66be8a33143899b7095f28a8145c9adf0b3eb611c098"},
{"name":"lefthook","version":"1.11.16","platform":"ruby","checksum":"1a273f1963b4a899c56f7cf14ccea90b17af239bf7acf09cbc9e4ca6298ee685"},
{"name":"letter_opener","version":"1.10.0","platform":"ruby","checksum":"2ff33f2e3b5c3c26d1959be54b395c086ca6d44826e8bf41a14ff96fdf1bdbb2"},
{"name":"letter_opener_web","version":"3.0.0","platform":"ruby","checksum":"3f391efe0e8b9b24becfab5537dfb17a5cf5eb532038f947daab58cb4b749860"},
{"name":"libyajl2","version":"2.1.0","platform":"ruby","checksum":"aa5df6c725776fc050c8418450de0f7c129cb7200b811907c4c0b3b5c0aea0ef"},

View File

@ -1099,7 +1099,7 @@ GEM
language_server-protocol (3.17.0.3)
launchy (2.5.2)
addressable (~> 2.8)
lefthook (1.11.14)
lefthook (1.11.16)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
letter_opener_web (3.0.0)

View File

@ -3,6 +3,7 @@ import TabsWithList from '~/groups_projects/components/tabs_with_list.vue';
import {
FILTERED_SEARCH_TOKEN_LANGUAGE,
FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL,
FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL,
PAGINATION_TYPE_KEYSET,
} from '~/groups_projects/constants';
import {
@ -31,6 +32,7 @@ export default {
filteredSearchSupportedTokens: [
FILTERED_SEARCH_TOKEN_LANGUAGE,
FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL,
FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL,
],
timestampTypeMap: {
[SORT_OPTION_CREATED.value]: TIMESTAMP_TYPE_CREATED_AT,

View File

@ -2,12 +2,14 @@ query getAdminProjectCounts(
$search: String
$minAccessLevel: AccessLevelEnum
$programmingLanguageName: String
$visibilityLevel: VisibilityLevelsEnum
) {
active: projects(
active: true
search: $search
minAccessLevel: $minAccessLevel
programmingLanguageName: $programmingLanguageName
visibilityLevel: $visibilityLevel
) {
count
}
@ -16,6 +18,7 @@ query getAdminProjectCounts(
search: $search
minAccessLevel: $minAccessLevel
programmingLanguageName: $programmingLanguageName
visibilityLevel: $visibilityLevel
) {
count
}

View File

@ -10,6 +10,7 @@ query getAdminProjects(
$sort: String
$search: String
$minAccessLevel: AccessLevelEnum
$visibilityLevel: VisibilityLevelsEnum
$programmingLanguageName: String
) {
projects(
@ -22,6 +23,7 @@ query getAdminProjects(
search: $search
minAccessLevel: $minAccessLevel
programmingLanguageName: $programmingLanguageName
visibilityLevel: $visibilityLevel
) {
count
nodes {

View File

@ -13,6 +13,12 @@ import {
ACCESS_LEVEL_OWNER_INTEGER,
ACCESS_LEVELS_INTEGER_TO_STRING,
} from '~/access_level/constants';
import {
VISIBILITY_LEVEL_LABELS,
VISIBILITY_LEVEL_PRIVATE_STRING,
VISIBILITY_LEVEL_INTERNAL_STRING,
VISIBILITY_LEVEL_PUBLIC_STRING,
} from '~/visibility_level/constants';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { InternalEvents } from '~/tracking';
import {
@ -23,6 +29,7 @@ import {
PAGINATION_TYPE_KEYSET,
PAGINATION_TYPE_OFFSET,
QUERY_PARAM_PAGE,
FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL,
} from '../constants';
import userPreferencesUpdateMutation from '../graphql/mutations/user_preferences_update.mutation.graphql';
import TabView from './tab_view.vue';
@ -186,6 +193,22 @@ export default {
},
],
},
{
type: FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL,
icon: 'eye',
title: __('Visibility'),
token: GlFilteredSearchToken,
unique: true,
operators: OPERATORS_IS,
options: [
VISIBILITY_LEVEL_PRIVATE_STRING,
VISIBILITY_LEVEL_INTERNAL_STRING,
VISIBILITY_LEVEL_PUBLIC_STRING,
].map((visibilityLevelString) => ({
value: visibilityLevelString,
title: VISIBILITY_LEVEL_LABELS[visibilityLevelString],
})),
},
].filter((filteredSearchToken) =>
this.filteredSearchSupportedTokens.includes(filteredSearchToken.type),
);
@ -236,9 +259,8 @@ export default {
},
filters() {
const filters = pick(this.routeQueryWithoutPagination, [
FILTERED_SEARCH_TOKEN_LANGUAGE,
FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL,
this.filteredSearchTermKey,
...this.filteredSearchSupportedTokens,
]);
// Normalize the property to Number since Vue Router 4 will
@ -265,10 +287,15 @@ export default {
this.programmingLanguages.find(({ id }) => id === parseInt(programmingLanguageId, 10))?.name
);
},
visibilityLevel() {
const visibilityLevel = this.filters[FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL];
return Array.isArray(visibilityLevel) ? visibilityLevel[0] : visibilityLevel;
},
filtersAsQueryVariables() {
return {
programmingLanguageName: this.programmingLanguageName,
minAccessLevel: this.minAccessLevel,
visibilityLevel: this.visibilityLevel,
};
},
timestampType() {
@ -509,8 +536,9 @@ export default {
size="sm"
class="gl-tab-counter-badge"
data-testid="tab-counter-badge"
>{{ tabCount(tab) }}</gl-badge
>
{{ tabCount(tab) }}
</gl-badge>
</div>
</template>

View File

@ -8,6 +8,7 @@ export const SORT_LABEL_STORAGE_SIZE = __('Storage size');
export const FILTERED_SEARCH_TOKEN_LANGUAGE = 'language';
export const FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL = 'min_access_level';
export const FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL = 'visibility_level';
export const SORT_DIRECTION_ASC = 'asc';
export const SORT_DIRECTION_DESC = 'desc';

View File

@ -2,7 +2,6 @@
import {
GlAvatarLabeled,
GlBadge,
GlEmptyState,
GlIcon,
GlKeysetPagination,
GlLoadingIcon,
@ -18,6 +17,7 @@ import { fetchPolicies } from '~/lib/graphql';
import { DEFAULT_PAGE_SIZE } from '~/members/constants';
import { helpPagePath } from '~/helpers/help_page_helper';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import EmptyResult from '~/vue_shared/components/empty_result.vue';
import {
PLACEHOLDER_STATUS_KEPT_AS_PLACEHOLDER,
PLACEHOLDER_STATUS_COMPLETED,
@ -34,7 +34,6 @@ export default {
components: {
GlAvatarLabeled,
GlBadge,
GlEmptyState,
GlIcon,
GlKeysetPagination,
GlLoadingIcon,
@ -43,6 +42,7 @@ export default {
GlLink,
PlaceholderActions,
HelpPopover,
EmptyResult,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -150,11 +150,6 @@ export default {
isSearchQueryTooShort() {
return this.querySearch && this.querySearch.trim().length < MINIMUM_QUERY_LENGTH;
},
emptyText() {
return this.isSearchQueryTooShort
? __('Enter at least three characters to search.')
: __('Edit your search and try again');
},
},
methods: {
@ -217,7 +212,9 @@ export default {
</template>
<template #empty>
<gl-empty-state :title="__('No results found')" :description="emptyText" />
<!-- EmptyResult shows minimum length message when searchMinimumLength is set,
otherwise shows generic search message -->
<empty-result type="search" :search-minimum-length="isSearchQueryTooShort ? 3 : null" />
</template>
<template #cell(user)="{ item }">

View File

@ -11,7 +11,6 @@ import { parseBoolean } from '~/lib/utils/common_utils';
import initSourcegraph from '~/sourcegraph';
import ZenMode from '~/zen_mode';
import initAwardsApp from '~/emoji/awards_app';
import { initMrExperienceSurvey } from '~/surveys/merge_request_experience';
import toast from '~/vue_shared/plugins/global_toast';
import getStateQuery from './queries/get_state.query.graphql';
import initCheckoutModal from './init_checkout_modal';
@ -23,7 +22,6 @@ export default function initMergeRequestShow(store, pinia) {
initSourcegraph();
initIssuableSidebar();
initAwardsApp(document.getElementById('js-vue-awards-block'));
initMrExperienceSurvey();
initCheckoutModal();
const el = document.querySelector('.js-mr-header');

View File

@ -1,69 +0,0 @@
<script>
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
name: 'SatisfactionRate',
components: {
GlButton,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
i18n: {
unhappy: s__('Surveys|Unhappy'),
delighted: s__('Surveys|Delighted'),
},
grades: [
{
title: s__('Surveys|Unhappy'),
icon: 'face-unhappy',
value: 1,
},
{
title: s__('Surveys|Sad'),
icon: 'slight-frown',
value: 2,
},
{
title: s__('Surveys|Neutral'),
icon: 'face-neutral',
value: 3,
},
{
title: s__('Surveys|Happy'),
icon: 'slight-smile',
value: 4,
},
{
title: s__('Surveys|Delighted'),
icon: 'smiley',
value: 5,
},
],
};
</script>
<template>
<div>
<ul class="gl-m-0 gl-flex gl-list-none gl-justify-between gl-p-0">
<li v-for="grade in $options.grades" :key="grade.value">
<gl-button
v-gl-tooltip="grade.title"
class="!gl-p-2"
variant="default"
category="tertiary"
:aria-label="grade.title"
@click="$emit('rate', grade.value)"
>
<gl-icon class="gl-align-top" :name="grade.icon" :size="24" />
</gl-button>
</li>
</ul>
<div class="gl-flex gl-justify-between gl-pt-3 gl-text-sm gl-text-subtle">
<div>{{ $options.i18n.unhappy }}</div>
<div>{{ $options.i18n.delighted }}</div>
</div>
</div>
</template>

View File

@ -1,60 +0,0 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import MergeRequestExperienceSurveyApp from '~/surveys/merge_request_experience/app.vue';
import createDefaultClient from '~/lib/graphql';
import Translate from '~/vue_shared/translate';
Vue.use(Translate);
Vue.use(VueApollo);
export const startMrSurveyApp = () => {
const mountEl = document.querySelector('#js-mr-experience-survey');
if (!mountEl) return;
let channel = null;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
const { accountAge } = mountEl.dataset;
const app = new Vue({
apolloProvider,
data() {
return {
hidden: false,
};
},
render(h) {
if (this.hidden) return null;
return h(MergeRequestExperienceSurveyApp, {
props: {
accountAge: Number(accountAge),
},
on: {
close: () => {
channel?.postMessage('close');
app.hidden = true;
},
rate: () => {
channel?.postMessage('close');
},
},
});
},
});
app.$mount(mountEl);
if (window.BroadcastChannel) {
channel = new BroadcastChannel('mr_survey');
channel.addEventListener('message', ({ data }) => {
if (data === 'close') {
app.hidden = true;
channel.close();
channel = null;
}
});
}
};

View File

@ -1,196 +0,0 @@
<script>
import { GlButton, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import gitlabLogo from '@gitlab/svgs/dist/illustrations/gitlab_logo.svg?raw';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, __ } from '~/locale';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import SatisfactionRate from '~/surveys/components/satisfaction_rate.vue';
import Tracking from '~/tracking';
import { PROMO_URL } from '~/constants';
const steps = [
{
label: 'overall',
question: s__('MrSurvey|Overall, how satisfied are you with merge requests?'),
},
{
label: 'performance',
question: s__(
'MrSurvey|How satisfied are you with %{strongStart}speed/performance%{strongEnd} of merge requests?',
),
},
];
const MR_RENDER_LS_KEY = 'mr_survey_rendered';
export default {
name: 'MergeRequestExperienceSurveyApp',
components: {
UserCalloutDismisser,
GlSprintf,
GlButton,
SatisfactionRate,
},
directives: {
SafeHtml,
tooltip: GlTooltipDirective,
},
mixins: [Tracking.mixin()],
props: {
accountAge: {
type: Number,
required: true,
},
},
i18n: {
survey: s__('MrSurvey|Merge request experience survey'),
close: __('Close'),
legal: s__(
'MrSurvey|By continuing, you acknowledge that responses will be used to improve GitLab and in accordance with the %{linkStart}GitLab Privacy Policy%{linkEnd}.',
),
thanks: s__('MrSurvey|Thank you for your feedback!'),
},
gitlabLogo,
privacyLink: `${PROMO_URL}/privacy/`,
data() {
return {
visible: false,
stepIndex: 0,
};
},
computed: {
step() {
return steps[this.stepIndex];
},
},
mounted() {
document.addEventListener('keyup', this.handleKeyup);
},
destroyed() {
document.removeEventListener('keyup', this.handleKeyup);
},
methods: {
onQueryLoaded({ shouldShowCallout }) {
this.visible = shouldShowCallout;
if (!this.visible) this.$emit('close');
else if (!localStorage?.getItem(MR_RENDER_LS_KEY)) {
this.track('survey:mr_experience', {
label: 'render',
extra: {
accountAge: this.accountAge,
},
});
localStorage?.setItem(MR_RENDER_LS_KEY, '1');
}
},
onRate(event) {
this.$refs.dismisser?.dismiss();
this.$emit('rate');
localStorage?.removeItem(MR_RENDER_LS_KEY);
this.track('survey:mr_experience', {
label: this.step.label,
value: event,
extra: {
accountAge: this.accountAge,
},
});
this.stepIndex += 1;
if (!this.step) {
setTimeout(() => {
this.$emit('close');
}, 5000);
}
},
handleKeyup(e) {
if (e.key !== 'Escape') return;
this.dismiss();
},
dismiss() {
this.$refs.dismisser?.dismiss();
this.$emit('close');
this.track('survey:mr_experience', {
label: 'dismiss',
extra: {
accountAge: this.accountAge,
},
});
localStorage?.removeItem(MR_RENDER_LS_KEY);
},
},
};
</script>
<template>
<user-callout-dismisser
ref="dismisser"
feature-name="mr_experience_survey"
@queryResult.once="onQueryLoaded"
>
<aside
class="mr-experience-survey-wrapper gl-fixed gl-bottom-0 gl-right-0 gl-p-5"
:aria-label="$options.i18n.survey"
>
<transition name="survey-slide-up">
<div
v-if="visible"
class="mr-experience-survey-body gl-border gl-relative gl-flex gl-flex-col gl-rounded-base gl-bg-overlap gl-p-5"
>
<gl-button
v-tooltip="$options.i18n.close"
:aria-label="$options.i18n.close"
variant="default"
category="tertiary"
class="gl-absolute gl-right-3 gl-top-4"
icon="close"
@click="dismiss"
/>
<div
v-if="stepIndex === 0"
class="mr-experience-survey-legal gl-border-t gl-mt-5 gl-pt-3 gl-text-sm gl-text-subtle"
role="note"
>
<p class="gl-m-0">
<gl-sprintf :message="$options.i18n.legal">
<template #link="{ content }">
<a
class="gl-text-subtle gl-underline"
:href="$options.privacyLink"
target="_blank"
rel="noreferrer nofollow"
v-text="content"
></a>
</template>
</gl-sprintf>
</p>
</div>
<div class="gl-relative">
<div class="gl-absolute">
<div
v-safe-html="$options.gitlabLogo"
aria-hidden="true"
class="mr-experience-survey-logo"
></div>
</div>
</div>
<section v-if="step">
<p id="mr_survey_question" ref="question" class="gl-m-0 gl-px-7">
<gl-sprintf :message="step.question">
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
</gl-sprintf>
</p>
<satisfaction-rate
aria-labelledby="mr_survey_question"
class="gl-mt-5"
@rate="onRate"
/>
</section>
<section v-else class="gl-px-7">
{{ $options.i18n.thanks }}
</section>
</div>
</transition>
</aside>
</user-callout-dismisser>
</template>

View File

@ -1,23 +0,0 @@
import { Tracker } from '~/tracking/tracker';
const MR_SURVEY_WAIT_DURATION = 10000;
const broadcastNotificationVisible = () => {
// We don't want to clutter up the UI by displaying the survey when broadcast message(s)
// are visible as well.
return Boolean(document.querySelector('.js-broadcast-notification-message'));
};
export const initMrExperienceSurvey = () => {
if (!gon.features?.mrExperienceSurvey) return;
if (!gon.current_user_id) return;
if (!Tracker.enabled()) return;
if (broadcastNotificationVisible()) return;
setTimeout(() => {
// eslint-disable-next-line promise/catch-or-return
import('./app').then(({ startMrSurveyApp }) => {
startMrSurveyApp();
});
}, MR_SURVEY_WAIT_DURATION);
};

View File

@ -44,11 +44,6 @@ export default {
};
},
computed: {
edited() {
const { createdAt, lastEditedAt } = this.updatedNote;
return new Date(createdAt).getTime() !== new Date(lastEditedAt).getTime();
},
editedText() {
return __('Edited');
},
@ -111,7 +106,7 @@ export default {
></div>
<note-edited-text
v-if="edited"
v-if="updatedNote.lastEditedBy"
:edited-at="updatedNote.lastEditedAt"
:edited-by="updatedNote.lastEditedBy"
:action-text="editedText"

View File

@ -272,30 +272,6 @@ $merge-request-sticky-header-height: 45px;
}
}
.mr-experience-survey-wrapper {
// setting this explicitly because:
// diff-files-holder has z-index 203
// z-index 9999 utility class breaks tooltips
z-index: 210;
}
.mr-experience-survey-body {
width: 300px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.mr-experience-survey-legal {
order: 1;
}
.mr-experience-survey-logo {
width: 16px;
svg {
max-width: 100%;
}
}
// These are composite classes for use with Vue Transition
// https://vuejs.org/guide/built-ins/transition
.survey-slide-up-enter {

View File

@ -17,12 +17,7 @@ module Admin
end
def installation_service
Integrations::SlackInstallation::InstanceService.new(
current_user: current_user,
params: params
.permit(:code)
.merge(organization_id: Current.organization.id)
)
Integrations::SlackInstallation::InstanceService.new(current_user: current_user, params: params.permit(:code))
end
end
end

View File

@ -35,7 +35,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action :check_user_can_push_to_source_branch!, only: [:rebase]
before_action only: [:show, :diffs, :rapid_diffs, :reports] do
push_frontend_feature_flag(:mr_experience_survey, project)
push_frontend_feature_flag(:mr_pipelines_graphql, project)
push_frontend_feature_flag(:notifications_todos_buttons, current_user)
push_frontend_feature_flag(:mr_review_batch_submit, current_user)

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
module Mutations
module Namespaces
class RegenerateNewWorkItemEmailAddress < BaseMutation
graphql_name 'NamespacesRegenerateNewWorkItemEmailAddress'
include ResolvesNamespace
argument :full_path, GraphQL::Types::ID,
required: true,
description: 'Full path of the namespace to regenerate the new work item email address for.'
field :namespace,
Types::NamespaceType,
null: true,
description: 'Namespace after regenerating the new work item email address.'
authorize :read_namespace
def resolve(full_path:)
namespace = authorized_find!(full_path)
unless namespace.is_a?(::Namespaces::ProjectNamespace)
return { namespace: nil, errors: [_('Work item creation via email is only supported for projects')] }
end
unless Gitlab::Email::IncomingEmail.supports_work_item_creation?
return { namespace: nil, errors: [_('Work item creation via email is not supported')] }
end
return { namespace: namespace, errors: [] } if current_user.reset_incoming_email_token!
{ namespace: nil, errors: [_('Failed to regenerate new work item email address')] }
end
private
def find_object(full_path)
resolve_namespace(full_path: full_path)
end
end
end
end

View File

@ -222,6 +222,7 @@ module Types
milestone: '17.7'
}
mount_mutation Mutations::Namespace::PackageSettings::Update
mount_mutation Mutations::Namespaces::RegenerateNewWorkItemEmailAddress, experiment: { milestone: '18.2' }
mount_mutation Mutations::Groups::Update
mount_mutation Mutations::UserCallouts::Create
mount_mutation Mutations::UserPreferences::Update

View File

@ -304,7 +304,6 @@ module Integrations
new_integration = integration.dup
new_integration.instance = false
new_integration.organization_id = nil
new_integration.project_id = project_id
new_integration.group_id = group_id
new_integration.inherit_from_id = integration.id if integration.inheritable?
@ -514,13 +513,12 @@ module Integrations
validates :project_id, presence: true, unless: -> { instance_level? || group_level? }
validates :group_id, presence: true, unless: -> { instance_level? || project_level? }
validates :project_id, :group, absence: true, if: -> { instance_level? }
validates :organization_id, presence: true, if: -> { instance_level? }
validates :project_id, :group_id, absence: true, if: -> { instance_level? }
validates :type, presence: true, exclusion: BASE_CLASSES
validates :type, uniqueness: { scope: :instance }, if: :instance_level?
validates :type, uniqueness: { scope: :project_id }, if: :project_level?
validates :type, uniqueness: { scope: :group_id }, if: :group_level?
validate :validate_belongs_to_one_of_project_group_or_organization
validate :validate_belongs_to_project_or_group
scope :external_issue_trackers, -> { where(category: 'issue_tracker').active }
scope :third_party_wikis, -> { where(category: 'third_party_wiki').active }
@ -813,10 +811,10 @@ module Integrations
private
def validate_belongs_to_one_of_project_group_or_organization
return if [group_id, project_id, organization_id].compact.one?
def validate_belongs_to_project_or_group
return unless project_level? && group_level?
errors.add(:base, 'The integration must belong to one organization, group, or project.')
errors.add(:project_id, 'The integration cannot belong to both a project and a group')
end
def validate_recipients?

View File

@ -56,7 +56,7 @@ module Users
user_reached_limit_free_plan_alert: 51, # EE-only
submit_license_usage_data_banner: 52, # EE-only
personal_project_limitations_banner: 53, # EE-only
mr_experience_survey: 54,
# 54 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/195384
# 55 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121920
namespace_storage_limit_alert_warning_threshold: 56, # EE-only
namespace_storage_limit_alert_alert_threshold: 57, # EE-only

View File

@ -18,9 +18,7 @@ module Integrations
end
def find_or_create_integration!
GitlabSlackApplication
.for_instance
.first_or_create!(organization_id: params[:organization_id]) # rubocop:disable CodeReuse/ActiveRecord: -- legacy use
GitlabSlackApplication.for_instance.first_or_create!
end
end
end

View File

@ -111,7 +111,4 @@
#js-reviewer-drawer-portal
- if current_user && Feature.enabled?(:mr_experience_survey, current_user)
#js-mr-experience-survey{ data: { account_age: current_user.account_age_in_days } }
= render 'shared/web_ide_path'

View File

@ -1,8 +0,0 @@
---
name: mr_experience_survey
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90036
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/366561
milestone: '15.2'
type: development
group: group::code review
default_enabled: false

View File

@ -0,0 +1,9 @@
---
name: log_database_sticking_operations
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/548158
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/193945
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/548413
milestone: '18.2'
group: group::database frameworks
type: gitlab_com_derisk
default_enabled: false

View File

@ -245,6 +245,10 @@ country_access_logs:
- table: users
column: user_id
on_delete: async_delete
dast_pre_scan_verification_steps:
- table: projects
column: project_id
on_delete: async_delete
dast_pre_scan_verifications:
- table: p_ci_pipelines
column: ci_pipeline_id

View File

@ -5,4 +5,4 @@ feature_category: package_registry
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/185223
milestone: '17.11'
queued_migration_version: 20250320085933
finalized_by: # version of the migration that finalized this BBM
finalized_by: '20250616235836'

View File

@ -0,0 +1,11 @@
---
migration_job_name: EncryptMissedCiRunnerTokens
description: >
We've encrypted plain tokens and migrated to token_encrypted (in milestone 11.6), so we need to remove the runner token column
and make encryption required (currently optional). However, plain tokens remain unencrypted in gitlab_com and likely in self-managed instances.
We must encrypt these remaining tokens and nullify them before removing the column completely.
feature_category: fleet_visibility
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/196265
milestone: '18.2'
queued_migration_version: 20250630163722
finalized_by: # to be updated

View File

@ -8,14 +8,6 @@ description: Verification step status for DAST Profiles
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105702
milestone: '15.7'
gitlab_schema: gitlab_sec
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: dast_pre_scan_verification_id
table: dast_pre_scan_verifications
sharding_key: project_id
belongs_to: dast_pre_scan_verification
sharding_key:
project_id: projects
table_size: small
desired_sharding_key_migration_job_name: BackfillDastPreScanVerificationStepsProjectId

View File

@ -6,10 +6,10 @@ class AddMultiNotNullConstraintToIntegrations < Gitlab::Database::Migration[2.3]
disable_ddl_transaction!
def up
add_multi_column_not_null_constraint(:integrations, :group_id, :project_id, :organization_id, validate: false)
# No-op to fix https://gitlab.com/gitlab-com/gl-infra/production/-/issues/20126
end
def down
remove_multi_column_not_null_constraint(:integrations, :group_id, :project_id, :organization_id)
# no-op to fix https://gitlab.com/gitlab-com/gl-infra/production/-/issues/20126
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class AddDuoSettingsIndexToNamespaceSettings < Gitlab::Database::Migration[2.3]
disable_ddl_transaction!
milestone '18.2'
TABLE = :namespace_settings
COLUMNS = [:duo_features_enabled, :lock_duo_features_enabled]
INDEX = "index_namespace_settings_on_duo_features"
def up
add_concurrent_index TABLE, COLUMNS,
where: 'duo_features_enabled IS NOT NULL', include: :namespace_id,
name: INDEX
end
def down
remove_concurrent_index_by_name TABLE, INDEX
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class RemoveNotNullIntegrationsConstraint < Gitlab::Database::Migration[2.3]
disable_ddl_transaction!
milestone '18.2'
def up
remove_check_constraint :integrations, 'check_2aae034509'
end
def down
# no-op
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class FinalizeBackfillPackagesDebianFileMetadataProjectId < Gitlab::Database::Migration[2.3]
milestone '18.2'
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_main_cell
def up
ensure_batched_background_migration_is_finished(
job_class_name: 'BackfillPackagesDebianFileMetadataProjectId',
table_name: :packages_debian_file_metadata,
column_name: :package_file_id,
job_arguments: [:project_id, :packages_package_files, :project_id, :package_file_id],
finalize: true
)
end
def down; end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class AddDastPreScanVerificationStepsProjectIdNotNull < Gitlab::Database::Migration[2.3]
milestone '18.2'
disable_ddl_transaction!
def up
add_not_null_constraint :dast_pre_scan_verification_steps, :project_id
end
def down
remove_not_null_constraint :dast_pre_scan_verification_steps, :project_id
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class QueueEncryptMissedCiRunnerTokens < Gitlab::Database::Migration[2.3]
milestone '18.2'
restrict_gitlab_migration gitlab_schema: :gitlab_ci
MIGRATION = "EncryptMissedCiRunnerTokens"
def up
queue_batched_background_migration(
MIGRATION,
:ci_runners,
:id
)
end
def down
delete_batched_background_migration(MIGRATION, :ci_runners, :id, [])
end
end

View File

@ -0,0 +1 @@
f5e13cacb2c1c5c16064567b512406b2a2eec8fb30ed8ba9420da247af2700d3

View File

@ -0,0 +1 @@
3726cd79a816694256fa6d449855d4515971b99e4fa0d59bdea410f49b712dfb

View File

@ -0,0 +1 @@
b53bc6e953754f56cd7e7e4814fe569b3986330bebd04d17fc329d642e76a7f4

View File

@ -0,0 +1 @@
debcbf90aec0669ce4914fe31b2f960584380520f4966df8dab09c039ae00745

View File

@ -0,0 +1 @@
06b024ebde4a483d5bf01cc65ebb9f0c4e5f36ca5fbc55675d5ac84511e15c28

View File

@ -13352,6 +13352,7 @@ CREATE TABLE dast_pre_scan_verification_steps (
verification_errors text[] DEFAULT '{}'::text[] NOT NULL,
check_type smallint DEFAULT 0 NOT NULL,
project_id bigint,
CONSTRAINT check_2cf67eeb54 CHECK ((project_id IS NOT NULL)),
CONSTRAINT check_cd216b95e4 CHECK ((char_length(name) <= 255))
);
@ -29777,9 +29778,6 @@ ALTER TABLE ONLY chat_teams
ALTER TABLE workspaces
ADD CONSTRAINT check_2a89035b04 CHECK ((personal_access_token_id IS NOT NULL)) NOT VALID;
ALTER TABLE integrations
ADD CONSTRAINT check_2aae034509 CHECK ((num_nonnulls(group_id, organization_id, project_id) = 1)) NOT VALID;
ALTER TABLE security_scans
ADD CONSTRAINT check_2d56d882f6 CHECK ((project_id IS NOT NULL)) NOT VALID;
@ -36683,6 +36681,8 @@ CREATE UNIQUE INDEX index_namespace_import_users_on_user_id ON namespace_import_
CREATE UNIQUE INDEX index_namespace_root_storage_statistics_on_namespace_id ON namespace_root_storage_statistics USING btree (namespace_id);
CREATE INDEX index_namespace_settings_on_duo_features ON namespace_settings USING btree (duo_features_enabled, lock_duo_features_enabled) INCLUDE (namespace_id) WHERE (duo_features_enabled IS NOT NULL);
CREATE INDEX index_namespace_settings_on_namespace_id_where_archived_true ON namespace_settings USING btree (namespace_id) WHERE (archived = true);
CREATE UNIQUE INDEX index_namespace_statistics_on_namespace_id ON namespace_statistics USING btree (namespace_id);

View File

@ -13,7 +13,7 @@ title: Create your GitLab Dedicated instance
{{< /details >}}
The instructions on this page guide you through the onboarding and initial setup of your GitLab Dedicated instance using [Switchboard](https://about.gitlab.com/direction/saas-platforms/switchboard/), the GitLab Dedicated portal.
The instructions on this page guide you through the onboarding and initial setup of your GitLab Dedicated instance using [Switchboard](https://about.gitlab.com/direction/platforms/switchboard/), the GitLab Dedicated portal.
## Step 1: Get access to Switchboard

View File

@ -8849,6 +8849,30 @@ Input type: `NamespaceSettingsUpdateInput`
| <a id="mutationnamespacesettingsupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationnamespacesettingsupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during the mutation. |
### `Mutation.namespacesRegenerateNewWorkItemEmailAddress`
{{< details >}}
**Introduced** in GitLab 18.2.
**Status**: Experiment.
{{< /details >}}
Input type: `NamespacesRegenerateNewWorkItemEmailAddressInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationnamespacesregeneratenewworkitememailaddressclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationnamespacesregeneratenewworkitememailaddressfullpath"></a>`fullPath` | [`ID!`](#id) | Full path of the namespace to regenerate the new work item email address for. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationnamespacesregeneratenewworkitememailaddressclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationnamespacesregeneratenewworkitememailaddresserrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during the mutation. |
| <a id="mutationnamespacesregeneratenewworkitememailaddressnamespace"></a>`namespace` | [`Namespace`](#namespace) | Namespace after regenerating the new work item email address. |
### `Mutation.noteConvertToThread`
Convert a standard comment to a resolvable thread.
@ -47551,7 +47575,6 @@ Name of the feature that the callout is for.
| <a id="usercalloutfeaturenameenumgold_trial_billings"></a>`GOLD_TRIAL_BILLINGS` | Callout feature name for gold_trial_billings. |
| <a id="usercalloutfeaturenameenumjoining_a_project_alert"></a>`JOINING_A_PROJECT_ALERT` | Callout feature name for joining_a_project_alert. |
| <a id="usercalloutfeaturenameenummerge_request_dashboard_display_preferences_popover"></a>`MERGE_REQUEST_DASHBOARD_DISPLAY_PREFERENCES_POPOVER` | Callout feature name for merge_request_dashboard_display_preferences_popover. |
| <a id="usercalloutfeaturenameenummr_experience_survey"></a>`MR_EXPERIENCE_SURVEY` | Callout feature name for mr_experience_survey. |
| <a id="usercalloutfeaturenameenumnamespace_over_storage_users_combined_alert"></a>`NAMESPACE_OVER_STORAGE_USERS_COMBINED_ALERT` | Callout feature name for namespace_over_storage_users_combined_alert. |
| <a id="usercalloutfeaturenameenumnamespace_storage_limit_alert_alert_threshold"></a>`NAMESPACE_STORAGE_LIMIT_ALERT_ALERT_THRESHOLD` | Callout feature name for namespace_storage_limit_alert_alert_threshold. |
| <a id="usercalloutfeaturenameenumnamespace_storage_limit_alert_error_threshold"></a>`NAMESPACE_STORAGE_LIMIT_ALERT_ERROR_THRESHOLD` | Callout feature name for namespace_storage_limit_alert_error_threshold. |

View File

@ -218,7 +218,7 @@ The following GitLab application features are not available:
- Service Desk
- Some GitLab Duo AI capabilities
- View the [list of supported AI features](../../user/ai_features.md)
- For more information, see the [Supporting AI Features on GitLab Dedicated](https://about.gitlab.com/direction/platforms/dedicated/#supporting-ai-features-on-gitlab-dedicated)
- For more information, see [supporting AI Features on GitLab Dedicated](https://about.gitlab.com/direction/gitlab_dedicated/#supporting-ai-features-on-gitlab-dedicated)
- Features other than [available features](#available-features) that must be configured outside of the GitLab user interface
- Any functionality or feature behind a feature flag that is turned `off` by default
- [Sigstore for keyless signing and verification](../../ci/yaml/signing_examples.md)

View File

@ -50,6 +50,10 @@ detected are marked **Resolved**.
- You can assign a maximum of five rules to each policy.
- You can assign a maximum of five vulnerability management policies to each security policy project.
- When a secret detection scan finds that a previously detected secret key is no longer detected,
the vulnerability is not auto-resolved. Instead, it remains in **Needs Triage** because the removed
secret key has already been exposed. The vulnerability status should be manually resolved only after
the secret key is revoked or rotated.
## Create a vulnerability management policy

View File

@ -69,14 +69,14 @@ push protection, to minimize the delay when pushing your commits and minimize th
alerts. For example, personal access tokens that use a custom prefix are not detected by secret push protection.
You can [exclude](../exclusions.md) selected secrets from detection by secret push protection.
## Enable secret push protection
## Getting started
On GitLab Dedicated and GitLab Self-Managed instances, you must:
1. [Allow secret push protection on the entire instance](#allow-the-use-of-secret-push-protection-in-your-gitlab-instance).
1. Enable secret push protection. You can either:
- [Enable secret push protection in a specific project](#enable-secret-push-protection-in-a-project).
- Use the API to [enable secret push protection for all projects in group](../../../../api/group_security_settings.md#update-secret_push_protection_enabled-setting). Ultimate only.
- Use the API to [enable secret push protection for all projects in group](../../../../api/group_security_settings.md#update-secret_push_protection_enabled-setting).
### Allow the use of secret push protection in your GitLab instance
@ -149,6 +149,103 @@ Secret push protection does not check a file in a commit when:
Secret push protection scans only the diffs of commits pushed over HTTP(S) and SSH.
If a secret is already present in a file and not part of the changes, it is not detected.
## Understanding the results
Secret push protection can identify various categories of secrets:
- **API keys and tokens**: Service-specific authentication credentials
- **Database connection strings**: URLs containing embedded credentials
- **Private keys**: Cryptographic keys for authentication or encryption
- **Generic high-entropy strings**: Patterns that appear to be randomly generated secrets
When a push is blocked, secret push protection provides detailed information to help you locate and address the detected secrets:
- **Commit ID**: The specific commit containing the secret. Useful for tracking changes in your Git history.
- **File path and line number**: The exact location of the detected pattern for quick navigation.
- **Secret type**: The classification of the detected pattern. For example, `GitLab Personal Access Token` or `AWS Access Key`.
### Common detection categories
Not all detections require immediate action. Consider the following when evaluating results:
- **True positives**: Legitimate secrets that should be rotated and removed. For example:
- [Valid](../../vulnerabilities/validity_check.md) API keys or tokens
- Production database credentials
- Private cryptographic keys
- Any credentials that could grant unauthorized access
- **False positives**: Detected patterns that aren't actual secrets. For example:
- Test data that resembles secrets but has no real-world value
- Placeholder values in configuration templates
- Example credentials in documentation
- Hash values or checksums that match secret patterns
Document common false positive patterns in your organization to streamline future evaluations.
## Optimization
Before deploying secret push protection widely, optimize the configuration to reduce false positives and improve accuracy for your specific environment.
### Reduce false positives
False positives can significantly impact developer productivity and lead to security fatigue.
To reduce false positives:
- Configure [exclusions](../exclusions.md) strategically:
- Create path-based exclusions for test directories, documentation, and third party dependencies.
- Use pattern-based exclusions for known false positive patterns specific to your codebase.
- Document your exclusion rules and review them regularly.
- Create standards for placeholder values and test credentials.
- Monitor false positive rates and continue to adjust exclusions accordingly.
### Optimize performance
Large repositories or frequent pushes can have performance impacts.
To optimize the performance of secret push protection:
- Monitor push times and establish baseline metrics before deployment.
- Use diff scanning to reduce the amount of content scanned on each push.
- Consider file size limits for repositories with large binary assets.
- Implement exclusions for directories that are unlikely to contain secrets.
### Integration with existing workflows
Ensure secret push protection complements your existing development practices:
- Configure pipeline secret detection and secret push protection to be sure you have defense in depth.
- Update developer documentation to include secret push protection procedures.
- Align with security training to educate developers on secure coding practices to minimize leaked secrets.
## Roll out
Successfully deploying secret push protection at scale requires careful planning and a phased implementation:
1. Choose two or three non-critical projects with active development to test the feature and understand its impact on developer workflows.
1. Turn on secret push protection for your selected test projects and monitor developer feedback.
1. Document processes for handling blocked pushes and train your development teams on the new workflows.
1. Track the number of secrets detected, false positive rates, and developer experience feedback during the pilot phase.
You should run the pilot phase for two to four weeks to gather sufficient data and identify any workflow adjustments needed before broader deployment.
Once you have completed the pilot, consider the next three phases for a scaled rollout:
1. Early adopters (weeks 3-6)
- Enable on 10-20% of active projects, prioritizing security-sensitive repositories.
- Focus on teams with strong security awareness and buy-in.
- Monitor performance impacts and developer experience.
- Refine processes based on real-world usage.
1. Broad deployment (weeks 7-12)
- Gradually enable across remaining projects in batches.
- Provide ongoing support and training to development teams.
- Monitor system performance and scale infrastructure if needed.
- Continue optimizing exclusion rules based on usage patterns.
1. Full coverage (weeks 13-16)
- Enable secret push protection on all remaining projects.
- Establish ongoing maintenance and review processes.
- Implement regular audits of exclusion rules and detected patterns.
## Resolve a blocked push
When secret push protection blocks a push, you can either:

View File

@ -51,7 +51,7 @@ To configure approval rules:
You can also configure:
- Additional [merge request approval settings](settings.md) for more control of the
- More [merge request approval settings](settings.md) for more control of the
level of oversight and security your project needs.
- Merge request approval rules with the
[Merge request approvals API](../../../../api/merge_request_approvals.md).
@ -86,8 +86,8 @@ Use cases include:
{{< /history >}}
You can see the approval status of a merge request in two places. On the [merge request itself](#for-a-single-merge-request)
and in the [list of merge requests](#in-the-list-of-merge-requests) for your project or group.
To view the approval status of a merge request, check the merge request itself, or the list of
merge requests for your project or group.
### For a single merge request
@ -135,7 +135,7 @@ To see the review and approval status for each reviewer:
1. Open the merge request.
1. Check the right sidebar.
Each reviewer's status is shown next to the their name.
Each reviewer's status is shown next to their name.
- {{< icon name="dash-circle" >}} Awaiting review
- {{< icon name="status_running" >}} Review in progress

View File

@ -72,7 +72,8 @@ This user has:
- 8 active merge requests ({{< icon name="merge-request-open" >}})
- 29 to-do items ({{< icon name="todo-done" >}})
Your merge request homepage shows more information about these merge requests. To see it, use any of these methods:
Your merge request homepage shows more information about these merge requests. To see it,
use any of these methods:
- Use the <kbd>Shift</kbd> + <kbd>m</kbd> [keyboard shortcut](../../shortcuts.md).
- On the left sidebar, select **Merge requests** ({{< icon name="merge-request-open">}}).
@ -106,12 +107,13 @@ into three tabs:
### Set your display preferences
On the top right of your merge request homepage, select **Display preferences** ({{< icon name="preferences">}})
to sort your merge requests by:
On the top right of your merge request homepage, select **Display preferences** ({{< icon name="preferences">}}):
- **Workflow**. This view groups merge requests by their status. GitLab shows the merge requests needing your attention
first, regardless of whether you are the author or the reviewer.
- **Role**. This view groups merge requests by whether you are the reviewer or the author.
- Toggle **Show labels** to show or hide labels for each merge request.
- Sorting preferences: **Workflow** or **Role**.
- **Workflow** groups merge requests by their status. GitLab shows the merge requests
needing your attention first, regardless of whether you are the author or the reviewer.
- **Role** groups merge requests by whether you are the reviewer or the author.
Active merge requests count toward the total shown on the left sidebar. GitLab excludes **Inactive**
merge requests from your review count.
@ -124,12 +126,13 @@ These merge requests need your attention. They count toward the total shown on t
Statuses:
- **Draft**: The merge request is a draft.
- **Reviewers needed**: The merge request is not a draft, but has no reviewers.
- **Review requested**: You're a reviewer. Review the merge request. Provide feedback. Optionally, approve or request changes.
Statuses:
- **Changes requested**: A reviewer has requested changes.
- **Review requested**: You're a reviewer. Review the merge request. Provide feedback. Optionally,
approve or request changes. Statuses:
- **Changes requested**: A reviewer has requested changes. The change request blocks the merge request,
but [can be bypassed](reviews/_index.md#bypass-a-request-for-changes).
- **Reviewer commented**: A reviewer has left comments but not requested changes.
- **Returned to you**: Reviewers have provided feedback, or requested changes. Address reviewer comments, and implement suggested changes.
Statuses:
- **Returned to you**: Reviewers have provided feedback, or requested changes. Address reviewer comments,
and apply suggested changes. Statuses:
- **Changes requested**: A reviewer has requested changes.
- **Reviewer commented**: A reviewer has left comments but not requested changes.
@ -137,12 +140,12 @@ These merge requests need your attention. They count toward the total shown on t
GitLab excludes these merge requests from the active count, because no action is required from you right now:
- **Waiting for assignee**: If you're the author, the merge request is awaiting review. If you're the reviewer, you've requested changes.
Statuses:
- **Waiting for assignee**: If you're the author, the merge request is awaiting review. If you're
the reviewer, you've requested changes. Statuses:
- **You requested changes**: You've completed your review and requested changes.
- **You commented**: You've commented, but have not completed your review.
- **Waiting for approvals**: Your assigned merge requests that are waiting for approvals, and reviews you have requested changes for.
Statuses:
- **Waiting for approvals**: Your assigned merge requests that are waiting for approvals, and reviews
you have requested changes for. Statuses:
- **Approvals required**: Number of required approvals remaining.
- **Approved**: Either you have approved, or all required approvals are satisfied.
- **Waiting for approvals**.
@ -150,8 +153,8 @@ GitLab excludes these merge requests from the active count, because no action is
Statuses:
- **Approved**: You've approved, and required approvals are satisfied.
- **Approval required**: You've approved, but not all required approvals are satisfied.
- **Approved by others**: Merge requests that have received approvals from other team members. Potentially ready to merge, if all requirements are met.
Statuses:
- **Approved by others**: Merge requests that have received approvals from other team members.
Potentially ready to merge, if all requirements are met. Statuses:
- **Approved**: Your merge request has received the necessary approvals.
### Role view

View File

@ -268,7 +268,7 @@ module API
def find_namespace(id)
if INTEGER_ID_REGEX.match?(id.to_s)
# We need to stick to an up-to-date replica or primary db here in order to properly observe the namespace
# recently created by GitlabSubscriptions::Trials::CreateService#create_group_flow.
# recently created by GitlabSubscriptions::Trials::UltimateCreateService.
# See https://gitlab.com/gitlab-org/customers-gitlab-com/-/issues/9808
::Namespace.sticking.find_caught_up_replica(:namespace, id)

View File

@ -0,0 +1,46 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
class EncryptMissedCiRunnerTokens < BatchedMigrationJob
operation_name :encrypt_missed_ci_runner_tokens
feature_category :fleet_visibility
def perform
each_sub_batch(
batching_scope: ->(relation) { relation.where.not(token: nil).where(token_encrypted: nil) }
) do |runners_to_encrypt|
token_encrypted_cases = []
runner_ids = []
runners_to_encrypt.pluck(:id, :token).each do |id, token|
encrypted_token = encode(token)
token_encrypted_cases <<
"WHEN id = #{connection.quote(id)} THEN #{connection.quote(encrypted_token)}"
runner_ids << connection.quote(id)
end
next if token_encrypted_cases.empty?
connection.execute(
<<~SQL
UPDATE ci_runners
SET token_encrypted = CASE
#{token_encrypted_cases.join(' ')}
END,
token = NULL
WHERE id IN (#{runner_ids.join(',')})
SQL
)
end
end
private
def encode(token)
Authn::TokenField::EncryptionHelper.encrypt_token(token)
end
end
end
end

View File

@ -47,6 +47,7 @@ module Gitlab
def stick(namespace, id)
with_primary_write_location do |location|
set_write_location_for(namespace, id, location)
capture_stick_logs(namespace, id, location)
end
use_primary!
end
@ -56,13 +57,31 @@ module Gitlab
ids.each do |id|
set_write_location_for(namespace, id, location)
end
capture_stick_logs(namespace, ids, location)
end
use_primary!
end
private
def log_database_sticking_operations_enabled?
Feature.enabled?(:log_database_sticking_operations, Feature.current_pod)
end
def capture_stick_logs(namespace, ids, location)
return unless log_database_sticking_operations_enabled?
return unless namespace.to_sym == :user
id = Array(ids).first # Only log the first ID to reduce log volume for bulk operations
::Gitlab::Database::LoadBalancing::Logger.info(
event: :load_balancer_stick_logging,
client_id: "#{namespace}/#{id}",
stick_id: id,
stick_type: namespace,
current_lsn: location)
end
def with_primary_write_location
# When only using the primary, there's no point in getting write
# locations, as the primary is always in sync with itself.

View File

@ -31,6 +31,8 @@ module Gitlab
enabled? && supports_wildcard?
end
alias_method :supports_work_item_creation?, :supports_issue_creation?
def reply_address(key)
incoming_email_config.address.sub(WILDCARD_PLACEHOLDER, key)
end

View File

@ -3530,9 +3530,6 @@ msgstr ""
msgid "Activate Service Desk"
msgstr ""
msgid "Activate my trial"
msgstr ""
msgid "Activated"
msgstr ""
@ -26623,6 +26620,9 @@ msgstr ""
msgid "Failed to refresh compliance requirement status. Error: %{error_message}"
msgstr ""
msgid "Failed to regenerate new work item email address"
msgstr ""
msgid "Failed to regenerate unique domain"
msgstr ""
@ -40391,21 +40391,6 @@ msgstr ""
msgid "MrReports|Reports"
msgstr ""
msgid "MrSurvey|By continuing, you acknowledge that responses will be used to improve GitLab and in accordance with the %{linkStart}GitLab Privacy Policy%{linkEnd}."
msgstr ""
msgid "MrSurvey|How satisfied are you with %{strongStart}speed/performance%{strongEnd} of merge requests?"
msgstr ""
msgid "MrSurvey|Merge request experience survey"
msgstr ""
msgid "MrSurvey|Overall, how satisfied are you with merge requests?"
msgstr ""
msgid "MrSurvey|Thank you for your feedback!"
msgstr ""
msgid "Multi-project"
msgstr ""
@ -61448,21 +61433,6 @@ msgstr ""
msgid "Support page URL"
msgstr ""
msgid "Surveys|Delighted"
msgstr ""
msgid "Surveys|Happy"
msgstr ""
msgid "Surveys|Neutral"
msgstr ""
msgid "Surveys|Sad"
msgstr ""
msgid "Surveys|Unhappy"
msgstr ""
msgid "Switch Branches"
msgstr ""
@ -65613,24 +65583,12 @@ msgstr ""
msgid "TrialWidget|Your trial of Ultimate with GitLab Duo Enterprise has ended"
msgstr ""
msgid "Trials|Create a new group and start your free trial."
msgstr ""
msgid "Trials|You can apply your free trial to a group."
msgstr ""
msgid "Trial|Activate my trial"
msgstr ""
msgid "Trial|Allowed characters: +, 0-9, -, and spaces."
msgstr ""
msgid "Trial|Apply your trial to a new group"
msgstr ""
msgid "Trial|Apply your trial to a new or existing group"
msgstr ""
msgid "Trial|By clicking \"%{buttonText}\" you accept the %{gitlabSubscriptionAgreement} and acknowledge the %{privacyStatement} and %{cookiePolicy}"
msgstr ""
@ -65661,9 +65619,6 @@ msgstr ""
msgid "Trial|Start your free trial"
msgstr ""
msgid "Trial|Start your free trial on %{group_name}"
msgstr ""
msgid "Trial|State or province"
msgstr ""
@ -70306,6 +70261,12 @@ msgstr ""
msgid "Work item"
msgstr ""
msgid "Work item creation via email is not supported"
msgstr ""
msgid "Work item creation via email is only supported for projects"
msgstr ""
msgid "Work item not supported"
msgstr ""

View File

@ -27,7 +27,6 @@ gem 'zeitwerk', '~> 2.7', '>= 2.7.3'
gem 'influxdb-client', '~> 3.2'
gem 'terminal-table', '~> 4.0.0', require: false
gem 'slack-notifier', '~> 2.4', require: false
gem 'googleauth', '~> 1.9.0', require: false # see: https://gitlab.com/gitlab-org/gitlab/-/issues/449019
gem 'fog-google', '~> 1.25', require: false
gem "warning", "~> 1.5"

View File

@ -175,11 +175,14 @@ GEM
google-apis-core (>= 0.15.0, < 2.a)
google-apis-storage_v1 (0.40.0)
google-apis-core (>= 0.15.0, < 2.a)
google-cloud-env (2.1.1)
google-cloud-env (2.3.1)
base64 (~> 0.2)
faraday (>= 1.0, < 3.a)
googleauth (1.9.2)
google-logging-utils (0.2.0)
googleauth (1.14.0)
faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.1)
google-cloud-env (~> 2.2)
google-logging-utils (~> 0.1)
jwt (>= 1.4, < 3.0)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
@ -386,7 +389,6 @@ DEPENDENCIES
gitlab-qa (~> 15, >= 15.5.0)
gitlab-utils!
gitlab_quality-test_tooling (~> 2.15.3)
googleauth (~> 1.9.0)
influxdb-client (~> 3.2)
junit_merge (~> 0.1.2)
knapsack (~> 4.0)

View File

@ -97,11 +97,11 @@ module QA
click_element 'expand-replies-button'
end
def has_comment?(comment_text)
def has_comment?(comment_text, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
has_element?(
'noteable-note-container',
text: comment_text,
wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME
wait: wait
)
end
@ -167,13 +167,13 @@ module QA
def start_review_with_comment(text)
fill_editor_element('comment-field', text)
click_element('start-review-button')
has_comment?(text)
has_comment?(text, 3)
end
def add_comment_to_review(text)
fill_editor_element('comment-field', text)
click_element('add-to-review-button')
has_comment?(text)
has_comment?(text, 3)
end
def toggle_comments(position)

View File

@ -180,8 +180,14 @@ module QA
end
end
all_elements('review-drawer-toggle', minimum: 1).first.click
click_element('submit-review-button')
retry_until(sleep_interval: 2, max_attempts: 5, message: "Retry closing review drawer") do
if has_element?('review-drawer-toggle', wait: 2)
all_elements('review-drawer-toggle', minimum: 1).first.click
end
click_element('submit-review-button')
has_no_element?('review-drawer-toggle', wait: 2)
end
# After clicking the button, wait for the review bar to disappear
# before moving on to the next part of the test

View File

@ -15,6 +15,7 @@ module QA
HTTP_STATUS_NOT_FOUND = 404
HTTP_STATUS_TOO_MANY_REQUESTS = 429
HTTP_STATUS_SERVER_ERROR = 500
HTTP_STATUS_SERVICE_UNAVAILABLE = 503
def post(url, payload, args = {})
request_args = {
@ -197,7 +198,10 @@ module QA
end
def fatal_response?(response_code)
[HTTP_STATUS_UNAUTHORIZED, HTTP_STATUS_SERVER_ERROR, HTTP_STATUS_BAD_REQUEST].include?(response_code)
[HTTP_STATUS_UNAUTHORIZED,
HTTP_STATUS_SERVER_ERROR,
HTTP_STATUS_BAD_REQUEST,
HTTP_STATUS_SERVICE_UNAVAILABLE].include?(response_code)
end
end
end

View File

@ -12,7 +12,7 @@ RSpec.describe Admin::IntegrationsController, :with_current_organization, featur
end
it_behaves_like Integrations::Actions do
let(:integration_attributes) { { instance: true, project: nil, organization: organization } }
let(:integration_attributes) { { instance: true, project: nil } }
let(:routing_params) do
{ id: integration.to_param }

View File

@ -23,7 +23,7 @@ RSpec.describe "Adding and removing exclusions to Beyond Identity integration",
end
context 'when the integration is active for the instance', :enable_admin_mode do
let(:instance_integration) { create(:beyond_identity_integration, :instance) }
let(:instance_integration) { create :beyond_identity_integration }
before do
::Integrations::PropagateService.new(instance_integration).execute
@ -126,7 +126,7 @@ RSpec.describe "Adding and removing exclusions to Beyond Identity integration",
end
context 'and the integration is activated for the instance' do
let(:instance_integration) { create(:beyond_identity_integration, :instance) }
let(:instance_integration) { create :beyond_identity_integration }
before do
::Integrations::PropagateService.new(instance_integration).execute
@ -144,7 +144,7 @@ RSpec.describe "Adding and removing exclusions to Beyond Identity integration",
it { expect(project.reload.beyond_identity_integration).not_to be_activated }
context 'and the integration is activated for the instance' do
let(:instance_integration) { create(:beyond_identity_integration, :instance) }
let(:instance_integration) { create :beyond_identity_integration }
before do
::Integrations::PropagateService.new(instance_integration).execute

View File

@ -75,12 +75,18 @@ RSpec.describe 'Sandboxed Mermaid rendering', :js, feature_category: :markdown d
project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
end
it 'includes mermaid frame correctly', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/553529' do
it 'includes mermaid frame correctly' do
visit(project_path(project))
wait_for_requests
wait_for_all_requests
expect(page.html).to include(expected)
page.within '.js-wiki-content' do
# the find is needed to ensure the lazy container is loaded, otherwise
# it can be a flaky test, similar to
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25408
#
expect(page.html).to include(expected)
end
end
end

View File

@ -80,7 +80,7 @@ RSpec.describe 'User Settings > GPG keys', feature_category: :user_profile do
end
context 'when external verification is required' do
let!(:beyond_identity_integration) { create(:beyond_identity_integration, :instance) }
let!(:beyond_identity_integration) { create(:beyond_identity_integration) }
let!(:gpg_key) do
create :gpg_key, externally_verified: externally_verified, user: user, key: GpgHelpers::User2.public_key
end

View File

@ -5,6 +5,7 @@ import { programmingLanguages } from 'jest/groups_projects/components/mock_data'
import {
FILTERED_SEARCH_TOKEN_LANGUAGE,
FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL,
FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL,
PAGINATION_TYPE_KEYSET,
} from '~/groups_projects/constants';
import {
@ -45,6 +46,7 @@ describe('AdminProjectsApp', () => {
filteredSearchSupportedTokens: [
FILTERED_SEARCH_TOKEN_LANGUAGE,
FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL,
FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL,
],
filteredSearchTermKey: FILTERED_SEARCH_TERM_KEY,
filteredSearchNamespace: FILTERED_SEARCH_NAMESPACE,

View File

@ -27,6 +27,7 @@ import {
SORT_DIRECTION_ASC,
PAGINATION_TYPE_KEYSET,
PAGINATION_TYPE_OFFSET,
FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL,
} from '~/groups_projects/constants';
import { RECENT_SEARCHES_STORAGE_KEY_PROJECTS } from '~/filtered_search/recent_searches_storage_keys';
import {
@ -50,6 +51,7 @@ import {
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper';
import { VISIBILITY_LEVEL_PRIVATE_STRING } from '~/visibility_level/constants';
import { programmingLanguages } from './mock_data';
jest.mock('~/alert');
@ -67,6 +69,7 @@ const defaultPropsData = {
filteredSearchSupportedTokens: [
FILTERED_SEARCH_TOKEN_LANGUAGE,
FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL,
FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL,
],
filteredSearchTermKey: FILTERED_SEARCH_TERM_KEY,
filteredSearchNamespace: FILTERED_SEARCH_NAMESPACE,
@ -271,6 +274,7 @@ describe('TabsWithList', () => {
[defaultPropsData.filteredSearchTermKey]: searchTerm,
[FILTERED_SEARCH_TOKEN_LANGUAGE]: ['5'],
[FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL]: ['50'],
[FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL]: [VISIBILITY_LEVEL_PRIVATE_STRING],
});
await waitForPromises();
});
@ -279,6 +283,7 @@ describe('TabsWithList', () => {
expect(successHandler).toHaveBeenCalledWith({
minAccessLevel: 'OWNER',
programmingLanguageName: 'CSS',
visibilityLevel: VISIBILITY_LEVEL_PRIVATE_STRING,
search: searchTerm,
skipContributed: false,
skipStarred: true,
@ -553,6 +558,7 @@ describe('TabsWithList', () => {
[defaultPropsData.filteredSearchTermKey]: 'foo',
[FILTERED_SEARCH_TOKEN_LANGUAGE]: '8',
[FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL]: ACCESS_LEVEL_OWNER_INTEGER,
[FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL]: VISIBILITY_LEVEL_PRIVATE_STRING,
[QUERY_PARAM_END_CURSOR]: mockEndCursor,
[QUERY_PARAM_START_CURSOR]: mockStartCursor,
};
@ -578,6 +584,7 @@ describe('TabsWithList', () => {
[defaultPropsData.filteredSearchTermKey]: query[defaultPropsData.filteredSearchTermKey],
[FILTERED_SEARCH_TOKEN_LANGUAGE]: query[FILTERED_SEARCH_TOKEN_LANGUAGE],
[FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL]: query[FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL],
[FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL]: query[FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL],
},
filtersAsQueryVariables: {
programmingLanguageName: 'CoffeeScript',

View File

@ -3,14 +3,7 @@ import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import VueApollo from 'vue-apollo';
import { mount, shallowMount } from '@vue/test-utils';
import {
GlAvatarLabeled,
GlBadge,
GlEmptyState,
GlKeysetPagination,
GlLoadingIcon,
GlTable,
} from '@gitlab/ui';
import { GlAvatarLabeled, GlBadge, GlKeysetPagination, GlLoadingIcon, GlTable } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import createMockApollo from 'helpers/mock_apollo_helper';
import { stubComponent } from 'helpers/stub_component';
@ -30,6 +23,7 @@ import {
} from '~/import_entities/import_groups/constants';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import EmptyResult from '~/vue_shared/components/empty_result.vue';
import importSourceUsersQuery from '~/members/placeholders/graphql/queries/import_source_users.query.graphql';
@ -93,7 +87,7 @@ describe('PlaceholdersTable', () => {
});
};
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findEmptyState = () => wrapper.findComponent(EmptyResult);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPagination = () => wrapper.findComponent(GlKeysetPagination);
const findTable = () => wrapper.findComponent(GlTable);
@ -363,9 +357,8 @@ describe('PlaceholdersTable', () => {
});
it('renders empty state with short query description', () => {
expect(findEmptyState().props('description')).toBe(
'Enter at least three characters to search.',
);
expect(findEmptyState().exists()).toBe(true);
expect(findEmptyState().props('searchMinimumLength')).toBe(3);
});
});
@ -382,7 +375,8 @@ describe('PlaceholdersTable', () => {
});
it('renders empty state with no results message', () => {
expect(findEmptyState().props('description')).toBe('Edit your search and try again');
expect(findEmptyState().exists()).toBe(true);
expect(findEmptyState().props('searchMinimumLength')).toBe(null);
});
});

View File

@ -1,212 +0,0 @@
import { nextTick } from 'vue';
import { GlButton, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking } from 'helpers/tracking_helper';
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
import MergeRequestExperienceSurveyApp from '~/surveys/merge_request_experience/app.vue';
import SatisfactionRate from '~/surveys/components/satisfaction_rate.vue';
const createRenderTrackedArguments = () => [
undefined,
'survey:mr_experience',
{
label: 'render',
extra: {
accountAge: 0,
},
},
];
describe('MergeRequestExperienceSurveyApp', () => {
let trackingSpy;
let wrapper;
let dismiss;
let dismisserComponent;
const findCloseButton = () =>
wrapper
.findAllComponents(GlButton)
.filter((button) => button.attributes('aria-label') === 'Close')
.at(0);
const createWrapper = ({ shouldShowCallout = true } = {}) => {
dismiss = jest.fn();
dismisserComponent = makeMockUserCalloutDismisser({
dismiss,
shouldShowCallout,
});
trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
wrapper = shallowMountExtended(MergeRequestExperienceSurveyApp, {
propsData: {
accountAge: 0,
},
stubs: {
UserCalloutDismisser: dismisserComponent,
GlSprintf,
},
});
};
beforeEach(() => {
localStorage.clear();
});
describe('when user callout is visible', () => {
beforeEach(() => {
createWrapper();
});
it('shows survey', () => {
expect(wrapper.html()).toContain('Overall, how satisfied are you with merge requests?');
expect(wrapper.findComponent(SatisfactionRate).exists()).toBe(true);
expect(wrapper.emitted().close).toBe(undefined);
});
it('tracks render once', () => {
expect(trackingSpy).toHaveBeenCalledWith(...createRenderTrackedArguments());
});
it("doesn't track subsequent renders", () => {
createWrapper();
expect(trackingSpy).toHaveBeenCalledWith(...createRenderTrackedArguments());
expect(trackingSpy).toHaveBeenCalledTimes(1);
});
describe('when close button clicked', () => {
beforeEach(() => {
findCloseButton().vm.$emit('click');
});
it('triggers user callout on close', () => {
expect(dismiss).toHaveBeenCalledTimes(1);
});
it('emits close event on close button click', () => {
expect(wrapper.emitted()).toMatchObject({ close: [[]] });
});
it('tracks dismissal', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', {
label: 'dismiss',
extra: {
accountAge: 0,
},
});
});
it('tracks subsequent renders', () => {
createWrapper();
expect(trackingSpy.mock.calls).toEqual([
createRenderTrackedArguments(),
expect.anything(),
createRenderTrackedArguments(),
]);
});
});
it('applies correct feature name for user callout', () => {
expect(wrapper.findComponent(dismisserComponent).props('featureName')).toBe(
'mr_experience_survey',
);
});
it('dismisses user callout on survey rate', () => {
const rate = wrapper.findComponent(SatisfactionRate);
expect(dismiss).not.toHaveBeenCalled();
rate.vm.$emit('rate', 5);
expect(dismiss).toHaveBeenCalledTimes(1);
});
it('steps through survey steps', async () => {
const rate = wrapper.findComponent(SatisfactionRate);
rate.vm.$emit('rate', 5);
await nextTick();
expect(wrapper.text()).toContain(
'How satisfied are you with speed/performance of merge requests?',
);
});
it('tracks survey rates', () => {
const rate = wrapper.findComponent(SatisfactionRate);
rate.vm.$emit('rate', 5);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', {
value: 5,
label: 'overall',
extra: {
accountAge: 0,
},
});
rate.vm.$emit('rate', 4);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', {
value: 4,
label: 'performance',
extra: {
accountAge: 0,
},
});
});
it('shows legal note', () => {
expect(wrapper.text()).toContain(
'By continuing, you acknowledge that responses will be used to improve GitLab and in accordance with the GitLab Privacy Policy.',
);
});
it('hides legal note after first step', async () => {
const rate = wrapper.findComponent(SatisfactionRate);
rate.vm.$emit('rate', 5);
await nextTick();
expect(wrapper.text()).not.toContain(
'By continuing, you acknowledge that responses will be used to improve GitLab and in accordance with the GitLab Privacy Policy.',
);
});
it('shows disappearing thanks message', async () => {
const rate = wrapper.findComponent(SatisfactionRate);
rate.vm.$emit('rate', 5);
await nextTick();
rate.vm.$emit('rate', 5);
await nextTick();
expect(wrapper.text()).toContain('Thank you for your feedback!');
expect(wrapper.emitted()).toMatchObject({});
jest.runOnlyPendingTimers();
expect(wrapper.emitted()).toMatchObject({ close: [[]] });
});
});
describe('when user callout is hidden', () => {
beforeEach(() => {
createWrapper({ shouldShowCallout: false });
});
it('emits close event', () => {
expect(wrapper.emitted()).toMatchObject({ close: [[]] });
});
it("doesn't track anything", () => {
expect(trackingSpy).toHaveBeenCalledTimes(0);
});
});
describe('when Escape key is pressed', () => {
beforeEach(() => {
createWrapper();
const event = new KeyboardEvent('keyup', { key: 'Escape' });
document.dispatchEvent(event);
});
it('emits close event', () => {
expect(wrapper.emitted()).toMatchObject({ close: [[]] });
expect(dismiss).toHaveBeenCalledTimes(1);
});
it('tracks dismissal', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', {
label: 'dismiss',
extra: {
accountAge: 0,
},
});
});
});
});

View File

@ -42,16 +42,16 @@ describe('NoteBody', () => {
expect(renderGFM).toHaveBeenCalled();
});
it('should not render "Edited" text when lastEdited is the same as createdAt', () => {
it('should not render "Edited" text when lastEditedBy is not present', () => {
const editedComponent = wrapper.findComponent(NoteEditedText);
expect(editedComponent.exists()).toBe(false);
});
it('should render "Edited" text when lastEditedAt is not the same as createdAt', () => {
it('should render "Edited" text when lastEditedBy is present', () => {
// remounting to trigger mounted function
wrapper = createWrapper({
note: { ...note, lastEditedAt: '2024-11-11T08:11:34Z' },
note: { ...note, lastEditedBy: { name: 'user', webPath: '/user' } },
noteableId,
});

View File

@ -1917,7 +1917,7 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
it { is_expected.to be_falsey }
context 'when beyond identity is disabled for a project' do
let_it_be(:integration) { create(:beyond_identity_integration, :instance, active: false) }
let_it_be(:integration) { create(:beyond_identity_integration, active: false) }
before do
allow(project).to receive(:beyond_identity_integration).and_return(integration)
@ -1927,7 +1927,7 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
end
context 'when a GPG key failed external validation and one GPC key is externally validated' do
let_it_be(:integration) { create(:beyond_identity_integration, :instance) }
let_it_be(:integration) { create(:beyond_identity_integration) }
before do
allow(project).to receive(:beyond_identity_integration).and_return(integration)
@ -1939,7 +1939,7 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
end
context 'when there are no GPG keys externally validated' do
let_it_be(:integration) { create(:beyond_identity_integration, :instance) }
let_it_be(:integration) { create(:beyond_identity_integration) }
before do
allow(project).to receive(:beyond_identity_integration).and_return(integration)
@ -1951,7 +1951,7 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
end
context 'when GPG keys are missing' do
let_it_be(:integration) { create(:beyond_identity_integration, :instance) }
let_it_be(:integration) { create(:beyond_identity_integration) }
before do
allow(project).to receive(:beyond_identity_integration).and_return(integration)

View File

@ -0,0 +1,52 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::EncryptMissedCiRunnerTokens,
migration: :gitlab_ci, feature_category: :fleet_visibility do
let(:connection) { Ci::ApplicationRecord.connection }
describe '#perform' do
let(:runners) { table(:ci_runners, primary_key: :id) }
let(:args) do
min, max = runners.pick('MIN(id)', 'MAX(id)')
{
start_id: min,
end_id: max,
batch_table: 'ci_runners',
batch_column: 'id',
sub_batch_size: 100,
pause_ms: 0,
connection: connection
}
end
let!(:runner_with_plain_token) { runners.create!(runner_type: 1, token: 'plain_token') }
let!(:runner_without_token) { runners.create!(runner_type: 1, token: nil, token_encrypted: nil) }
let!(:runner_with_encrypted_token) do
runners.create!(runner_type: 1, token: nil,
token_encrypted: Gitlab::CryptoHelper.aes256_gcm_encrypt(SecureRandom.hex(32)))
end
let!(:runner_with_plain_and_encrypted_token) do
runners.create!(runner_type: 1, token: 'plain_token2',
token_encrypted: Gitlab::CryptoHelper.aes256_gcm_encrypt('encrypted_token2'))
end
subject(:perform_migration) { described_class.new(**args).perform }
it 'encrypts plain tokens', :aggregate_failures do
expect { perform_migration }
.to change { runner_with_plain_token.reload.token_encrypted }
.from(nil).to(Authn::TokenField::EncryptionHelper.encrypt_token('plain_token'))
.and change { runner_with_plain_token.reload.token }.to(nil)
.and not_change { runner_with_encrypted_token.reload.token }
.and not_change { runner_with_encrypted_token.reload.token_encrypted }
.and not_change { runner_with_plain_and_encrypted_token.reload.token }
.and not_change { runner_with_plain_and_encrypted_token.reload.token_encrypted }
.and not_change { runner_without_token.reload.token }
.and not_change { runner_without_token.reload.token_encrypted }
end
end
end

View File

@ -3,17 +3,10 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::UpdateJiraTrackerDataDeploymentTypeBasedOnUrl do
let(:organization) { table(:organizations).create!(name: 'organization', path: 'organization') }
let(:namespaces_table) { table(:namespaces) }
let(:group1) { namespaces_table.create!(name: 'group1', path: 'group1', organization_id: organization.id) }
let(:group2) { namespaces_table.create!(name: 'group2', path: 'group2', organization_id: organization.id) }
let(:group3) { namespaces_table.create!(name: 'group3', path: 'group3', organization_id: organization.id) }
let(:integrations_table) { table(:integrations) }
let(:service_jira_cloud) { integrations_table.create!(id: 1, type_new: 'JiraService', group_id: group1.id) }
let(:service_jira_server) { integrations_table.create!(id: 2, type_new: 'JiraService', group_id: group2.id) }
let(:service_jira_unknown) { integrations_table.create!(id: 3, type_new: 'JiraService', group_id: group3.id) }
let(:service_jira_cloud) { integrations_table.create!(id: 1, type_new: 'JiraService') }
let(:service_jira_server) { integrations_table.create!(id: 2, type_new: 'JiraService') }
let(:service_jira_unknown) { integrations_table.create!(id: 3, type_new: 'JiraService') }
let(:table_name) { :jira_tracker_data }
let(:batch_column) { :id }

View File

@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe ::Gitlab::BeyondIdentity::Client, feature_category: :source_code_management do
let_it_be_with_reload(:integration) { create(:beyond_identity_integration, :instance) }
let_it_be_with_reload(:integration) { create(:beyond_identity_integration) }
let(:stubbed_response) do
{ 'authorized' => true }.to_json

View File

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Checks::Integrations::BeyondIdentityCheck, feature_category: :source_code_management do
include_context 'changes access checks context'
let(:integration_check) { Gitlab::Checks::IntegrationsCheck.new(changes_access) }
let!(:beyond_identity_integration) { create(:beyond_identity_integration, :instance) }
let!(:beyond_identity_integration) { create(:beyond_identity_integration) }
subject(:check) { described_class.new(integration_check) }
@ -65,7 +65,7 @@ RSpec.describe Gitlab::Checks::Integrations::BeyondIdentityCheck, feature_catego
context 'when service accounts are excluded' do
let!(:beyond_identity_integration) do
create(:beyond_identity_integration, :instance, exclude_service_accounts: true)
create(:beyond_identity_integration, exclude_service_accounts: true)
end
it 'does not raise an error' do

View File

@ -2,10 +2,12 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis, feature_category: :database do
let(:load_balancer) { ActiveRecord::Base.load_balancer }
let(:primary_write_location) { 'the-primary-lsn' }
let(:last_write_location) { 'the-last-write-lsn' }
let(:namespace) { 'user' }
let(:location) { '0/1234ABED' }
let(:sticking) do
described_class.new(load_balancer)
@ -141,4 +143,88 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
subject { sticking.bulk_stick(:user, ids) }
end
end
describe '#log_database_sticking_operations_enabled?' do
it 'returns true when the feature flag is enabled' do
stub_feature_flags(log_database_sticking_operations: true)
expect(sticking.send(:log_database_sticking_operations_enabled?)).to be true
end
it 'returns false when the feature flag is disabled' do
stub_feature_flags(log_database_sticking_operations: false)
expect(sticking.send(:log_database_sticking_operations_enabled?)).to be false
end
end
describe '#capture_stick_logs' do
let(:id) { 123 }
let(:ids) { [124, 125, 16] }
context 'when logging is enabled and namespace is user' do
before do
stub_feature_flags(log_database_sticking_operations: true)
allow(sticking).to receive(:with_primary_write_location).and_yield(location)
allow(sticking).to receive(:set_write_location_for)
allow(sticking).to receive(:use_primary!)
end
it 'logs the sticking operation with correct parameters' do
expect(::Gitlab::Database::LoadBalancing::Logger).to receive(:info).with(
event: :load_balancer_stick_logging,
client_id: "#{namespace}/#{id}",
stick_id: id,
stick_type: namespace,
current_lsn: location
)
sticking.stick(namespace, id)
end
it 'logs only the first ID for bulk sticking operations with correct parameters' do
expect(::Gitlab::Database::LoadBalancing::Logger).to receive(:info).with(
event: :load_balancer_stick_logging,
client_id: "#{namespace}/#{ids.first}",
stick_id: ids.first,
stick_type: namespace,
current_lsn: location
)
sticking.bulk_stick(namespace, ids)
end
end
context 'when logging is disabled' do
before do
stub_feature_flags(log_database_sticking_operations: false)
allow(sticking).to receive(:with_primary_write_location).and_yield(location)
allow(sticking).to receive(:set_write_location_for)
allow(sticking).to receive(:use_primary!)
end
it 'does not log anything' do
expect(::Gitlab::Database::LoadBalancing::Logger).not_to receive(:info)
sticking.stick(namespace, id)
end
end
context 'when logging is enabled and namespace is not user' do
let(:namespace) { 'project' }
before do
stub_feature_flags(log_database_sticking_operations: true)
allow(sticking).to receive(:with_primary_write_location).and_yield(location)
allow(sticking).to receive(:set_write_location_for)
allow(sticking).to receive(:use_primary!)
end
it 'does not log anything' do
expect(::Gitlab::Database::LoadBalancing::Logger).not_to receive(:info)
sticking.bulk_stick(namespace, ids)
end
end
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe QueueEncryptMissedCiRunnerTokens, migration: :gitlab_ci, feature_category: :fleet_visibility do
let!(:batched_migration) { described_class::MIGRATION }
it 'schedules a new batched migration' do
reversible_migration do |migration|
migration.before -> {
expect(batched_migration).not_to have_scheduled_batched_migration
}
migration.after -> {
expect(batched_migration).to have_scheduled_batched_migration(
gitlab_schema: :gitlab_ci,
table_name: :ci_runners,
column_name: :id
)
}
end
end
end

View File

@ -0,0 +1,74 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe CleanupUntetheredIntegrations, feature_category: :integrations do
let(:organizations) { table(:organizations) }
let(:integrations) { table(:integrations) }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let!(:default_organization) { organizations.create!(id: 1, name: 'Default', path: 'default') }
let!(:other_organization) { organizations.create!(id: 2, name: 'Other', path: 'other') }
let!(:group) { namespaces.create!(name: 'bar', path: 'bar', type: 'Group', organization_id: 1) }
let!(:project) do
projects.create!(
name: 'project',
path: 'project',
namespace_id: group.id,
organization_id: 1,
project_namespace_id: group.id
)
end
let!(:untethered_integration) do
integrations.create!(
instance: false,
group_id: nil,
project_id: nil,
type_new: 'Integrations::MockMonitoring'
)
end
let!(:another_untethered_integration) do
integrations.create!(
instance: false,
group_id: nil,
project_id: nil,
type_new: 'Integrations::MockCi'
)
end
let!(:tethered_integration) do
integrations.create!(
instance: false,
group_id: group.id,
type_new: 'Integrations::MockCi'
)
end
let!(:another_tethered_integration) do
integrations.create!(
instance: false,
project_id: project.id,
group_id: nil,
type_new: 'Integrations::Asana'
)
end
describe "#up" do
it 'removes untethered integrations' do
expect(integrations.count).to eq(4)
expect(untethered_integration).to be_present
expect(another_untethered_integration).to be_present
migrate!
expect(integrations.count).to eq(2)
expect(tethered_integration.reload).to be_present
expect(another_tethered_integration.reload).to be_present
end
end
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe RemoveNotNullIntegrationsConstraint, feature_category: :integrations do
include Database::TableSchemaHelpers
describe '#up' do
before do
ApplicationRecord
.connection
.execute(
'ALTER TABLE integrations ADD CONSTRAINT check_2aae034509 ' \
'CHECK ((num_nonnulls(group_id, organization_id, project_id) = 1)) NOT VALID;'
)
end
it 'removes the check constraint' do
expect(check_constraint_definition(:integrations, 'check_2aae034509'))
.to eq('CHECK ((num_nonnulls(group_id, organization_id, project_id) = 1)) NOT VALID')
migrate!
expect(check_constraint_definition(:integrations, 'check_2aae034509')).to be_nil
end
end
end

View File

@ -53,28 +53,21 @@ RSpec.describe Integration, feature_category: :integrations do
it { is_expected.to validate_presence_of(:type) }
it { is_expected.to validate_exclusion_of(:type).in_array(described_class::BASE_CLASSES) }
where(:project_id, :group_id, :instance, :organization_id, :valid) do
1 | nil | false | nil | true
nil | 1 | false | nil | true
nil | nil | true | 1 | true
nil | nil | false | nil | false
1 | 1 | false | 1 | false
1 | nil | true | nil | false
nil | 1 | false | nil | true
nil | 1 | true | 1 | false
where(:project_id, :group_id, :instance, :valid) do
1 | nil | false | true
nil | 1 | false | true
nil | nil | true | true
nil | nil | false | false
1 | 1 | false | false
1 | nil | false | true
1 | nil | true | false
nil | 1 | false | true
nil | 1 | true | false
end
with_them do
it 'validates the integration' do
expect(
build(
:integration,
project_id: project_id,
group_id: group_id,
organization_id: organization_id,
instance: instance
).valid?
).to eq(valid)
expect(build(:integration, project_id: project_id, group_id: group_id, instance: instance).valid?).to eq(valid)
end
end
@ -363,95 +356,57 @@ RSpec.describe Integration, feature_category: :integrations do
end
describe '.find_or_initialize_all_non_project_specific' do
shared_examples 'integration instances' do |include_instance_specific|
it 'returns the available integration instances' do
integrations = described_class.find_or_initialize_all_non_project_specific(
described_class.for_instance, include_instance_specific: include_instance_specific
).map(&:to_param)
shared_examples 'integration instances' do
[false, true].each do |include_instance_specific|
context "with include_instance_specific value equal to #{include_instance_specific}" do
it 'returns the available integration instances' do
integrations = described_class.find_or_initialize_all_non_project_specific(
described_class.for_instance, include_instance_specific: include_instance_specific
).map(&:to_param)
expect(integrations).to match_array(
described_class.available_integration_names(
include_project_specific: false,
include_instance_specific: include_instance_specific
)
)
end
expect(integrations).to match_array(
described_class.available_integration_names(
include_project_specific: false,
include_instance_specific: include_instance_specific)
)
end
it 'does not create integration instances' do
expect do
described_class.find_or_initialize_all_non_project_specific(
described_class.for_instance,
include_instance_specific: include_instance_specific
)
end.not_to change { described_class.count }
it 'does not create integration instances' do
expect do
described_class.find_or_initialize_all_non_project_specific(
described_class.for_instance,
include_instance_specific: include_instance_specific
)
end.not_to change { described_class.count }
end
end
end
end
context 'without existing instance integrations' do
it_behaves_like 'integration instances', false
it_behaves_like 'integration instances', true
end
it_behaves_like 'integration instances'
context 'with existing instances' do
context 'with all existing instances' do
def integration_hash(type)
integration_hash = Integration.new(type: type).to_database_hash
integration_hash[:organization_id] = create(:organization).id
integration_hash[:instance] = true
integration_hash
Integration.new(instance: true, type: type).to_database_hash
end
context 'when all instance integrations are present' do
context 'when include_instance_specific is true' do
before do
attrs = described_class
.available_integration_types(
include_project_specific: false,
include_instance_specific: true
)
.map do |integration_type|
integration_hash(integration_type)
end
described_class.insert_all(attrs)
end
it_behaves_like 'integration instances', true
context 'with a previous existing integration (:mock_ci) and a new integration (:asana)' do
before do
described_class.insert(integration_hash(:mock_ci))
described_class.delete_by(**integration_hash(:asana))
end
it_behaves_like 'integration instances', true
end
before do
attrs = described_class.available_integration_types(include_project_specific: false).map do |integration_type|
integration_hash(integration_type)
end
context 'when include_instance_specific is false' do
before do
attrs = described_class
.available_integration_types(
include_project_specific: false,
include_instance_specific: false
)
.map do |integration_type|
integration_hash(integration_type)
end
described_class.insert_all(attrs)
end
described_class.insert_all(attrs)
end
it_behaves_like 'integration instances'
it_behaves_like 'integration instances', false
context 'with a previous existing integration (:mock_ci) and a new integration (:asana)' do
before do
described_class.insert(integration_hash(:mock_ci))
described_class.delete_by(**integration_hash(:asana))
end
it_behaves_like 'integration instances', false
end
context 'with a previous existing integration (:mock_ci) and a new integration (:asana)' do
before do
described_class.insert(integration_hash(:mock_ci))
described_class.delete_by(**integration_hash(:asana))
end
it_behaves_like 'integration instances'
end
end
@ -460,8 +415,7 @@ RSpec.describe Integration, feature_category: :integrations do
create(:jira_integration, :instance)
end
it_behaves_like 'integration instances', true
it_behaves_like 'integration instances', false
it_behaves_like 'integration instances'
end
end
@ -648,8 +602,8 @@ RSpec.describe Integration, feature_category: :integrations do
end
describe '.create_from_default_integrations' do
let!(:instance_integration) { create(:prometheus_integration, :instance, api_url: 'https://prometheus.instance.com/', organization: create(:organization)) }
let!(:instance_level_instance_specific_integration) { create(:beyond_identity_integration, :instance) }
let!(:instance_integration) { create(:prometheus_integration, :instance, api_url: 'https://prometheus.instance.com/') }
let!(:instance_level_instance_specific_integration) { create(:beyond_identity_integration) }
it 'creates integrations from default integrations' do
expect(described_class).to receive(:create_from_active_default_integrations)
@ -783,7 +737,7 @@ RSpec.describe Integration, feature_category: :integrations do
end
context 'when the integration is instance specific' do
let!(:instance_integration) { create(:beyond_identity_integration, :instance) }
let!(:instance_integration) { create(:beyond_identity_integration) }
it 'does not create an integration from the instance level instance specific integration' do
described_class.create_from_active_default_integrations(project, :project_id)
@ -796,7 +750,7 @@ RSpec.describe Integration, feature_category: :integrations do
describe '.create_from_default_instance_specific_integrations' do
context 'with an active instance-level integration' do
let!(:instance_integration) { create(:beyond_identity_integration, :instance) }
let!(:instance_integration) { create(:beyond_identity_integration) }
it 'creates an integration from the instance-level integration' do
described_class.create_from_default_instance_specific_integrations(project, :project_id)
@ -1514,7 +1468,7 @@ RSpec.describe Integration, feature_category: :integrations do
it 'saves correctly using insert_all' do
hash = record.to_database_hash
hash[:project_id] = project.id
hash[:project_id] = project
expect do
described_class.insert_all([hash])
@ -1522,9 +1476,6 @@ RSpec.describe Integration, feature_category: :integrations do
expect(described_class.last).not_to eq record
expect(described_class.last).to have_attributes(properties: db_props)
expect(described_class.last.project_id).to eq(project.id)
expect(described_class.last.group_id).to be_nil
expect(described_class.last.organization_id).to be_nil
end
end
end

View File

@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Integrations::BeyondIdentity, feature_category: :integrations do
subject(:integration) { create(:beyond_identity_integration, :instance) }
subject(:integration) { create(:beyond_identity_integration) }
describe 'validations' do
context 'when inactive' do
@ -65,28 +65,22 @@ RSpec.describe Integrations::BeyondIdentity, feature_category: :integrations do
end
describe '.activated_for_instance?' do
let!(:integration) do
create(:beyond_identity_integration, instance: instance, active: active, group: group, organization: organization)
end
let!(:integration) { create(:beyond_identity_integration, instance: instance, active: active, group: group) }
let_it_be(:group_for_integration) { create(:group) }
let_it_be(:organization_for_integration) { create(:organization) }
subject(:activated) { described_class.activated_for_instance? }
subject { described_class.activated_for_instance? }
using RSpec::Parameterized::TableSyntax
where(:instance, :group, :active, :expected, :organization) do
true | nil | true | true | lazy { organization_for_integration }
false | lazy { group_for_integration } | true | false | nil
true | nil | false | false | lazy { organization_for_integration }
false | lazy { group_for_integration } | false | false | nil
where(:instance, :group, :active, :expected) do
true | nil | true | true
false | lazy { group_for_integration } | true | false
true | nil | false | false
false | lazy { group_for_integration } | false | false
end
with_them do
it 'returns true if integration is activated' do
expect(activated).to eq(expected)
end
it { is_expected.to eq(expected) }
end
end
end

View File

@ -133,7 +133,6 @@ RSpec.describe Integrations::DroneCi, :use_clean_rails_memory_store_caching, fea
it 'does not create a hook if project is not present' do
integration.project = nil
integration.instance = true
integration.organization = create(:organization)
expect { integration.save! }.not_to change(ServiceHook, :count)
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Admin::SlacksController, :enable_admin_mode, :with_current_organization, feature_category: :integrations do
RSpec.describe Admin::SlacksController, :enable_admin_mode, feature_category: :integrations do
let_it_be(:user) { create(:admin) }
before do

View File

@ -22,7 +22,7 @@ RSpec.describe 'Querying for integration exclusions', feature_category: :integra
end
context 'when the user is authorized' do
let!(:instance_integration) { create(:beyond_identity_integration, :instance) }
let!(:instance_integration) { create(:beyond_identity_integration) }
let!(:integration_exclusion) do
create(:beyond_identity_integration, active: false, instance: false, project: project2, inherit_from_id: nil)
end

View File

@ -60,7 +60,7 @@ RSpec.describe Mutations::Integrations::Exclusions::Create, feature_category: :i
end
context 'when integrations exist for the projects' do
let!(:instance_exclusion) { create(:beyond_identity_integration, :instance) }
let!(:instance_exclusion) { create(:beyond_identity_integration) }
let!(:existing_exclusion) do
create(:beyond_identity_integration, project: project2, active: false, inherit_from_id: instance_exclusion.id,
instance: false)

View File

@ -48,7 +48,7 @@ RSpec.describe Mutations::Integrations::Exclusions::Delete, feature_category: :i
end
context 'and the integration is active for the instance' do
let!(:instance_integration) { create(:beyond_identity_integration, :instance) }
let!(:instance_integration) { create(:beyond_identity_integration) }
context 'and there is a group exclusion', :sidekiq_inline do
let!(:group_exclusion) do

View File

@ -0,0 +1,146 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'NamespacesRegenerateNewWorkItemEmailAddress', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :private) }
let_it_be(:project) { create(:project, group: group) }
let(:mutation) do
graphql_mutation(
:namespaces_regenerate_new_work_item_email_address,
{ full_path: namespace.full_path },
<<~FIELDS
namespace {
id
fullPath
}
errors
FIELDS
)
end
shared_examples 'successful regeneration' do
it 'regenerates the new work item email address' do
expect { post_graphql_mutation(mutation, current_user: user) }
.to change { user.reload.incoming_email_token }
expect(response).to have_gitlab_http_status(:success)
expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['namespace']['fullPath'])
.to eq(namespace.full_path)
expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['errors']).to be_empty
end
end
shared_examples 'permission denied' do
it 'returns an error' do
post_graphql_mutation(mutation, current_user: user)
expect(graphql_errors).to include(
a_hash_including(
'message' => 'The resource that you are attempting to access does not exist or you don\'t have ' \
'permission to perform this action'
)
)
end
end
describe 'for group namespace' do
let(:namespace) { group }
context 'when work item creation via email is supported' do
before do
stub_incoming_email_setting(enabled: true, address: 'incoming+%{key}@localhost.com')
user.ensure_incoming_email_token!
end
context 'when user has access to namespace' do
before_all do
group.add_developer(user)
end
it 'returns an error about work item creation only being supported for projects' do
post_graphql_mutation(mutation, current_user: user)
expect(response).to have_gitlab_http_status(:success)
expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['namespace']).to be_nil
expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['errors'])
.to include('Work item creation via email is only supported for projects')
end
end
context 'when user does not have access to namespace' do
it_behaves_like 'permission denied'
end
end
end
describe 'for project namespace' do
let(:namespace) { project.project_namespace }
context 'when work item creation via email is supported' do
before do
stub_incoming_email_setting(enabled: true, address: 'incoming+%{key}@localhost.com')
user.ensure_incoming_email_token!
end
context 'when user has access to namespace' do
before_all do
project.add_developer(user)
end
it_behaves_like 'successful regeneration'
end
context 'when user does not have access to namespace' do
it_behaves_like 'permission denied'
end
end
context 'when work item creation via email is not supported' do
before do
stub_incoming_email_setting(enabled: false)
end
before_all do
project.add_developer(user)
end
it 'returns an error about work item creation not being supported' do
post_graphql_mutation(mutation, current_user: user)
expect(response).to have_gitlab_http_status(:success)
expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['namespace']).to be_nil
expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['errors'])
.to include('Work item creation via email is not supported')
end
end
context 'when token reset fails' do
before do
stub_incoming_email_setting(enabled: true, address: 'incoming+%{key}@localhost.com')
user.ensure_incoming_email_token!
allow_next_found_instance_of(User) do |instance|
allow(instance).to receive(:reset_incoming_email_token!).and_return(false)
end
end
before_all do
project.add_developer(user)
end
it 'returns an error about failed regeneration' do
post_graphql_mutation(mutation, current_user: user)
expect(response).to have_gitlab_http_status(:success)
expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['namespace']).to be_nil
expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['errors'])
.to include('Failed to regenerate new work item email address')
end
end
end
end

View File

@ -18,7 +18,7 @@ RSpec.describe GpgKeys::ValidateIntegrationsService, feature_category: :source_c
end
context 'when BeyondIdentity integration is not activated' do
let_it_be(:integration) { create(:beyond_identity_integration, :instance, active: false) }
let_it_be(:integration) { create(:beyond_identity_integration, active: false) }
it 'return false' do
expect(::Gitlab::BeyondIdentity::Client).not_to receive(:new)
@ -28,7 +28,7 @@ RSpec.describe GpgKeys::ValidateIntegrationsService, feature_category: :source_c
end
context 'when BeyondIdentity integration is activated' do
let_it_be(:integration) { create(:beyond_identity_integration, :instance) }
let_it_be(:integration) { create(:beyond_identity_integration) }
it 'returns true on successful check' do
expect_next_instance_of(::Gitlab::BeyondIdentity::Client) do |instance|

View File

@ -321,7 +321,7 @@ RSpec.describe Groups::CreateService, '#execute', feature_category: :groups_and_
end
context 'when an instance-level instance specific integration' do
let_it_be(:instance_specific_integration) { create(:beyond_identity_integration, :instance) }
let_it_be(:instance_specific_integration) { create(:beyond_identity_integration) }
it 'creates integration inheriting from the instance level integration' do
expect(created_group.integrations.count).to eq(1)

View File

@ -366,7 +366,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline, feature_category: :grou
end
context 'with instance specific integration' do
let_it_be(:instance_specific_integration) { create(:beyond_identity_integration, :instance) }
let_it_be(:instance_specific_integration) { create(:beyond_identity_integration) }
let_it_be(:group_instance_specific_integration) do
create(
:beyond_identity_integration,

View File

@ -72,7 +72,7 @@ RSpec.describe Integrations::Exclusions::CreateService, feature_category: :sourc
end
context 'when there are existing custom settings' do
let_it_be(:instance_level_integration) { create(:beyond_identity_integration, :instance) }
let_it_be(:instance_level_integration) { create(:beyond_identity_integration) }
let_it_be(:project_level_integration) do
create(
:beyond_identity_integration,

View File

@ -67,7 +67,7 @@ RSpec.describe Integrations::Exclusions::DestroyService, feature_category: :sour
end
context 'and the integration is active for the instance' do
let!(:instance_integration) { create(:beyond_identity_integration, :instance) }
let!(:instance_integration) { create(:beyond_identity_integration) }
it 'updates the exclusion integration to be active' do
expect { execute }.to change { exclusion.reload.active }.from(false).to(true)

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Integrations::SlackInstallation::InstanceService, :enable_admin_mode, :with_current_organization, feature_category: :integrations do
RSpec.describe Integrations::SlackInstallation::InstanceService, :enable_admin_mode, feature_category: :integrations do
let_it_be(:user) { create(:admin) }
let(:params) { {} }
@ -13,10 +13,9 @@ RSpec.describe Integrations::SlackInstallation::InstanceService, :enable_admin_m
let(:integration) { Integrations::GitlabSlackApplication.for_instance.first }
let(:redirect_url) { Gitlab::Routing.url_helpers.slack_auth_admin_application_settings_slack_url }
let(:enqueues_propagation_worker) { true }
let(:params) { { code: oauth_code, organization_id: current_organization.id } }
def create_gitlab_slack_application_integration!
Integrations::GitlabSlackApplication.create!(instance: true, organization: current_organization)
Integrations::GitlabSlackApplication.create!(instance: true)
end
end
end

View File

@ -935,7 +935,7 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :groups_an
subject(:project) { create_project(user, opts) }
context 'when an instance-level instance specific integration' do
let!(:instance_specific_integration) { create(:beyond_identity_integration, :instance) }
let!(:instance_specific_integration) { create(:beyond_identity_integration) }
it 'creates integration inheriting from the instance level integration' do
expect(project.integrations.count).to eq(1)

View File

@ -284,7 +284,7 @@ RSpec.describe Projects::TransferService, feature_category: :groups_and_projects
end
context 'when the new default integration is instance specific and deactivated' do
let!(:instance_specific_integration) { create(:beyond_identity_integration, :instance) }
let!(:instance_specific_integration) { create(:beyond_identity_integration) }
let!(:project_instance_specific_integration) do
create(
:beyond_identity_integration,

View File

@ -288,9 +288,6 @@ RSpec.configure do |config|
# (ie. ApplicationSetting#auto_devops_enabled)
stub_feature_flags(force_autodevops_on_by_default: false)
# The survey popover can block the diffs causing specs to fail
stub_feature_flags(mr_experience_survey: false)
# Using FortiAuthenticator as OTP provider is disabled by default in
# tests, until we introduce it in user settings
stub_feature_flags(forti_authenticator: false)

View File

@ -75,13 +75,7 @@ RSpec.describe Projects::PostCreationWorker, feature_category: :source_code_mana
end
it 'cleans invalid record and logs warning', :aggregate_failures do
invalid_integration_record = build(
:prometheus_integration,
properties: { api_url: nil, manual_configuration: true },
project: build(:project),
group: build(:group)
)
invalid_integration_record = build(:prometheus_integration, properties: { api_url: nil, manual_configuration: true })
allow(::Integrations::Prometheus).to receive(:new).and_return(invalid_integration_record)
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(an_instance_of(ActiveRecord::RecordInvalid), include(extra: { project_id: a_kind_of(Integer) })).twice