Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-03-04 18:20:01 +00:00
parent 73e15fde38
commit 698fe342b9
107 changed files with 1394 additions and 950 deletions

View File

@ -1,11 +1,13 @@
import $ from 'jquery';
import { memoize, throttle } from 'lodash';
import createEventHub from '~/helpers/event_hub_factory';
class DirtySubmitForm {
constructor(form) {
this.form = form;
this.dirtyInputs = [];
this.isDisabled = true;
this.events = createEventHub();
this.init();
}
@ -36,11 +38,21 @@ class DirtySubmitForm {
this.form.addEventListener('submit', (event) => this.formSubmit(event));
}
addInputsListener(callback) {
this.events.$on('input', callback);
}
removeInputsListener(callback) {
this.events.$off('input', callback);
}
updateDirtyInput(event) {
const { target } = event;
if (!target.dataset.isDirtySubmitInput) return;
this.events.$emit('input', event);
this.updateDirtyInputs(target);
this.toggleSubmission();
}

View File

@ -92,6 +92,9 @@ export default {
hasUpstreamPipelines() {
return Boolean(this.pipeline?.upstream?.length > 0);
},
isMultiProjectVizAvailable() {
return Boolean(this.pipeline?.user?.namespace?.crossProjectPipelineAvailable);
},
isStageView() {
return this.viewType === STAGE_VIEW;
},
@ -178,6 +181,7 @@ export default {
<linked-pipelines-column
v-if="showUpstreamPipelines"
:config-paths="configPaths"
:is-multi-project-viz-available="isMultiProjectVizAvailable"
:linked-pipelines="upstreamPipelines"
:column-title="__('Upstream')"
:show-links="showJobLinks"
@ -226,6 +230,7 @@ export default {
v-if="showDownstreamPipelines"
class="gl-mr-6"
:config-paths="configPaths"
:is-multi-project-viz-available="isMultiProjectVizAvailable"
:linked-pipelines="downstreamPipelines"
:column-title="__('Downstream')"
:show-links="showJobLinks"

View File

@ -1,12 +1,29 @@
<script>
import { GlBadge, GlButton, GlLink, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import {
GlBadge,
GlButton,
GlLink,
GlLoadingIcon,
GlPopover,
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
import TierBadge from '~/vue_shared/components/tier_badge.vue';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale';
import { __, s__, sprintf } from '~/locale';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import { reportToSentry } from '../../utils';
import { DOWNSTREAM, UPSTREAM } from './constants';
export default {
i18n: {
popover: {
title: s__('Pipelines|Multi-project pipeline graphs'),
description: s__(
'Pipelines|Gitlab Premium users have access to the multi-project pipeline graph to improve the visualization of these pipelines. %{linkStart}Learn More%{linkEnd}',
),
},
},
directives: {
GlTooltip: GlTooltipDirective,
},
@ -16,7 +33,11 @@ export default {
GlButton,
GlLink,
GlLoadingIcon,
GlPopover,
GlSprintf,
TierBadge,
},
inject: ['multiProjectHelpPath'],
props: {
columnTitle: {
type: String,
@ -26,6 +47,10 @@ export default {
type: Boolean,
required: true,
},
isMultiProjectVizAvailable: {
type: Boolean,
required: true,
},
isLoading: {
type: Boolean,
required: true,
@ -90,6 +115,9 @@ export default {
pipelineStatus() {
return this.pipeline.status;
},
popoverContainerId() {
return `popoverContainer-${this.pipeline.id}`;
},
projectName() {
return this.pipeline.project.name;
},
@ -128,16 +156,21 @@ export default {
<template>
<div
ref="linkedPipeline"
v-gl-tooltip
class="gl-h-full gl-display-flex! gl-border-solid gl-border-gray-100 gl-border-1"
:class="flexDirection"
:title="tooltipText"
data-qa-selector="child_pipeline"
data-testid="linkedPipeline"
@mouseover="onDownstreamHovered"
@mouseleave="onDownstreamHoverLeave"
>
<div class="gl-w-full gl-bg-white gl-p-3" :class="cardSpacingClass">
<div
v-gl-tooltip
class="gl-w-full gl-bg-white gl-p-3"
:class="cardSpacingClass"
data-testid="linkedPipelineBody"
data-qa-selector="linked_pipeline_body"
:title="tooltipText"
>
<div class="gl-display-flex gl-pr-3">
<ci-status
v-if="!pipelineIsLoading"
@ -163,17 +196,38 @@ export default {
</gl-badge>
</div>
</div>
<div class="gl-display-flex">
<div :id="popoverContainerId" class="gl-display-flex">
<gl-button
:id="buttonId"
class="gl-shadow-none! gl-rounded-0!"
:class="`js-pipeline-expand-${pipeline.id} ${buttonBorderClass}`"
:icon="expandedIcon"
:aria-label="__('Expand pipeline')"
:disabled="!isMultiProjectVizAvailable"
data-testid="expand-pipeline-button"
data-qa-selector="expand_pipeline_button"
@click="onClickLinkedPipeline"
/>
<gl-popover
v-if="!isMultiProjectVizAvailable"
placement="top"
:target="popoverContainerId"
triggers="hover"
>
<template #title>
<b>{{ $options.i18n.popover.title }}</b>
<tier-badge class="gl-mt-3" tier="premium"
/></template>
<p class="gl-my-0">
<gl-sprintf :message="$options.i18n.popover.description">
<template #link="{ content }"
><gl-link :href="multiProjectHelpPath" class="gl-font-sm" target="_blank">{{
content
}}</gl-link>
</template>
</gl-sprintf>
</p>
</gl-popover>
</div>
</div>
</template>

View File

@ -28,6 +28,10 @@ export default {
required: true,
validator: validateConfigPaths,
},
isMultiProjectVizAvailable: {
type: Boolean,
required: true,
},
linkedPipelines: {
type: Array,
required: true,
@ -208,6 +212,7 @@ export default {
<linked-pipeline
class="gl-display-inline-block"
:is-loading="isLoadingPipeline(pipeline.id)"
:is-multi-project-viz-available="isMultiProjectVizAvailable"
:pipeline="pipeline"
:column-title="columnTitle"
:type="type"

View File

@ -60,6 +60,15 @@ export default {
iid: this.pipelineIid,
};
},
loading() {
return this.$apollo.queries.jobs.loading;
},
showSkeletonLoader() {
return this.firstLoad && this.loading;
},
showLoadingSpinner() {
return !this.firstLoad && this.loading;
},
},
mounted() {
eventHub.$on('jobActionPerformed', this.handleJobAction);
@ -69,7 +78,7 @@ export default {
},
methods: {
handleJobAction() {
this.firstLoad = true;
this.firstLoad = false;
this.$apollo.queries.jobs.refetch();
},
@ -98,7 +107,7 @@ export default {
<template>
<div>
<div v-if="$apollo.loading && firstLoad" class="gl-mt-5">
<div v-if="showSkeletonLoader" class="gl-mt-5">
<gl-skeleton-loader :width="1248" :height="73">
<circle cx="748.031" cy="37.7193" r="15.0307" />
<circle cx="787.241" cy="37.7193" r="15.0307" />
@ -118,7 +127,7 @@ export default {
<jobs-table v-else :jobs="jobs" :table-fields="$options.fields" data-testid="jobs-tab-table" />
<gl-intersection-observer v-if="jobsPageInfo.hasNextPage" @appear="fetchMoreJobs">
<gl-loading-icon v-if="$apollo.loading" size="md" />
<gl-loading-icon v-if="showLoadingSpinner" size="md" />
</gl-intersection-observer>
</div>
</template>

View File

@ -1,8 +1,7 @@
<script>
import { GlIcon, GlLink, GlPopover, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { __ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { SCHEDULE_ORIGIN, ICONS } from '../../constants';
@ -18,7 +17,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagMixin()],
inject: {
targetProjectFullPath: {
default: '',
@ -139,28 +137,14 @@ export default {
commitTitle() {
return this.pipeline?.commit?.title;
},
hasAuthor() {
return (
this.commitAuthor?.avatar_url && this.commitAuthor?.path && this.commitAuthor?.username
);
},
userImageAltDescription() {
return this.commitAuthor?.username
? sprintf(__("%{username}'s avatar"), { username: this.commitAuthor.username })
: null;
},
rearrangePipelinesTable() {
return this.glFeatures?.rearrangePipelinesTable;
},
},
};
</script>
<template>
<div class="pipeline-tags" data-testid="pipeline-url-table-cell">
<template v-if="rearrangePipelinesTable">
<div class="commit-title gl-mb-2" data-testid="commit-title-container">
<span v-if="commitTitle" class="gl-display-flex">
<tooltip-on-truncate :title="commitTitle" class="flex-truncate-child gl-flex-grow-1">
<tooltip-on-truncate :title="commitTitle" class="gl-flex-grow-1 gl-text-truncate">
<gl-link
:href="commitUrl"
class="commit-row-message gl-text-gray-900"
@ -214,16 +198,6 @@ export default {
}}</gl-link>
<!--End of commit row-->
</div>
</template>
<gl-link
v-if="!rearrangePipelinesTable"
:href="pipeline.path"
class="gl-text-decoration-underline"
data-testid="pipeline-url-link"
data-qa-selector="pipeline_url_link"
>
#{{ pipeline[pipelineKey] }}
</gl-link>
<div class="label-container gl-mt-1">
<gl-badge
v-if="isScheduled"

View File

@ -1,85 +0,0 @@
<script>
import { CHILD_VIEW } from '~/pipelines/constants';
import CommitComponent from '~/vue_shared/components/commit.vue';
export default {
components: {
CommitComponent,
},
props: {
pipeline: {
type: Object,
required: true,
},
viewType: {
type: String,
required: true,
},
},
computed: {
commitAuthor() {
let commitAuthorInformation;
if (!this.pipeline || !this.pipeline.commit) {
return null;
}
// 1. person who is an author of a commit might be a GitLab user
if (this.pipeline.commit.author) {
// 2. if person who is an author of a commit is a GitLab user
// they can have a GitLab avatar
if (this.pipeline.commit.author.avatar_url) {
commitAuthorInformation = this.pipeline.commit.author;
// 3. If GitLab user does not have avatar, they might have a Gravatar
} else if (this.pipeline.commit.author_gravatar_url) {
commitAuthorInformation = {
...this.pipeline.commit.author,
avatar_url: this.pipeline.commit.author_gravatar_url,
};
}
// 4. If committer is not a GitLab User, they can have a Gravatar
} else {
commitAuthorInformation = {
avatar_url: this.pipeline.commit.author_gravatar_url,
path: `mailto:${this.pipeline.commit.author_email}`,
username: this.pipeline.commit.author_name,
};
}
return commitAuthorInformation;
},
commitTag() {
return this.pipeline?.ref?.tag;
},
commitRef() {
return this.pipeline?.ref;
},
commitUrl() {
return this.pipeline?.commit?.commit_path;
},
commitShortSha() {
return this.pipeline?.commit?.short_id;
},
commitTitle() {
return this.pipeline?.commit?.title;
},
isChildView() {
return this.viewType === CHILD_VIEW;
},
},
};
</script>
<template>
<commit-component
:tag="commitTag"
:commit-ref="commitRef"
:commit-url="commitUrl"
:merge-request-ref="pipeline.merge_request"
:short-sha="commitShortSha"
:title="commitTitle"
:author="commitAuthor"
:show-ref-info="!isChildView"
/>
</template>

View File

@ -3,7 +3,6 @@ import CodeQualityWalkthrough from '~/code_quality_walkthrough/components/step.v
import { PIPELINE_STATUSES } from '~/code_quality_walkthrough/constants';
import { CHILD_VIEW } from '~/pipelines/constants';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import PipelinesTimeago from './time_ago.vue';
export default {
@ -12,7 +11,6 @@ export default {
CiBadge,
PipelinesTimeago,
},
mixins: [glFeatureFlagsMixin()],
props: {
pipeline: {
type: Object,
@ -44,9 +42,6 @@ export default {
codeQualityBuildPath() {
return this.pipeline?.details?.code_quality_build_path;
},
rearrangePipelinesTable() {
return this.glFeatures?.rearrangePipelinesTable;
},
},
};
</script>
@ -61,7 +56,7 @@ export default {
:icon-classes="'gl-vertical-align-middle!'"
data-qa-selector="pipeline_commit_status"
/>
<pipelines-timeago v-if="rearrangePipelinesTable" class="gl-mt-3" :pipeline="pipeline" />
<pipelines-timeago class="gl-mt-3" :pipeline="pipeline" />
<code-quality-walkthrough
v-if="shouldRenderCodeQualityWalkthrough"
:step="codeQualityStep"

View File

@ -1,16 +1,13 @@
<script>
import { GlTableLite, GlTooltipDirective } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../../event_hub';
import PipelineMiniGraph from './pipeline_mini_graph.vue';
import PipelineOperations from './pipeline_operations.vue';
import PipelineStopModal from './pipeline_stop_modal.vue';
import PipelineTriggerer from './pipeline_triggerer.vue';
import PipelineUrl from './pipeline_url.vue';
import PipelinesCommit from './pipelines_commit.vue';
import PipelinesStatusBadge from './pipelines_status_badge.vue';
import PipelinesTimeago from './time_ago.vue';
const DEFAULT_TD_CLASS = 'gl-p-5!';
const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!';
@ -22,19 +19,16 @@ export default {
GlTableLite,
LinkedPipelinesMiniList: () =>
import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
PipelinesCommit,
PipelineMiniGraph,
PipelineOperations,
PipelinesStatusBadge,
PipelineStopModal,
PipelinesTimeago,
PipelineTriggerer,
PipelineUrl,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagMixin()],
props: {
pipelines: {
type: Array,
@ -74,18 +68,16 @@ export default {
key: 'status',
label: s__('Pipeline|Status'),
thClass: DEFAULT_TH_CLASSES,
columnClass: this.rearrangePipelinesTable ? 'gl-w-15p' : 'gl-w-10p',
columnClass: 'gl-w-15p',
tdClass: DEFAULT_TD_CLASS,
thAttr: { 'data-testid': 'status-th' },
},
{
key: 'pipeline',
label: this.rearrangePipelinesTable ? __('Pipeline') : this.pipelineKeyOption.label,
label: __('Pipeline'),
thClass: DEFAULT_TH_CLASSES,
tdClass: this.rearrangePipelinesTable
? `${DEFAULT_TD_CLASS}`
: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`,
columnClass: this.rearrangePipelinesTable ? 'gl-w-30p' : 'gl-w-10p',
tdClass: `${DEFAULT_TD_CLASS}`,
columnClass: 'gl-w-30p',
thAttr: { 'data-testid': 'pipeline-th' },
},
{
@ -96,14 +88,6 @@ export default {
columnClass: 'gl-w-10p',
thAttr: { 'data-testid': 'triggerer-th' },
},
{
key: 'commit',
label: s__('Pipeline|Commit'),
thClass: DEFAULT_TH_CLASSES,
tdClass: DEFAULT_TD_CLASS,
columnClass: 'gl-w-20p',
thAttr: { 'data-testid': 'commit-th' },
},
{
key: 'stages',
label: s__('Pipeline|Stages'),
@ -112,14 +96,6 @@ export default {
columnClass: 'gl-w-quarter',
thAttr: { 'data-testid': 'stages-th' },
},
{
key: 'timeago',
label: s__('Pipeline|Duration'),
thClass: DEFAULT_TH_CLASSES,
tdClass: DEFAULT_TD_CLASS,
columnClass: this.rearrangePipelinesTable ? 'gl-w-5p' : 'gl-w-15p',
thAttr: { 'data-testid': 'timeago-th' },
},
{
key: 'actions',
thClass: DEFAULT_TH_CLASSES,
@ -129,12 +105,7 @@ export default {
},
];
return !this.rearrangePipelinesTable
? fields
: fields.filter((field) => !['commit', 'timeago'].includes(field.key));
},
rearrangePipelinesTable() {
return this.glFeatures?.rearrangePipelinesTable;
return fields;
},
},
watch: {
@ -200,10 +171,6 @@ export default {
<pipeline-triggerer :pipeline="item" />
</template>
<template #cell(commit)="{ item }">
<pipelines-commit :pipeline="item" :view-type="viewType" />
</template>
<template #cell(stages)="{ item }">
<div class="stage-cell">
<!-- This empty div should be removed, see https://gitlab.com/gitlab-org/gitlab/-/issues/323488 -->
@ -229,10 +196,6 @@ export default {
</div>
</template>
<template #cell(timeago)="{ item }">
<pipelines-timeago :pipeline="item" />
</template>
<template #cell(actions)="{ item }">
<pipeline-operations :pipeline="item" :canceling-pipeline="cancelingPipeline" />
</template>

View File

@ -1,6 +1,5 @@
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
@ -8,7 +7,7 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: { GlIcon },
mixins: [timeagoMixin, glFeatureFlagMixin()],
mixins: [timeagoMixin],
props: {
pipeline: {
type: Object,
@ -54,14 +53,11 @@ export default {
showSkipped() {
return !this.duration && !this.finishedTime && this.skipped;
},
shouldDisplayAsBlock() {
return this.glFeatures?.rearrangePipelinesTable;
},
},
};
</script>
<template>
<div class="{ 'gl-display-block': shouldDisplayAsBlock }">
<div class="gl-display-block">
<span v-if="showInProgress" data-testid="pipeline-in-progress">
<gl-icon v-if="stuck" name="warning" class="gl-mr-2" :size="12" data-testid="warning-icon" />
<gl-icon

View File

@ -8,7 +8,7 @@ Vue.use(VueApollo);
const createPipelinesDetailApp = (
selector,
apolloProvider,
{ pipelineProjectPath, pipelineIid, metricsPath, graphqlResourceEtag } = {},
{ pipelineProjectPath, pipelineIid, metricsPath, graphqlResourceEtag, multiProjectHelpPath } = {},
) => {
// eslint-disable-next-line no-new
new Vue({
@ -22,6 +22,7 @@ const createPipelinesDetailApp = (
pipelineProjectPath,
pipelineIid,
graphqlResourceEtag,
multiProjectHelpPath,
},
errorCaptured(err, _vm, info) {
reportToSentry('pipeline_details_graph', `error: ${err}, info: ${info}`);

View File

@ -1,12 +1,7 @@
<script>
import { GlLink, GlIcon } from '@gitlab/ui';
import { escape } from 'lodash';
import { GlLink, GlIcon, GlSprintf } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
function buildDocsLinkStart(path) {
return `<a href="${escape(path)}" target="_blank" rel="noopener noreferrer">`;
}
const NoteableTypeText = {
Issue: __('issue'),
Epic: __('epic'),
@ -17,6 +12,7 @@ export default {
components: {
GlIcon,
GlLink,
GlSprintf,
},
props: {
isLocked: {
@ -59,20 +55,6 @@ export default {
noteableTypeText() {
return NoteableTypeText[this.noteableType];
},
confidentialAndLockedDiscussionText() {
return sprintf(
__(
'This %{noteableTypeText} is %{confidentialLinkStart}confidential%{linkEnd} and %{lockedLinkStart}locked%{linkEnd}.',
),
{
noteableTypeText: this.noteableTypeText,
confidentialLinkStart: buildDocsLinkStart(this.confidentialNoteableDocsPath),
lockedLinkStart: buildDocsLinkStart(this.lockedNoteableDocsPath),
linkEnd: '</a>',
},
false,
);
},
confidentialContextText() {
return sprintf(__('This is a confidential %{noteableTypeText}.'), {
noteableTypeText: this.noteableTypeText,
@ -91,9 +73,23 @@ export default {
<gl-icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" />
<span v-if="isLockedAndConfidential" ref="lockedAndConfidential">
<span
v-html="confidentialAndLockedDiscussionText /* eslint-disable-line vue/no-v-html */"
></span>
<span>
<gl-sprintf
:message="
__(
'This %{noteableTypeText} is %{confidentialLinkStart}confidential%{confidentialLinkEnd} and %{lockedLinkStart}locked%{lockedLinkEnd}.',
)
"
>
<template #noteableTypeText>{{ noteableTypeText }}</template>
<template #confidentialLink="{ content }">
<gl-link :href="confidentialNoteableDocsPath" target="_blank">{{ content }}</gl-link>
</template>
<template #lockedLink="{ content }">
<gl-link :href="lockedNoteableDocsPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</span>
{{
__("People without permission will never get a notification and won't be able to comment.")
}}

View File

@ -0,0 +1,40 @@
<script>
import { GlBadge, GlIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
const gitlabTiers = {
free: s__('GitlabTiers|Free'),
premium: s__('GitlabTiers|Premium'),
ultimate: s__('GitlabTiers|Ultimate'),
};
export default {
components: {
GlBadge,
GlIcon,
},
props: {
tier: {
type: String,
required: true,
validator: (value) => Object.keys(gitlabTiers).includes(value),
},
size: {
type: String,
required: false,
default: 'md',
},
},
computed: {
tierName() {
return gitlabTiers[this.tier];
},
},
};
</script>
<template>
<gl-badge :size="size" class="gl-text-purple-600! gl-font-weight-bold gl-bg-purple-50!">
<gl-icon name="license" /> {{ tierName }}
</gl-badge>
</template>

View File

@ -5,6 +5,8 @@ class Admin::CohortsController < Admin::ApplicationController
feature_category :devops_reports
urgency :low
def index
@cohorts = load_cohorts
track_cohorts_visit

View File

@ -9,6 +9,8 @@ class Admin::DevOpsReportController < Admin::ApplicationController
feature_category :devops_reports
urgency :low
# rubocop: disable CodeReuse/ActiveRecord
def show
@metric = DevOpsReport::Metric.order(:created_at).last&.present

View File

@ -2,6 +2,8 @@
class Admin::InstanceReviewController < Admin::ApplicationController
feature_category :devops_reports
urgency :low
def index
redirect_to("#{Gitlab::SubscriptionPortal.subscriptions_instance_review_url}?#{instance_review_params}")
end

View File

@ -7,6 +7,8 @@ class Admin::UsageTrendsController < Admin::ApplicationController
feature_category :devops_reports
urgency :low
def index
end
end

View File

@ -45,7 +45,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:restructured_mr_widget, project, default_enabled: :yaml)
push_frontend_feature_flag(:refactor_mr_widgets_extensions, project, default_enabled: :yaml)
push_frontend_feature_flag(:rebase_without_ci_ui, project, default_enabled: :yaml)
push_frontend_feature_flag(:rearrange_pipelines_table, project, default_enabled: :yaml)
push_frontend_feature_flag(:markdown_continue_lists, project, default_enabled: :yaml)
# Usage data feature flags
push_frontend_feature_flag(:users_expanding_widgets_usage_data, project, default_enabled: :yaml)

View File

@ -16,9 +16,6 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_create_pipeline!, only: [:new, :create, :config_variables]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action :ensure_pipeline, only: [:show, :downloadable_artifacts]
before_action do
push_frontend_feature_flag(:rearrange_pipelines_table, project, default_enabled: :yaml)
end
# Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596
before_action :redirect_for_legacy_scope_filter, only: [:index], if: -> { request.format.html? }

View File

@ -47,6 +47,15 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) {
id
iid
complete
user {
__typename
id
namespace {
__typename
id
crossProjectPipelineAvailable
}
}
usesNeeds
userPermissions {
updatePipeline

View File

@ -38,7 +38,7 @@ module Types
description(enum_mod.description) if use_description
enum_mod.definition.each do |key, content|
value(key.to_s.upcase, **content)
value(key.to_s.upcase, value: key.to_s, description: content[:description])
end
end
# rubocop: enable Graphql/Descriptions

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module Projects
module PipelineHelper
def js_pipeline_details_data(project, pipeline)
{
graphql_resource_etag: graphql_etag_pipeline_path(pipeline),
metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: project.namespace, project_id: project, format: :json),
multi_project_help_path: help_page_path('ci/pipelines/multi_project_pipelines.md', anchor: 'multi-project-pipeline-visualization'),
pipeline_iid: pipeline.iid,
pipeline_project_path: project.full_path
}
end
end
end

View File

@ -12,21 +12,32 @@ module Boards
end
# rubocop: disable CodeReuse/ActiveRecord
def metadata
issuables = item_model.arel_table
keys = metadata_fields.keys
def metadata(required_fields = [:issue_count, :total_issue_weight])
fields = metadata_fields(required_fields)
keys = fields.keys
# TODO: eliminate need for SQL literal fragment
columns = Arel.sql(metadata_fields.values_at(*keys).join(', '))
results = item_model.where(id: init_collection.select(issuables[:id])).pluck(columns)
columns = Arel.sql(fields.values_at(*keys).join(', '))
results = item_model.where(id: collection_ids)
results = query_additions(results, required_fields)
results = results.select(columns)
Hash[keys.zip(results.flatten)]
Hash[keys.zip(results.pluck(columns).flatten)]
end
# rubocop: enable CodeReuse/ActiveRecord
private
def metadata_fields
{ size: 'COUNT(*)' }
# override if needed
def query_additions(items, required_fields)
items
end
def collection_ids
@collection_ids ||= init_collection.select(item_model.arel_table[:id])
end
def metadata_fields(required_fields)
required_fields&.include?(:issue_count) ? { size: 'COUNT(*)' } : {}
end
def order(items)

View File

@ -4,11 +4,13 @@ module IssuableLinks
class DestroyService < BaseService
include IncidentManagement::UsageData
attr_reader :link, :current_user
attr_reader :link, :current_user, :source, :target
def initialize(link, user)
@link = link
@current_user = user
@source = link.source
@target = link.target
end
def execute
@ -22,6 +24,11 @@ module IssuableLinks
private
def create_notes
SystemNoteService.unrelate_issuable(source, target, current_user)
SystemNoteService.unrelate_issuable(target, source, current_user)
end
def after_destroy
create_notes
track_event

View File

@ -4,23 +4,10 @@ module IssueLinks
class DestroyService < IssuableLinks::DestroyService
private
def source
@source ||= link.source
end
def target
@target ||= link.target
end
def permission_to_remove_relation?
can?(current_user, :admin_issue_link, source) && can?(current_user, :admin_issue_link, target)
end
def create_notes
SystemNoteService.unrelate_issue(source, target, current_user)
SystemNoteService.unrelate_issue(target, source, current_user)
end
def track_event
track_incident_action(current_user, target, :incident_unrelate)
end

View File

@ -53,8 +53,8 @@ module SystemNoteService
::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).relate_issue(noteable_ref)
end
def unrelate_issue(noteable, noteable_ref, user)
::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).unrelate_issue(noteable_ref)
def unrelate_issuable(noteable, noteable_ref, user)
::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).unrelate_issuable(noteable_ref)
end
# Called when the due_date of a Noteable is changed

View File

@ -26,8 +26,8 @@ module SystemNotes
# "removed the relation with gitlab-foss#9001"
#
# Returns the created Note object
def unrelate_issue(noteable_ref)
body = "removed the relation with #{noteable_ref.to_reference(noteable.project)}"
def unrelate_issuable(noteable_ref)
body = "removed the relation with #{noteable_ref.to_reference(noteable.resource_parent)}"
issue_activity_counter.track_issue_unrelated_action(author: author) if noteable.is_a?(Issue)

View File

@ -42,10 +42,20 @@ module WebHooks
hook.failed!
end
end
rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
raise if raise_lock_error?
end
def lock_name
"web_hooks:update_hook_failure_state:#{hook.id}"
end
# Allow an error to be raised after failing to obtain a lease only if the hook
# is not already in the correct failure state.
def raise_lock_error?
hook.reset # Reload so properties are guaranteed to be current.
hook.executable? != (response_category == :ok)
end
end
end

View File

@ -29,4 +29,4 @@
#js-pipeline-notification{ data: { deprecated_keywords_doc_path: help_page_path('ci/yaml/index.md', anchor: 'deprecated-keywords'), full_path: @project.full_path, pipeline_iid: @pipeline.iid } }
= render "projects/pipelines/with_tabs", pipeline: @pipeline, stages: @stages, pipeline_has_errors: pipeline_has_errors
.js-pipeline-details-vue{ data: { metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: @project.namespace, project_id: @project, format: :json), pipeline_project_path: @project.full_path, pipeline_iid: @pipeline.iid, graphql_resource_etag: graphql_etag_pipeline_path(@pipeline) } }
.js-pipeline-details-vue{ data: js_pipeline_details_data(@project, @pipeline) }

View File

@ -31,6 +31,7 @@
- continuous_delivery
- continuous_integration
- continuous_integration_scaling
- continuous_verification
- database
- dataops
- delivery
@ -74,7 +75,6 @@
- kubernetes_management
- license
- license_compliance
- live_preview
- logging
- memory
- merge_trains
@ -110,7 +110,6 @@
- secrets_management
- security_benchmarking
- security_orchestration
- self_monitoring
- service_desk
- service_ping
- sharding
@ -119,7 +118,6 @@
- static_application_security_testing
- static_site_editor
- subgroups
- synthetic_monitoring
- team_planning
- tracing
- usage_ping

View File

@ -1,7 +1,7 @@
---
name: ci_pending_builds_maintain_denormalized_data
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75425
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/332951
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354496
milestone: '14.6'
type: development
group: group::pipeline execution

View File

@ -1,7 +1,7 @@
---
name: ci_pending_builds_queue_source
introduced_by_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350884
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354496
milestone: '14.0'
type: development
group: group::pipeline execution

View File

@ -1,7 +1,7 @@
---
name: ci_queuing_use_denormalized_data_strategy
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76543
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/332951
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354496
milestone: '14.6'
type: development
group: group::pipeline execution

View File

@ -1,8 +1,8 @@
---
name: rearrange_pipelines_table
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72545
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/343286
milestone: '14.8'
name: container_registry_follow_redirects_middleware
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81056
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353291
milestone: '14.9'
type: development
group: group::pipeline execution
default_enabled: true
group: group::package
default_enabled: false

View File

@ -62,3 +62,27 @@
- 'i_testing_group_code_coverage_visit_total'
- 'i_testing_load_performance_widget_total'
- 'i_testing_metrics_report_widget_total'
- name: xmau_plan
operator: OR
source: redis
time_frame: [7d, 28d]
events:
- users_creating_work_items
- users_updating_work_item_title
feature_flag: track_work_items_activity
- name: xmau_project_management
operator: OR
source: redis
time_frame: [7d, 28d]
events:
- users_creating_work_items
- users_updating_work_item_title
feature_flag: track_work_items_activity
- name: users_work_items
operator: OR
source: redis
time_frame: [7d, 28d]
events:
- users_creating_work_items
- users_updating_work_item_title
feature_flag: track_work_items_activity

View File

@ -0,0 +1,21 @@
---
key_path: counts_monthly.aggregated_metrics.xmau_plan
description: Unique users interacting with Plan features
product_category: team planning
product_section: dev
product_stage: plan
product_group: group::project management
value_type: number
status: active
milestone: '14.9'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81336
time_frame: 28d
data_source: redis_hll
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,21 @@
---
key_path: counts_monthly.aggregated_metrics.xmau_project_management
description: Unique users interacting with Project Management features
product_category: team planning
product_section: dev
product_stage: plan
product_group: group::project management
value_type: number
status: active
milestone: '14.9'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81336
time_frame: 28d
data_source: redis_hll
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,21 @@
---
key_path: counts_monthly.aggregated_metrics.users_work_items
description: Unique users interacting with work items
product_category: team planning
product_section: dev
product_stage: plan
product_group: group::product planning
value_type: number
status: active
milestone: '14.9'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81336
time_frame: 28d
data_source: redis_hll
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,21 @@
---
key_path: counts_weekly.aggregated_metrics.xmau_plan
description: Unique users interacting with Plan features
product_category: team planning
product_section: dev
product_stage: plan
product_group: group::project management
value_type: number
status: active
milestone: '14.9'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81336
time_frame: 7d
data_source: redis_hll
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,21 @@
---
key_path: counts_weekly.aggregated_metrics.xmau_project_management
description: Unique users interacting with Project Management features
product_category: team planning
product_section: dev
product_stage: plan
product_group: group::project management
value_type: number
status: active
milestone: '14.9'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81336
time_frame: 7d
data_source: redis_hll
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,21 @@
---
key_path: counts_weekly.aggregated_metrics.users_work_items
description: Unique users interacting with work items
product_category: team planning
product_section: dev
product_stage: plan
product_group: group::product planning
value_type: number
status: active
milestone: '14.9'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81336
time_frame: 7d
data_source: redis_hll
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -2,7 +2,7 @@
announcement_milestone: "14.8" # The milestone when this feature was first announced as deprecated.
announcement_date: "2022-02-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_milestone: "15.0" # The milestone when this feature is planned to be removed
removal_date: 2022-05-22 # The date of the milestone release when this feature is planned to be removed. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_date: "2022-05-22" # The date of the milestone release when this feature is planned to be removed. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
breaking_change: true # If this deprecation is a breaking change, set this value to true
reporter: jacobvosmaer-gitlab # GitLab username of the person reporting the deprecation
body: | # Do not modify this line, instead modify the lines below.

View File

@ -2,7 +2,7 @@
announcement_milestone: "14.8" # The milestone when this feature was first announced as deprecated.
announcement_date: "2021-02-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_milestone: "15.0" # The milestone when this feature is planned to be removed
removal_date: 2021-05-22 # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_date: "2021-05-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
breaking_change: true # If this deprecation is a breaking change, set this value to true
body: | # Do not modify this line, instead modify the lines below.
For those using Dependency Scanning for Python projects, we are deprecating the default `gemnasium-python:2` image which uses Python 3.6 as well as the custom `gemnasium-python:2-python-3.9` image which uses Python 3.9. The new default image as of GitLab 15.0 will be for Python 3.9 as it is a [supported version](https://endoflife.date/python) and 3.6 [is no longer supported](https://endoflife.date/python).

View File

@ -2,7 +2,7 @@
announcement_milestone: "14.8" # The milestone when this feature was first announced as deprecated.
announcement_date: "2022-02-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_milestone: "15.0" # The milestone when this feature is planned to be removed
removal_date: 2022-05-22 # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_date: "2022-05-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
breaking_change: true # If this deprecation is a breaking change, set this value to true
body: | # Do not modify this line, instead modify the lines below.
By default, all new applications expire access tokens after 2 hours. In GitLab 14.2 and earlier, OAuth access tokens

View File

@ -2,7 +2,7 @@
announcement_milestone: "14.0" # The milestone when this feature was first announced as deprecated.
announcement_date: "2021-06-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_milestone: "15.0" # The milestone when this feature is planned to be removed
removal_date: 2022-05-22 # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_date: "2022-05-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
breaking_change: yes # If this deprecation is a breaking change, set this value to true
body: | # Do not modify this line, instead modify the lines below.
The OAuth implicit grant authorization flow will be removed in our next major release, GitLab 15.0. Any applications that use OAuth implicit grant should switch to alternative [supported OAuth flows](https://docs.gitlab.com/ee/api/oauth2.html).

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddIndexToIssues < Gitlab::Database::Migration[1.0]
DOWNTIME = false
disable_ddl_transaction!
INDEX_NAME = 'index_issues_on_id_and_weight'
def up
add_concurrent_index :issues, [:id, :weight], name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :issues, INDEX_NAME
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
class ScheduleMigratePersonalNamespaceProjectMaintainerToOwner < Gitlab::Database::Migration[1.0]
MIGRATION = 'MigratePersonalNamespaceProjectMaintainerToOwner'
INTERVAL = 2.minutes
BATCH_SIZE = 1_000
SUB_BATCH_SIZE = 200
disable_ddl_transaction!
def up
queue_batched_background_migration(
MIGRATION,
:members,
:id,
job_interval: INTERVAL,
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
end
def down
# no-op
end
end

View File

@ -0,0 +1 @@
688232dde01ea4e8574dca73459094264bde405d799ecaf1a5867adb72576b98

View File

@ -0,0 +1 @@
84346c2f608792f259ab91dbc2c8aac8397a2997f890f8e077aad809276bb7cd

View File

@ -27884,6 +27884,8 @@ CREATE INDEX index_issues_on_description_trigram ON issues USING gin (descriptio
CREATE INDEX index_issues_on_duplicated_to_id ON issues USING btree (duplicated_to_id) WHERE (duplicated_to_id IS NOT NULL);
CREATE INDEX index_issues_on_id_and_weight ON issues USING btree (id, weight);
CREATE INDEX index_issues_on_incident_issue_type ON issues USING btree (issue_type) WHERE (issue_type = 1);
CREATE INDEX index_issues_on_last_edited_by_id ON issues USING btree (last_edited_by_id);

View File

@ -208,13 +208,18 @@ When the number exceeds the limit the page displays an alert and links to a pagi
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/51401) in GitLab 11.10.
The number of pipelines that can be created in a single push is 4.
This limit prevents the accidental creation of pipelines when `git push --all`
or `git push --mirror` is used.
When pushing multiple changes with a single Git push, like multiple tags or branches,
only four tag or branch pipelines can be triggered. This limit prevents the accidental
creation of a large number of pipelines when using `git push --all` or `git push --mirror`.
This limit does not affect any of the updated merge request pipelines.
All updated merge requests have a pipeline created when using
[merge request pipelines](../ci/pipelines/merge_request_pipelines.md).
[Merge request pipelines](../ci/pipelines/merge_request_pipelines.md) are not limited.
If the Git push updates multiple merge requests at the same time, a merge request pipeline
can trigger for every updated merge request.
To remove the limit so that any number of pipelines can trigger for a single Git push event,
administrators can enable the `git_push_create_all_pipelines` [feature flag](feature_flags.md).
Enabling this feature flag is not recommended, as it can cause excessive load on the GitLab
instance if too many changes are pushed at once and a flood of pipelines are created accidentally.
## Retention of activity history

View File

@ -10759,10 +10759,11 @@ Represents an epic board list.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="epiclistcollapsed"></a>`collapsed` | [`Boolean`](#boolean) | Indicates if this list is collapsed for this user. |
| <a id="epiclistepicscount"></a>`epicsCount` | [`Int`](#int) | Count of epics in the list. |
| <a id="epiclistepicscount"></a>`epicsCount` **{warning-solid}** | [`Int`](#int) | **Deprecated** in 14.9. This was renamed. Use: `metadata`. |
| <a id="epiclistid"></a>`id` | [`BoardsEpicListID!`](#boardsepiclistid) | Global ID of the board list. |
| <a id="epiclistlabel"></a>`label` | [`Label`](#label) | Label of the list. |
| <a id="epiclistlisttype"></a>`listType` | [`String!`](#string) | Type of the list. |
| <a id="epiclistmetadata"></a>`metadata` | [`EpicListMetadata`](#epiclistmetadata) | Epic list metatada. |
| <a id="epiclistposition"></a>`position` | [`Int`](#int) | Position of the list within the board. |
| <a id="epiclisttitle"></a>`title` | [`String!`](#string) | Title of the list. |
@ -10784,6 +10785,17 @@ four standard [pagination arguments](#connection-pagination-arguments):
| ---- | ---- | ----------- |
| <a id="epiclistepicsfilters"></a>`filters` | [`EpicFilters`](#epicfilters) | Filters applied when selecting epics in the board list. |
### `EpicListMetadata`
Represents epic board list metadata.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="epiclistmetadataepicscount"></a>`epicsCount` | [`Int`](#int) | Count of epics in the list. |
| <a id="epiclistmetadatatotalweight"></a>`totalWeight` | [`Int`](#int) | Total weight of all issues in the list. Available only when feature flag `epic_board_total_weight` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. |
### `EpicPermissions`
Check permissions for the current user on an epic.
@ -15271,6 +15283,7 @@ Represents the security scan information.
| ---- | ---- | ----------- |
| <a id="scanerrors"></a>`errors` | [`[String!]!`](#string) | List of errors. |
| <a id="scanname"></a>`name` | [`String!`](#string) | Name of the scan. |
| <a id="scanstatus"></a>`status` | [`ScanStatus!`](#scanstatus) | Indicates the status of the scan. |
| <a id="scanwarnings"></a>`warnings` | [`[String!]!`](#string) | List of warnings. |
### `ScanExecutionPolicy`
@ -18142,6 +18155,20 @@ Size of UI component in SAST configuration page.
| <a id="sastuicomponentsizemedium"></a>`MEDIUM` | Size of UI component in SAST configuration page is medium. |
| <a id="sastuicomponentsizesmall"></a>`SMALL` | Size of UI component in SAST configuration page is small. |
### `ScanStatus`
The status of the security scan.
| Value | Description |
| ----- | ----------- |
| <a id="scanstatuscreated"></a>`CREATED` | The scan has been created. |
| <a id="scanstatusjob_failed"></a>`JOB_FAILED` | The related CI build failed. |
| <a id="scanstatuspreparation_failed"></a>`PREPARATION_FAILED` | Report couldn't be prepared. |
| <a id="scanstatuspreparing"></a>`PREPARING` | Preparing the report for the scan. |
| <a id="scanstatuspurged"></a>`PURGED` | Report for the scan has been removed from the database. |
| <a id="scanstatusreport_error"></a>`REPORT_ERROR` | The report artifact provided by the CI build couldn't be parsed. |
| <a id="scanstatussucceeded"></a>`SUCCEEDED` | The report has been successfully prepared. |
### `SecurityReportTypeEnum`
| Value | Description |

View File

@ -12,9 +12,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - It's enabled on GitLab.com.
> - It's recommended for production use.
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
Usage Trends gives you an overview of how much data your instance contains, and how quickly this volume is changing over time.
Usage Trends data refreshes daily.

View File

@ -219,7 +219,7 @@ security issues:
WARNING:
This feature is in its end-of-life process. It is [deprecated](../../update/deprecations.md#vulnerability-check)
for use in GitLab 14.8, and is planned for removal in GitLab 15.0. Users should migrate to the new
[Security Approval Policies](policies/#scan-result-policy-editor).
[Security Approval Policies](policies/scan-result-policies.md).
To prevent a merge request introducing a security vulnerability in a project, enable the
Vulnerability-Check rule. While this rule is enabled, additional merge request approval by

View File

@ -12,16 +12,15 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Moved](https://gitlab.com/groups/gitlab-org/-/epics/6290) to GitLab Free in 14.5.
> - Support for Omnibus installations was [introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5686) in GitLab 14.5.
You can use GitLab CI/CD to safely deploy to and update your Kubernetes clusters.
You can use a GitLab CI/CD workflow to safely deploy to and update your Kubernetes clusters.
To do so, you install a GitLab agent in your cluster. Then in your GitLab CI/CD pipelines,
you can refer to the cluster connection as a Kubernetes context.
Then you can run Kubernetes API commands as part of your GitLab CI/CD pipeline.
To do so, you must first [install an agent in your cluster](install/index.md). When done, you have a Kubernetes context and can
run Kubernetes API commands in your GitLab CI/CD pipeline.
To ensure access to your cluster is safe:
- Each agent has a separate context (`kubecontext`).
- Only the project where the agent is, and any additional projects you authorize can access the agent in your cluster.
- Only the project where the agent is configured, and any additional projects you authorize, can access the agent in your cluster.
You do not need to have a runner in the cluster with the agent.

View File

@ -14,7 +14,22 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Renamed](https://gitlab.com/groups/gitlab-org/-/epics/7167) from "GitLab Kubernetes Agent" to "GitLab agent for Kubernetes" in GitLab 14.6.
You can connect your Kubernetes cluster with GitLab to deploy, manage,
and monitor your cloud-native solutions. You can choose from two primary workflows.
and monitor your cloud-native solutions.
To connect a Kubernetes cluster to GitLab, you must first [install an agent in your cluster](install/index.md).
The agent runs in the cluster, and you can use it to:
- Communicate with a cluster, which is behind a firewall or NAT.
- Access API endpoints in a cluster in real time.
- Push information about events happening in the cluster.
- Enable a cache of Kubernetes objects, which are kept up-to-date with very low latency.
For more details about the agent's purpose and architecture, see the [architecture documentation](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/architecture.md).
## Workflows
You can choose from two primary workflows.
In a [**GitOps** workflow](gitops.md), you keep your Kubernetes manifests in GitLab. You install a GitLab agent in your cluster, and
any time you update your manifests, the agent updates the cluster. This workflow is fully driven with Git and is considered pull-based,
@ -23,8 +38,6 @@ because the cluster is pulling updates from your GitLab repository.
In a [**CI/CD** workflow](ci_cd_tunnel.md), you use GitLab CI/CD to query and update your cluster by using the Kubernetes API.
This workflow is considered push-based, because GitLab is pushing requests from GitLab CI/CD to your cluster.
Both of these workflows require you to [install an agent in your cluster](install/index.md).
## Supported cluster versions
GitLab supports the following Kubernetes versions. You can upgrade your

View File

@ -12,7 +12,7 @@ module API
desc 'Get the current application statistics' do
success Entities::ApplicationStatistics
end
get "application/statistics" do
get "application/statistics", urgency: :low do
counts = Gitlab::Database::Count.approximate_counts(COUNTED_ITEMS)
present counts, with: Entities::ApplicationStatistics
end

View File

@ -10,6 +10,21 @@ module ContainerRegistry
REGISTRY_FEATURES_HEADER = 'gitlab-container-registry-features'
REGISTRY_TAG_DELETE_FEATURE = 'tag_delete'
ALLOWED_REDIRECT_SCHEMES = %w[http https].freeze
REDIRECT_OPTIONS = {
clear_authorization_header: true,
limit: 3,
cookies: [],
callback: -> (response_env, request_env) do
request_env.request_headers.delete(::FaradayMiddleware::FollowRedirects::AUTH_HEADER)
redirect_to = request_env.url
unless redirect_to.scheme.in?(ALLOWED_REDIRECT_SCHEMES)
raise ArgumentError, "Invalid scheme for #{redirect_to}"
end
end
}.freeze
def self.supports_tag_delete?
with_dummy_client(return_value_if_disabled: false) do |client|
client.supports_tag_delete?
@ -136,6 +151,10 @@ module ContainerRegistry
def faraday_blob
@faraday_blob ||= faraday_base do |conn|
initialize_connection(conn, @options)
if Feature.enabled?(:container_registry_follow_redirects_middleware)
conn.use ::FaradayMiddleware::FollowRedirects, REDIRECT_OPTIONS
end
end
end
end

View File

@ -0,0 +1,45 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# Migrates personal namespace project `maintainer` memberships (for the associated user only) to OWNER
# Does not create any missing records, simply migrates existing ones
class MigratePersonalNamespaceProjectMaintainerToOwner
include Gitlab::Database::DynamicModelHelpers
def perform(start_id, end_id, batch_table, batch_column, sub_batch_size, pause_ms)
parent_batch_relation = relation_scoped_to_range(batch_table, batch_column, start_id, end_id)
parent_batch_relation.each_batch(column: batch_column, of: sub_batch_size) do |sub_batch|
batch_metrics.time_operation(:update_all) do
sub_batch.update_all('access_level = 50')
end
pause_ms = 0 if pause_ms < 0
sleep(pause_ms * 0.001)
end
end
def batch_metrics
@batch_metrics ||= Gitlab::Database::BackgroundMigration::BatchMetrics.new
end
private
def relation_scoped_to_range(source_table, source_key_column, start_id, stop_id)
# members of projects within their own personal namespace
# rubocop: disable CodeReuse/ActiveRecord
define_batchable_model(:members, connection: ApplicationRecord.connection)
.where(source_key_column => start_id..stop_id)
.joins("INNER JOIN projects ON members.source_id = projects.id")
.joins("INNER JOIN namespaces ON projects.namespace_id = namespaces.id")
.where(type: 'ProjectMember')
.where("namespaces.type = 'User'")
.where('members.access_level < 50')
.where('namespaces.owner_id = members.user_id')
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
end

View File

@ -16777,6 +16777,15 @@ msgstr ""
msgid "GithubIntegration|This requires mirroring your GitHub repository to this project. %{docs_link}"
msgstr ""
msgid "GitlabTiers|Free"
msgstr ""
msgid "GitlabTiers|Premium"
msgstr ""
msgid "GitlabTiers|Ultimate"
msgstr ""
msgid "Gitpod"
msgstr ""
@ -26990,6 +26999,9 @@ msgstr ""
msgid "Pipelines|GitLab Runner is an application that works with GitLab CI/CD to run jobs in a pipeline. There are active runners available to run your jobs right now. If you prefer, you can %{settingsLinkStart}configure your runners%{settingsLinkEnd} or %{docsLinkStart}learn more%{docsLinkEnd} about runners."
msgstr ""
msgid "Pipelines|Gitlab Premium users have access to the multi-project pipeline graph to improve the visualization of these pipelines. %{linkStart}Learn More%{linkEnd}"
msgstr ""
msgid "Pipelines|If you are unsure, please ask a project maintainer to review it for you."
msgstr ""
@ -27029,6 +27041,9 @@ msgstr ""
msgid "Pipelines|More Information"
msgstr ""
msgid "Pipelines|Multi-project pipeline graphs"
msgstr ""
msgid "Pipelines|No runners detected"
msgstr ""
@ -27179,9 +27194,6 @@ msgstr ""
msgid "Pipeline|Checking pipeline status."
msgstr ""
msgid "Pipeline|Commit"
msgstr ""
msgid "Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation%{linkEnd}."
msgstr ""
@ -27197,9 +27209,6 @@ msgstr ""
msgid "Pipeline|Detached merge request pipeline"
msgstr ""
msgid "Pipeline|Duration"
msgstr ""
msgid "Pipeline|Failed"
msgstr ""
@ -27743,6 +27752,9 @@ msgstr ""
msgid "Preferences|When you type in a description or comment box, selected text is surrounded by the corresponding character after typing one of the following characters: %{supported_characters}."
msgstr ""
msgid "Preparing the report for the scan."
msgstr ""
msgid "Prev"
msgstr ""
@ -30798,6 +30810,12 @@ msgstr ""
msgid "Report abuse to admin"
msgstr ""
msgid "Report couldn't be prepared."
msgstr ""
msgid "Report for the scan has been removed from the database."
msgstr ""
msgid "Reported %{timeAgo} by %{reportedBy}"
msgstr ""
@ -36831,6 +36849,9 @@ msgstr ""
msgid "The regular expression used to find test coverage output in the job log. For example, use %{regex} for Simplecov (Ruby). Leave blank to disable."
msgstr ""
msgid "The related CI build failed."
msgstr ""
msgid "The remote mirror URL is invalid."
msgstr ""
@ -36840,6 +36861,12 @@ msgstr ""
msgid "The remote repository is being updated..."
msgstr ""
msgid "The report artifact provided by the CI build couldn't be parsed."
msgstr ""
msgid "The report has been successfully prepared."
msgstr ""
msgid "The repository can be committed to, and issues, comments and other entities can be created."
msgstr ""
@ -36861,6 +36888,9 @@ msgstr ""
msgid "The same shared runner executes code from multiple projects, unless you configure autoscaling with %{link} set to 1 (which it is on GitLab.com)."
msgstr ""
msgid "The scan has been created."
msgstr ""
msgid "The snippet can be accessed without any authentication."
msgstr ""
@ -37314,7 +37344,7 @@ msgstr ""
msgid "This %{issuable} is locked. Only %{strong_open}project members%{strong_close} can comment."
msgstr ""
msgid "This %{noteableTypeText} is %{confidentialLinkStart}confidential%{linkEnd} and %{lockedLinkStart}locked%{linkEnd}."
msgid "This %{noteableTypeText} is %{confidentialLinkStart}confidential%{confidentialLinkEnd} and %{lockedLinkStart}locked%{lockedLinkEnd}."
msgstr ""
msgid "This %{noteableTypeText} is locked."

View File

@ -24,6 +24,7 @@ module QA
view 'app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue' do
element :expand_pipeline_button
element :child_pipeline
element :linked_pipeline_body
end
view 'app/assets/javascripts/reports/components/report_section.vue' do
@ -93,7 +94,7 @@ module QA
end
def find_child_pipeline_by_title(title)
child_pipelines.find { |pipeline| pipeline[:title].include?(title) }
find_element(:child_pipeline, text: title)
end
def expand_child_pipeline(title: nil)

View File

@ -1,104 +0,0 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Verify', :runner do
describe 'Pass dotenv variables to downstream via bridge' do
let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(8)}" }
let(:upstream_var) { Faker::Alphanumeric.alphanumeric(8) }
let(:group) { Resource::Group.fabricate_via_api! }
let(:upstream_project) do
Resource::Project.fabricate_via_api! do |project|
project.group = group
project.name = 'upstream-project-with-bridge'
end
end
let(:downstream_project) do
Resource::Project.fabricate_via_api! do |project|
project.group = group
project.name = 'downstream-project-with-bridge'
end
end
let!(:runner) do
Resource::Runner.fabricate! do |runner|
runner.name = executor
runner.tags = [executor]
runner.token = group.reload!.runners_token
end
end
before do
Flow::Login.sign_in
add_ci_file(downstream_project, downstream_ci_file)
add_ci_file(upstream_project, upstream_ci_file)
upstream_project.visit!
Flow::Pipeline.visit_latest_pipeline(pipeline_condition: 'succeeded')
end
after do
runner.remove_via_api!
[upstream_project, downstream_project].each(&:remove_via_api!)
end
it 'runs the pipeline with composed config', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348088' do
Page::Project::Pipeline::Show.perform do |parent_pipeline|
Support::Waiter.wait_until { parent_pipeline.has_child_pipeline? }
parent_pipeline.expand_child_pipeline
parent_pipeline.click_job('downstream_test')
end
Page::Project::Job::Show.perform do |show|
expect(show).to have_passed(timeout: 360)
expect(show.output).to have_content(upstream_var)
end
end
private
def add_ci_file(project, file)
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project
commit.commit_message = 'Add config file'
commit.add_files([file])
end
end
def upstream_ci_file
{
file_path: '.gitlab-ci.yml',
content: <<~YAML
build:
stage: build
tags: ["#{executor}"]
script:
- echo "DYNAMIC_ENVIRONMENT_VAR=#{upstream_var}" >> variables.env
artifacts:
reports:
dotenv: variables.env
trigger:
stage: deploy
variables:
PASSED_MY_VAR: $DYNAMIC_ENVIRONMENT_VAR
trigger: #{downstream_project.full_path}
YAML
}
end
def downstream_ci_file
{
file_path: '.gitlab-ci.yml',
content: <<~YAML
downstream_test:
stage: test
tags: ["#{executor}"]
script:
- echo $PASSED_MY_VAR
YAML
}
end
end
end
end

View File

@ -56,7 +56,7 @@ module QA
end
Page::Project::Job::Show.perform do |show|
expect(show).to have_passed(timeout: 360)
expect(show).to have_passed(timeout: 800)
end
end
end

View File

@ -125,7 +125,6 @@ RSpec.describe 'Merge request > User sees pipelines', :js do
before do
stub_feature_flags(ci_disallow_to_create_merge_request_pipelines_in_target_project: false)
stub_feature_flags(rearrange_pipelines_table: false)
end
it 'creates a pipeline in the parent project when user proceeds with the warning' do

View File

@ -161,7 +161,7 @@ RSpec.describe 'Pipelines', :js do
end
end
context 'when pipeline is detached merge request pipeline, with rearrange_pipelines_table feature flag turned off' do
context 'when pipeline is detached merge request pipeline' do
let(:merge_request) do
create(:merge_request,
:with_detached_merge_request_pipeline,
@ -174,52 +174,6 @@ RSpec.describe 'Pipelines', :js do
let(:target_project) { project }
before do
stub_feature_flags(rearrange_pipelines_table: false)
visit project_pipelines_path(source_project)
end
shared_examples_for 'detached merge request pipeline' do
it 'shows pipeline information without pipeline ref', :sidekiq_might_not_need_inline do
within '.pipeline-tags' do
expect(page).to have_content(expected_detached_mr_tag)
end
within '.branch-commit' do
expect(page).to have_link(merge_request.iid,
href: project_merge_request_path(project, merge_request))
end
within '.branch-commit' do
expect(page).not_to have_link(pipeline.ref)
end
end
end
it_behaves_like 'detached merge request pipeline'
context 'when source project is a forked project' do
let(:source_project) { fork_project(project, user, repository: true) }
it_behaves_like 'detached merge request pipeline'
end
end
context 'when pipeline is detached merge request pipeline, with rearrange_pipelines_table feature flag turned on' do
let(:merge_request) do
create(:merge_request,
:with_detached_merge_request_pipeline,
source_project: source_project,
target_project: target_project)
end
let!(:pipeline) { merge_request.all_pipelines.first }
let(:source_project) { project }
let(:target_project) { project }
before do
stub_feature_flags(rearrange_pipelines_table: true)
visit project_pipelines_path(source_project)
end
@ -245,7 +199,7 @@ RSpec.describe 'Pipelines', :js do
end
end
context 'when pipeline is merge request pipeline, with rearrange_pipelines_table feature flag turned off' do
context 'when pipeline is merge request pipeline' do
let(:merge_request) do
create(:merge_request,
:with_merge_request_pipeline,
@ -259,53 +213,6 @@ RSpec.describe 'Pipelines', :js do
let(:target_project) { project }
before do
stub_feature_flags(rearrange_pipelines_table: false)
visit project_pipelines_path(source_project)
end
shared_examples_for 'Correct merge request pipeline information' do
it 'does not show detached tag for the pipeline, and shows the link of the merge request, and does not show the ref of the pipeline', :sidekiq_might_not_need_inline do
within '.pipeline-tags' do
expect(page).not_to have_content(expected_detached_mr_tag)
end
within '.branch-commit' do
expect(page).to have_link(merge_request.iid,
href: project_merge_request_path(project, merge_request))
end
within '.branch-commit' do
expect(page).not_to have_link(pipeline.ref)
end
end
end
it_behaves_like 'Correct merge request pipeline information'
context 'when source project is a forked project' do
let(:source_project) { fork_project(project, user, repository: true) }
it_behaves_like 'Correct merge request pipeline information'
end
end
context 'when pipeline is merge request pipeline, with rearrange_pipelines_table feature flag turned on' do
let(:merge_request) do
create(:merge_request,
:with_merge_request_pipeline,
source_project: source_project,
target_project: target_project,
merge_sha: target_project.commit.sha)
end
let!(:pipeline) { merge_request.all_pipelines.first }
let(:source_project) { project }
let(:target_project) { project }
before do
stub_feature_flags(rearrange_pipelines_table: true)
visit project_pipelines_path(source_project)
end
@ -676,28 +583,6 @@ RSpec.describe 'Pipelines', :js do
context 'with pipeline key selection' do
before do
stub_feature_flags(rearrange_pipelines_table: false)
visit project_pipelines_path(project)
wait_for_requests
end
it 'changes the Pipeline ID column for Pipeline IID' do
page.find('[data-testid="pipeline-key-dropdown"]').click
within '.gl-new-dropdown-contents' do
dropdown_options = page.find_all '.gl-new-dropdown-item'
dropdown_options[1].click
end
expect(page.find('[data-testid="pipeline-th"]')).to have_content 'Pipeline IID'
expect(page.find('[data-testid="pipeline-url-link"]')).to have_content "##{pipeline.iid}"
end
end
context 'with pipeline key selection and rearrange_pipelines_table ff on' do
before do
stub_feature_flags(rearrange_pipelines_table: true)
visit project_pipelines_path(project)
wait_for_requests
end

View File

@ -93,5 +93,38 @@ describe('DirtySubmitForm', () => {
expect(updateDirtyInputSpy).toHaveBeenCalledTimes(range.length);
});
describe('when inputs listener is added', () => {
it('calls listener when changes are made to an input', () => {
const { form, input } = createForm();
const inputsListener = jest.fn();
const dirtySubmitForm = new DirtySubmitForm(form);
dirtySubmitForm.addInputsListener(inputsListener);
setInputValue(input, 'new value');
jest.runOnlyPendingTimers();
expect(inputsListener).toHaveBeenCalledTimes(1);
});
describe('when inputs listener is removed', () => {
it('does not call listener when changes are made to an input', () => {
const { form, input } = createForm();
const inputsListener = jest.fn();
const dirtySubmitForm = new DirtySubmitForm(form);
dirtySubmitForm.addInputsListener(inputsListener);
dirtySubmitForm.removeInputsListener(inputsListener);
setInputValue(input, 'new value');
jest.runOnlyPendingTimers();
expect(inputsListener).not.toHaveBeenCalled();
});
});
});
});
});

View File

@ -1,4 +1,4 @@
import { GlIntersectionObserver, GlSkeletonLoader } from '@gitlab/ui';
import { GlIntersectionObserver, GlSkeletonLoader, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
@ -19,6 +19,7 @@ describe('Jobs app', () => {
let resolverSpy;
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
const findJobsTable = () => wrapper.findComponent(JobsTable);
const triggerInfiniteScroll = () =>
@ -48,7 +49,29 @@ describe('Jobs app', () => {
wrapper.destroy();
});
it('displays the loading state', () => {
describe('loading spinner', () => {
beforeEach(async () => {
createComponent(resolverSpy);
await waitForPromises();
triggerInfiniteScroll();
});
it('displays loading spinner when fetching more jobs', () => {
expect(findLoadingSpinner().exists()).toBe(true);
expect(findSkeletonLoader().exists()).toBe(false);
});
it('hides loading spinner after jobs have been fetched', async () => {
await waitForPromises();
expect(findLoadingSpinner().exists()).toBe(false);
expect(findSkeletonLoader().exists()).toBe(false);
});
});
it('displays the skeleton loader', () => {
createComponent(resolverSpy);
expect(findSkeletonLoader().exists()).toBe(true);
@ -91,7 +114,7 @@ describe('Jobs app', () => {
});
});
it('does not display main loading state again after fetchMore', async () => {
it('does not display skeleton loader again after fetchMore', async () => {
createComponent(resolverSpy);
expect(findSkeletonLoader().exists()).toBe(true);

View File

@ -66,7 +66,6 @@ describe('graph component', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('with data', () => {

View File

@ -1,5 +1,5 @@
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { GlButton, GlLoadingIcon, GlPopover } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants';
import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue';
@ -9,15 +9,20 @@ import mockPipeline from './linked_pipelines_mock_data';
describe('Linked pipeline', () => {
let wrapper;
const defaultProps = {
pipeline: mockPipeline,
columnTitle: 'Downstream',
type: DOWNSTREAM,
expanded: false,
isLoading: false,
isMultiProjectVizAvailable: true,
};
const downstreamProps = {
pipeline: {
...mockPipeline,
multiproject: false,
},
columnTitle: 'Downstream',
type: DOWNSTREAM,
expanded: false,
isLoading: false,
};
const upstreamProps = {
@ -27,21 +32,29 @@ describe('Linked pipeline', () => {
};
const findButton = () => wrapper.find(GlButton);
const findDownstreamPipelineTitle = () => wrapper.find('[data-testid="downstream-title"]');
const findPipelineLabel = () => wrapper.find('[data-testid="downstream-pipeline-label"]');
const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' });
const findDownstreamPipelineTitle = () => wrapper.findByTestId('downstream-title');
const findPipelineLabel = () => wrapper.findByTestId('downstream-pipeline-label');
const findLinkedPipeline = () => wrapper.findByTestId('linkedPipeline');
const findLinkedPipelineBody = () => wrapper.findByTestId('linkedPipelineBody');
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findPipelineLink = () => wrapper.find('[data-testid="pipelineLink"]');
const findExpandButton = () => wrapper.find('[data-testid="expand-pipeline-button"]');
const findPipelineLink = () => wrapper.findByTestId('pipelineLink');
const findExpandButton = () => wrapper.findByTestId('expand-pipeline-button');
const findPopover = () => wrapper.find(GlPopover);
const createWrapper = (propsData, data = []) => {
wrapper = mount(LinkedPipelineComponent, {
propsData,
wrapper = mountExtended(LinkedPipelineComponent, {
propsData: {
...defaultProps,
...propsData,
},
data() {
return {
...data,
};
},
provide: {
multiProjectHelpPath: '/ci/pipelines/multi-project-pipelines',
},
});
};
@ -92,7 +105,7 @@ describe('Linked pipeline', () => {
});
it('should render the tooltip text as the title attribute', () => {
const titleAttr = findLinkedPipeline().attributes('title');
const titleAttr = findLinkedPipelineBody().attributes('title');
expect(titleAttr).toContain(mockPipeline.project.name);
expect(titleAttr).toContain(mockPipeline.status.label);
@ -168,10 +181,6 @@ describe('Linked pipeline', () => {
describe('when isLoading is true', () => {
const props = {
pipeline: mockPipeline,
columnTitle: 'Downstream',
type: DOWNSTREAM,
expanded: false,
isLoading: true,
};
@ -184,19 +193,43 @@ describe('Linked pipeline', () => {
});
});
describe('on click/hover', () => {
const props = {
pipeline: mockPipeline,
columnTitle: 'Downstream',
type: DOWNSTREAM,
expanded: false,
isLoading: false,
};
describe('when the user does not have access to the multi-project pipeline viz feature', () => {
beforeEach(() => {
const props = { isMultiProjectVizAvailable: false };
createWrapper(props);
});
it('the multi-project expand button is disabled', () => {
expect(findExpandButton().props('disabled')).toBe(true);
});
it('it adds the popover text inside the DOM', () => {
expect(findPopover().exists()).toBe(true);
expect(findPopover().text()).toContain(
'Gitlab Premium users have access to the multi-project pipeline graph to improve the visualization of these pipelines.',
);
});
});
describe('when the user has access to the multi-project pipeline viz feature', () => {
beforeEach(() => {
createWrapper();
});
it('the multi-project expand button is enabled', () => {
expect(findExpandButton().props('disabled')).toBe(false);
});
it('does not add the popover text inside the DOM', () => {
expect(findPopover().exists()).toBe(false);
});
});
describe('on click/hover', () => {
beforeEach(() => {
createWrapper();
});
it('emits `pipelineClicked` event', () => {
jest.spyOn(wrapper.vm, '$emit');
findButton().trigger('click');

View File

@ -26,6 +26,7 @@ const processedPipeline = pipelineWithUpstreamDownstream(mockPipelineResponse);
describe('Linked Pipelines Column', () => {
const defaultProps = {
columnTitle: 'Downstream',
isMultiProjectVizAvailable: true,
linkedPipelines: processedPipeline.downstream,
showLinks: false,
type: DOWNSTREAM,
@ -51,6 +52,9 @@ describe('Linked Pipelines Column', () => {
...defaultProps,
...props,
},
provide: {
multiProjectHelpPath: 'ci/pipelines/multi-project-pipeline',
},
});
};
@ -67,7 +71,6 @@ describe('Linked Pipelines Column', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('it renders correctly', () => {

View File

@ -13,6 +13,15 @@ export const mockPipelineResponse = {
usesNeeds: true,
downstream: null,
upstream: null,
user: {
__typename: 'UserCore',
id: 'gid://gitlab/User/1',
namespace: {
__typename: 'Namespace',
id: 'gid://gitlab/Namespaces::UserNamespace/1',
crossProjectPipelineAvailable: true,
},
},
userPermissions: {
__typename: 'PipelinePermissions',
updatePipeline: true,
@ -780,6 +789,15 @@ export const wrappedPipelineReturn = {
id: 'gid://gitlab/Ci::Pipeline/175',
iid: '38',
complete: true,
user: {
__typename: 'UserCore',
id: 'gid://gitlab/User/1',
namespace: {
__typename: 'Namespace',
id: 'gid://gitlab/Namespaces::UserNamespace/1',
crossProjectPipelineAvailable: true,
},
},
usesNeeds: true,
userPermissions: {
__typename: 'PipelinePermissions',

View File

@ -30,14 +30,11 @@ describe('Pipeline Url Component', () => {
const defaultProps = mockPipeline(projectPath);
const createComponent = (props, rearrangePipelinesTable = false) => {
const createComponent = (props) => {
wrapper = shallowMountExtended(PipelineUrlComponent, {
propsData: { ...defaultProps, ...props },
provide: {
targetProjectFullPath: projectPath,
glFeatures: {
rearrangePipelinesTable,
},
},
});
};
@ -47,7 +44,6 @@ describe('Pipeline Url Component', () => {
wrapper = null;
});
describe('with the rearrangePipelinesTable feature flag turned off', () => {
it('should render pipeline url table cell', () => {
createComponent();
@ -194,15 +190,6 @@ describe('Pipeline Url Component', () => {
expect(findTrainTag().exists()).toBe(false);
});
it('should not render the commit wrapper and commit-short-sha', () => {
createComponent();
expect(findCommitTitleContainer().exists()).toBe(false);
expect(findCommitShortSha().exists()).toBe(false);
});
});
describe('with the rearrangePipelinesTable feature flag turned on', () => {
it('should render the commit title, commit reference and commit-short-sha', () => {
createComponent({}, true);
@ -224,13 +211,9 @@ describe('Pipeline Url Component', () => {
${mockPipelineTag()} | ${'Tag'}
${mockPipelineBranch()} | ${'Branch'}
${mockPipeline()} | ${'Merge Request'}
`(
'should render tooltip $expectedTitle for commit icon type',
({ pipeline, expectedTitle }) => {
`('should render tooltip $expectedTitle for commit icon type', ({ pipeline, expectedTitle }) => {
createComponent(pipeline, true);
expect(findCommitIconType().attributes('title')).toBe(expectedTitle);
},
);
});
});

View File

@ -17,7 +17,6 @@ import {
import eventHub from '~/pipelines/event_hub';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import CommitComponent from '~/vue_shared/components/commit.vue';
jest.mock('~/pipelines/event_hub');
@ -37,18 +36,13 @@ describe('Pipelines Table', () => {
return pipelines.find((p) => p.user !== null && p.commit !== null);
};
const createComponent = (props = {}, rearrangePipelinesTable = false) => {
const createComponent = (props = {}) => {
wrapper = extendedWrapper(
mount(PipelinesTable, {
propsData: {
...defaultProps,
...props,
},
provide: {
glFeatures: {
rearrangePipelinesTable,
},
},
}),
);
};
@ -57,7 +51,6 @@ describe('Pipelines Table', () => {
const findStatusBadge = () => wrapper.findComponent(CiBadge);
const findPipelineInfo = () => wrapper.findComponent(PipelineUrl);
const findTriggerer = () => wrapper.findComponent(PipelineTriggerer);
const findCommit = () => wrapper.findComponent(CommitComponent);
const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
const findTimeAgo = () => wrapper.findComponent(PipelinesTimeago);
const findActions = () => wrapper.findComponent(PipelineOperations);
@ -65,10 +58,7 @@ describe('Pipelines Table', () => {
const findTableRows = () => wrapper.findAllByTestId('pipeline-table-row');
const findStatusTh = () => wrapper.findByTestId('status-th');
const findPipelineTh = () => wrapper.findByTestId('pipeline-th');
const findTriggererTh = () => wrapper.findByTestId('triggerer-th');
const findCommitTh = () => wrapper.findByTestId('commit-th');
const findStagesTh = () => wrapper.findByTestId('stages-th');
const findTimeAgoTh = () => wrapper.findByTestId('timeago-th');
const findActionsTh = () => wrapper.findByTestId('actions-th');
const findRetryBtn = () => wrapper.findByTestId('pipelines-retry-button');
const findCancelBtn = () => wrapper.findByTestId('pipelines-cancel-button');
@ -82,7 +72,7 @@ describe('Pipelines Table', () => {
wrapper = null;
});
describe('Pipelines Table with rearrangePipelinesTable feature flag turned off', () => {
describe('Pipelines Table', () => {
beforeEach(() => {
createComponent({ pipelines: [pipeline], viewType: 'root' });
});
@ -93,11 +83,8 @@ describe('Pipelines Table', () => {
it('should render table head with correct columns', () => {
expect(findStatusTh().text()).toBe('Status');
expect(findPipelineTh().text()).toBe('Pipeline ID');
expect(findTriggererTh().text()).toBe('Triggerer');
expect(findCommitTh().text()).toBe('Commit');
expect(findPipelineTh().text()).toBe('Pipeline');
expect(findStagesTh().text()).toBe('Stages');
expect(findTimeAgoTh().text()).toBe('Duration');
expect(findActionsTh().text()).toBe('Actions');
});
@ -125,27 +112,6 @@ describe('Pipelines Table', () => {
});
});
describe('triggerer cell', () => {
it('should render the pipeline triggerer', () => {
expect(findTriggerer().exists()).toBe(true);
});
});
describe('commit cell', () => {
it('should render commit information', () => {
expect(findCommit().exists()).toBe(true);
});
it('should display and link to commit', () => {
expect(findCommit().text()).toContain(pipeline.commit.short_id);
expect(findCommit().props('commitUrl')).toBe(pipeline.commit.commit_path);
});
it('should display the commit author', () => {
expect(findCommit().props('author')).toEqual(pipeline.commit.author);
});
});
describe('stages cell', () => {
it('should render a pipeline mini graph', () => {
expect(findPipelineMiniGraph().exists()).toBe(true);
@ -163,7 +129,7 @@ describe('Pipelines Table', () => {
pipeline = createMockPipeline();
pipeline.details.stages = null;
createComponent({ pipelines: [pipeline] }, true);
createComponent({ pipelines: [pipeline] });
});
it('stages are not rendered', () => {
@ -176,7 +142,7 @@ describe('Pipelines Table', () => {
});
it('when update graph dropdown is set, should update graph dropdown', () => {
createComponent({ pipelines: [pipeline], updateGraphDropdown: true }, true);
createComponent({ pipelines: [pipeline], updateGraphDropdown: true });
expect(findPipelineMiniGraph().props('updateDropdown')).toBe(true);
});
@ -207,30 +173,11 @@ describe('Pipelines Table', () => {
expect(findCancelBtn().attributes('title')).toBe(BUTTON_TOOLTIP_CANCEL);
});
});
});
describe('Pipelines Table with rearrangePipelinesTable feature flag turned on', () => {
beforeEach(() => {
createComponent({ pipelines: [pipeline], viewType: 'root' }, true);
});
it('should render table head with correct columns', () => {
expect(findStatusTh().text()).toBe('Status');
expect(findPipelineTh().text()).toBe('Pipeline');
expect(findStagesTh().text()).toBe('Stages');
expect(findActionsTh().text()).toBe('Actions');
});
describe('triggerer cell', () => {
it('should render the pipeline triggerer', () => {
expect(findTriggerer().exists()).toBe(true);
});
});
describe('commit cell', () => {
it('should not render commit information', () => {
expect(findCommit().exists()).toBe(false);
});
});
});
});

View File

@ -34,21 +34,19 @@ exports[`Issue Warning Component when noteable is locked and confidential render
<span>
<span>
This issue is
<a
<gl-link-stub
href=""
rel="noopener noreferrer"
target="_blank"
>
confidential
</a>
</gl-link-stub>
and
<a
<gl-link-stub
href=""
rel="noopener noreferrer"
target="_blank"
>
locked
</a>
</gl-link-stub>
.
</span>

View File

@ -1,4 +1,4 @@
import { GlIcon } from '@gitlab/ui';
import { GlIcon, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
@ -16,6 +16,9 @@ describe('Issue Warning Component', () => {
propsData: {
...props,
},
stubs: {
GlSprintf,
},
});
afterEach(() => {

View File

@ -0,0 +1,51 @@
import { shallowMount } from '@vue/test-utils';
import { GlBadge, GlIcon } from '@gitlab/ui';
import TierBadge from '~/vue_shared/components/tier_badge.vue';
describe('Tier badge component', () => {
let wrapper;
const createComponent = (props) =>
shallowMount(TierBadge, {
propsData: {
...props,
},
});
const findBadge = () => wrapper.findComponent(GlBadge);
const findTierText = () => findBadge().text();
const findIcon = () => wrapper.findComponent(GlIcon);
afterEach(() => {
wrapper.destroy();
});
describe('tiers name', () => {
it.each`
tier | tierText
${'free'} | ${'Free'}
${'premium'} | ${'Premium'}
${'ultimate'} | ${'Ultimate'}
`(
'shows $tierText text in the badge and the license icon when $tier prop is passed',
({ tier, tierText }) => {
wrapper = createComponent({ tier });
expect(findTierText()).toBe(tierText);
expect(findIcon().exists()).toBe(true);
expect(findIcon().props().name).toBe('license');
},
);
});
describe('badge size', () => {
const newSize = 'lg';
beforeEach(() => {
wrapper = createComponent({ tier: 'free', size: newSize });
});
it('passes down the size prop to the GlBadge component', () => {
expect(findBadge().props().size).toBe(newSize);
});
});
});

View File

@ -102,9 +102,9 @@ RSpec.describe Types::BaseEnum do
it 'sets the values defined by the declarative enum' do
set_declarative_enum
expect(enum_type.values.keys).to eq(['FOO'])
expect(enum_type.values.values.map(&:description)).to eq(['description of foo'])
expect(enum_type.values.values.map(&:value)).to eq([0])
expect(enum_type.values.keys).to contain_exactly('FOO')
expect(enum_type.values.values.map(&:description)).to contain_exactly('description of foo')
expect(enum_type.values.values.map(&:value)).to contain_exactly('foo')
end
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::PipelineHelper do
describe '#js_pipeline_details_data' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
subject(:pipeline_details_data) { helper.js_pipeline_details_data(project, pipeline) }
it 'returns pipeline details data' do
expect(pipeline_details_data).to eq({
graphql_resource_etag: graphql_etag_pipeline_path(pipeline),
metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: project.namespace, project_id: project, format: :json),
multi_project_help_path: help_page_path('ci/pipelines/multi_project_pipelines.md', anchor: 'multi-project-pipeline-visualization'),
pipeline_iid: pipeline.iid,
pipeline_project_path: project.full_path
})
end
end
end

View File

@ -168,24 +168,100 @@ RSpec.describe ContainerRegistry::Client do
expect(subject).to eq('Blob')
end
it 'follows 307 redirect for GET /v2/:name/blobs/:digest' do
context 'with a 307 redirect' do
let(:redirect_location) { 'http://redirected' }
before do
stub_request(method, url)
.with(headers: blob_headers)
.to_return(status: 307, body: '', headers: { Location: 'http://redirected' })
.to_return(status: 307, body: '', headers: { Location: redirect_location })
# We should probably use hash_excluding here, but that requires an update to WebMock:
# https://github.com/bblimke/webmock/blob/master/lib/webmock/matchers/hash_excluding_matcher.rb
stub_request(:get, "http://redirected/")
stub_request(:get, redirect_location)
.with(headers: redirect_header) do |request|
!request.headers.include?('Authorization')
end
.to_return(status: 200, body: "Successfully redirected")
end
shared_examples 'handling redirects' do
it 'follows the redirect' do
expect(Faraday::Utils).not_to receive(:escape).with('signature=')
expect_new_faraday
expect(subject).to eq('Successfully redirected')
end
end
it_behaves_like 'handling redirects'
context 'with a redirect location with params ending with =' do
let(:redirect_location) { 'http://redirect?foo=bar&test=signature=' }
it_behaves_like 'handling redirects'
context 'with container_registry_follow_redirects_middleware disabled' do
before do
stub_feature_flags(container_registry_follow_redirects_middleware: false)
end
it 'follows the redirect' do
expect(Faraday::Utils).to receive(:escape).with('foo').and_call_original
expect(Faraday::Utils).to receive(:escape).with('bar').and_call_original
expect(Faraday::Utils).to receive(:escape).with('test').and_call_original
expect(Faraday::Utils).to receive(:escape).with('signature=').and_call_original
expect_new_faraday(times: 2)
expect(subject).to eq('Successfully redirected')
end
end
end
context 'with a redirect location with params ending with %3D' do
let(:redirect_location) { 'http://redirect?foo=bar&test=signature%3D' }
it_behaves_like 'handling redirects'
context 'with container_registry_follow_redirects_middleware disabled' do
before do
stub_feature_flags(container_registry_follow_redirects_middleware: false)
end
it 'follows the redirect' do
expect(Faraday::Utils).to receive(:escape).with('foo').and_call_original
expect(Faraday::Utils).to receive(:escape).with('bar').and_call_original
expect(Faraday::Utils).to receive(:escape).with('test').and_call_original
expect(Faraday::Utils).to receive(:escape).with('signature=').and_call_original
expect_new_faraday(times: 2)
expect(subject).to eq('Successfully redirected')
end
end
end
end
it_behaves_like 'handling timeouts'
# TODO Remove this context along with the
# container_registry_follow_redirects_middleware feature flag
# See https://gitlab.com/gitlab-org/gitlab/-/issues/353291
context 'faraday blob' do
subject { client.send(:faraday_blob) }
it 'has a follow redirects middleware' do
expect(subject.builder.handlers).to include(::FaradayMiddleware::FollowRedirects)
end
context 'with container_registry_follow_redirects_middleware is disabled' do
before do
stub_feature_flags(container_registry_follow_redirects_middleware: false)
end
it 'has not a follow redirects middleware' do
expect(subject.builder.handlers).not_to include(::FaradayMiddleware::FollowRedirects)
end
end
end
end
describe '#upload_blob' do

View File

@ -0,0 +1,82 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::MigratePersonalNamespaceProjectMaintainerToOwner, :migration, schema: 20220208080921 do
let(:migration) { described_class.new }
let(:users_table) { table(:users) }
let(:members_table) { table(:members) }
let(:namespaces_table) { table(:namespaces) }
let(:projects_table) { table(:projects) }
let(:table_name) { 'members' }
let(:batch_column) { :id }
let(:sub_batch_size) { 10 }
let(:pause_ms) { 0 }
let(:owner_access) { 50 }
let(:maintainer_access) { 40 }
let(:developer_access) { 30 }
subject(:perform_migration) { migration.perform(1, 10, table_name, batch_column, sub_batch_size, pause_ms) }
before do
users_table.create!(id: 101, name: "user1", email: "user1@example.com", projects_limit: 5)
users_table.create!(id: 102, name: "user2", email: "user2@example.com", projects_limit: 5)
namespaces_table.create!(id: 201, name: 'user1s-namespace', path: 'user1s-namespace-path', type: 'User', owner_id: 101)
namespaces_table.create!(id: 202, name: 'user2s-namespace', path: 'user2s-namespace-path', type: 'User', owner_id: 102)
namespaces_table.create!(id: 203, name: 'group', path: 'group', type: 'Group')
namespaces_table.create!(id: 204, name: 'project-namespace', path: 'project-namespace-path', type: 'Project')
projects_table.create!(id: 301, name: 'user1-namespace-project', path: 'project-path-1', namespace_id: 201)
projects_table.create!(id: 302, name: 'user2-namespace-project', path: 'project-path-2', namespace_id: 202)
projects_table.create!(id: 303, name: 'user2s-namespace-project2', path: 'project-path-3', namespace_id: 202)
projects_table.create!(id: 304, name: 'group-project3', path: 'group-project-path-3', namespace_id: 203)
# user1 member of their own namespace project, maintainer access (change)
create_project_member(id: 1, user_id: 101, project_id: 301, level: maintainer_access)
# user2 member of their own namespace project, owner access (no change)
create_project_member(id: 2, user_id: 102, project_id: 302, level: owner_access)
# user1 member of user2's personal namespace project, maintainer access (no change)
create_project_member(id: 3, user_id: 101, project_id: 302, level: maintainer_access)
# user1 member of group project, maintainer access (no change)
create_project_member(id: 4, user_id: 101, project_id: 304, level: maintainer_access)
# user1 member of group, Maintainer role (no change)
create_group_member(id: 5, user_id: 101, group_id: 203, level: maintainer_access)
# user2 member of their own namespace project, maintainer access, but out of batch range (no change)
create_project_member(id: 601, user_id: 102, project_id: 303, level: maintainer_access)
end
it 'migrates MAINTAINER membership records for personal namespaces to OWNER', :aggregate_failures do
expect(members_table.where(access_level: owner_access).count).to eq 1
expect(members_table.where(access_level: maintainer_access).count).to eq 5
queries = ActiveRecord::QueryRecorder.new do
perform_migration
end
expect(queries.count).to eq(3)
expect(members_table.where(access_level: owner_access).pluck(:id)).to match_array([1, 2])
expect(members_table.where(access_level: maintainer_access).pluck(:id)).to match_array([3, 4, 5, 601])
end
it 'tracks timings of queries' do
expect(migration.batch_metrics.timings).to be_empty
expect { perform_migration }.to change { migration.batch_metrics.timings }
end
def create_group_member(id:, user_id:, group_id:, level:)
members_table.create!(id: id, user_id: user_id, source_id: group_id, access_level: level, source_type: "Namespace", type: "GroupMember", notification_level: 3)
end
def create_project_member(id:, user_id:, project_id:, level:)
members_table.create!(id: id, user_id: user_id, source_id: project_id, access_level: level, source_type: "Namespace", type: "ProjectMember", notification_level: 3)
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe ScheduleMigratePersonalNamespaceProjectMaintainerToOwner do
let_it_be(:migration) { described_class::MIGRATION }
describe '#up' do
it 'schedules background jobs for each batch of members' do
migrate!
expect(migration).to have_scheduled_batched_migration(
table_name: :members,
column_name: :id,
interval: described_class::INTERVAL
)
end
end
end

View File

@ -4,40 +4,22 @@ require 'spec_helper'
RSpec.describe IssueLinks::DestroyService do
describe '#execute' do
let(:project) { create(:project_empty_repo) }
let(:user) { create(:user) }
let_it_be(:project) { create(:project_empty_repo, :private) }
let_it_be(:user) { create(:user) }
let_it_be(:issue_a) { create(:issue, project: project) }
let_it_be(:issue_b) { create(:issue, project: project) }
subject { described_class.new(issue_link, user).execute }
let!(:issuable_link) { create(:issue_link, source: issue_a, target: issue_b) }
context 'when successfully removes an issue link' do
let(:issue_a) { create(:issue, project: project) }
let(:issue_b) { create(:issue, project: project) }
subject { described_class.new(issuable_link, user).execute }
let!(:issue_link) { create(:issue_link, source: issue_a, target: issue_b) }
it_behaves_like 'a destroyable issuable link'
context 'when target is an incident' do
before do
project.add_reporter(user)
end
it 'removes related issue' do
expect { subject }.to change(IssueLink, :count).from(1).to(0)
end
it 'creates notes' do
# Two-way notes creation
expect(SystemNoteService).to receive(:unrelate_issue)
.with(issue_link.source, issue_link.target, user)
expect(SystemNoteService).to receive(:unrelate_issue)
.with(issue_link.target, issue_link.source, user)
subject
end
it 'returns success message' do
is_expected.to eq(message: 'Relation was removed', status: :success)
end
context 'target is an incident' do
let(:issue_b) { create(:incident, project: project) }
it_behaves_like 'an incident management tracked event', :incident_management_incident_unrelate do
@ -45,25 +27,4 @@ RSpec.describe IssueLinks::DestroyService do
end
end
end
context 'when failing to remove an issue link' do
let(:unauthorized_project) { create(:project) }
let(:issue_a) { create(:issue, project: project) }
let(:issue_b) { create(:issue, project: unauthorized_project) }
let!(:issue_link) { create(:issue_link, source: issue_a, target: issue_b) }
it 'does not remove relation' do
expect { subject }.not_to change(IssueLink, :count).from(1)
end
it 'does not create notes' do
expect(SystemNoteService).not_to receive(:unrelate_issue)
end
it 'returns error message' do
is_expected.to eq(message: 'No Issue Link found', status: :error, http_status: 404)
end
end
end
end

View File

@ -117,7 +117,7 @@ RSpec.describe SystemNoteService do
end
end
describe '.unrelate_issue' do
describe '.unrelate_issuable' do
let(:noteable_ref) { double }
let(:noteable) { double }
@ -127,10 +127,10 @@ RSpec.describe SystemNoteService do
it 'calls IssuableService' do
expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
expect(service).to receive(:unrelate_issue).with(noteable_ref)
expect(service).to receive(:unrelate_issuable).with(noteable_ref)
end
described_class.unrelate_issue(noteable, noteable_ref, double)
described_class.unrelate_issuable(noteable, noteable_ref, double)
end
end

View File

@ -30,10 +30,10 @@ RSpec.describe ::SystemNotes::IssuablesService do
end
end
describe '#unrelate_issue' do
describe '#unrelate_issuable' do
let(:noteable_ref) { create(:issue) }
subject { service.unrelate_issue(noteable_ref) }
subject { service.unrelate_issuable(noteable_ref) }
it_behaves_like 'a system note' do
let(:action) { 'unrelate' }

View File

@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe WebHooks::LogExecutionService do
include ExclusiveLeaseHelpers
using RSpec::Parameterized::TableSyntax
describe '#execute' do
around do |example|
@ -34,11 +35,13 @@ RSpec.describe WebHooks::LogExecutionService do
expect(WebHookLog.recent.first).to have_attributes(data)
end
context 'obtaining an exclusive lease' do
let(:lease_key) { "web_hooks:update_hook_failure_state:#{project_hook.id}" }
it 'updates failure state using a lease that ensures fresh state is written' do
service = described_class.new(hook: project_hook, log_data: data, response_category: :error)
WebHook.find(project_hook.id).update!(backoff_count: 1)
lease_key = "web_hooks:update_hook_failure_state:#{project_hook.id}"
lease = stub_exclusive_lease(lease_key, timeout: described_class::LOCK_TTL)
expect(lease).to receive(:try_obtain)
@ -46,6 +49,35 @@ RSpec.describe WebHooks::LogExecutionService do
expect { service.execute }.to change { WebHook.find(project_hook.id).backoff_count }.to(2)
end
context 'when a lease cannot be obtained' do
where(:response_category, :executable, :needs_updating) do
:ok | true | false
:ok | false | true
:failed | true | true
:failed | false | false
:error | true | true
:error | false | false
end
with_them do
subject(:service) { described_class.new(hook: project_hook, log_data: data, response_category: response_category) }
before do
stub_exclusive_lease_taken(lease_key, timeout: described_class::LOCK_TTL)
allow(project_hook).to receive(:executable?).and_return(executable)
end
it 'raises an error if the hook needs to be updated' do
if needs_updating
expect { service.execute }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
else
expect { service.execute }.not_to raise_error
end
end
end
end
end
context 'when response_category is :ok' do
it 'does not increment the failure count' do
expect { service.execute }.not_to change(project_hook, :recent_failures)

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
shared_examples 'a destroyable issuable link' do
context 'when successfully removes an issuable link' do
before do
issuable_link.source.resource_parent.add_reporter(user)
issuable_link.target.resource_parent.add_reporter(user)
end
it 'removes related issue' do
expect { subject }.to change(issuable_link.class, :count).by(-1)
end
it 'creates notes' do
# Two-way notes creation
expect(SystemNoteService).to receive(:unrelate_issuable)
.with(issuable_link.source, issuable_link.target, user)
expect(SystemNoteService).to receive(:unrelate_issuable)
.with(issuable_link.target, issuable_link.source, user)
subject
end
it 'returns success message' do
is_expected.to eq(message: 'Relation was removed', status: :success)
end
end
context 'when failing to remove an issuable link' do
it 'does not remove relation' do
expect { subject }.not_to change(issuable_link.class, :count).from(1)
end
it 'does not create notes' do
expect(SystemNoteService).not_to receive(:unrelate_issuable)
end
it 'returns error message' do
is_expected.to eq(message: "No #{issuable_link.class.model_name.human.titleize} found", status: :error, http_status: 404)
end
end
end