Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-11-27 21:09:52 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-11-27 21:09:52 +0300
commit28f1931ae84034333abf651ecde369683697ddaf (patch)
tree7701b2caa8e0e7edfc2d4dca38855c1c03ed8de3
parent4c39dd11dcbdab4fdd9424a62320a1fc773c2918 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/pipelines/components/graph/accessors.js10
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/constants.js3
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue98
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue18
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_name_component.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue117
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue108
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue32
-rw-r--r--app/assets/stylesheets/page_bundles/pipeline.scss45
-rw-r--r--app/assets/stylesheets/pages/tree.scss2
-rw-r--r--app/models/concerns/enums/ci/pipeline.rb3
-rw-r--r--app/models/packages/package_file.rb11
-rw-r--r--app/presenters/ci/pipeline_presenter.rb3
-rw-r--r--app/services/ci/create_pipeline_service.rb1
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml2
-rw-r--r--changelogs/unreleased/254979-double-border-split-button.yml5
-rw-r--r--changelogs/unreleased/add-limits-for-deployments-per-pipeline.yml5
-rw-r--r--changelogs/unreleased/ak-add-index-on-builds.yml5
-rw-r--r--db/migrate/20201030223933_add_ci_pipeline_deployments_to_plan_limits.rb9
-rw-r--r--db/post_migrate/20201120140210_add_runner_id_and_id_desc_index_to_ci_builds.rb21
-rw-r--r--db/schema_migrations/202010302239331
-rw-r--r--db/schema_migrations/202011201402101
-rw-r--r--db/structure.sql5
-rw-r--r--doc/administration/instance_limits.md23
-rw-r--r--doc/administration/integration/terminal.md4
-rw-r--r--doc/integration/README.md4
-rw-r--r--doc/integration/akismet.md4
-rw-r--r--doc/integration/auth0.md4
-rw-r--r--doc/integration/azure.md4
-rw-r--r--doc/integration/bitbucket.md4
-rw-r--r--doc/integration/cas.md4
-rw-r--r--doc/integration/external-issue-tracker.md4
-rw-r--r--doc/integration/facebook.md4
-rw-r--r--doc/integration/github.md4
-rw-r--r--doc/integration/gitlab.md4
-rw-r--r--doc/integration/gmail_action_buttons_for_gitlab.md4
-rw-r--r--doc/integration/google.md4
-rw-r--r--doc/integration/jenkins.md4
-rw-r--r--doc/integration/jenkins_deprecated.md4
-rw-r--r--doc/integration/oauth2_generic.md4
-rw-r--r--doc/integration/oauth_provider.md4
-rw-r--r--doc/integration/omniauth.md4
-rw-r--r--doc/integration/openid_connect_provider.md4
-rw-r--r--doc/integration/recaptcha.md4
-rw-r--r--doc/integration/salesforce.md4
-rw-r--r--doc/integration/shibboleth.md4
-rw-r--r--doc/integration/slash_commands.md4
-rw-r--r--doc/integration/trello_power_up.md4
-rw-r--r--doc/integration/twitter.md4
-rw-r--r--doc/user/project/integrations/ewm.md4
-rw-r--r--lib/gitlab/ci/limit.rb34
-rw-r--r--lib/gitlab/ci/pipeline/chain/limit/deployments.rb39
-rw-r--r--lib/gitlab/ci/pipeline/quota/deployments.rb54
-rw-r--r--lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml2
-rw-r--r--lib/gitlab/metrics/transaction.rb8
-rw-r--r--spec/frontend/pipelines/graph/graph_component_legacy_spec.js4
-rw-r--r--spec/frontend/pipelines/graph/stage_column_component_legacy_spec.js135
-rw-r--r--spec/frontend/pipelines/graph/stage_column_component_spec.js142
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb122
-rw-r--r--spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb107
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb8
-rw-r--r--spec/models/packages/package_file_spec.rb4
65 files changed, 1010 insertions, 313 deletions
diff --git a/app/assets/javascripts/pipelines/components/graph/accessors.js b/app/assets/javascripts/pipelines/components/graph/accessors.js
new file mode 100644
index 00000000000..31fbab00ee8
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph/accessors.js
@@ -0,0 +1,10 @@
+import { REST, GRAPHQL } from './constants';
+
+export const accessors = {
+ [REST]: {
+ groupId: 'id',
+ },
+ [GRAPHQL]: {
+ groupId: 'name',
+ },
+};
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index a580ee11627..e35817a4dbb 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -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" />
diff --git a/app/assets/javascripts/pipelines/components/graph/constants.js b/app/assets/javascripts/pipelines/components/graph/constants.js
index ba1922b6dae..6f0deccfef6 100644
--- a/app/assets/javascripts/pipelines/components/graph/constants.js
+++ b/app/assets/javascripts/pipelines/components/graph/constants.js
@@ -1,3 +1,6 @@
export const DOWNSTREAM = 'downstream';
export const MAIN = 'main';
export const UPSTREAM = 'upstream';
+
+export const REST = 'rest';
+export const GRAPHQL = 'graphql';
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index b440f300d27..7704f2ba0fd 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -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>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue
index c1a939af6d3..2ef8940cf65 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue
@@ -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="{
diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
index 49591a80752..38d48517bd6 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
@@ -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">
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index 4ed0aae0d1e..ef443d93c62 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -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
diff --git a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
index 1b71949784a..23a38fc053e 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
@@ -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>
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index 258b6bf6b6d..5e2d79758e1 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -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>
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue
new file mode 100644
index 00000000000..258b6bf6b6d
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue
@@ -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>
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue b/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue
new file mode 100644
index 00000000000..205ee0fb414
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue
@@ -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>
diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss
index 1de66aa73da..f50b2d816c3 100644
--- a/app/assets/stylesheets/page_bundles/pipeline.scss
+++ b/app/assets/stylesheets/page_bundles/pipeline.scss
@@ -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 {
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 429181c2ad4..f8b96772ed7 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -69,7 +69,7 @@
}
.btn {
- margin: 10px 0 0;
+ margin-top: 10px;
}
}
}
diff --git a/app/models/concerns/enums/ci/pipeline.rb b/app/models/concerns/enums/ci/pipeline.rb
index bb8df37f649..28eb4c11746 100644
--- a/app/models/concerns/enums/ci/pipeline.rb
+++ b/app/models/concerns/enums/ci/pipeline.rb
@@ -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
diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb
index d68f75140ac..e8d1dd1e8c4 100644
--- a/app/models/packages/package_file.rb
+++ b/app/models/packages/package_file.rb
@@ -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
diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb
index da610f13899..f3bb63b31c3 100644
--- a/app/presenters/ci/pipeline_presenter.rb
+++ b/app/presenters/ci/pipeline_presenter.rb
@@ -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
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index e3bab2de44e..32670d81906 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -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,
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index 70c1a939878..58b364f52d4 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -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
diff --git a/changelogs/unreleased/254979-double-border-split-button.yml b/changelogs/unreleased/254979-double-border-split-button.yml
new file mode 100644
index 00000000000..afa69697166
--- /dev/null
+++ b/changelogs/unreleased/254979-double-border-split-button.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed double-border style on WebIDE button
+merge_request: 48605
+author:
+type: fixed
diff --git a/changelogs/unreleased/add-limits-for-deployments-per-pipeline.yml b/changelogs/unreleased/add-limits-for-deployments-per-pipeline.yml
new file mode 100644
index 00000000000..64e123e0d00
--- /dev/null
+++ b/changelogs/unreleased/add-limits-for-deployments-per-pipeline.yml
@@ -0,0 +1,5 @@
+---
+title: Limit maximum deployments per pipeline to 500
+merge_request: 46931
+author:
+type: added
diff --git a/changelogs/unreleased/ak-add-index-on-builds.yml b/changelogs/unreleased/ak-add-index-on-builds.yml
new file mode 100644
index 00000000000..498801cf8c0
--- /dev/null
+++ b/changelogs/unreleased/ak-add-index-on-builds.yml
@@ -0,0 +1,5 @@
+---
+title: Adds id desc to index_ci_builds_on_runner_id_and_id_desc
+merge_request: 48241
+author:
+type: fixed
diff --git a/db/migrate/20201030223933_add_ci_pipeline_deployments_to_plan_limits.rb b/db/migrate/20201030223933_add_ci_pipeline_deployments_to_plan_limits.rb
new file mode 100644
index 00000000000..60f0ff9d6ed
--- /dev/null
+++ b/db/migrate/20201030223933_add_ci_pipeline_deployments_to_plan_limits.rb
@@ -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
diff --git a/db/post_migrate/20201120140210_add_runner_id_and_id_desc_index_to_ci_builds.rb b/db/post_migrate/20201120140210_add_runner_id_and_id_desc_index_to_ci_builds.rb
new file mode 100644
index 00000000000..5eda0e25dbe
--- /dev/null
+++ b/db/post_migrate/20201120140210_add_runner_id_and_id_desc_index_to_ci_builds.rb
@@ -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
diff --git a/db/schema_migrations/20201030223933 b/db/schema_migrations/20201030223933
new file mode 100644
index 00000000000..2fb5f394989
--- /dev/null
+++ b/db/schema_migrations/20201030223933
@@ -0,0 +1 @@
+a3aa783f2648a95e3ff8b503ef15b8153759c74ac85b30bf94e39710824e57b0 \ No newline at end of file
diff --git a/db/schema_migrations/20201120140210 b/db/schema_migrations/20201120140210
new file mode 100644
index 00000000000..5a281f95f5d
--- /dev/null
+++ b/db/schema_migrations/20201120140210
@@ -0,0 +1 @@
+6b88d79aa8d373fa1d9aa2698a9d20c09aff14ef16af4c123abd4e7c98e41311 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index a895fd6414e..d75d3d6f5c9 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -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);
diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md
index e7cd54c0e83..f202a912696 100644
--- a/doc/administration/instance_limits.md
+++ b/doc/administration/instance_limits.md
@@ -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.
diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md
index 3548a11febe..b692ca1d288 100644
--- a/doc/administration/integration/terminal.md
+++ b/doc/administration/integration/terminal.md
@@ -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
---
diff --git a/doc/integration/README.md b/doc/integration/README.md
index 396090eb420..3843c451e6b 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -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
---
diff --git a/doc/integration/akismet.md b/doc/integration/akismet.md
index 2307fdc5ac3..c04ff31b10a 100644
--- a/doc/integration/akismet.md
+++ b/doc/integration/akismet.md
@@ -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
---
diff --git a/doc/integration/auth0.md b/doc/integration/auth0.md
index d479e9ead3f..7e531682faf 100644
--- a/doc/integration/auth0.md
+++ b/doc/integration/auth0.md
@@ -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
---
diff --git a/doc/integration/azure.md b/doc/integration/azure.md
index b5b809b6a0a..f22a94a01c7 100644
--- a/doc/integration/azure.md
+++ b/doc/integration/azure.md
@@ -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
---
diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md
index bf8c9a4fc20..9c4a8e27747 100644
--- a/doc/integration/bitbucket.md
+++ b/doc/integration/bitbucket.md
@@ -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
---
diff --git a/doc/integration/cas.md b/doc/integration/cas.md
index 8bb25f4f6af..5a198e85f5c 100644
--- a/doc/integration/cas.md
+++ b/doc/integration/cas.md
@@ -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
---
diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md
index 7c6048d7683..215a2a8c21d 100644
--- a/doc/integration/external-issue-tracker.md
+++ b/doc/integration/external-issue-tracker.md
@@ -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
---
diff --git a/doc/integration/facebook.md b/doc/integration/facebook.md
index d16bfb118db..b86958726a7 100644
--- a/doc/integration/facebook.md
+++ b/doc/integration/facebook.md
@@ -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
---
diff --git a/doc/integration/github.md b/doc/integration/github.md
index 7634afa65ea..61bcb8a25b3 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -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
---
diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md
index 0b249e05dca..37c91aedb15 100644
--- a/doc/integration/gitlab.md
+++ b/doc/integration/gitlab.md
@@ -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
---
diff --git a/doc/integration/gmail_action_buttons_for_gitlab.md b/doc/integration/gmail_action_buttons_for_gitlab.md
index 449d0906601..1158d6c9bc8 100644
--- a/doc/integration/gmail_action_buttons_for_gitlab.md
+++ b/doc/integration/gmail_action_buttons_for_gitlab.md
@@ -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
---
diff --git a/doc/integration/google.md b/doc/integration/google.md
index 5c174862f4e..cd00c854fea 100644
--- a/doc/integration/google.md
+++ b/doc/integration/google.md
@@ -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
---
diff --git a/doc/integration/jenkins.md b/doc/integration/jenkins.md
index 1d33eeb8b25..70a7f417c75 100644
--- a/doc/integration/jenkins.md
+++ b/doc/integration/jenkins.md
@@ -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
---
diff --git a/doc/integration/jenkins_deprecated.md b/doc/integration/jenkins_deprecated.md
index 07cd1b9fcb9..f023dbaef41 100644
--- a/doc/integration/jenkins_deprecated.md
+++ b/doc/integration/jenkins_deprecated.md
@@ -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
---
diff --git a/doc/integration/oauth2_generic.md b/doc/integration/oauth2_generic.md
index ce4a18a84a1..6cef4b0294c 100644
--- a/doc/integration/oauth2_generic.md
+++ b/doc/integration/oauth2_generic.md
@@ -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
---
diff --git a/doc/integration/oauth_provider.md b/doc/integration/oauth_provider.md
index 2347a507846..6e26aa947da 100644
--- a/doc/integration/oauth_provider.md
+++ b/doc/integration/oauth_provider.md
@@ -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
---
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index 33d9974873e..c3f78015590 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -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
---
diff --git a/doc/integration/openid_connect_provider.md b/doc/integration/openid_connect_provider.md
index 742a93865ee..0287bd4bcbd 100644
--- a/doc/integration/openid_connect_provider.md
+++ b/doc/integration/openid_connect_provider.md
@@ -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
---
diff --git a/doc/integration/recaptcha.md b/doc/integration/recaptcha.md
index 606c791fab3..61b60b7962d 100644
--- a/doc/integration/recaptcha.md
+++ b/doc/integration/recaptcha.md
@@ -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
---
diff --git a/doc/integration/salesforce.md b/doc/integration/salesforce.md
index 76ba1225223..ae1fc1ed5a2 100644
--- a/doc/integration/salesforce.md
+++ b/doc/integration/salesforce.md
@@ -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
---
diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md
index b070e9c5ffc..7a4b8d982e7 100644
--- a/doc/integration/shibboleth.md
+++ b/doc/integration/shibboleth.md
@@ -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
---
diff --git a/doc/integration/slash_commands.md b/doc/integration/slash_commands.md
index 5f58a5a8e88..6820ff8a0aa 100644
--- a/doc/integration/slash_commands.md
+++ b/doc/integration/slash_commands.md
@@ -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
---
diff --git a/doc/integration/trello_power_up.md b/doc/integration/trello_power_up.md
index 49db107fa4e..7545afcf06f 100644
--- a/doc/integration/trello_power_up.md
+++ b/doc/integration/trello_power_up.md
@@ -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
---
diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md
index 945e057faf5..8404352d0e9 100644
--- a/doc/integration/twitter.md
+++ b/doc/integration/twitter.md
@@ -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
---
diff --git a/doc/user/project/integrations/ewm.md b/doc/user/project/integrations/ewm.md
index 5401b1bfd43..58507ded499 100644
--- a/doc/user/project/integrations/ewm.md
+++ b/doc/user/project/integrations/ewm.md
@@ -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
---
diff --git a/lib/gitlab/ci/limit.rb b/lib/gitlab/ci/limit.rb
new file mode 100644
index 00000000000..c22a3c503d5
--- /dev/null
+++ b/lib/gitlab/ci/limit.rb
@@ -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
diff --git a/lib/gitlab/ci/pipeline/chain/limit/deployments.rb b/lib/gitlab/ci/pipeline/chain/limit/deployments.rb
new file mode 100644
index 00000000000..d684eedcaac
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/limit/deployments.rb
@@ -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
diff --git a/lib/gitlab/ci/pipeline/quota/deployments.rb b/lib/gitlab/ci/pipeline/quota/deployments.rb
new file mode 100644
index 00000000000..59d695c61c1
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/quota/deployments.rb
@@ -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
diff --git a/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
index 3b87d53f165..895e6e8ea6d 100644
--- a/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
@@ -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
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index 95bc90f9dad..3ebafb5c5e4 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -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
diff --git a/spec/frontend/pipelines/graph/graph_component_legacy_spec.js b/spec/frontend/pipelines/graph/graph_component_legacy_spec.js
index bdfe546f0e4..3313f71e102 100644
--- a/spec/frontend/pipelines/graph/graph_component_legacy_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_legacy_spec.js
@@ -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(() => {
diff --git a/spec/frontend/pipelines/graph/stage_column_component_legacy_spec.js b/spec/frontend/pipelines/graph/stage_column_component_legacy_spec.js
new file mode 100644
index 00000000000..463e4c12c7d
--- /dev/null
+++ b/spec/frontend/pipelines/graph/stage_column_component_legacy_spec.js
@@ -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-&lt;img src=x onerror=alert(document.domain)&gt;',
+ );
+ });
+ });
+
+ 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);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/graph/stage_column_component_spec.js b/spec/frontend/pipelines/graph/stage_column_component_spec.js
index d32534326c5..abf13823cb9 100644
--- a/spec/frontend/pipelines/graph/stage_column_component_spec.js
+++ b/spec/frontend/pipelines/graph/stage_column_component_spec.js
@@ -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';
-
-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',
- },
+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', () => {
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,
},
});
- });
+ };
- it('should render provided title', () => {
- expect(
- wrapper
- .find('.stage-name')
- .text()
- .trim(),
- ).toBe('foo');
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
});
- it('should render the provided groups', () => {
- expect(wrapper.findAll('.builds-container > ul > li').length).toBe(
- wrapper.props('groups').length,
- );
+ 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);
+ });
});
- 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)>',
},
});
+ });
+
+ it('capitalizes and escapes name', () => {
+ expect(findStageColumnTitle().text()).toBe(
+ 'Test &lt;img src=x onerror=alert(document.domain)&gt;',
+ );
+ });
- expect(wrapper.find('.builds-container li').attributes('id')).toBe(
+ it('escapes id', () => {
+ expect(findStageColumnGroup().attributes('id')).toBe(
'ci-badge-&lt;img src=x onerror=alert(document.domain)&gt;',
);
});
});
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);
});
});
});
diff --git a/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb
new file mode 100644
index 00000000000..225c5c2b7b6
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb
new file mode 100644
index 00000000000..5af27435d6b
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
index 88293f11149..d4e5a1a94f2 100644
--- a/spec/lib/gitlab/metrics/transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -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
diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb
index ef09fb037e9..82ac159b9cc 100644
--- a/spec/models/packages/package_file_spec.rb
+++ b/spec/models/packages/package_file_spec.rb
@@ -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