Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-11-27 18:09:52 +00:00
parent 4c39dd11dc
commit 28f1931ae8
65 changed files with 1013 additions and 316 deletions

View file

@ -0,0 +1,10 @@
import { REST, GRAPHQL } from './constants';
export const accessors = {
[REST]: {
groupId: 'id',
},
[GRAPHQL]: {
groupId: 'name',
},
};

View file

@ -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" />

View file

@ -1,3 +1,6 @@
export const DOWNSTREAM = 'downstream';
export const MAIN = 'main';
export const UPSTREAM = 'upstream';
export const REST = 'rest';
export const GRAPHQL = 'graphql';

View file

@ -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>

View file

@ -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="{

View file

@ -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">

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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 {

View file

@ -69,7 +69,7 @@
}
.btn {
margin: 10px 0 0;
margin-top: 10px;
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -0,0 +1,5 @@
---
title: Fixed double-border style on WebIDE button
merge_request: 48605
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Limit maximum deployments per pipeline to 500
merge_request: 46931
author:
type: added

View 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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
a3aa783f2648a95e3ff8b503ef15b8153759c74ac85b30bf94e39710824e57b0

View file

@ -0,0 +1 @@
6b88d79aa8d373fa1d9aa2698a9d20c09aff14ef16af4c123abd4e7c98e41311

View file

@ -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);

View file

@ -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.

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
---

View file

@ -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
View 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

View 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

View 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

View file

@ -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

View file

@ -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

View file

@ -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(() => {

View file

@ -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);
});
});
});

View file

@ -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 &lt;img src=x onerror=alert(document.domain)&gt;',
);
});
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);
});
});
});

View 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

View 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

View file

@ -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

View file

@ -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