Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4c39dd11dc
commit
28f1931ae8
65 changed files with 1013 additions and 316 deletions
|
@ -0,0 +1,10 @@
|
|||
import { REST, GRAPHQL } from './constants';
|
||||
|
||||
export const accessors = {
|
||||
[REST]: {
|
||||
groupId: 'id',
|
||||
},
|
||||
[GRAPHQL]: {
|
||||
groupId: 'name',
|
||||
},
|
||||
};
|
|
@ -87,7 +87,7 @@ export default {
|
|||
:title="tooltipText"
|
||||
:class="cssClass"
|
||||
:disabled="isDisabled"
|
||||
class="js-ci-action ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center"
|
||||
class="js-ci-action gl-ci-action-icon-container ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center"
|
||||
@click.stop="onClickAction"
|
||||
>
|
||||
<gl-loading-icon v-if="isLoading" class="js-action-icon-loading" />
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
export const DOWNSTREAM = 'downstream';
|
||||
export const MAIN = 'main';
|
||||
export const UPSTREAM = 'upstream';
|
||||
|
||||
export const REST = 'rest';
|
||||
export const GRAPHQL = 'graphql';
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
<script>
|
||||
import { escape, capitalize } from 'lodash';
|
||||
import StageColumnComponent from './stage_column_component.vue';
|
||||
import GraphBundleMixin from '../../mixins/graph_pipeline_bundle_mixin';
|
||||
import { MAIN } from './constants';
|
||||
|
||||
export default {
|
||||
|
@ -9,7 +7,6 @@ export default {
|
|||
components: {
|
||||
StageColumnComponent,
|
||||
},
|
||||
mixins: [GraphBundleMixin],
|
||||
props: {
|
||||
isLinkedPipeline: {
|
||||
type: Boolean,
|
||||
|
@ -31,96 +28,21 @@ export default {
|
|||
return this.pipeline.stages;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
capitalizeStageName(name) {
|
||||
const escapedName = escape(name);
|
||||
return capitalize(escapedName);
|
||||
},
|
||||
isFirstColumn(index) {
|
||||
return index === 0;
|
||||
},
|
||||
stageConnectorClass(index, stage) {
|
||||
let className;
|
||||
|
||||
// If it's the first stage column and only has one job
|
||||
if (this.isFirstColumn(index) && stage.groups.length === 1) {
|
||||
className = 'no-margin';
|
||||
} else if (index > 0) {
|
||||
// If it is not the first column
|
||||
className = 'left-margin';
|
||||
}
|
||||
|
||||
return className;
|
||||
},
|
||||
refreshPipelineGraph() {
|
||||
this.$emit('refreshPipelineGraph');
|
||||
},
|
||||
/**
|
||||
* CSS class is applied:
|
||||
* - if pipeline graph contains only one stage column component
|
||||
*
|
||||
* @param {number} index
|
||||
* @returns {boolean}
|
||||
*/
|
||||
shouldAddRightMargin(index) {
|
||||
return !(index === this.graph.length - 1);
|
||||
},
|
||||
handleClickedDownstream(pipeline, clickedIndex, downstreamNode) {
|
||||
/**
|
||||
* Calculates the margin top of the clicked downstream pipeline by
|
||||
* subtracting the clicked downstream pipelines offsetTop by it's parent's
|
||||
* offsetTop and then subtracting 15
|
||||
*/
|
||||
this.downstreamMarginTop = this.calculateMarginTop(downstreamNode, 15);
|
||||
|
||||
/**
|
||||
* If the expanded trigger is defined and the id is different than the
|
||||
* pipeline we clicked, then it means we clicked on a sibling downstream link
|
||||
* and we want to reset the pipeline store. Triggering the reset without
|
||||
* this condition would mean not allowing downstreams of downstreams to expand
|
||||
*/
|
||||
if (this.expandedDownstream?.id !== pipeline.id) {
|
||||
this.$emit('onResetDownstream', this.pipeline, pipeline);
|
||||
}
|
||||
|
||||
this.$emit('onClickDownstreamPipeline', pipeline);
|
||||
},
|
||||
calculateMarginTop(downstreamNode, pixelDiff) {
|
||||
return `${downstreamNode.offsetTop - downstreamNode.offsetParent.offsetTop - pixelDiff}px`;
|
||||
},
|
||||
hasOnlyOneJob(stage) {
|
||||
return stage.groups.length === 1;
|
||||
},
|
||||
hasUpstreamColumn(index) {
|
||||
return index === 0 && this.hasUpstream;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="build-content middle-block js-pipeline-graph">
|
||||
<div class="js-pipeline-graph">
|
||||
<div
|
||||
class="pipeline-visualization pipeline-graph"
|
||||
:class="{ 'pipeline-tab-content': !isLinkedPipeline }"
|
||||
class="gl-pipeline-min-h gl-display-flex gl-position-relative gl-overflow-auto gl-bg-gray-10 gl-white-space-nowrap"
|
||||
:class="{ 'gl-py-5': !isLinkedPipeline }"
|
||||
>
|
||||
<div>
|
||||
<ul class="stage-column-list align-top">
|
||||
<stage-column-component
|
||||
v-for="(stage, index) in graph"
|
||||
:key="stage.name"
|
||||
:class="{
|
||||
'has-only-one-job': hasOnlyOneJob(stage),
|
||||
'gl-mr-26': shouldAddRightMargin(index),
|
||||
}"
|
||||
:title="capitalizeStageName(stage.name)"
|
||||
:groups="stage.groups"
|
||||
:stage-connector-class="stageConnectorClass(index, stage)"
|
||||
:is-first-column="isFirstColumn(index)"
|
||||
:action="stage.status.action"
|
||||
@refreshPipelineGraph="refreshPipelineGraph"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
<stage-column-component
|
||||
v-for="stage in graph"
|
||||
:key="stage.name"
|
||||
:title="stage.name"
|
||||
:groups="stage.groups"
|
||||
:action="stage.status.action"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { escape, capitalize } from 'lodash';
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import StageColumnComponent from './stage_column_component.vue';
|
||||
import StageColumnComponentLegacy from './stage_column_component_legacy.vue';
|
||||
import GraphWidthMixin from '../../mixins/graph_width_mixin';
|
||||
import LinkedPipelinesColumn from './linked_pipelines_column.vue';
|
||||
import GraphBundleMixin from '../../mixins/graph_pipeline_bundle_mixin';
|
||||
|
@ -10,7 +10,7 @@ import { UPSTREAM, DOWNSTREAM, MAIN } from './constants';
|
|||
export default {
|
||||
name: 'PipelineGraphLegacy',
|
||||
components: {
|
||||
StageColumnComponent,
|
||||
StageColumnComponentLegacy,
|
||||
GlLoadingIcon,
|
||||
LinkedPipelinesColumn,
|
||||
},
|
||||
|
@ -220,7 +220,7 @@ export default {
|
|||
}"
|
||||
class="stage-column-list align-top"
|
||||
>
|
||||
<stage-column-component
|
||||
<stage-column-component-legacy
|
||||
v-for="(stage, index) in graph"
|
||||
:key="stage.name"
|
||||
:class="{
|
||||
|
|
|
@ -44,17 +44,19 @@ export default {
|
|||
type="button"
|
||||
data-toggle="dropdown"
|
||||
data-display="static"
|
||||
class="dropdown-menu-toggle build-content"
|
||||
class="dropdown-menu-toggle build-content gl-build-content"
|
||||
>
|
||||
<ci-icon :status="group.status" />
|
||||
<div class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
|
||||
<span class="gl-display-flex gl-align-items-center">
|
||||
<ci-icon :status="group.status" :size="24" />
|
||||
|
||||
<span
|
||||
class="gl-text-truncate mw-70p gl-pl-2 gl-display-inline-block gl-vertical-align-bottom"
|
||||
>
|
||||
{{ group.name }}
|
||||
</span>
|
||||
<span class="gl-text-truncate mw-70p gl-pl-3 gl-display-inline-block">
|
||||
{{ group.name }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="dropdown-counter-badge"> {{ group.size }} </span>
|
||||
<span class="gl-font-weight-100 gl-font-size-lg gl-pr-2"> {{ group.size }} </span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<ul class="dropdown-menu big-pipeline-graph-dropdown-menu js-grouped-pipeline-dropdown">
|
||||
|
|
|
@ -129,19 +129,23 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="ci-job-component" data-qa-selector="job_item_container">
|
||||
<div
|
||||
class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between"
|
||||
data-qa-selector="job_item_container"
|
||||
>
|
||||
<gl-link
|
||||
v-if="status.has_details"
|
||||
v-gl-tooltip="{ boundary, placement: 'bottom', customClass: 'gl-pointer-events-none' }"
|
||||
:href="status.details_path"
|
||||
:title="tooltipText"
|
||||
:class="jobClasses"
|
||||
class="js-pipeline-graph-job-link qa-job-link menu-item"
|
||||
class="js-pipeline-graph-job-link qa-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none
|
||||
gl-focus-text-decoration-none"
|
||||
data-testid="job-with-link"
|
||||
@click.stop="hideTooltips"
|
||||
@mouseout="hideTooltips"
|
||||
>
|
||||
<job-name-component :name="job.name" :status="job.status" />
|
||||
<job-name-component :name="job.name" :status="job.status" :icon-size="24" />
|
||||
</gl-link>
|
||||
|
||||
<div
|
||||
|
@ -153,7 +157,7 @@ export default {
|
|||
data-testid="job-without-link"
|
||||
@mouseout="hideTooltips"
|
||||
>
|
||||
<job-name-component :name="job.name" :status="job.status" />
|
||||
<job-name-component :name="job.name" :status="job.status" :icon-size="24" />
|
||||
</div>
|
||||
|
||||
<action-component
|
||||
|
|
|
@ -16,18 +16,22 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
status: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
iconSize: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 16,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<span class="ci-job-name-component mw-100">
|
||||
<ci-icon :status="status" />
|
||||
<span class="gl-text-truncate mw-70p gl-pl-2 gl-display-inline-block gl-vertical-align-bottom">
|
||||
<span class="ci-job-name-component mw-100 gl-display-flex gl-align-items-center">
|
||||
<ci-icon :size="iconSize" :status="status" />
|
||||
<span class="gl-text-truncate mw-70p gl-pl-3 gl-display-inline-block">
|
||||
{{ name }}
|
||||
</span>
|
||||
</span>
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
<script>
|
||||
import { isEmpty, escape } from 'lodash';
|
||||
import stageColumnMixin from '../../mixins/stage_column_mixin';
|
||||
import { capitalize, escape, isEmpty } from 'lodash';
|
||||
import MainGraphWrapper from '../graph_shared/main_graph_wrapper.vue';
|
||||
import JobItem from './job_item.vue';
|
||||
import JobGroupDropdown from './job_group_dropdown.vue';
|
||||
import ActionComponent from './action_component.vue';
|
||||
import { GRAPHQL } from './constants';
|
||||
import { accessors } from './accessors';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
JobItem,
|
||||
JobGroupDropdown,
|
||||
ActionComponent,
|
||||
JobGroupDropdown,
|
||||
JobItem,
|
||||
MainGraphWrapper,
|
||||
},
|
||||
mixins: [stageColumnMixin],
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
|
@ -21,16 +23,6 @@ export default {
|
|||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
isFirstColumn: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
stageConnectorClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
action: {
|
||||
type: Object,
|
||||
required: false,
|
||||
|
@ -47,62 +39,67 @@ export default {
|
|||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
accessors,
|
||||
titleClasses: [
|
||||
'gl-font-weight-bold',
|
||||
'gl-pipeline-job-width',
|
||||
'gl-text-truncate',
|
||||
'gl-line-height-36',
|
||||
'gl-pl-3',
|
||||
],
|
||||
computed: {
|
||||
formattedTitle() {
|
||||
return capitalize(escape(this.title));
|
||||
},
|
||||
hasAction() {
|
||||
return !isEmpty(this.action);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getAccessor(property) {
|
||||
return accessors[GRAPHQL][property];
|
||||
},
|
||||
groupId(group) {
|
||||
return `ci-badge-${escape(group.name)}`;
|
||||
},
|
||||
pipelineActionRequestComplete() {
|
||||
this.$emit('refreshPipelineGraph');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<li :class="stageConnectorClass" class="stage-column">
|
||||
<div class="stage-name position-relative" data-testid="stage-column-title">
|
||||
{{ title }}
|
||||
<action-component
|
||||
v-if="hasAction"
|
||||
:action-icon="action.icon"
|
||||
:tooltip-text="action.title"
|
||||
:link="action.path"
|
||||
class="js-stage-action stage-action rounded"
|
||||
@pipelineActionRequestComplete="pipelineActionRequestComplete"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="builds-container">
|
||||
<ul>
|
||||
<li
|
||||
v-for="(group, index) in groups"
|
||||
:id="groupId(group)"
|
||||
:key="group.id"
|
||||
:class="buildConnnectorClass(index)"
|
||||
class="build"
|
||||
>
|
||||
<div class="curve"></div>
|
||||
|
||||
<job-item
|
||||
v-if="group.size === 1"
|
||||
:job="group.jobs[0]"
|
||||
:job-hovered="jobHovered"
|
||||
:pipeline-expanded="pipelineExpanded"
|
||||
css-class-job-name="build-content"
|
||||
@pipelineActionRequestComplete="pipelineActionRequestComplete"
|
||||
/>
|
||||
|
||||
<job-group-dropdown
|
||||
v-if="group.size > 1"
|
||||
:group="group"
|
||||
@pipelineActionRequestComplete="pipelineActionRequestComplete"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<main-graph-wrapper>
|
||||
<template #stages>
|
||||
<div
|
||||
data-testid="stage-column-title"
|
||||
class="gl-display-flex gl-justify-content-space-between gl-relative"
|
||||
:class="$options.titleClasses"
|
||||
>
|
||||
<div>{{ formattedTitle }}</div>
|
||||
<action-component
|
||||
v-if="hasAction"
|
||||
:action-icon="action.icon"
|
||||
:tooltip-text="action.title"
|
||||
:link="action.path"
|
||||
class="js-stage-action stage-action rounded"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #jobs>
|
||||
<div
|
||||
v-for="group in groups"
|
||||
:id="groupId(group)"
|
||||
:key="group[getAccessor('groupId')]"
|
||||
data-testid="stage-column-group"
|
||||
class="gl-relative gl-mb-3 gl-white-space-normal gl-pipeline-job-width"
|
||||
>
|
||||
<job-item
|
||||
v-if="group.size === 1"
|
||||
:job="group.jobs[0]"
|
||||
:job-hovered="jobHovered"
|
||||
:pipeline-expanded="pipelineExpanded"
|
||||
css-class-job-name="gl-build-content"
|
||||
/>
|
||||
<job-group-dropdown v-else :group="group" />
|
||||
</div>
|
||||
</template>
|
||||
</main-graph-wrapper>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
<script>
|
||||
import { isEmpty, escape } from 'lodash';
|
||||
import stageColumnMixin from '../../mixins/stage_column_mixin';
|
||||
import JobItem from './job_item.vue';
|
||||
import JobGroupDropdown from './job_group_dropdown.vue';
|
||||
import ActionComponent from './action_component.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
JobItem,
|
||||
JobGroupDropdown,
|
||||
ActionComponent,
|
||||
},
|
||||
mixins: [stageColumnMixin],
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
groups: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
isFirstColumn: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
stageConnectorClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
action: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
jobHovered: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
pipelineExpanded: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hasAction() {
|
||||
return !isEmpty(this.action);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
groupId(group) {
|
||||
return `ci-badge-${escape(group.name)}`;
|
||||
},
|
||||
pipelineActionRequestComplete() {
|
||||
this.$emit('refreshPipelineGraph');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<li :class="stageConnectorClass" class="stage-column">
|
||||
<div class="stage-name position-relative" data-testid="stage-column-title">
|
||||
{{ title }}
|
||||
<action-component
|
||||
v-if="hasAction"
|
||||
:action-icon="action.icon"
|
||||
:tooltip-text="action.title"
|
||||
:link="action.path"
|
||||
class="js-stage-action stage-action rounded"
|
||||
@pipelineActionRequestComplete="pipelineActionRequestComplete"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="builds-container">
|
||||
<ul>
|
||||
<li
|
||||
v-for="(group, index) in groups"
|
||||
:id="groupId(group)"
|
||||
:key="group.id"
|
||||
:class="buildConnnectorClass(index)"
|
||||
class="build"
|
||||
>
|
||||
<div class="curve"></div>
|
||||
|
||||
<job-item
|
||||
v-if="group.size === 1"
|
||||
:job="group.jobs[0]"
|
||||
:job-hovered="jobHovered"
|
||||
:pipeline-expanded="pipelineExpanded"
|
||||
css-class-job-name="build-content"
|
||||
@pipelineActionRequestComplete="pipelineActionRequestComplete"
|
||||
/>
|
||||
|
||||
<job-group-dropdown
|
||||
v-if="group.size > 1"
|
||||
:group="group"
|
||||
@pipelineActionRequestComplete="pipelineActionRequestComplete"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
|
@ -0,0 +1,32 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
stageClasses: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
jobClasses: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="gl-display-flex gl-align-items-center gl-w-full gl-px-8 gl-py-4 gl-mb-5"
|
||||
:class="stageClasses"
|
||||
>
|
||||
<slot name="stages"> </slot>
|
||||
</div>
|
||||
<div
|
||||
class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full gl-px-8"
|
||||
:class="jobClasses"
|
||||
>
|
||||
<slot name="jobs"> </slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -129,6 +129,51 @@
|
|||
overflow: auto;
|
||||
}
|
||||
|
||||
// Move to Gitlab UI
|
||||
.gl-font-weight-100 {
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
.gl-active-text-decoration-none:active,
|
||||
.gl-focus-text-decoration-none:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// These are single-value classes to use with utility-class style CSS
|
||||
// but to still access this variable. Do not add other styles.
|
||||
.gl-pipeline-min-h {
|
||||
min-height: $dropdown-max-height-lg;
|
||||
}
|
||||
|
||||
.gl-pipeline-job-width {
|
||||
width: 186px;
|
||||
}
|
||||
|
||||
.gl-pipeline-title-width {
|
||||
width: 176px;
|
||||
}
|
||||
|
||||
.gl-build-content {
|
||||
@include build-content();
|
||||
}
|
||||
|
||||
.gl-ci-action-icon-container {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 50% !important;
|
||||
transform: translateY(-50%);
|
||||
|
||||
// Action Icons in big pipeline-graph nodes
|
||||
&.ci-action-icon-wrapper {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
border-radius: 100%;
|
||||
display: block;
|
||||
padding: 0;
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Pipeline graph, used at
|
||||
// app/assets/javascripts/pipelines/components/graph/graph_component.vue
|
||||
.pipeline-graph {
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
}
|
||||
|
||||
.btn {
|
||||
margin: 10px 0 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,8 @@ module Enums
|
|||
{
|
||||
unknown_failure: 0,
|
||||
config_error: 1,
|
||||
external_validation_failure: 2
|
||||
external_validation_failure: 2,
|
||||
deployments_limit_exceeded: 23
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
class Packages::PackageFile < ApplicationRecord
|
||||
include UpdateProjectStatistics
|
||||
include FileStoreMounter
|
||||
|
||||
delegate :project, :project_id, to: :package
|
||||
delegate :conan_file_type, to: :conan_file_metadatum
|
||||
|
@ -35,20 +36,12 @@ class Packages::PackageFile < ApplicationRecord
|
|||
.where(packages_conan_file_metadata: { conan_package_reference: conan_package_reference })
|
||||
end
|
||||
|
||||
mount_uploader :file, Packages::PackageFileUploader
|
||||
|
||||
after_save :update_file_metadata, if: :saved_change_to_file?
|
||||
mount_file_store_uploader Packages::PackageFileUploader
|
||||
|
||||
update_project_statistics project_statistics_name: :packages_size
|
||||
|
||||
before_save :update_size_from_file
|
||||
|
||||
def update_file_metadata
|
||||
# The file.object_store is set during `uploader.store!`
|
||||
# which happens after object is inserted/updated
|
||||
self.update_column(:file_store, file.object_store)
|
||||
end
|
||||
|
||||
def download_path
|
||||
Gitlab::Routing.url_helpers.download_project_package_file_path(project, self)
|
||||
end
|
||||
|
|
|
@ -10,7 +10,8 @@ module Ci
|
|||
def self.failure_reasons
|
||||
{ unknown_failure: 'Unknown pipeline failure!',
|
||||
config_error: 'CI/CD YAML configuration error!',
|
||||
external_validation_failure: 'External pipeline validation failed!' }
|
||||
external_validation_failure: 'External pipeline validation failed!',
|
||||
deployments_limit_exceeded: 'Pipeline deployments limit exceeded!' }
|
||||
end
|
||||
|
||||
presents :pipeline
|
||||
|
|
|
@ -18,6 +18,7 @@ module Ci
|
|||
Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules,
|
||||
Gitlab::Ci::Pipeline::Chain::Seed,
|
||||
Gitlab::Ci::Pipeline::Chain::Limit::Size,
|
||||
Gitlab::Ci::Pipeline::Chain::Limit::Deployments,
|
||||
Gitlab::Ci::Pipeline::Chain::Validate::External,
|
||||
Gitlab::Ci::Pipeline::Chain::Populate,
|
||||
Gitlab::Ci::Pipeline::Chain::StopDryRun,
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
.detail-page-header-actions.js-issuable-actions
|
||||
.clearfix.issue-btn-group.dropdown
|
||||
%button.gl-button.btn.btn-default.float-left{ type: "button", data: { toggle: "dropdown" } }
|
||||
%button.gl-button.btn.btn-default.float-left.gl-display-md-none{ type: "button", data: { toggle: "dropdown" } }
|
||||
Options
|
||||
= sprite_icon('chevron-down', css_class: 'gl-text-gray-500')
|
||||
.dropdown-menu.dropdown-menu-right
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fixed double-border style on WebIDE button
|
||||
merge_request: 48605
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Limit maximum deployments per pipeline to 500
|
||||
merge_request: 46931
|
||||
author:
|
||||
type: added
|
5
changelogs/unreleased/ak-add-index-on-builds.yml
Normal file
5
changelogs/unreleased/ak-add-index-on-builds.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Adds id desc to index_ci_builds_on_runner_id_and_id_desc
|
||||
merge_request: 48241
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddCiPipelineDeploymentsToPlanLimits < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :plan_limits, :ci_pipeline_deployments, :integer, default: 500, null: false
|
||||
end
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddRunnerIdAndIdDescIndexToCiBuilds < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
NEW_INDEX = 'index_ci_builds_on_runner_id_and_id_desc'
|
||||
OLD_INDEX = 'index_ci_builds_on_runner_id'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :ci_builds, %i[runner_id id], name: NEW_INDEX, order: { id: :desc }
|
||||
remove_concurrent_index_by_name :ci_builds, OLD_INDEX
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index :ci_builds, %i[runner_id], name: OLD_INDEX
|
||||
remove_concurrent_index_by_name :ci_builds, NEW_INDEX
|
||||
end
|
||||
end
|
1
db/schema_migrations/20201030223933
Normal file
1
db/schema_migrations/20201030223933
Normal file
|
@ -0,0 +1 @@
|
|||
a3aa783f2648a95e3ff8b503ef15b8153759c74ac85b30bf94e39710824e57b0
|
1
db/schema_migrations/20201120140210
Normal file
1
db/schema_migrations/20201120140210
Normal file
|
@ -0,0 +1 @@
|
|||
6b88d79aa8d373fa1d9aa2698a9d20c09aff14ef16af4c123abd4e7c98e41311
|
|
@ -14797,7 +14797,8 @@ CREATE TABLE plan_limits (
|
|||
golang_max_file_size bigint DEFAULT 104857600 NOT NULL,
|
||||
debian_max_file_size bigint DEFAULT '3221225472'::bigint NOT NULL,
|
||||
project_feature_flags integer DEFAULT 200 NOT NULL,
|
||||
ci_max_artifact_size_api_fuzzing integer DEFAULT 0 NOT NULL
|
||||
ci_max_artifact_size_api_fuzzing integer DEFAULT 0 NOT NULL,
|
||||
ci_pipeline_deployments integer DEFAULT 500 NOT NULL
|
||||
);
|
||||
|
||||
CREATE SEQUENCE plan_limits_id_seq
|
||||
|
@ -20482,7 +20483,7 @@ CREATE INDEX index_ci_builds_on_protected ON ci_builds USING btree (protected);
|
|||
|
||||
CREATE INDEX index_ci_builds_on_queued_at ON ci_builds USING btree (queued_at);
|
||||
|
||||
CREATE INDEX index_ci_builds_on_runner_id ON ci_builds USING btree (runner_id);
|
||||
CREATE INDEX index_ci_builds_on_runner_id_and_id_desc ON ci_builds USING btree (runner_id, id DESC);
|
||||
|
||||
CREATE INDEX index_ci_builds_on_stage_id ON ci_builds USING btree (stage_id);
|
||||
|
||||
|
|
|
@ -250,6 +250,29 @@ Plan.default.actual_limits.update!(ci_active_jobs: 500)
|
|||
|
||||
Set the limit to `0` to disable it.
|
||||
|
||||
### Maximum number of deployment jobs in a pipeline
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46931) in GitLab 13.7.
|
||||
|
||||
You can limit the maximum number of deployment jobs in a pipeline. A deployment is
|
||||
any job with an [`environment`](../ci/environments/index.md) specified. The number
|
||||
of deployments in a pipeline is checked at pipeline creation. Pipelines that have
|
||||
too many deployments fail with a `deployments_limit_exceeded` error.
|
||||
|
||||
The default limit is 500 for all [self-managed and GitLab.com plans](https://about.gitlab.com/pricing/).
|
||||
|
||||
To change the limit on a self-managed installation, change the `default` plan limit with the following
|
||||
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session) command:
|
||||
|
||||
```ruby
|
||||
# If limits don't exist for the default plan, you can create one with:
|
||||
# Plan.default.create_limits!
|
||||
|
||||
Plan.default.actual_limits.update!(ci_pipeline_deployments: 500)
|
||||
```
|
||||
|
||||
Set the limit to `0` to disable it.
|
||||
|
||||
### Number of CI/CD subscriptions to a project
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9045) in GitLab 12.9.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
comments: false
|
||||
---
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Create
|
||||
group: Ecosystem
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
34
lib/gitlab/ci/limit.rb
Normal file
34
lib/gitlab/ci/limit.rb
Normal file
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Ci
|
||||
##
|
||||
# Abstract base class for CI/CD Quotas
|
||||
#
|
||||
class Limit
|
||||
LimitExceededError = Class.new(StandardError)
|
||||
|
||||
def initialize(_context, _resource)
|
||||
end
|
||||
|
||||
def enabled?
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def exceeded?
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def message
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def log_error!(extra_context = {})
|
||||
error = LimitExceededError.new(message)
|
||||
# TODO: change this to Gitlab::ErrorTracking.log_exception(error, extra_context)
|
||||
# https://gitlab.com/gitlab-org/gitlab/issues/32906
|
||||
::Gitlab::ErrorTracking.track_exception(error, extra_context)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
39
lib/gitlab/ci/pipeline/chain/limit/deployments.rb
Normal file
39
lib/gitlab/ci/pipeline/chain/limit/deployments.rb
Normal file
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Ci
|
||||
module Pipeline
|
||||
module Chain
|
||||
module Limit
|
||||
class Deployments < Chain::Base
|
||||
extend ::Gitlab::Utils::Override
|
||||
include ::Gitlab::Ci::Pipeline::Chain::Helpers
|
||||
|
||||
attr_reader :limit
|
||||
private :limit
|
||||
|
||||
def initialize(*)
|
||||
super
|
||||
|
||||
@limit = ::Gitlab::Ci::Pipeline::Quota::Deployments
|
||||
.new(project.namespace, pipeline, command)
|
||||
end
|
||||
|
||||
override :perform!
|
||||
def perform!
|
||||
return unless limit.exceeded?
|
||||
|
||||
limit.log_error!(project_id: project.id, plan: project.actual_plan_name)
|
||||
error(limit.message, drop_reason: :deployments_limit_exceeded)
|
||||
end
|
||||
|
||||
override :break?
|
||||
def break?
|
||||
limit.exceeded?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
54
lib/gitlab/ci/pipeline/quota/deployments.rb
Normal file
54
lib/gitlab/ci/pipeline/quota/deployments.rb
Normal file
|
@ -0,0 +1,54 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Ci
|
||||
module Pipeline
|
||||
module Quota
|
||||
class Deployments < ::Gitlab::Ci::Limit
|
||||
include ::Gitlab::Utils::StrongMemoize
|
||||
include ActionView::Helpers::TextHelper
|
||||
|
||||
def initialize(namespace, pipeline, command)
|
||||
@namespace = namespace
|
||||
@pipeline = pipeline
|
||||
@command = command
|
||||
end
|
||||
|
||||
def enabled?
|
||||
limit > 0
|
||||
end
|
||||
|
||||
def exceeded?
|
||||
return false unless enabled?
|
||||
|
||||
pipeline_deployment_count > limit
|
||||
end
|
||||
|
||||
def message
|
||||
return unless exceeded?
|
||||
|
||||
"Pipeline has too many deployments! Requested #{pipeline_deployment_count}, but the limit is #{limit}."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def pipeline_deployment_count
|
||||
strong_memoize(:pipeline_deployment_count) do
|
||||
@command.stage_seeds.sum do |stage_seed|
|
||||
stage_seed.seeds.count do |build_seed|
|
||||
build_seed.attributes[:environment].present?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def limit
|
||||
strong_memoize(:limit) do
|
||||
@namespace.actual_limits.ci_pipeline_deployments
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,6 +2,8 @@ test:
|
|||
variables:
|
||||
POSTGRES_VERSION: 9.6.16
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_USER: user
|
||||
POSTGRES_PASSWORD: testing-password
|
||||
services:
|
||||
- "postgres:${POSTGRES_VERSION}"
|
||||
stage: test
|
||||
|
|
|
@ -48,23 +48,15 @@ module Gitlab
|
|||
@finished_at ? (@finished_at - @started_at) : 0.0
|
||||
end
|
||||
|
||||
def thread_cpu_duration
|
||||
System.thread_cpu_duration(@thread_cputime_start)
|
||||
end
|
||||
|
||||
def run
|
||||
Thread.current[THREAD_KEY] = self
|
||||
|
||||
@started_at = System.monotonic_time
|
||||
@thread_cputime_start = System.thread_cpu_time
|
||||
|
||||
yield
|
||||
ensure
|
||||
@finished_at = System.monotonic_time
|
||||
|
||||
observe(:gitlab_transaction_cputime_seconds, thread_cpu_duration) do
|
||||
buckets SMALL_BUCKETS
|
||||
end
|
||||
observe(:gitlab_transaction_duration_seconds, duration) do
|
||||
buckets SMALL_BUCKETS
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils';
|
|||
import { setHTMLFixture } from 'helpers/fixtures';
|
||||
import PipelineStore from '~/pipelines/stores/pipeline_store';
|
||||
import GraphComponentLegacy from '~/pipelines/components/graph/graph_component_legacy.vue';
|
||||
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
|
||||
import StageColumnComponentLegacy from '~/pipelines/components/graph/stage_column_component_legacy.vue';
|
||||
import linkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
|
||||
import graphJSON from './mock_data_legacy';
|
||||
import linkedPipelineJSON from './linked_pipelines_mock_data';
|
||||
|
@ -16,7 +16,7 @@ describe('graph component', () => {
|
|||
|
||||
const findExpandPipelineBtn = () => wrapper.find('[data-testid="expandPipelineButton"]');
|
||||
const findAllExpandPipelineBtns = () => wrapper.findAll('[data-testid="expandPipelineButton"]');
|
||||
const findStageColumns = () => wrapper.findAll(StageColumnComponent);
|
||||
const findStageColumns = () => wrapper.findAll(StageColumnComponentLegacy);
|
||||
const findStageColumnAt = i => findStageColumns().at(i);
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import StageColumnComponentLegacy from '~/pipelines/components/graph/stage_column_component_legacy.vue';
|
||||
|
||||
describe('stage column component', () => {
|
||||
const mockJob = {
|
||||
id: 4250,
|
||||
name: 'test',
|
||||
status: {
|
||||
icon: 'status_success',
|
||||
text: 'passed',
|
||||
label: 'passed',
|
||||
group: 'success',
|
||||
details_path: '/root/ci-mock/builds/4250',
|
||||
action: {
|
||||
icon: 'retry',
|
||||
title: 'Retry',
|
||||
path: '/root/ci-mock/builds/4250/retry',
|
||||
method: 'post',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockGroups = [];
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
const mockedJob = { ...mockJob };
|
||||
mockedJob.id += i;
|
||||
mockGroups.push(mockedJob);
|
||||
}
|
||||
|
||||
wrapper = shallowMount(StageColumnComponentLegacy, {
|
||||
propsData: {
|
||||
title: 'foo',
|
||||
groups: mockGroups,
|
||||
hasTriggeredBy: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should render provided title', () => {
|
||||
expect(
|
||||
wrapper
|
||||
.find('.stage-name')
|
||||
.text()
|
||||
.trim(),
|
||||
).toBe('foo');
|
||||
});
|
||||
|
||||
it('should render the provided groups', () => {
|
||||
expect(wrapper.findAll('.builds-container > ul > li').length).toBe(
|
||||
wrapper.props('groups').length,
|
||||
);
|
||||
});
|
||||
|
||||
describe('jobId', () => {
|
||||
it('escapes job name', () => {
|
||||
wrapper = shallowMount(StageColumnComponentLegacy, {
|
||||
propsData: {
|
||||
groups: [
|
||||
{
|
||||
id: 4259,
|
||||
name: '<img src=x onerror=alert(document.domain)>',
|
||||
status: {
|
||||
icon: 'status_success',
|
||||
label: 'success',
|
||||
tooltip: '<img src=x onerror=alert(document.domain)>',
|
||||
},
|
||||
},
|
||||
],
|
||||
title: 'test',
|
||||
hasTriggeredBy: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find('.builds-container li').attributes('id')).toBe(
|
||||
'ci-badge-<img src=x onerror=alert(document.domain)>',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with action', () => {
|
||||
it('renders action button', () => {
|
||||
wrapper = shallowMount(StageColumnComponentLegacy, {
|
||||
propsData: {
|
||||
groups: [
|
||||
{
|
||||
id: 4259,
|
||||
name: '<img src=x onerror=alert(document.domain)>',
|
||||
status: {
|
||||
icon: 'status_success',
|
||||
label: 'success',
|
||||
tooltip: '<img src=x onerror=alert(document.domain)>',
|
||||
},
|
||||
},
|
||||
],
|
||||
title: 'test',
|
||||
hasTriggeredBy: false,
|
||||
action: {
|
||||
icon: 'play',
|
||||
title: 'Play all',
|
||||
path: 'action',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find('.js-stage-action').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('without action', () => {
|
||||
it('does not render action button', () => {
|
||||
wrapper = shallowMount(StageColumnComponentLegacy, {
|
||||
propsData: {
|
||||
groups: [
|
||||
{
|
||||
id: 4259,
|
||||
name: '<img src=x onerror=alert(document.domain)>',
|
||||
status: {
|
||||
icon: 'status_success',
|
||||
label: 'success',
|
||||
tooltip: '<img src=x onerror=alert(document.domain)>',
|
||||
},
|
||||
},
|
||||
],
|
||||
title: 'test',
|
||||
hasTriggeredBy: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find('.js-stage-action').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,64 +1,77 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { mount, shallowMount } from '@vue/test-utils';
|
||||
import ActionComponent from '~/pipelines/components/graph/action_component.vue';
|
||||
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
|
||||
|
||||
import stageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
|
||||
const mockJob = {
|
||||
id: 4250,
|
||||
name: 'test',
|
||||
status: {
|
||||
icon: 'status_success',
|
||||
text: 'passed',
|
||||
label: 'passed',
|
||||
group: 'success',
|
||||
details_path: '/root/ci-mock/builds/4250',
|
||||
action: {
|
||||
icon: 'retry',
|
||||
title: 'Retry',
|
||||
path: '/root/ci-mock/builds/4250/retry',
|
||||
method: 'post',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockGroups = Array(4)
|
||||
.fill(0)
|
||||
.map((item, idx) => {
|
||||
return { ...mockJob, id: idx, name: `fish-${idx}` };
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
title: 'Fish',
|
||||
groups: mockGroups,
|
||||
};
|
||||
|
||||
describe('stage column component', () => {
|
||||
const mockJob = {
|
||||
id: 4250,
|
||||
name: 'test',
|
||||
status: {
|
||||
icon: 'status_success',
|
||||
text: 'passed',
|
||||
label: 'passed',
|
||||
group: 'success',
|
||||
details_path: '/root/ci-mock/builds/4250',
|
||||
action: {
|
||||
icon: 'retry',
|
||||
title: 'Retry',
|
||||
path: '/root/ci-mock/builds/4250/retry',
|
||||
method: 'post',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockGroups = [];
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
const mockedJob = { ...mockJob };
|
||||
mockedJob.id += i;
|
||||
mockGroups.push(mockedJob);
|
||||
}
|
||||
const findStageColumnTitle = () => wrapper.find('[data-testid="stage-column-title"]');
|
||||
const findStageColumnGroup = () => wrapper.find('[data-testid="stage-column-group"]');
|
||||
const findAllStageColumnGroups = () => wrapper.findAll('[data-testid="stage-column-group"]');
|
||||
const findActionComponent = () => wrapper.find(ActionComponent);
|
||||
|
||||
wrapper = shallowMount(stageColumnComponent, {
|
||||
const createComponent = ({ method = shallowMount, props = {} } = {}) => {
|
||||
wrapper = method(StageColumnComponent, {
|
||||
propsData: {
|
||||
title: 'foo',
|
||||
groups: mockGroups,
|
||||
hasTriggeredBy: false,
|
||||
...defaultProps,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
describe('when mounted', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ method: mount });
|
||||
});
|
||||
|
||||
it('should render provided title', () => {
|
||||
expect(findStageColumnTitle().text()).toBe(defaultProps.title);
|
||||
});
|
||||
|
||||
it('should render the provided groups', () => {
|
||||
expect(findAllStageColumnGroups().length).toBe(mockGroups.length);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render provided title', () => {
|
||||
expect(
|
||||
wrapper
|
||||
.find('.stage-name')
|
||||
.text()
|
||||
.trim(),
|
||||
).toBe('foo');
|
||||
});
|
||||
|
||||
it('should render the provided groups', () => {
|
||||
expect(wrapper.findAll('.builds-container > ul > li').length).toBe(
|
||||
wrapper.props('groups').length,
|
||||
);
|
||||
});
|
||||
|
||||
describe('jobId', () => {
|
||||
it('escapes job name', () => {
|
||||
wrapper = shallowMount(stageColumnComponent, {
|
||||
propsData: {
|
||||
describe('job', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
method: mount,
|
||||
props: {
|
||||
groups: [
|
||||
{
|
||||
id: 4259,
|
||||
|
@ -70,21 +83,29 @@ describe('stage column component', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
title: 'test',
|
||||
hasTriggeredBy: false,
|
||||
title: 'test <img src=x onerror=alert(document.domain)>',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(wrapper.find('.builds-container li').attributes('id')).toBe(
|
||||
it('capitalizes and escapes name', () => {
|
||||
expect(findStageColumnTitle().text()).toBe(
|
||||
'Test <img src=x onerror=alert(document.domain)>',
|
||||
);
|
||||
});
|
||||
|
||||
it('escapes id', () => {
|
||||
expect(findStageColumnGroup().attributes('id')).toBe(
|
||||
'ci-badge-<img src=x onerror=alert(document.domain)>',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with action', () => {
|
||||
it('renders action button', () => {
|
||||
wrapper = shallowMount(stageColumnComponent, {
|
||||
propsData: {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
method: mount,
|
||||
props: {
|
||||
groups: [
|
||||
{
|
||||
id: 4259,
|
||||
|
@ -105,15 +126,18 @@ describe('stage column component', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(wrapper.find('.js-stage-action').exists()).toBe(true);
|
||||
it('renders action button', () => {
|
||||
expect(findActionComponent().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('without action', () => {
|
||||
it('does not render action button', () => {
|
||||
wrapper = shallowMount(stageColumnComponent, {
|
||||
propsData: {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
method: mount,
|
||||
props: {
|
||||
groups: [
|
||||
{
|
||||
id: 4259,
|
||||
|
@ -129,8 +153,10 @@ describe('stage column component', () => {
|
|||
hasTriggeredBy: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(wrapper.find('.js-stage-action').exists()).toBe(false);
|
||||
it('does not render action button', () => {
|
||||
expect(findActionComponent().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
122
spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb
Normal file
122
spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb
Normal file
|
@ -0,0 +1,122 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::Deployments do
|
||||
let_it_be(:namespace) { create(:namespace) }
|
||||
let_it_be(:project, reload: true) { create(:project, namespace: namespace) }
|
||||
let_it_be(:plan_limits, reload: true) { create(:plan_limits, :default_plan) }
|
||||
|
||||
let(:stage_seeds) do
|
||||
[
|
||||
double(:test, seeds: [
|
||||
double(:test, attributes: {})
|
||||
]),
|
||||
double(:staging, seeds: [
|
||||
double(:staging, attributes: { environment: 'staging' })
|
||||
]),
|
||||
double(:production, seeds: [
|
||||
double(:production, attributes: { environment: 'production' })
|
||||
])
|
||||
]
|
||||
end
|
||||
|
||||
let(:save_incompleted) { false }
|
||||
|
||||
let(:command) do
|
||||
double(:command,
|
||||
project: project,
|
||||
stage_seeds: stage_seeds,
|
||||
save_incompleted: save_incompleted
|
||||
)
|
||||
end
|
||||
|
||||
let(:pipeline) { build(:ci_pipeline, project: project) }
|
||||
let(:step) { described_class.new(pipeline, command) }
|
||||
|
||||
subject(:perform) { step.perform! }
|
||||
|
||||
context 'when pipeline deployments limit is exceeded' do
|
||||
before do
|
||||
plan_limits.update!(ci_pipeline_deployments: 1)
|
||||
end
|
||||
|
||||
context 'when saving incompleted pipelines' do
|
||||
let(:save_incompleted) { true }
|
||||
|
||||
it 'drops the pipeline' do
|
||||
perform
|
||||
|
||||
expect(pipeline).to be_persisted
|
||||
expect(pipeline.reload).to be_failed
|
||||
end
|
||||
|
||||
it 'breaks the chain' do
|
||||
perform
|
||||
|
||||
expect(step.break?).to be true
|
||||
end
|
||||
|
||||
it 'sets a valid failure reason' do
|
||||
perform
|
||||
|
||||
expect(pipeline.deployments_limit_exceeded?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not saving incomplete pipelines' do
|
||||
let(:save_incompleted) { false }
|
||||
|
||||
it 'does not persist the pipeline' do
|
||||
perform
|
||||
|
||||
expect(pipeline).not_to be_persisted
|
||||
end
|
||||
|
||||
it 'breaks the chain' do
|
||||
perform
|
||||
|
||||
expect(step.break?).to be true
|
||||
end
|
||||
|
||||
it 'adds an informative error to the pipeline' do
|
||||
perform
|
||||
|
||||
expect(pipeline.errors.messages).to include(base: ['Pipeline has too many deployments! Requested 2, but the limit is 1.'])
|
||||
end
|
||||
end
|
||||
|
||||
it 'logs the error' do
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
|
||||
instance_of(Gitlab::Ci::Limit::LimitExceededError),
|
||||
project_id: project.id, plan: namespace.actual_plan_name
|
||||
)
|
||||
|
||||
perform
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pipeline deployments limit is not exceeded' do
|
||||
before do
|
||||
plan_limits.update!(ci_pipeline_deployments: 100)
|
||||
end
|
||||
|
||||
it 'does not break the chain' do
|
||||
perform
|
||||
|
||||
expect(step.break?).to be false
|
||||
end
|
||||
|
||||
it 'does not invalidate the pipeline' do
|
||||
perform
|
||||
|
||||
expect(pipeline.errors).to be_empty
|
||||
end
|
||||
|
||||
it 'does not log any error' do
|
||||
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
|
||||
|
||||
perform
|
||||
end
|
||||
end
|
||||
end
|
107
spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb
Normal file
107
spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb
Normal file
|
@ -0,0 +1,107 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Pipeline::Quota::Deployments do
|
||||
let_it_be(:namespace) { create(:namespace) }
|
||||
let_it_be(:default_plan, reload: true) { create(:default_plan) }
|
||||
let_it_be(:project, reload: true) { create(:project, :repository, namespace: namespace) }
|
||||
let_it_be(:plan_limits) { create(:plan_limits, plan: default_plan) }
|
||||
|
||||
let(:pipeline) { build_stubbed(:ci_pipeline, project: project) }
|
||||
|
||||
let(:stage_seeds) do
|
||||
[
|
||||
double(:test, seeds: [
|
||||
double(:test, attributes: {})
|
||||
]),
|
||||
double(:staging, seeds: [
|
||||
double(:staging, attributes: { environment: 'staging' })
|
||||
]),
|
||||
double(:production, seeds: [
|
||||
double(:production, attributes: { environment: 'production' })
|
||||
])
|
||||
]
|
||||
end
|
||||
|
||||
let(:command) do
|
||||
double(:command,
|
||||
project: project,
|
||||
stage_seeds: stage_seeds,
|
||||
save_incompleted: true
|
||||
)
|
||||
end
|
||||
|
||||
let(:ci_pipeline_deployments_limit) { 0 }
|
||||
|
||||
before do
|
||||
plan_limits.update!(ci_pipeline_deployments: ci_pipeline_deployments_limit)
|
||||
end
|
||||
|
||||
subject(:quota) { described_class.new(namespace, pipeline, command) }
|
||||
|
||||
shared_context 'limit exceeded' do
|
||||
let(:ci_pipeline_deployments_limit) { 1 }
|
||||
end
|
||||
|
||||
shared_context 'limit not exceeded' do
|
||||
let(:ci_pipeline_deployments_limit) { 2 }
|
||||
end
|
||||
|
||||
describe '#enabled?' do
|
||||
context 'when limit is enabled in plan' do
|
||||
let(:ci_pipeline_deployments_limit) { 10 }
|
||||
|
||||
it 'is enabled' do
|
||||
expect(quota).to be_enabled
|
||||
end
|
||||
end
|
||||
|
||||
context 'when limit is not enabled' do
|
||||
let(:ci_pipeline_deployments_limit) { 0 }
|
||||
|
||||
it 'is not enabled' do
|
||||
expect(quota).not_to be_enabled
|
||||
end
|
||||
end
|
||||
|
||||
context 'when limit does not exist' do
|
||||
before do
|
||||
allow(namespace).to receive(:actual_plan) { create(:default_plan) }
|
||||
end
|
||||
|
||||
it 'is enabled by default' do
|
||||
expect(quota).to be_enabled
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#exceeded?' do
|
||||
context 'when limit is exceeded' do
|
||||
include_context 'limit exceeded'
|
||||
|
||||
it 'is exceeded' do
|
||||
expect(quota).to be_exceeded
|
||||
end
|
||||
end
|
||||
|
||||
context 'when limit is not exceeded' do
|
||||
include_context 'limit not exceeded'
|
||||
|
||||
it 'is not exceeded' do
|
||||
expect(quota).not_to be_exceeded
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#message' do
|
||||
context 'when limit is exceeded' do
|
||||
include_context 'limit exceeded'
|
||||
|
||||
it 'returns info about pipeline deployment limit exceeded' do
|
||||
expect(quota.message)
|
||||
.to eq "Pipeline has too many deployments! Requested 2, but the limit is 1."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -20,14 +20,6 @@ RSpec.describe Gitlab::Metrics::Transaction do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#thread_cpu_duration' do
|
||||
it 'returns the duration of a transaction in seconds' do
|
||||
transaction.run { }
|
||||
|
||||
expect(transaction.thread_cpu_duration).to be > 0
|
||||
end
|
||||
end
|
||||
|
||||
describe '#run' do
|
||||
it 'yields the supplied block' do
|
||||
expect { |b| transaction.run(&b) }.to yield_control
|
||||
|
|
|
@ -61,14 +61,14 @@ RSpec.describe Packages::PackageFile, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#update_file_metadata callback' do
|
||||
describe '#update_file_store callback' do
|
||||
let_it_be(:package_file) { build(:package_file, :nuget, size: nil) }
|
||||
|
||||
subject { package_file.save! }
|
||||
|
||||
it 'updates metadata columns' do
|
||||
expect(package_file)
|
||||
.to receive(:update_file_metadata)
|
||||
.to receive(:update_file_store)
|
||||
.and_call_original
|
||||
|
||||
# This expectation uses a stub because we can no longer test a change from
|
||||
|
|
Loading…
Reference in a new issue