Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
dab4155f21
commit
b1c7c08640
|
|
@ -189,11 +189,57 @@ async function fetchTraces(tracingUrl, { filters = {}, pageToken, pageSize } = {
|
|||
}
|
||||
}
|
||||
|
||||
export function buildClient({ provisioningUrl, tracingUrl }) {
|
||||
async function fetchServices(servicesUrl) {
|
||||
try {
|
||||
const { data } = await axios.get(servicesUrl, {
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
if (!Array.isArray(data.services)) {
|
||||
throw new Error('failed to fetch services. invalid response'); // eslint-disable-line @gitlab/require-i18n-strings
|
||||
}
|
||||
|
||||
return data.services;
|
||||
} catch (e) {
|
||||
return reportErrorAndThrow(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchOperations(operationsUrl, serviceName) {
|
||||
try {
|
||||
if (!serviceName) {
|
||||
throw new Error('fetchOperations() - serviceName is required.');
|
||||
}
|
||||
if (!operationsUrl.includes('$SERVICE_NAME$')) {
|
||||
throw new Error('fetchOperations() - operationsUrl must contain $SERVICE_NAME$');
|
||||
}
|
||||
const url = operationsUrl.replace('$SERVICE_NAME$', serviceName);
|
||||
const { data } = await axios.get(url, {
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
if (!Array.isArray(data.operations)) {
|
||||
throw new Error('failed to fetch operations. invalid response'); // eslint-disable-line @gitlab/require-i18n-strings
|
||||
}
|
||||
|
||||
return data.operations;
|
||||
} catch (e) {
|
||||
return reportErrorAndThrow(e);
|
||||
}
|
||||
}
|
||||
|
||||
export function buildClient({ provisioningUrl, tracingUrl, servicesUrl, operationsUrl } = {}) {
|
||||
if (!provisioningUrl || !tracingUrl || !servicesUrl || !operationsUrl) {
|
||||
throw new Error(
|
||||
'missing required params. provisioningUrl, tracingUrl, servicesUrl, operationsUrl are required',
|
||||
);
|
||||
}
|
||||
return {
|
||||
enableTraces: () => enableTraces(provisioningUrl),
|
||||
isTracingEnabled: () => isTracingEnabled(provisioningUrl),
|
||||
fetchTraces: (filters) => fetchTraces(tracingUrl, filters),
|
||||
fetchTrace: (traceId) => fetchTrace(tracingUrl, traceId),
|
||||
fetchServices: () => fetchServices(servicesUrl),
|
||||
fetchOperations: (serviceName) => fetchOperations(operationsUrl, serviceName),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,11 +13,19 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
provisioningUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
tracingUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
provisioningUrl: {
|
||||
servicesUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
operationsUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
|
@ -58,6 +66,8 @@ export default {
|
|||
this.observabilityClient = buildClient({
|
||||
provisioningUrl: this.provisioningUrl,
|
||||
tracingUrl: this.tracingUrl,
|
||||
servicesUrl: this.servicesUrl,
|
||||
operationsUrl: this.operationsUrl,
|
||||
});
|
||||
this.$refs.observabilitySkeleton?.onContentLoaded();
|
||||
} else if (status === 'error') {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
|
||||
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
|
||||
import { TYPE_ISSUE } from '~/issues/constants';
|
||||
|
|
@ -11,6 +12,7 @@ export default {
|
|||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
name: 'Reviewers',
|
||||
components: {
|
||||
GlButton,
|
||||
CollapsedReviewerList,
|
||||
UncollapsedReviewerList,
|
||||
},
|
||||
|
|
@ -64,15 +66,16 @@ export default {
|
|||
{{ __('None') }}
|
||||
<template v-if="editable">
|
||||
-
|
||||
<button
|
||||
type="button"
|
||||
class="gl-button btn-link gl-reset-color!"
|
||||
<gl-button
|
||||
category="tertiary"
|
||||
variant="link"
|
||||
class="gl-ml-2"
|
||||
data-testid="assign-yourself"
|
||||
data-qa-selector="assign_yourself_button"
|
||||
@click="assignSelf"
|
||||
>
|
||||
{{ __('assign yourself') }}
|
||||
</button>
|
||||
<span class="gl-text-gray-500 gl-hover-text-blue-800">{{ __('assign yourself') }}</span>
|
||||
</gl-button>
|
||||
</template>
|
||||
</span>
|
||||
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ export default {
|
|||
:title="setTooltip(btn)"
|
||||
:href="btn.href"
|
||||
:target="btn.target"
|
||||
:class="[{ 'gl-mr-1': index !== tertiaryButtons.length - 1 }, btn.class]"
|
||||
:class="[{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }, btn.class]"
|
||||
:data-clipboard-text="btn.dataClipboardText"
|
||||
:data-method="btn.dataMethod"
|
||||
:icon="btn.icon"
|
||||
|
|
|
|||
|
|
@ -24,10 +24,6 @@ export default {
|
|||
GlSprintf,
|
||||
},
|
||||
mixins: [approvalsMixin, glFeatureFlagsMixin()],
|
||||
provide: {
|
||||
expandDetailsTooltip: __('Expand eligible approvers'),
|
||||
collapseDetailsTooltip: __('Collapse eligible approvers'),
|
||||
},
|
||||
props: {
|
||||
mr: {
|
||||
type: Object,
|
||||
|
|
@ -248,6 +244,8 @@ export default {
|
|||
is-collapsible
|
||||
collapse-on-desktop
|
||||
:collapsed="collapsed"
|
||||
:expand-details-tooltip="__('Expand eligible approvers')"
|
||||
:collapse-details-tooltip="__('Collapse eligible approvers')"
|
||||
@toggle="() => $emit('toggle')"
|
||||
>
|
||||
<template v-if="$apollo.queries.approvals.loading">{{ $options.FETCH_LOADING }}</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
<script>
|
||||
import { __ } from '~/locale';
|
||||
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
|
||||
import conflictsStateQuery from '../../queries/states/conflicts.query.graphql';
|
||||
import ActionButtons from '../action_buttons.vue';
|
||||
import MergeChecksMessage from './message.vue';
|
||||
|
||||
export default {
|
||||
name: 'MergeChecksConflicts',
|
||||
components: {
|
||||
MergeChecksMessage,
|
||||
ActionButtons,
|
||||
},
|
||||
mixins: [mergeRequestQueryVariablesMixin],
|
||||
apollo: {
|
||||
state: {
|
||||
query: conflictsStateQuery,
|
||||
variables() {
|
||||
return this.mergeRequestQueryVariables;
|
||||
},
|
||||
update: (data) => data?.project?.mergeRequest,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
check: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
mr: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
state: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isLoading() {
|
||||
return this.$apollo.queries.state.loading;
|
||||
},
|
||||
userPermissions() {
|
||||
return this.state.userPermissions;
|
||||
},
|
||||
showResolveButton() {
|
||||
return (
|
||||
this.mr.conflictResolutionPath &&
|
||||
this.userPermissions.pushToSourceBranch &&
|
||||
!this.state.sourceBranchProtected
|
||||
);
|
||||
},
|
||||
tertiaryActionsButtons() {
|
||||
if (this.state.shouldBeRebased) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
text: __('Resolve locally'),
|
||||
class: 'js-check-out-modal-trigger',
|
||||
},
|
||||
this.showResolveButton && {
|
||||
text: __('Resolve conflicts'),
|
||||
category: 'default',
|
||||
href: this.mr.conflictResolutionPath,
|
||||
},
|
||||
].filter((b) => b);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<merge-checks-message :check="check">
|
||||
<action-buttons v-if="!isLoading" :tertiary-buttons="tertiaryActionsButtons" />
|
||||
</merge-checks-message>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<script>
|
||||
import StatusIcon from '../widget/status_icon.vue';
|
||||
|
||||
const ICON_NAMES = {
|
||||
failed: 'failed',
|
||||
allowed_to_fail: 'neutral',
|
||||
passed: 'success',
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'MergeChecksMessage',
|
||||
components: {
|
||||
StatusIcon,
|
||||
},
|
||||
props: {
|
||||
check: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
mr: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
iconName() {
|
||||
return ICON_NAMES[this.check.result];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-py-3 gl-pl-7">
|
||||
<div class="gl-display-flex">
|
||||
<status-icon :icon-name="iconName" :level="2" />
|
||||
<div class="gl-w-full gl-min-w-0">
|
||||
<div class="gl-display-flex">{{ check.failureReason }}</div>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import mergeChecksQuery from '../queries/merge_checks.query.graphql';
|
||||
import conflictsStateQuery from '../queries/states/conflicts.query.graphql';
|
||||
import MergeChecks from './merge_checks.vue';
|
||||
|
||||
const stylesheetsRequireCtx = require.context(
|
||||
'../../../stylesheets',
|
||||
true,
|
||||
/(page_bundles\/merge_requests)\.scss$/,
|
||||
);
|
||||
|
||||
stylesheetsRequireCtx('./page_bundles/merge_requests.scss');
|
||||
|
||||
const defaultRender = (apolloProvider) => ({
|
||||
components: { MergeChecks },
|
||||
apolloProvider,
|
||||
data() {
|
||||
return { mr: { conflictResolutionPath: 'https://gitlab.com' } };
|
||||
},
|
||||
template: '<merge-checks :mr="mr" />',
|
||||
});
|
||||
|
||||
const Template = ({ canMerge, failed, pushToSourceBranch }) => {
|
||||
const requestHandlers = [
|
||||
[
|
||||
mergeChecksQuery,
|
||||
() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
project: {
|
||||
id: 1,
|
||||
mergeRequest: {
|
||||
id: 1,
|
||||
userPermissions: { canMerge },
|
||||
mergeChecks: [
|
||||
{
|
||||
failureReason: 'Unresolved discussions',
|
||||
identifier: 'unresolved_discussions',
|
||||
result: failed ? 'failed' : 'passed',
|
||||
},
|
||||
{
|
||||
failureReason: 'Resolve conflicts',
|
||||
identifier: 'conflicts',
|
||||
result: failed ? 'failed' : 'passed',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
[
|
||||
conflictsStateQuery,
|
||||
() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
project: {
|
||||
id: 1,
|
||||
mergeRequest: {
|
||||
id: 1,
|
||||
shouldBeRebased: false,
|
||||
sourceBranchProtected: false,
|
||||
userPermissions: { pushToSourceBranch },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
];
|
||||
const apolloProvider = createMockApollo(requestHandlers);
|
||||
|
||||
return defaultRender(apolloProvider);
|
||||
};
|
||||
|
||||
const LoadingTemplate = () => {
|
||||
const requestHandlers = [[mergeChecksQuery, () => new Promise(() => {})]];
|
||||
const apolloProvider = createMockApollo(requestHandlers);
|
||||
|
||||
return defaultRender(apolloProvider);
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = { canMerge: true, failed: true, pushToSourceBranch: true };
|
||||
|
||||
export const Loading = LoadingTemplate.bind({});
|
||||
Loading.args = {};
|
||||
|
||||
export default {
|
||||
title: 'vue_merge_request_widget/merge_checks',
|
||||
component: MergeChecks,
|
||||
};
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
<script>
|
||||
import { GlSkeletonLoader } from '@gitlab/ui';
|
||||
import { n__, __, sprintf } from '~/locale';
|
||||
import mergeRequestQueryVariablesMixin from '../mixins/merge_request_query_variables';
|
||||
import mergeChecksQuery from '../queries/merge_checks.query.graphql';
|
||||
import StateContainer from './state_container.vue';
|
||||
import BoldText from './bold_text.vue';
|
||||
|
||||
const COMPONENTS = {
|
||||
conflicts: () => import('./checks/conflicts.vue'),
|
||||
default: () => import('./checks/message.vue'),
|
||||
};
|
||||
|
||||
export default {
|
||||
apollo: {
|
||||
state: {
|
||||
query: mergeChecksQuery,
|
||||
skip() {
|
||||
return !this.mr;
|
||||
},
|
||||
variables() {
|
||||
return this.mergeRequestQueryVariables;
|
||||
},
|
||||
update: (data) => data?.project?.mergeRequest,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
GlSkeletonLoader,
|
||||
StateContainer,
|
||||
BoldText,
|
||||
},
|
||||
mixins: [mergeRequestQueryVariablesMixin],
|
||||
props: {
|
||||
mr: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
collapsed: true,
|
||||
state: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isLoading() {
|
||||
return this.$apollo.queries.state.loading;
|
||||
},
|
||||
statusIcon() {
|
||||
return this.failedChecks.length ? 'failed' : 'success';
|
||||
},
|
||||
summaryText() {
|
||||
if (!this.failedChecks.length) {
|
||||
return this.state?.userPermissions?.canMerge
|
||||
? __('%{boldStart}Ready to merge!%{boldEnd}')
|
||||
: __(
|
||||
'%{boldStart}Ready to merge by members who can write to the target branch.%{boldEnd}',
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
n__(
|
||||
'%{boldStart}Merge blocked:%{boldEnd} %{count} check failed',
|
||||
'%{boldStart}Merge blocked:%{boldEnd} %{count} checks failed',
|
||||
this.failedChecks.length,
|
||||
),
|
||||
{ count: this.failedChecks.length },
|
||||
);
|
||||
},
|
||||
checks() {
|
||||
return this.state.mergeChecks || [];
|
||||
},
|
||||
failedChecks() {
|
||||
return this.checks.filter((c) => c.result === 'failed');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleCollapsed() {
|
||||
this.collapsed = !this.collapsed;
|
||||
},
|
||||
checkComponent(check) {
|
||||
return COMPONENTS[check.identifier] || COMPONENTS.default;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<state-container
|
||||
:is-loading="isLoading"
|
||||
:status="statusIcon"
|
||||
is-collapsible
|
||||
collapse-on-desktop
|
||||
:collapsed="collapsed"
|
||||
:expand-details-tooltip="__('Expand merge checks')"
|
||||
:collapse-details-tooltip="__('Collapse merge checks')"
|
||||
@toggle="toggleCollapsed"
|
||||
>
|
||||
<template v-if="isLoading" #loading>
|
||||
<gl-skeleton-loader :width="334" :height="24">
|
||||
<rect x="0" y="0" width="24" height="24" rx="4" />
|
||||
<rect x="32" y="2" width="302" height="20" rx="4" />
|
||||
</gl-skeleton-loader>
|
||||
</template>
|
||||
<template v-else>
|
||||
<bold-text :message="summaryText" />
|
||||
</template>
|
||||
</state-container>
|
||||
<div
|
||||
v-if="!collapsed"
|
||||
class="gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-relative gl-bg-gray-10"
|
||||
data-testid="merge-checks-full"
|
||||
>
|
||||
<div class="gl-px-5">
|
||||
<component
|
||||
:is="checkComponent(check)"
|
||||
v-for="(check, index) in checks"
|
||||
:key="index"
|
||||
:class="{
|
||||
'gl-border-b-solid gl-border-b-1 gl-border-gray-100': index !== checks.length - 1,
|
||||
}"
|
||||
:check="check"
|
||||
:mr="mr"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import { STATUS_CLOSED, STATUS_MERGED } from '~/issues/constants';
|
||||
import StatusIcon from './mr_widget_status_icon.vue';
|
||||
import Actions from './action_buttons.vue';
|
||||
|
|
@ -13,14 +14,6 @@ export default {
|
|||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
inject: {
|
||||
expandDetailsTooltip: {
|
||||
default: '',
|
||||
},
|
||||
collapseDetailsTooltip: {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
props: {
|
||||
isCollapsible: {
|
||||
type: Boolean,
|
||||
|
|
@ -57,6 +50,16 @@ export default {
|
|||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
expandDetailsTooltip: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: __('Expand merge details'),
|
||||
},
|
||||
collapseDetailsTooltip: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: __('Collapse merge details'),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
wrapperClasses() {
|
||||
|
|
@ -120,6 +123,7 @@ export default {
|
|||
<gl-button
|
||||
v-gl-tooltip
|
||||
:title="collapsed ? expandDetailsTooltip : collapseDetailsTooltip"
|
||||
:aria-label="collapsed ? expandDetailsTooltip : collapseDetailsTooltip"
|
||||
:icon="collapsed ? 'chevron-lg-down' : 'chevron-lg-up'"
|
||||
category="tertiary"
|
||||
size="small"
|
||||
|
|
|
|||
|
|
@ -153,10 +153,6 @@ export default {
|
|||
},
|
||||
},
|
||||
mixins: [mergeRequestQueryVariablesMixin],
|
||||
provide: {
|
||||
expandDetailsTooltip: __('Expand merge details'),
|
||||
collapseDetailsTooltip: __('Collapse merge details'),
|
||||
},
|
||||
props: {
|
||||
mrData: {
|
||||
type: Object,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
query mergeChecks($projectPath: ID!, $iid: String!) {
|
||||
project(fullPath: $projectPath) {
|
||||
id
|
||||
mergeRequest(iid: $iid) {
|
||||
id
|
||||
userPermissions {
|
||||
canMerge
|
||||
}
|
||||
mergeChecks @client
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,9 @@ query workInProgress($projectPath: ID!, $iid: String!) {
|
|||
id
|
||||
shouldBeRebased
|
||||
sourceBranchProtected
|
||||
userPermissions {
|
||||
pushToSourceBranch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@ export default {
|
|||
<div class="content-editor-switcher gl-display-inline-flex gl-align-items-center">
|
||||
<gl-button
|
||||
:id="$options.richTextEditorButtonId"
|
||||
class="btn btn-default btn-sm gl-button btn-default-tertiary gl-font-sm! gl-text-secondary! gl-px-4!"
|
||||
size="small"
|
||||
category="tertiary"
|
||||
class="gl-font-sm! gl-text-secondary! gl-px-4!"
|
||||
data-testid="editing-mode-switcher"
|
||||
@click="$emit('switch')"
|
||||
>{{ text }}</gl-button
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ module Clusters
|
|||
has_one :cluster_project, -> { order(id: :desc) }, class_name: 'Clusters::Project'
|
||||
has_many :deployment_clusters
|
||||
has_many :deployments, inverse_of: :cluster, through: :deployment_clusters
|
||||
has_many :successful_deployments, -> { success }, class_name: 'Deployment'
|
||||
has_many :environments, -> { distinct }, through: :deployments
|
||||
|
||||
has_many :cluster_groups, class_name: 'Clusters::Group'
|
||||
|
|
|
|||
|
|
@ -2000,15 +2000,11 @@ class MergeRequest < ApplicationRecord
|
|||
end
|
||||
|
||||
def base_pipeline
|
||||
@base_pipeline ||= project.ci_pipelines
|
||||
.order(id: :desc)
|
||||
.find_by(sha: diff_base_sha, ref: target_branch)
|
||||
@base_pipeline ||= base_pipelines.last
|
||||
end
|
||||
|
||||
def merge_base_pipeline
|
||||
@merge_base_pipeline ||= project.ci_pipelines
|
||||
.order(id: :desc)
|
||||
.find_by(sha: actual_head_pipeline.target_sha, ref: target_branch)
|
||||
@merge_base_pipeline ||= merge_base_pipelines.last
|
||||
end
|
||||
|
||||
def discussions_rendered_on_frontend?
|
||||
|
|
@ -2176,6 +2172,19 @@ class MergeRequest < ApplicationRecord
|
|||
|
||||
attr_accessor :skip_fetch_ref
|
||||
|
||||
def merge_base_pipelines
|
||||
target_branch_pipelines_for(sha: actual_head_pipeline.target_sha)
|
||||
end
|
||||
|
||||
def base_pipelines
|
||||
target_branch_pipelines_for(sha: diff_base_sha)
|
||||
end
|
||||
|
||||
def target_branch_pipelines_for(sha:)
|
||||
project.ci_pipelines
|
||||
.where(sha: sha, ref: target_branch)
|
||||
end
|
||||
|
||||
def set_draft_status
|
||||
self.draft = draft?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -37,3 +37,5 @@ module Branches
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
Branches::DeleteService.prepend_mod
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ module Git
|
|||
|
||||
return if branch_to_sync.nil? && commits_to_sync.empty?
|
||||
|
||||
if commits_to_sync.any? && Feature.enabled?(:batch_delay_jira_branch_sync_worker, project)
|
||||
if commits_to_sync.any?
|
||||
commits_to_sync.each_slice(JIRA_SYNC_BATCH_SIZE).with_index do |commits, i|
|
||||
JiraConnect::SyncBranchWorker.perform_in(
|
||||
JIRA_SYNC_BATCH_DELAY * i,
|
||||
|
|
|
|||
|
|
@ -20,10 +20,13 @@ module Todos
|
|||
private
|
||||
|
||||
def delete_todos
|
||||
authorized_users = User.from_union([
|
||||
group.project_users_with_descendants.select(:id),
|
||||
group.members_with_parents.select(:user_id)
|
||||
], remove_duplicates: false)
|
||||
authorized_users = Member.from_union(
|
||||
[
|
||||
group.descendant_project_members_with_inactive.select(:user_id),
|
||||
group.members_with_parents.select(:user_id)
|
||||
],
|
||||
remove_duplicates: false
|
||||
).select(:user_id)
|
||||
|
||||
todos.not_in_users(authorized_users).delete_all
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: batch_delay_jira_branch_sync_worker
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120866
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/411865
|
||||
milestone: '16.1'
|
||||
type: development
|
||||
group: group::source code
|
||||
default_enabled: true
|
||||
|
|
@ -51,13 +51,14 @@ depends on where you mention the Jira issue ID in GitLab.
|
|||
|
||||
| GitLab: where you mention the Jira issue ID | Jira development panel: what information is displayed |
|
||||
|------------------------------------------------|-------------------------------------------------------|
|
||||
| Merge request title or description | Link to the merge request<br>Link to the deployment<br>Link to the pipeline through merge request title<br>Link to the pipeline through merge request description <sup>1</sup><br>Link to the branch <sup>2</sup> |
|
||||
| Merge request title or description | Link to the merge request<br>Link to the deployment<br>Link to the pipeline through merge request title<br>Link to the pipeline through merge request description <sup>1</sup><br>Link to the branch <sup>2</sup><br>Reviewer information and approval status <sup>3</sup> |
|
||||
| Branch name | Link to the branch<br>Link to the deployment |
|
||||
| Commit message | Link to the commit<br>Link to the deployment from up to 5,000 commits after the last successful deployment to the environment <sup>3</sup> <sup>4</sup> |
|
||||
| Commit message | Link to the commit<br>Link to the deployment from up to 5,000 commits after the last successful deployment to the environment <sup>4</sup> <sup>5</sup> |
|
||||
| [Jira Smart Commit](#jira-smart-commits) | Custom comment, logged time, or workflow transition |
|
||||
|
||||
1. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/390888) in GitLab 15.10.
|
||||
1. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/354373) in GitLab 15.11.
|
||||
1. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/364273) in GitLab 16.5.
|
||||
1. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/300031) in GitLab 16.2 [with a flag](../../administration/feature_flags.md) named `jira_deployment_issue_keys`. Enabled by default.
|
||||
1. [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/415025) in GitLab 16.3. Feature flag `jira_deployment_issue_keys` removed.
|
||||
|
||||
|
|
|
|||
|
|
@ -169,11 +169,12 @@ separation of duties is:
|
|||
|
||||
### Export a report of merge request compliance violations on projects in a group
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/356791) in GitLab 16.4 [with a flag](../../../administration/feature_flags.md) named `compliance_violation_csv_export`. Disabled by default.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/356791) in GitLab 16.4 [with a flag](../../../administration/feature_flags.md) named `compliance_violation_csv_export`. Disabled by default.
|
||||
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/424447) in GitLab 16.5.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named
|
||||
`compliance_violation_csv_export`. On GitLab.com, this feature is not available. The feature is not ready for production use.
|
||||
On self-managed GitLab, by default this feature is available. To hide the feature, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named
|
||||
`compliance_violation_csv_export`. On GitLab.com, this feature is available.
|
||||
|
||||
Export a report of merge request compliance violations on merge requests belonging to projects in a group. Reports:
|
||||
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ prevent a project from being shared with other groups:
|
|||
1. Select **Projects in `<group_name>` cannot be shared with other groups**.
|
||||
1. Select **Save changes**.
|
||||
|
||||
This setting applies to all subgroups unless overridden by a group Owner. Groups already
|
||||
This setting, when enabled, applies to all subgroups unless overridden by a group Owner. Groups already
|
||||
added to a project lose access when the setting is enabled.
|
||||
|
||||
## Prevent users from requesting access to a group
|
||||
|
|
|
|||
|
|
@ -30,13 +30,18 @@ module Gitlab
|
|||
|
||||
def validate_regex!
|
||||
return unless spec.key?(:regex)
|
||||
return if actual_value.match?(spec[:regex])
|
||||
|
||||
safe_regex = ::Gitlab::UntrustedRegexp.new(spec[:regex])
|
||||
|
||||
return if safe_regex.match?(actual_value)
|
||||
|
||||
if value.nil?
|
||||
error('default value does not match required RegEx pattern')
|
||||
else
|
||||
error('provided value does not match required RegEx pattern')
|
||||
end
|
||||
rescue RegexpError
|
||||
error('invalid regular expression')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -548,6 +548,17 @@ msgstr ""
|
|||
msgid "%{board_target} not found"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{boldStart}Merge blocked:%{boldEnd} %{count} check failed"
|
||||
msgid_plural "%{boldStart}Merge blocked:%{boldEnd} %{count} checks failed"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%{boldStart}Ready to merge by members who can write to the target branch.%{boldEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{boldStart}Ready to merge!%{boldEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{bold_start}%{count}%{bold_end} issue"
|
||||
msgid_plural "%{bold_start}%{count}%{bold_end} issues"
|
||||
msgstr[0] ""
|
||||
|
|
@ -11817,6 +11828,9 @@ msgstr ""
|
|||
msgid "Collapse jobs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Collapse merge checks"
|
||||
msgstr ""
|
||||
|
||||
msgid "Collapse merge details"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -16018,6 +16032,9 @@ msgstr ""
|
|||
msgid "Deleting a project places it into a read-only state until %{date}, at which point the project will be permanently deleted. Are you ABSOLUTELY sure?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Deleting protected branches is blocked by security policies"
|
||||
msgstr ""
|
||||
|
||||
msgid "Deleting the project will delete its repository and all related resources, including issues and merge requests."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -19549,6 +19566,9 @@ msgstr ""
|
|||
msgid "Expand jobs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Expand merge checks"
|
||||
msgstr ""
|
||||
|
||||
msgid "Expand merge details"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ describe('buildClient', () => {
|
|||
|
||||
const tracingUrl = 'https://example.com/tracing';
|
||||
const provisioningUrl = 'https://example.com/provisioning';
|
||||
|
||||
const servicesUrl = 'https://example.com/services';
|
||||
const operationsUrl = 'https://example.com/services/$SERVICE_NAME$/operations';
|
||||
const FETCHING_TRACES_ERROR = 'traces are missing/invalid in the response';
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -22,6 +23,8 @@ describe('buildClient', () => {
|
|||
client = buildClient({
|
||||
tracingUrl,
|
||||
provisioningUrl,
|
||||
servicesUrl,
|
||||
operationsUrl,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -29,6 +32,27 @@ describe('buildClient', () => {
|
|||
axiosMock.restore();
|
||||
});
|
||||
|
||||
describe('buildClient', () => {
|
||||
it('rejects if params are missing', () => {
|
||||
const e = new Error(
|
||||
'missing required params. provisioningUrl, tracingUrl, servicesUrl, operationsUrl are required',
|
||||
);
|
||||
expect(() =>
|
||||
buildClient({ tracingUrl: 'test', servicesUrl: 'test', operationsUrl: 'test' }),
|
||||
).toThrow(e);
|
||||
expect(() =>
|
||||
buildClient({ provisioningUrl: 'test', servicesUrl: 'test', operationsUrl: 'test' }),
|
||||
).toThrow(e);
|
||||
expect(() =>
|
||||
buildClient({ provisioningUrl: 'test', tracingUrl: 'test', operationsUrl: 'test' }),
|
||||
).toThrow(e);
|
||||
expect(() =>
|
||||
buildClient({ provisioningUrl: 'test', tracingUrl: 'test', servicesUrl: 'test' }),
|
||||
).toThrow(e);
|
||||
expect(() => buildClient({})).toThrow(e);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isTracingEnabled', () => {
|
||||
it('returns true if requests succeedes', async () => {
|
||||
axiosMock.onGet(provisioningUrl).reply(200, {
|
||||
|
|
@ -301,4 +325,77 @@ describe('buildClient', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchServices', () => {
|
||||
it('fetches services from the services URL', async () => {
|
||||
const mockResponse = {
|
||||
services: [{ name: 'service-1' }, { name: 'service-2' }],
|
||||
};
|
||||
|
||||
axiosMock.onGet(servicesUrl).reply(200, mockResponse);
|
||||
|
||||
const result = await client.fetchServices();
|
||||
|
||||
expect(axios.get).toHaveBeenCalledTimes(1);
|
||||
expect(axios.get).toHaveBeenCalledWith(servicesUrl, {
|
||||
withCredentials: true,
|
||||
});
|
||||
expect(result).toEqual(mockResponse.services);
|
||||
});
|
||||
|
||||
it('rejects if services are missing', async () => {
|
||||
axiosMock.onGet(servicesUrl).reply(200, {});
|
||||
|
||||
const e = 'failed to fetch services. invalid response';
|
||||
await expect(client.fetchServices()).rejects.toThrow(e);
|
||||
expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchOperations', () => {
|
||||
const serviceName = 'test-service';
|
||||
const parsedOperationsUrl = `https://example.com/services/${serviceName}/operations`;
|
||||
|
||||
it('fetches operations from the operations URL', async () => {
|
||||
const mockResponse = {
|
||||
operations: [{ name: 'operation-1' }, { name: 'operation-2' }],
|
||||
};
|
||||
|
||||
axiosMock.onGet(parsedOperationsUrl).reply(200, mockResponse);
|
||||
|
||||
const result = await client.fetchOperations(serviceName);
|
||||
|
||||
expect(axios.get).toHaveBeenCalledTimes(1);
|
||||
expect(axios.get).toHaveBeenCalledWith(parsedOperationsUrl, {
|
||||
withCredentials: true,
|
||||
});
|
||||
expect(result).toEqual(mockResponse.operations);
|
||||
});
|
||||
|
||||
it('rejects if serviceName is missing', async () => {
|
||||
const e = 'fetchOperations() - serviceName is required.';
|
||||
await expect(client.fetchOperations()).rejects.toThrow(e);
|
||||
expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
|
||||
});
|
||||
|
||||
it('rejects if operationUrl does not contain $SERVICE_NAME$', async () => {
|
||||
client = buildClient({
|
||||
tracingUrl,
|
||||
provisioningUrl,
|
||||
servicesUrl,
|
||||
operationsUrl: 'something',
|
||||
});
|
||||
const e = 'fetchOperations() - operationsUrl must contain $SERVICE_NAME$';
|
||||
await expect(client.fetchOperations(serviceName)).rejects.toThrow(e);
|
||||
expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
|
||||
});
|
||||
|
||||
it('rejects if operations are missing', async () => {
|
||||
axiosMock.onGet(parsedOperationsUrl).reply(200, {});
|
||||
|
||||
const e = 'failed to fetch operations. invalid response';
|
||||
await expect(client.fetchOperations(serviceName)).rejects.toThrow(e);
|
||||
expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ describe('ObservabilityContainer', () => {
|
|||
const OAUTH_URL = 'https://example.com/oauth';
|
||||
const TRACING_URL = 'https://example.com/tracing';
|
||||
const PROVISIONING_URL = 'https://example.com/provisioning';
|
||||
const SERVICES_URL = 'https://example.com/services';
|
||||
const OPERATIONS_URL = 'https://example.com/operations';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(console, 'error').mockImplementation();
|
||||
|
|
@ -27,6 +29,8 @@ describe('ObservabilityContainer', () => {
|
|||
oauthUrl: OAUTH_URL,
|
||||
tracingUrl: TRACING_URL,
|
||||
provisioningUrl: PROVISIONING_URL,
|
||||
servicesUrl: SERVICES_URL,
|
||||
operationsUrl: OPERATIONS_URL,
|
||||
},
|
||||
stubs: {
|
||||
ObservabilitySkeleton: stubComponent(ObservabilitySkeleton, {
|
||||
|
|
@ -93,6 +97,8 @@ describe('ObservabilityContainer', () => {
|
|||
expect(buildClient).toHaveBeenCalledWith({
|
||||
provisioningUrl: PROVISIONING_URL,
|
||||
tracingUrl: TRACING_URL,
|
||||
servicesUrl: SERVICES_URL,
|
||||
operationsUrl: OPERATIONS_URL,
|
||||
});
|
||||
expect(findIframe().exists()).toBe(false);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
import VueApollo from 'vue-apollo';
|
||||
import Vue from 'vue';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import ConflictsComponent from '~/vue_merge_request_widget/components/checks/conflicts.vue';
|
||||
import conflictsStateQuery from '~/vue_merge_request_widget/queries/states/conflicts.query.graphql';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
let wrapper;
|
||||
let apolloProvider;
|
||||
|
||||
function factory({
|
||||
result = 'passed',
|
||||
canMerge = true,
|
||||
pushToSourceBranch = true,
|
||||
shouldBeRebased = false,
|
||||
sourceBranchProtected = false,
|
||||
mr = {},
|
||||
} = {}) {
|
||||
apolloProvider = createMockApollo([
|
||||
[
|
||||
conflictsStateQuery,
|
||||
jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
project: {
|
||||
id: 1,
|
||||
mergeRequest: {
|
||||
id: 1,
|
||||
shouldBeRebased,
|
||||
sourceBranchProtected,
|
||||
userPermissions: { canMerge, pushToSourceBranch },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
]);
|
||||
|
||||
wrapper = mountExtended(ConflictsComponent, {
|
||||
apolloProvider,
|
||||
propsData: {
|
||||
mr,
|
||||
check: { result, failureReason: 'Conflicts message' },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe('Merge request merge checks conflicts component', () => {
|
||||
afterEach(() => {
|
||||
apolloProvider = null;
|
||||
});
|
||||
|
||||
it('renders failure reason text', () => {
|
||||
factory();
|
||||
|
||||
expect(wrapper.text()).toEqual('Conflicts message');
|
||||
});
|
||||
|
||||
it.each`
|
||||
conflictResolutionPath | pushToSourceBranch | sourceBranchProtected | rendersConflictButton | rendersConflictButtonText
|
||||
${'https://gitlab.com'} | ${true} | ${false} | ${true} | ${'renders'}
|
||||
${undefined} | ${true} | ${false} | ${false} | ${'does not render'}
|
||||
${'https://gitlab.com'} | ${false} | ${false} | ${false} | ${'does not render'}
|
||||
${'https://gitlab.com'} | ${true} | ${true} | ${false} | ${'does not render'}
|
||||
${'https://gitlab.com'} | ${false} | ${false} | ${false} | ${'does not render'}
|
||||
${undefined} | ${false} | ${false} | ${false} | ${'does not render'}
|
||||
`(
|
||||
'$rendersConflictButtonText the conflict button for $conflictResolutionPath $pushToSourceBranch $sourceBranchProtected $rendersConflictButton',
|
||||
async ({
|
||||
conflictResolutionPath,
|
||||
pushToSourceBranch,
|
||||
sourceBranchProtected,
|
||||
rendersConflictButton,
|
||||
}) => {
|
||||
factory({ mr: { conflictResolutionPath }, pushToSourceBranch, sourceBranchProtected });
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.findAllByTestId('extension-actions-button').length).toBe(
|
||||
rendersConflictButton ? 2 : 1,
|
||||
);
|
||||
|
||||
expect(wrapper.findAllByTestId('extension-actions-button').at(-1).text()).toBe(
|
||||
rendersConflictButton ? 'Resolve conflicts' : 'Resolve locally',
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import MessageComponent from '~/vue_merge_request_widget/components/checks/message.vue';
|
||||
import StatusIcon from '~/vue_merge_request_widget/components/widget/status_icon.vue';
|
||||
|
||||
let wrapper;
|
||||
|
||||
function factory(propsData = {}) {
|
||||
wrapper = mountExtended(MessageComponent, {
|
||||
propsData,
|
||||
});
|
||||
}
|
||||
|
||||
describe('Merge request merge checks message component', () => {
|
||||
it('renders failure reason text', () => {
|
||||
factory({ check: { result: 'passed', failureReason: 'Failed message' } });
|
||||
|
||||
expect(wrapper.text()).toEqual('Failed message');
|
||||
});
|
||||
|
||||
it.each`
|
||||
result | icon
|
||||
${'passed'} | ${'success'}
|
||||
${'failed'} | ${'failed'}
|
||||
${'allowed_to_fail'} | ${'neutral'}
|
||||
`('renders $icon icon for $result result', ({ result, icon }) => {
|
||||
factory({ check: { result, failureReason: 'Failed message' } });
|
||||
|
||||
expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe(icon);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
import VueApollo from 'vue-apollo';
|
||||
import Vue from 'vue';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import MergeChecksComponent from '~/vue_merge_request_widget/components/merge_checks.vue';
|
||||
import mergeChecksQuery from '~/vue_merge_request_widget/queries/merge_checks.query.graphql';
|
||||
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
let wrapper;
|
||||
let apolloProvider;
|
||||
|
||||
function factory({ canMerge = true, mergeChecks = [] } = {}) {
|
||||
apolloProvider = createMockApollo([
|
||||
[
|
||||
mergeChecksQuery,
|
||||
jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
project: {
|
||||
id: 1,
|
||||
mergeRequest: { id: 1, userPermissions: { canMerge }, mergeChecks },
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
]);
|
||||
|
||||
wrapper = mountExtended(MergeChecksComponent, {
|
||||
apolloProvider,
|
||||
propsData: {
|
||||
mr: {},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe('Merge request merge checks component', () => {
|
||||
afterEach(() => {
|
||||
apolloProvider = null;
|
||||
});
|
||||
|
||||
it('renders ready to merge text if user can merge', async () => {
|
||||
factory({ canMerge: true });
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.text()).toBe('Ready to merge!');
|
||||
});
|
||||
|
||||
it('renders ready to merge by members text if user can not merge', async () => {
|
||||
factory({ canMerge: false });
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.text()).toBe('Ready to merge by members who can write to the target branch.');
|
||||
});
|
||||
|
||||
it.each`
|
||||
mergeChecks | text
|
||||
${[{ identifier: 'discussions', result: 'failed' }]} | ${'Merge blocked: 1 check failed'}
|
||||
${[{ identifier: 'discussions', result: 'failed' }, { identifier: 'rebase', result: 'failed' }]} | ${'Merge blocked: 2 checks failed'}
|
||||
`('renders $text for $mergeChecks', async ({ mergeChecks, text }) => {
|
||||
factory({ mergeChecks });
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.text()).toBe(text);
|
||||
});
|
||||
|
||||
it.each`
|
||||
result | statusIcon
|
||||
${'failed'} | ${'failed'}
|
||||
${'passed'} | ${'success'}
|
||||
`('renders $statusIcon for $result result', async ({ result, statusIcon }) => {
|
||||
factory({ mergeChecks: [{ result, identifier: 'discussions' }] });
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe(statusIcon);
|
||||
});
|
||||
|
||||
it('expands collapsed area', async () => {
|
||||
factory();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
await wrapper.findByTestId('widget-toggle').trigger('click');
|
||||
|
||||
expect(wrapper.findByTestId('merge-checks-full').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -294,5 +294,17 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::Inputs, feature_category: :pip
|
|||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the pattern is unsafe' do
|
||||
let(:specs) { { test_input: { regex: 'a++' } } }
|
||||
let(:args) { { test_input: 'aaaaaaaaaaaaaaaaaaaaa' } }
|
||||
|
||||
it 'is invalid' do
|
||||
expect(inputs).not_to be_valid
|
||||
expect(inputs.errors).to contain_exactly(
|
||||
'`test_input` input: invalid regular expression'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching,
|
|||
it { is_expected.to have_many(:kubernetes_namespaces) }
|
||||
it { is_expected.to have_one(:cluster_project) }
|
||||
it { is_expected.to have_many(:deployment_clusters) }
|
||||
it { is_expected.to have_many(:successful_deployments) }
|
||||
it { is_expected.to have_many(:environments).through(:deployments) }
|
||||
|
||||
it { is_expected.to delegate_method(:status).to(:provider) }
|
||||
|
|
|
|||
|
|
@ -685,44 +685,21 @@ RSpec.describe Git::BranchPushService, :use_clean_rails_redis_caching, services:
|
|||
let(:commits_to_sync) { [] }
|
||||
|
||||
shared_examples 'enqueues Jira sync worker' do
|
||||
context "batch_delay_jira_branch_sync_worker feature flag is enabled" do
|
||||
before do
|
||||
stub_feature_flags(batch_delay_jira_branch_sync_worker: true)
|
||||
end
|
||||
|
||||
specify :aggregate_failures do
|
||||
Sidekiq::Testing.fake! do
|
||||
if commits_to_sync.any?
|
||||
expect(JiraConnect::SyncBranchWorker)
|
||||
.to receive(:perform_in)
|
||||
.with(kind_of(Numeric), project.id, branch_to_sync, commits_to_sync, kind_of(Numeric))
|
||||
.and_call_original
|
||||
else
|
||||
expect(JiraConnect::SyncBranchWorker)
|
||||
.to receive(:perform_async)
|
||||
.with(project.id, branch_to_sync, commits_to_sync, kind_of(Numeric))
|
||||
.and_call_original
|
||||
end
|
||||
|
||||
expect { subject }.to change(JiraConnect::SyncBranchWorker.jobs, :size).by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "batch_delay_jira_branch_sync_worker feature flag is disabled" do
|
||||
before do
|
||||
stub_feature_flags(batch_delay_jira_branch_sync_worker: false)
|
||||
end
|
||||
|
||||
specify :aggregate_failures do
|
||||
Sidekiq::Testing.fake! do
|
||||
specify :aggregate_failures do
|
||||
Sidekiq::Testing.fake! do
|
||||
if commits_to_sync.any?
|
||||
expect(JiraConnect::SyncBranchWorker)
|
||||
.to receive(:perform_in)
|
||||
.with(kind_of(Numeric), project.id, branch_to_sync, commits_to_sync, kind_of(Numeric))
|
||||
.and_call_original
|
||||
else
|
||||
expect(JiraConnect::SyncBranchWorker)
|
||||
.to receive(:perform_async)
|
||||
.with(project.id, branch_to_sync, commits_to_sync, kind_of(Numeric))
|
||||
.and_call_original
|
||||
|
||||
expect { subject }.to change(JiraConnect::SyncBranchWorker.jobs, :size).by(1)
|
||||
end
|
||||
|
||||
expect { subject }.to change(JiraConnect::SyncBranchWorker.jobs, :size).by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue