Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
2cfa1fc75d
commit
c0d8f9f3f9
65 changed files with 1716 additions and 347 deletions
|
@ -1,22 +1,23 @@
|
||||||
<script>
|
<script>
|
||||||
/* eslint-disable @gitlab/vue-i18n/no-bare-strings */
|
/* eslint-disable @gitlab/vue-i18n/no-bare-strings */
|
||||||
import { format } from 'timeago.js';
|
|
||||||
import _ from 'underscore';
|
import _ from 'underscore';
|
||||||
import { GlTooltipDirective } from '@gitlab/ui';
|
import { GlTooltipDirective } from '@gitlab/ui';
|
||||||
import environmentItemMixin from 'ee_else_ce/environments/mixins/environment_item_mixin';
|
import { __, sprintf } from '~/locale';
|
||||||
|
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||||
|
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||||
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
|
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||||
|
import CommitComponent from '~/vue_shared/components/commit.vue';
|
||||||
import Icon from '~/vue_shared/components/icon.vue';
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
|
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
|
||||||
import { __, sprintf } from '~/locale';
|
import environmentItemMixin from 'ee_else_ce/environments/mixins/environment_item_mixin';
|
||||||
|
import eventHub from '../event_hub';
|
||||||
import ActionsComponent from './environment_actions.vue';
|
import ActionsComponent from './environment_actions.vue';
|
||||||
import ExternalUrlComponent from './environment_external_url.vue';
|
import ExternalUrlComponent from './environment_external_url.vue';
|
||||||
import StopComponent from './environment_stop.vue';
|
|
||||||
import RollbackComponent from './environment_rollback.vue';
|
|
||||||
import TerminalButtonComponent from './environment_terminal_button.vue';
|
|
||||||
import MonitoringButtonComponent from './environment_monitoring.vue';
|
import MonitoringButtonComponent from './environment_monitoring.vue';
|
||||||
import CommitComponent from '../../vue_shared/components/commit.vue';
|
import PinComponent from './environment_pin.vue';
|
||||||
import eventHub from '../event_hub';
|
import RollbackComponent from './environment_rollback.vue';
|
||||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
import StopComponent from './environment_stop.vue';
|
||||||
|
import TerminalButtonComponent from './environment_terminal_button.vue';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Environment Item Component
|
* Environment Item Component
|
||||||
|
@ -26,21 +27,22 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
CommitComponent,
|
|
||||||
Icon,
|
|
||||||
ActionsComponent,
|
ActionsComponent,
|
||||||
|
CommitComponent,
|
||||||
ExternalUrlComponent,
|
ExternalUrlComponent,
|
||||||
StopComponent,
|
Icon,
|
||||||
RollbackComponent,
|
|
||||||
TerminalButtonComponent,
|
|
||||||
MonitoringButtonComponent,
|
MonitoringButtonComponent,
|
||||||
|
PinComponent,
|
||||||
|
RollbackComponent,
|
||||||
|
StopComponent,
|
||||||
|
TerminalButtonComponent,
|
||||||
TooltipOnTruncate,
|
TooltipOnTruncate,
|
||||||
UserAvatarLink,
|
UserAvatarLink,
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
GlTooltip: GlTooltipDirective,
|
GlTooltip: GlTooltipDirective,
|
||||||
},
|
},
|
||||||
mixins: [environmentItemMixin],
|
mixins: [environmentItemMixin, timeagoMixin],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
canReadEnvironment: {
|
canReadEnvironment: {
|
||||||
|
@ -52,7 +54,12 @@ export default {
|
||||||
model: {
|
model: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
default: () => ({}),
|
},
|
||||||
|
|
||||||
|
shouldShowAutoStopDate: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
tableData: {
|
tableData: {
|
||||||
|
@ -76,6 +83,16 @@ export default {
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checkes whether the row displayed is a folder.
|
||||||
|
*
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
|
||||||
|
isFolder() {
|
||||||
|
return this.model.isFolder;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checkes whether the environment is protected.
|
* Checkes whether the environment is protected.
|
||||||
* (`is_protected` currently only set in EE)
|
* (`is_protected` currently only set in EE)
|
||||||
|
@ -112,24 +129,64 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies if the date to be shown is present.
|
* Verifies if the autostop date is present.
|
||||||
|
*
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
canShowAutoStopDate() {
|
||||||
|
if (!this.model.auto_stop_at) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoStopDate = new Date(this.model.auto_stop_at);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
return now < autoStopDate;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Human readable deployment date.
|
||||||
|
*
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
autoStopDate() {
|
||||||
|
if (this.canShowAutoStopDate) {
|
||||||
|
return {
|
||||||
|
formatted: this.timeFormatted(this.model.auto_stop_at),
|
||||||
|
tooltip: this.tooltipTitle(this.model.auto_stop_at),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
formatted: '',
|
||||||
|
tooltip: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies if the deployment date is present.
|
||||||
*
|
*
|
||||||
* @returns {Boolean|Undefined}
|
* @returns {Boolean|Undefined}
|
||||||
*/
|
*/
|
||||||
canShowDate() {
|
canShowDeploymentDate() {
|
||||||
return this.model && this.model.last_deployment && this.model.last_deployment.deployed_at;
|
return this.model && this.model.last_deployment && this.model.last_deployment.deployed_at;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Human readable date.
|
* Human readable deployment date.
|
||||||
*
|
*
|
||||||
* @returns {String}
|
* @returns {String}
|
||||||
*/
|
*/
|
||||||
deployedDate() {
|
deployedDate() {
|
||||||
if (this.canShowDate) {
|
if (this.canShowDeploymentDate) {
|
||||||
return format(this.model.last_deployment.deployed_at);
|
return {
|
||||||
|
formatted: this.timeFormatted(this.model.last_deployment.deployed_at),
|
||||||
|
tooltip: this.tooltipTitle(this.model.last_deployment.deployed_at),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return '';
|
return {
|
||||||
|
formatted: '',
|
||||||
|
tooltip: '',
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
actions() {
|
actions() {
|
||||||
|
@ -344,6 +401,15 @@ export default {
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checkes whether to display no deployment text.
|
||||||
|
*
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
showNoDeployments() {
|
||||||
|
return !this.hasLastDeploymentKey && !this.isFolder;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies if the build name column should be rendered by verifing
|
* Verifies if the build name column should be rendered by verifing
|
||||||
* if all the information needed is present
|
* if all the information needed is present
|
||||||
|
@ -353,7 +419,7 @@ export default {
|
||||||
*/
|
*/
|
||||||
shouldRenderBuildName() {
|
shouldRenderBuildName() {
|
||||||
return (
|
return (
|
||||||
!this.model.isFolder &&
|
!this.isFolder &&
|
||||||
!_.isEmpty(this.model.last_deployment) &&
|
!_.isEmpty(this.model.last_deployment) &&
|
||||||
!_.isEmpty(this.model.last_deployment.deployable)
|
!_.isEmpty(this.model.last_deployment.deployable)
|
||||||
);
|
);
|
||||||
|
@ -383,11 +449,7 @@ export default {
|
||||||
* @return {String}
|
* @return {String}
|
||||||
*/
|
*/
|
||||||
externalURL() {
|
externalURL() {
|
||||||
if (this.model && this.model.external_url) {
|
return this.model.external_url || '';
|
||||||
return this.model.external_url;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -399,26 +461,22 @@ export default {
|
||||||
*/
|
*/
|
||||||
shouldRenderDeploymentID() {
|
shouldRenderDeploymentID() {
|
||||||
return (
|
return (
|
||||||
!this.model.isFolder &&
|
!this.isFolder &&
|
||||||
!_.isEmpty(this.model.last_deployment) &&
|
!_.isEmpty(this.model.last_deployment) &&
|
||||||
this.model.last_deployment.iid !== undefined
|
this.model.last_deployment.iid !== undefined
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
environmentPath() {
|
environmentPath() {
|
||||||
if (this.model && this.model.environment_path) {
|
return this.model.environment_path || '';
|
||||||
return this.model.environment_path;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
},
|
},
|
||||||
|
|
||||||
monitoringUrl() {
|
monitoringUrl() {
|
||||||
if (this.model && this.model.metrics_path) {
|
return this.model.metrics_path || '';
|
||||||
return this.model.metrics_path;
|
},
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
autoStopUrl() {
|
||||||
|
return this.model.cancel_auto_stop_path || '';
|
||||||
},
|
},
|
||||||
|
|
||||||
displayEnvironmentActions() {
|
displayEnvironmentActions() {
|
||||||
|
@ -447,7 +505,7 @@ export default {
|
||||||
<div
|
<div
|
||||||
:class="{
|
:class="{
|
||||||
'js-child-row environment-child-row': model.isChildren,
|
'js-child-row environment-child-row': model.isChildren,
|
||||||
'folder-row': model.isFolder,
|
'folder-row': isFolder,
|
||||||
}"
|
}"
|
||||||
class="gl-responsive-table-row"
|
class="gl-responsive-table-row"
|
||||||
role="row"
|
role="row"
|
||||||
|
@ -457,7 +515,7 @@ export default {
|
||||||
:class="tableData.name.spacing"
|
:class="tableData.name.spacing"
|
||||||
role="gridcell"
|
role="gridcell"
|
||||||
>
|
>
|
||||||
<div v-if="!model.isFolder" class="table-mobile-header" role="rowheader">
|
<div v-if="!isFolder" class="table-mobile-header" role="rowheader">
|
||||||
{{ tableData.name.title }}
|
{{ tableData.name.title }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -466,7 +524,7 @@ export default {
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
v-if="!model.isFolder"
|
v-if="!isFolder"
|
||||||
v-gl-tooltip
|
v-gl-tooltip
|
||||||
:title="model.name"
|
:title="model.name"
|
||||||
class="environment-name table-mobile-content"
|
class="environment-name table-mobile-content"
|
||||||
|
@ -506,7 +564,7 @@ export default {
|
||||||
{{ deploymentInternalId }}
|
{{ deploymentInternalId }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span v-if="!model.isFolder && deploymentHasUser" class="text-break-word">
|
<span v-if="!isFolder && deploymentHasUser" class="text-break-word">
|
||||||
by
|
by
|
||||||
<user-avatar-link
|
<user-avatar-link
|
||||||
:link-href="deploymentUser.web_url"
|
:link-href="deploymentUser.web_url"
|
||||||
|
@ -516,6 +574,10 @@ export default {
|
||||||
class="js-deploy-user-container float-none"
|
class="js-deploy-user-container float-none"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<div v-if="showNoDeployments" class="commit-title table-mobile-content">
|
||||||
|
{{ s__('Environments|No deployments yet') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -536,14 +598,8 @@ export default {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div v-if="!isFolder" class="table-section" :class="tableData.commit.spacing" role="gridcell">
|
||||||
v-if="!model.isFolder"
|
|
||||||
class="table-section"
|
|
||||||
:class="tableData.commit.spacing"
|
|
||||||
role="gridcell"
|
|
||||||
>
|
|
||||||
<div role="rowheader" class="table-mobile-header">{{ tableData.commit.title }}</div>
|
<div role="rowheader" class="table-mobile-header">{{ tableData.commit.title }}</div>
|
||||||
|
|
||||||
<div v-if="hasLastDeploymentKey" class="js-commit-component table-mobile-content">
|
<div v-if="hasLastDeploymentKey" class="js-commit-component table-mobile-content">
|
||||||
<commit-component
|
<commit-component
|
||||||
:tag="commitTag"
|
:tag="commitTag"
|
||||||
|
@ -554,31 +610,51 @@ export default {
|
||||||
:author="commitAuthor"
|
:author="commitAuthor"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!hasLastDeploymentKey" class="commit-title table-mobile-content">
|
|
||||||
{{ s__('Environments|No deployments yet') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div v-if="!isFolder" class="table-section" :class="tableData.date.spacing" role="gridcell">
|
||||||
v-if="!model.isFolder"
|
|
||||||
class="table-section"
|
|
||||||
:class="tableData.date.spacing"
|
|
||||||
role="gridcell"
|
|
||||||
>
|
|
||||||
<div role="rowheader" class="table-mobile-header">{{ tableData.date.title }}</div>
|
<div role="rowheader" class="table-mobile-header">{{ tableData.date.title }}</div>
|
||||||
|
<span
|
||||||
<span v-if="canShowDate" class="environment-created-date-timeago table-mobile-content">
|
v-if="canShowDeploymentDate"
|
||||||
{{ deployedDate }}
|
v-gl-tooltip
|
||||||
|
:title="deployedDate.tooltip"
|
||||||
|
class="environment-created-date-timeago table-mobile-content flex-truncate-parent"
|
||||||
|
>
|
||||||
|
<span class="flex-truncate-child">
|
||||||
|
{{ deployedDate.formatted }}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="!model.isFolder && displayEnvironmentActions"
|
v-if="!isFolder && shouldShowAutoStopDate"
|
||||||
|
class="table-section"
|
||||||
|
:class="tableData.autoStop.spacing"
|
||||||
|
role="gridcell"
|
||||||
|
>
|
||||||
|
<div role="rowheader" class="table-mobile-header">{{ tableData.autoStop.title }}</div>
|
||||||
|
<span
|
||||||
|
v-if="canShowAutoStopDate"
|
||||||
|
v-gl-tooltip
|
||||||
|
:title="autoStopDate.tooltip"
|
||||||
|
class="table-mobile-content flex-truncate-parent"
|
||||||
|
>
|
||||||
|
<span class="flex-truncate-child js-auto-stop">{{ autoStopDate.formatted }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!isFolder && displayEnvironmentActions"
|
||||||
class="table-section table-button-footer"
|
class="table-section table-button-footer"
|
||||||
:class="tableData.actions.spacing"
|
:class="tableData.actions.spacing"
|
||||||
role="gridcell"
|
role="gridcell"
|
||||||
>
|
>
|
||||||
<div class="btn-group table-action-buttons" role="group">
|
<div class="btn-group table-action-buttons" role="group">
|
||||||
|
<pin-component
|
||||||
|
v-if="canShowAutoStopDate && shouldShowAutoStopDate"
|
||||||
|
:auto-stop-url="autoStopUrl"
|
||||||
|
/>
|
||||||
|
|
||||||
<external-url-component
|
<external-url-component
|
||||||
v-if="externalURL && canReadEnvironment"
|
v-if="externalURL && canReadEnvironment"
|
||||||
:external-url="externalURL"
|
:external-url="externalURL"
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* Renders a prevent auto-stop button.
|
||||||
|
* Used in environments table.
|
||||||
|
*/
|
||||||
|
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||||
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
|
import { __ } from '~/locale';
|
||||||
|
import eventHub from '../event_hub';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Icon,
|
||||||
|
GlButton,
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
GlTooltip: GlTooltipDirective,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
autoStopUrl: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onPinClick() {
|
||||||
|
eventHub.$emit('cancelAutoStop', this.autoStopUrl);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
title: __('Prevent environment from auto-stopping'),
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<gl-button v-gl-tooltip :title="$options.title" :aria-label="$options.title" @click="onPinClick">
|
||||||
|
<icon name="thumbtack" />
|
||||||
|
</gl-button>
|
||||||
|
</template>
|
|
@ -6,6 +6,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
|
||||||
import _ from 'underscore';
|
import _ from 'underscore';
|
||||||
import environmentTableMixin from 'ee_else_ce/environments/mixins/environments_table_mixin';
|
import environmentTableMixin from 'ee_else_ce/environments/mixins/environments_table_mixin';
|
||||||
import { s__ } from '~/locale';
|
import { s__ } from '~/locale';
|
||||||
|
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
import EnvironmentItem from './environment_item.vue';
|
import EnvironmentItem from './environment_item.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -16,7 +17,7 @@ export default {
|
||||||
CanaryDeploymentCallout: () =>
|
CanaryDeploymentCallout: () =>
|
||||||
import('ee_component/environments/components/canary_deployment_callout.vue'),
|
import('ee_component/environments/components/canary_deployment_callout.vue'),
|
||||||
},
|
},
|
||||||
mixins: [environmentTableMixin],
|
mixins: [environmentTableMixin, glFeatureFlagsMixin()],
|
||||||
props: {
|
props: {
|
||||||
environments: {
|
environments: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
@ -42,6 +43,9 @@ export default {
|
||||||
: env,
|
: env,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
shouldShowAutoStopDate() {
|
||||||
|
return this.glFeatures.autoStopEnvironments;
|
||||||
|
},
|
||||||
tableData() {
|
tableData() {
|
||||||
return {
|
return {
|
||||||
// percent spacing for cols, should add up to 100
|
// percent spacing for cols, should add up to 100
|
||||||
|
@ -65,8 +69,12 @@ export default {
|
||||||
title: s__('Environments|Updated'),
|
title: s__('Environments|Updated'),
|
||||||
spacing: 'section-10',
|
spacing: 'section-10',
|
||||||
},
|
},
|
||||||
|
autoStop: {
|
||||||
|
title: s__('Environments|Auto stop in'),
|
||||||
|
spacing: 'section-5',
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
spacing: 'section-30',
|
spacing: this.shouldShowAutoStopDate ? 'section-25' : 'section-30',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -123,6 +131,14 @@ export default {
|
||||||
<div class="table-section" :class="tableData.date.spacing" role="columnheader">
|
<div class="table-section" :class="tableData.date.spacing" role="columnheader">
|
||||||
{{ tableData.date.title }}
|
{{ tableData.date.title }}
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="shouldShowAutoStopDate"
|
||||||
|
class="table-section"
|
||||||
|
:class="tableData.autoStop.spacing"
|
||||||
|
role="columnheader"
|
||||||
|
>
|
||||||
|
{{ tableData.autoStop.title }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-for="(model, i) in sortedEnvironments" :model="model">
|
<template v-for="(model, i) in sortedEnvironments" :model="model">
|
||||||
<div
|
<div
|
||||||
|
@ -130,6 +146,7 @@ export default {
|
||||||
:key="`environment-item-${i}`"
|
:key="`environment-item-${i}`"
|
||||||
:model="model"
|
:model="model"
|
||||||
:can-read-environment="canReadEnvironment"
|
:can-read-environment="canReadEnvironment"
|
||||||
|
:should-show-auto-stop-date="shouldShowAutoStopDate"
|
||||||
:table-data="tableData"
|
:table-data="tableData"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -90,16 +90,19 @@ export default {
|
||||||
Flash(s__('Environments|An error occurred while fetching the environments.'));
|
Flash(s__('Environments|An error occurred while fetching the environments.'));
|
||||||
},
|
},
|
||||||
|
|
||||||
postAction({ endpoint, errorMessage }) {
|
postAction({
|
||||||
|
endpoint,
|
||||||
|
errorMessage = s__('Environments|An error occurred while making the request.'),
|
||||||
|
}) {
|
||||||
if (!this.isMakingRequest) {
|
if (!this.isMakingRequest) {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
this.service
|
this.service
|
||||||
.postAction(endpoint)
|
.postAction(endpoint)
|
||||||
.then(() => this.fetchEnvironments())
|
.then(() => this.fetchEnvironments())
|
||||||
.catch(() => {
|
.catch(err => {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
Flash(errorMessage || s__('Environments|An error occurred while making the request.'));
|
Flash(_.isFunction(errorMessage) ? errorMessage(err.response.data) : errorMessage);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -138,6 +141,13 @@ export default {
|
||||||
);
|
);
|
||||||
this.postAction({ endpoint: retryUrl, errorMessage });
|
this.postAction({ endpoint: retryUrl, errorMessage });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
cancelAutoStop(autoStopPath) {
|
||||||
|
const errorMessage = ({ message }) =>
|
||||||
|
message ||
|
||||||
|
s__('Environments|An error occurred while canceling the auto stop, please try again');
|
||||||
|
this.postAction({ endpoint: autoStopPath, errorMessage });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -199,6 +209,8 @@ export default {
|
||||||
|
|
||||||
eventHub.$on('requestRollbackEnvironment', this.updateRollbackModal);
|
eventHub.$on('requestRollbackEnvironment', this.updateRollbackModal);
|
||||||
eventHub.$on('rollbackEnvironment', this.rollbackEnvironment);
|
eventHub.$on('rollbackEnvironment', this.rollbackEnvironment);
|
||||||
|
|
||||||
|
eventHub.$on('cancelAutoStop', this.cancelAutoStop);
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
@ -208,5 +220,7 @@ export default {
|
||||||
|
|
||||||
eventHub.$off('requestRollbackEnvironment', this.updateRollbackModal);
|
eventHub.$off('requestRollbackEnvironment', this.updateRollbackModal);
|
||||||
eventHub.$off('rollbackEnvironment', this.rollbackEnvironment);
|
eventHub.$off('rollbackEnvironment', this.rollbackEnvironment);
|
||||||
|
|
||||||
|
eventHub.$off('cancelAutoStop', this.cancelAutoStop);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,4 +4,13 @@ class Profiles::ActiveSessionsController < Profiles::ApplicationController
|
||||||
def index
|
def index
|
||||||
@sessions = ActiveSession.list(current_user).reject(&:is_impersonated)
|
@sessions = ActiveSession.list(current_user).reject(&:is_impersonated)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
ActiveSession.destroy_with_public_id(current_user, params[:id])
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { redirect_to profile_active_sessions_url, status: :found }
|
||||||
|
format.js { head :ok }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,6 +15,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController
|
||||||
before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
|
before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
|
||||||
push_frontend_feature_flag(:prometheus_computed_alerts)
|
push_frontend_feature_flag(:prometheus_computed_alerts)
|
||||||
end
|
end
|
||||||
|
before_action do
|
||||||
|
push_frontend_feature_flag(:auto_stop_environments)
|
||||||
|
end
|
||||||
after_action :expire_etag_cache, only: [:cancel_auto_stop]
|
after_action :expire_etag_cache, only: [:cancel_auto_stop]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
|
|
@ -17,7 +17,7 @@ class PipelinesFinder
|
||||||
return Ci::Pipeline.none
|
return Ci::Pipeline.none
|
||||||
end
|
end
|
||||||
|
|
||||||
items = pipelines
|
items = pipelines.no_child
|
||||||
items = by_scope(items)
|
items = by_scope(items)
|
||||||
items = by_status(items)
|
items = by_status(items)
|
||||||
items = by_ref(items)
|
items = by_ref(items)
|
||||||
|
|
|
@ -6,9 +6,11 @@ class ActiveSession
|
||||||
SESSION_BATCH_SIZE = 200
|
SESSION_BATCH_SIZE = 200
|
||||||
ALLOWED_NUMBER_OF_ACTIVE_SESSIONS = 100
|
ALLOWED_NUMBER_OF_ACTIVE_SESSIONS = 100
|
||||||
|
|
||||||
|
attr_writer :session_id
|
||||||
|
|
||||||
attr_accessor :created_at, :updated_at,
|
attr_accessor :created_at, :updated_at,
|
||||||
:session_id, :ip_address,
|
:ip_address, :browser, :os,
|
||||||
:browser, :os, :device_name, :device_type,
|
:device_name, :device_type,
|
||||||
:is_impersonated
|
:is_impersonated
|
||||||
|
|
||||||
def current?(session)
|
def current?(session)
|
||||||
|
@ -21,6 +23,11 @@ class ActiveSession
|
||||||
device_type&.titleize
|
device_type&.titleize
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def public_id
|
||||||
|
encrypted_id = Gitlab::CryptoHelper.aes256_gcm_encrypt(session_id)
|
||||||
|
CGI.escape(encrypted_id)
|
||||||
|
end
|
||||||
|
|
||||||
def self.set(user, request)
|
def self.set(user, request)
|
||||||
Gitlab::Redis::SharedState.with do |redis|
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
session_id = request.session.id
|
session_id = request.session.id
|
||||||
|
@ -70,6 +77,11 @@ class ActiveSession
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.destroy_with_public_id(user, public_id)
|
||||||
|
session_id = decrypt_public_id(public_id)
|
||||||
|
destroy(user, session_id) unless session_id.nil?
|
||||||
|
end
|
||||||
|
|
||||||
def self.destroy_sessions(redis, user, session_ids)
|
def self.destroy_sessions(redis, user, session_ids)
|
||||||
key_names = session_ids.map {|session_id| key_name(user.id, session_id) }
|
key_names = session_ids.map {|session_id| key_name(user.id, session_id) }
|
||||||
session_names = session_ids.map {|session_id| "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}" }
|
session_names = session_ids.map {|session_id| "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}" }
|
||||||
|
@ -146,9 +158,9 @@ class ActiveSession
|
||||||
# remove sessions if there are more than ALLOWED_NUMBER_OF_ACTIVE_SESSIONS.
|
# remove sessions if there are more than ALLOWED_NUMBER_OF_ACTIVE_SESSIONS.
|
||||||
sessions = active_session_entries(session_ids, user.id, redis)
|
sessions = active_session_entries(session_ids, user.id, redis)
|
||||||
sessions.sort_by! {|session| session.updated_at }.reverse!
|
sessions.sort_by! {|session| session.updated_at }.reverse!
|
||||||
sessions = sessions.drop(ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
|
destroyable_sessions = sessions.drop(ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
|
||||||
sessions = sessions.map { |session| session.session_id }
|
destroyable_session_ids = destroyable_sessions.map { |session| session.send :session_id } # rubocop:disable GitlabSecurity/PublicSend
|
||||||
destroy_sessions(redis, user, sessions) if sessions.any?
|
destroy_sessions(redis, user, destroyable_session_ids) if destroyable_session_ids.any?
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.cleaned_up_lookup_entries(redis, user)
|
def self.cleaned_up_lookup_entries(redis, user)
|
||||||
|
@ -167,4 +179,15 @@ class ActiveSession
|
||||||
|
|
||||||
entries.compact
|
entries.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private_class_method def self.decrypt_public_id(public_id)
|
||||||
|
decoded_id = CGI.unescape(public_id)
|
||||||
|
Gitlab::CryptoHelper.aes256_gcm_decrypt(decoded_id)
|
||||||
|
rescue
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :session_id
|
||||||
end
|
end
|
||||||
|
|
|
@ -54,6 +54,10 @@ module Ci
|
||||||
def to_partial_path
|
def to_partial_path
|
||||||
'projects/generic_commit_statuses/generic_commit_status'
|
'projects/generic_commit_statuses/generic_commit_status'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def yaml_for_downstream
|
||||||
|
nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,9 @@ module Ci
|
||||||
has_one :chat_data, class_name: 'Ci::PipelineChatData'
|
has_one :chat_data, class_name: 'Ci::PipelineChatData'
|
||||||
|
|
||||||
has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline
|
has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline
|
||||||
|
has_many :child_pipelines, -> { merge(Ci::Sources::Pipeline.same_project) }, through: :sourced_pipelines, source: :pipeline
|
||||||
has_one :triggered_by_pipeline, through: :source_pipeline, source: :source_pipeline
|
has_one :triggered_by_pipeline, through: :source_pipeline, source: :source_pipeline
|
||||||
|
has_one :parent_pipeline, -> { merge(Ci::Sources::Pipeline.same_project) }, through: :source_pipeline, source: :source_pipeline
|
||||||
has_one :source_job, through: :source_pipeline, source: :source_job
|
has_one :source_job, through: :source_pipeline, source: :source_job
|
||||||
|
|
||||||
has_one :pipeline_config, class_name: 'Ci::PipelineConfig', inverse_of: :pipeline
|
has_one :pipeline_config, class_name: 'Ci::PipelineConfig', inverse_of: :pipeline
|
||||||
|
@ -213,6 +215,7 @@ module Ci
|
||||||
end
|
end
|
||||||
|
|
||||||
scope :internal, -> { where(source: internal_sources) }
|
scope :internal, -> { where(source: internal_sources) }
|
||||||
|
scope :no_child, -> { where.not(source: :parent_pipeline) }
|
||||||
scope :ci_sources, -> { where(config_source: ::Ci::PipelineEnums.ci_config_sources_values) }
|
scope :ci_sources, -> { where(config_source: ::Ci::PipelineEnums.ci_config_sources_values) }
|
||||||
scope :for_user, -> (user) { where(user: user) }
|
scope :for_user, -> (user) { where(user: user) }
|
||||||
scope :for_sha, -> (sha) { where(sha: sha) }
|
scope :for_sha, -> (sha) { where(sha: sha) }
|
||||||
|
@ -508,10 +511,6 @@ module Ci
|
||||||
builds.skipped.after_stage(stage_idx).find_each(&:process)
|
builds.skipped.after_stage(stage_idx).find_each(&:process)
|
||||||
end
|
end
|
||||||
|
|
||||||
def child?
|
|
||||||
false
|
|
||||||
end
|
|
||||||
|
|
||||||
def latest?
|
def latest?
|
||||||
return false unless git_ref && commit.present?
|
return false unless git_ref && commit.present?
|
||||||
|
|
||||||
|
@ -694,6 +693,24 @@ module Ci
|
||||||
all_merge_requests.order(id: :desc)
|
all_merge_requests.order(id: :desc)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# If pipeline is a child of another pipeline, include the parent
|
||||||
|
# and the siblings, otherwise return only itself.
|
||||||
|
def same_family_pipeline_ids
|
||||||
|
if (parent = parent_pipeline)
|
||||||
|
[parent.id] + parent.child_pipelines.pluck(:id)
|
||||||
|
else
|
||||||
|
[self.id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def child?
|
||||||
|
parent_pipeline.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def parent?
|
||||||
|
child_pipelines.exists?
|
||||||
|
end
|
||||||
|
|
||||||
def detailed_status(current_user)
|
def detailed_status(current_user)
|
||||||
Gitlab::Ci::Status::Pipeline::Factory
|
Gitlab::Ci::Status::Pipeline::Factory
|
||||||
.new(self, current_user)
|
.new(self, current_user)
|
||||||
|
|
|
@ -23,10 +23,11 @@ module Ci
|
||||||
schedule: 4,
|
schedule: 4,
|
||||||
api: 5,
|
api: 5,
|
||||||
external: 6,
|
external: 6,
|
||||||
pipeline: 7,
|
cross_project_pipeline: 7,
|
||||||
chat: 8,
|
chat: 8,
|
||||||
merge_request_event: 10,
|
merge_request_event: 10,
|
||||||
external_pull_request_event: 11
|
external_pull_request_event: 11,
|
||||||
|
parent_pipeline: 12
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -38,7 +39,8 @@ module Ci
|
||||||
repository_source: 1,
|
repository_source: 1,
|
||||||
auto_devops_source: 2,
|
auto_devops_source: 2,
|
||||||
remote_source: 4,
|
remote_source: 4,
|
||||||
external_project_source: 5
|
external_project_source: 5,
|
||||||
|
bridge_source: 6
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,8 @@ module Ci
|
||||||
validates :source_project, presence: true
|
validates :source_project, presence: true
|
||||||
validates :source_job, presence: true
|
validates :source_job, presence: true
|
||||||
validates :source_pipeline, presence: true
|
validates :source_pipeline, presence: true
|
||||||
|
|
||||||
|
scope :same_project, -> { where(arel_table[:source_project_id].eq(arel_table[:project_id])) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,6 +13,7 @@ module Issuable
|
||||||
include CacheMarkdownField
|
include CacheMarkdownField
|
||||||
include Participable
|
include Participable
|
||||||
include Mentionable
|
include Mentionable
|
||||||
|
include Milestoneable
|
||||||
include Subscribable
|
include Subscribable
|
||||||
include StripAttribute
|
include StripAttribute
|
||||||
include Awardable
|
include Awardable
|
||||||
|
@ -56,7 +57,6 @@ module Issuable
|
||||||
belongs_to :author, class_name: 'User'
|
belongs_to :author, class_name: 'User'
|
||||||
belongs_to :updated_by, class_name: 'User'
|
belongs_to :updated_by, class_name: 'User'
|
||||||
belongs_to :last_edited_by, class_name: 'User'
|
belongs_to :last_edited_by, class_name: 'User'
|
||||||
belongs_to :milestone
|
|
||||||
|
|
||||||
has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :destroy do # rubocop:disable Cop/ActiveRecordDependent
|
has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :destroy do # rubocop:disable Cop/ActiveRecordDependent
|
||||||
def authors_loaded?
|
def authors_loaded?
|
||||||
|
@ -89,18 +89,12 @@ module Issuable
|
||||||
# to avoid breaking the existing Issuables which may have their descriptions longer
|
# to avoid breaking the existing Issuables which may have their descriptions longer
|
||||||
validates :description, length: { maximum: DESCRIPTION_LENGTH_MAX }, allow_blank: true, on: :create
|
validates :description, length: { maximum: DESCRIPTION_LENGTH_MAX }, allow_blank: true, on: :create
|
||||||
validate :description_max_length_for_new_records_is_valid, on: :update
|
validate :description_max_length_for_new_records_is_valid, on: :update
|
||||||
validate :milestone_is_valid
|
|
||||||
|
|
||||||
before_validation :truncate_description_on_import!
|
before_validation :truncate_description_on_import!
|
||||||
|
|
||||||
scope :authored, ->(user) { where(author_id: user) }
|
scope :authored, ->(user) { where(author_id: user) }
|
||||||
scope :recent, -> { reorder(id: :desc) }
|
scope :recent, -> { reorder(id: :desc) }
|
||||||
scope :of_projects, ->(ids) { where(project_id: ids) }
|
scope :of_projects, ->(ids) { where(project_id: ids) }
|
||||||
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
|
|
||||||
scope :any_milestone, -> { where('milestone_id IS NOT NULL') }
|
|
||||||
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
|
|
||||||
scope :any_release, -> { joins_milestone_releases }
|
|
||||||
scope :with_release, -> (tag, project_id) { joins_milestone_releases.where( milestones: { releases: { tag: tag, project_id: project_id } } ) }
|
|
||||||
scope :opened, -> { with_state(:opened) }
|
scope :opened, -> { with_state(:opened) }
|
||||||
scope :only_opened, -> { with_state(:opened) }
|
scope :only_opened, -> { with_state(:opened) }
|
||||||
scope :closed, -> { with_state(:closed) }
|
scope :closed, -> { with_state(:closed) }
|
||||||
|
@ -118,20 +112,6 @@ module Issuable
|
||||||
end
|
end
|
||||||
# rubocop:enable GitlabSecurity/SqlInjection
|
# rubocop:enable GitlabSecurity/SqlInjection
|
||||||
|
|
||||||
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
|
|
||||||
scope :order_milestone_due_desc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC')) }
|
|
||||||
scope :order_milestone_due_asc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC')) }
|
|
||||||
|
|
||||||
scope :without_release, -> do
|
|
||||||
joins("LEFT OUTER JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id")
|
|
||||||
.where('milestone_releases.release_id IS NULL')
|
|
||||||
end
|
|
||||||
|
|
||||||
scope :joins_milestone_releases, -> do
|
|
||||||
joins("JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id
|
|
||||||
JOIN releases ON milestone_releases.release_id = releases.id").distinct
|
|
||||||
end
|
|
||||||
|
|
||||||
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
|
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
|
||||||
scope :any_label, -> { joins(:label_links).group(:id) }
|
scope :any_label, -> { joins(:label_links).group(:id) }
|
||||||
scope :join_project, -> { joins(:project) }
|
scope :join_project, -> { joins(:project) }
|
||||||
|
@ -164,10 +144,6 @@ module Issuable
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def milestone_is_valid
|
|
||||||
errors.add(:milestone_id, message: "is invalid") if respond_to?(:milestone_id) && milestone_id.present? && !milestone_available?
|
|
||||||
end
|
|
||||||
|
|
||||||
def description_max_length_for_new_records_is_valid
|
def description_max_length_for_new_records_is_valid
|
||||||
if new_record? && description.length > Issuable::DESCRIPTION_LENGTH_MAX
|
if new_record? && description.length > Issuable::DESCRIPTION_LENGTH_MAX
|
||||||
errors.add(:description, :too_long, count: Issuable::DESCRIPTION_LENGTH_MAX)
|
errors.add(:description, :too_long, count: Issuable::DESCRIPTION_LENGTH_MAX)
|
||||||
|
@ -332,10 +308,6 @@ module Issuable
|
||||||
project
|
project
|
||||||
end
|
end
|
||||||
|
|
||||||
def milestone_available?
|
|
||||||
project_id == milestone&.project_id || project.ancestors_upto.compact.include?(milestone&.group)
|
|
||||||
end
|
|
||||||
|
|
||||||
def assignee_or_author?(user)
|
def assignee_or_author?(user)
|
||||||
author_id == user.id || assignees.exists?(user.id)
|
author_id == user.id || assignees.exists?(user.id)
|
||||||
end
|
end
|
||||||
|
@ -482,13 +454,6 @@ module Issuable
|
||||||
def wipless_title_changed(old_title)
|
def wipless_title_changed(old_title)
|
||||||
old_title != title
|
old_title != title
|
||||||
end
|
end
|
||||||
|
|
||||||
##
|
|
||||||
# Overridden on EE module
|
|
||||||
#
|
|
||||||
def supports_milestone?
|
|
||||||
respond_to?(:milestone_id)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Issuable.prepend_if_ee('EE::Issuable') # rubocop: disable Cop/InjectEnterpriseEditionModule
|
Issuable.prepend_if_ee('EE::Issuable') # rubocop: disable Cop/InjectEnterpriseEditionModule
|
||||||
|
|
62
app/models/concerns/milestoneable.rb
Normal file
62
app/models/concerns/milestoneable.rb
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Milestoneable concern
|
||||||
|
#
|
||||||
|
# Contains functionality related to objects that can be assigned Milestones
|
||||||
|
#
|
||||||
|
# Used by Issuable
|
||||||
|
#
|
||||||
|
module Milestoneable
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
belongs_to :milestone
|
||||||
|
|
||||||
|
validate :milestone_is_valid
|
||||||
|
|
||||||
|
after_save :write_to_new_milestone_relationship
|
||||||
|
|
||||||
|
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
|
||||||
|
scope :any_milestone, -> { where('milestone_id IS NOT NULL') }
|
||||||
|
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
|
||||||
|
scope :any_release, -> { joins_milestone_releases }
|
||||||
|
scope :with_release, -> (tag, project_id) { joins_milestone_releases.where( milestones: { releases: { tag: tag, project_id: project_id } } ) }
|
||||||
|
|
||||||
|
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
|
||||||
|
scope :order_milestone_due_desc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC')) }
|
||||||
|
scope :order_milestone_due_asc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC')) }
|
||||||
|
|
||||||
|
scope :without_release, -> do
|
||||||
|
joins("LEFT OUTER JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id")
|
||||||
|
.where('milestone_releases.release_id IS NULL')
|
||||||
|
end
|
||||||
|
|
||||||
|
scope :joins_milestone_releases, -> do
|
||||||
|
joins("JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id
|
||||||
|
JOIN releases ON milestone_releases.release_id = releases.id").distinct
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def milestone_is_valid
|
||||||
|
errors.add(:milestone_id, message: "is invalid") if respond_to?(:milestone_id) && milestone_id.present? && !milestone_available?
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_to_new_milestone_relationship
|
||||||
|
self.milestones = [milestone].compact if supports_milestone? && saved_change_to_milestone_id?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def milestone_available?
|
||||||
|
project_id == milestone&.project_id || project.ancestors_upto.compact.include?(milestone&.group)
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Overridden on EE module
|
||||||
|
#
|
||||||
|
def supports_milestone?
|
||||||
|
respond_to?(:milestone_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Milestoneable.prepend_if_ee('EE::Milestoneable')
|
|
@ -33,6 +33,9 @@ class Issue < ApplicationRecord
|
||||||
|
|
||||||
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.issues&.maximum(:iid) }
|
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.issues&.maximum(:iid) }
|
||||||
|
|
||||||
|
has_many :issue_milestones
|
||||||
|
has_many :milestones, through: :issue_milestones
|
||||||
|
|
||||||
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
|
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
|
||||||
|
|
||||||
has_many :merge_requests_closing_issues,
|
has_many :merge_requests_closing_issues,
|
||||||
|
|
6
app/models/issue_milestone.rb
Normal file
6
app/models/issue_milestone.rb
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class IssueMilestone < ApplicationRecord
|
||||||
|
belongs_to :milestone
|
||||||
|
belongs_to :issue
|
||||||
|
end
|
|
@ -35,6 +35,9 @@ class MergeRequest < ApplicationRecord
|
||||||
|
|
||||||
has_many :merge_request_diffs
|
has_many :merge_request_diffs
|
||||||
|
|
||||||
|
has_many :merge_request_milestones
|
||||||
|
has_many :milestones, through: :merge_request_milestones
|
||||||
|
|
||||||
has_one :merge_request_diff,
|
has_one :merge_request_diff,
|
||||||
-> { order('merge_request_diffs.id DESC') }, inverse_of: :merge_request
|
-> { order('merge_request_diffs.id DESC') }, inverse_of: :merge_request
|
||||||
|
|
||||||
|
|
6
app/models/merge_request_milestone.rb
Normal file
6
app/models/merge_request_milestone.rb
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class MergeRequestMilestone < ApplicationRecord
|
||||||
|
belongs_to :milestone
|
||||||
|
belongs_to :merge_request
|
||||||
|
end
|
|
@ -38,6 +38,9 @@ class Milestone < ApplicationRecord
|
||||||
has_many :merge_requests
|
has_many :merge_requests
|
||||||
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
|
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
|
||||||
|
|
||||||
|
has_many :issue_milestones
|
||||||
|
has_many :merge_request_milestones
|
||||||
|
|
||||||
scope :of_projects, ->(ids) { where(project_id: ids) }
|
scope :of_projects, ->(ids) { where(project_id: ids) }
|
||||||
scope :of_groups, ->(ids) { where(group_id: ids) }
|
scope :of_groups, ->(ids) { where(group_id: ids) }
|
||||||
scope :active, -> { with_state(:active) }
|
scope :active, -> { with_state(:active) }
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class PipelineDetailsEntity < PipelineEntity
|
class PipelineDetailsEntity < PipelineEntity
|
||||||
|
expose :project, using: ProjectEntity
|
||||||
|
|
||||||
expose :flags do
|
expose :flags do
|
||||||
expose :latest?, as: :latest
|
expose :latest?, as: :latest
|
||||||
end
|
end
|
||||||
|
|
|
@ -41,6 +41,7 @@ class PipelineSerializer < BaseSerializer
|
||||||
def preloaded_relations
|
def preloaded_relations
|
||||||
[
|
[
|
||||||
:latest_statuses_ordered_by_stage,
|
:latest_statuses_ordered_by_stage,
|
||||||
|
:project,
|
||||||
:stages,
|
:stages,
|
||||||
{
|
{
|
||||||
failed_builds: %i(project metadata)
|
failed_builds: %i(project metadata)
|
||||||
|
|
|
@ -23,7 +23,7 @@ module Ci
|
||||||
Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze
|
Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze
|
||||||
|
|
||||||
# rubocop: disable Metrics/ParameterLists
|
# rubocop: disable Metrics/ParameterLists
|
||||||
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, external_pull_request: nil, **options, &block)
|
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, external_pull_request: nil, bridge: nil, **options, &block)
|
||||||
@pipeline = Ci::Pipeline.new
|
@pipeline = Ci::Pipeline.new
|
||||||
|
|
||||||
command = Gitlab::Ci::Pipeline::Chain::Command.new(
|
command = Gitlab::Ci::Pipeline::Chain::Command.new(
|
||||||
|
@ -46,6 +46,7 @@ module Ci
|
||||||
current_user: current_user,
|
current_user: current_user,
|
||||||
push_options: params[:push_options] || {},
|
push_options: params[:push_options] || {},
|
||||||
chat_data: params[:chat_data],
|
chat_data: params[:chat_data],
|
||||||
|
bridge: bridge,
|
||||||
**extra_options(options))
|
**extra_options(options))
|
||||||
|
|
||||||
sequence = Gitlab::Ci::Pipeline::Chain::Sequence
|
sequence = Gitlab::Ci::Pipeline::Chain::Sequence
|
||||||
|
@ -104,14 +105,14 @@ module Ci
|
||||||
if Feature.enabled?(:ci_support_interruptible_pipelines, project, default_enabled: true)
|
if Feature.enabled?(:ci_support_interruptible_pipelines, project, default_enabled: true)
|
||||||
project.ci_pipelines
|
project.ci_pipelines
|
||||||
.where(ref: pipeline.ref)
|
.where(ref: pipeline.ref)
|
||||||
.where.not(id: pipeline.id)
|
.where.not(id: pipeline.same_family_pipeline_ids)
|
||||||
.where.not(sha: project.commit(pipeline.ref).try(:id))
|
.where.not(sha: project.commit(pipeline.ref).try(:id))
|
||||||
.alive_or_scheduled
|
.alive_or_scheduled
|
||||||
.with_only_interruptible_builds
|
.with_only_interruptible_builds
|
||||||
else
|
else
|
||||||
project.ci_pipelines
|
project.ci_pipelines
|
||||||
.where(ref: pipeline.ref)
|
.where(ref: pipeline.ref)
|
||||||
.where.not(id: pipeline.id)
|
.where.not(id: pipeline.same_family_pipeline_ids)
|
||||||
.where.not(sha: project.commit(pipeline.ref).try(:id))
|
.where.not(sha: project.commit(pipeline.ref).try(:id))
|
||||||
.created_or_pending
|
.created_or_pending
|
||||||
end
|
end
|
||||||
|
|
|
@ -44,7 +44,7 @@ module Ci
|
||||||
return error("400 Job has to be running", 400) unless job.running?
|
return error("400 Job has to be running", 400) unless job.running?
|
||||||
|
|
||||||
pipeline = Ci::CreatePipelineService.new(project, job.user, ref: params[:ref])
|
pipeline = Ci::CreatePipelineService.new(project, job.user, ref: params[:ref])
|
||||||
.execute(:pipeline, ignore_skip_ci: true) do |pipeline|
|
.execute(:cross_project_pipeline, ignore_skip_ci: true) do |pipeline|
|
||||||
source = job.sourced_pipelines.build(
|
source = job.sourced_pipelines.build(
|
||||||
source_pipeline: job.pipeline,
|
source_pipeline: job.pipeline,
|
||||||
source_project: job.project,
|
source_project: job.project,
|
||||||
|
|
|
@ -24,3 +24,9 @@
|
||||||
%strong= _('Signed in')
|
%strong= _('Signed in')
|
||||||
= s_('ProfileSession|on')
|
= s_('ProfileSession|on')
|
||||||
= l(active_session.created_at, format: :short)
|
= l(active_session.created_at, format: :short)
|
||||||
|
|
||||||
|
- unless is_current_session
|
||||||
|
.float-right
|
||||||
|
= link_to profile_active_session_path(active_session.public_id), data: { confirm: _('Are you sure? The device will be signed out of GitLab.') }, method: :delete, class: "btn btn-danger prepend-left-10" do
|
||||||
|
%span.sr-only= _('Revoke')
|
||||||
|
= _('Revoke')
|
||||||
|
|
3
app/views/projects/environments/_pin_button.html.haml
Normal file
3
app/views/projects/environments/_pin_button.html.haml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
- if environment.auto_stop_at? && environment.available?
|
||||||
|
= button_to cancel_auto_stop_project_environment_path(environment.project, environment), class: 'btn btn-secondary has-tooltip', title: _('Prevent environment from auto-stopping') do
|
||||||
|
= sprite_icon('thumbtack')
|
|
@ -32,9 +32,14 @@
|
||||||
= button_to stop_project_environment_path(@project, @environment), class: 'btn btn-danger has-tooltip', method: :post do
|
= button_to stop_project_environment_path(@project, @environment), class: 'btn btn-danger has-tooltip', method: :post do
|
||||||
= s_('Environments|Stop environment')
|
= s_('Environments|Stop environment')
|
||||||
|
|
||||||
.top-area
|
.top-area.justify-content-between
|
||||||
%h3.page-title= @environment.name
|
.d-flex
|
||||||
.nav-controls.ml-auto.my-2
|
%h3.page-title= @environment.name
|
||||||
|
- if @environment.auto_stop_at?
|
||||||
|
%p.align-self-end.prepend-left-8
|
||||||
|
= s_('Environments|Auto stops %{auto_stop_time}').html_safe % {auto_stop_time: time_ago_with_tooltip(@environment.auto_stop_at)}
|
||||||
|
.nav-controls.my-2
|
||||||
|
= render 'projects/environments/pin_button', environment: @environment
|
||||||
= render 'projects/environments/terminal_button', environment: @environment
|
= render 'projects/environments/terminal_button', environment: @environment
|
||||||
= render 'projects/environments/external_url', environment: @environment
|
= render 'projects/environments/external_url', environment: @environment
|
||||||
= render 'projects/environments/metrics_button', environment: @environment
|
= render 'projects/environments/metrics_button', environment: @environment
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
%h4.sub-header
|
%h4.sub-header
|
||||||
= _("Programming languages used in this repository")
|
= _("Programming languages used in this repository")
|
||||||
|
|
||||||
|
%p
|
||||||
|
= _("Measured in bytes of code. Excludes generated and vendored code.")
|
||||||
|
|
||||||
.row
|
.row
|
||||||
.col-md-4
|
.col-md-4
|
||||||
%ul.bordered-list
|
%ul.bordered-list
|
||||||
|
|
101
app/workers/concerns/reenqueuer.rb
Normal file
101
app/workers/concerns/reenqueuer.rb
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
#
|
||||||
|
# A concern that helps run exactly one instance of a worker, over and over,
|
||||||
|
# until it returns false or raises.
|
||||||
|
#
|
||||||
|
# To ensure the worker is always up, you can schedule it every minute with
|
||||||
|
# sidekiq-cron. Excess jobs will immediately exit due to an exclusive lease.
|
||||||
|
#
|
||||||
|
# The worker must define:
|
||||||
|
#
|
||||||
|
# - `#perform`
|
||||||
|
# - `#lease_timeout`
|
||||||
|
#
|
||||||
|
# The worker spec should include `it_behaves_like 'reenqueuer'` and
|
||||||
|
# `it_behaves_like 'it is rate limited to 1 call per'`.
|
||||||
|
#
|
||||||
|
# Optionally override `#minimum_duration` to adjust the rate limit.
|
||||||
|
#
|
||||||
|
# When `#perform` returns false, the job will not be reenqueued. Instead, we
|
||||||
|
# will wait for the next one scheduled by sidekiq-cron.
|
||||||
|
#
|
||||||
|
# #lease_timeout should be longer than the longest possible `#perform`.
|
||||||
|
# The lease is normally released in an ensure block, but it is possible to
|
||||||
|
# orphan the lease by killing Sidekiq, so it should also be as short as
|
||||||
|
# possible. Consider that long-running jobs are generally not recommended.
|
||||||
|
# Ideally, every job finishes within 25 seconds because that is the default
|
||||||
|
# wait time for graceful termination.
|
||||||
|
#
|
||||||
|
# Timing: It runs as often as Sidekiq allows. We rate limit with sleep for
|
||||||
|
# now: https://gitlab.com/gitlab-org/gitlab/issues/121697
|
||||||
|
module Reenqueuer
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
prepended do
|
||||||
|
include ExclusiveLeaseGuard
|
||||||
|
include ReenqueuerSleeper
|
||||||
|
|
||||||
|
sidekiq_options retry: false
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform(*args)
|
||||||
|
try_obtain_lease do
|
||||||
|
reenqueue(*args) do
|
||||||
|
ensure_minimum_duration(minimum_duration) do
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def reenqueue(*args)
|
||||||
|
self.class.perform_async(*args) if yield
|
||||||
|
end
|
||||||
|
|
||||||
|
# Override as needed
|
||||||
|
def minimum_duration
|
||||||
|
5.seconds
|
||||||
|
end
|
||||||
|
|
||||||
|
# We intend to get rid of sleep:
|
||||||
|
# https://gitlab.com/gitlab-org/gitlab/issues/121697
|
||||||
|
module ReenqueuerSleeper
|
||||||
|
# The block will run, and then sleep until the minimum duration. Returns the
|
||||||
|
# block's return value.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
#
|
||||||
|
# ensure_minimum_duration(5.seconds) do
|
||||||
|
# # do something
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
def ensure_minimum_duration(minimum_duration)
|
||||||
|
start_time = Time.now
|
||||||
|
|
||||||
|
result = yield
|
||||||
|
|
||||||
|
sleep_if_time_left(minimum_duration, start_time)
|
||||||
|
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def sleep_if_time_left(minimum_duration, start_time)
|
||||||
|
time_left = calculate_time_left(minimum_duration, start_time)
|
||||||
|
|
||||||
|
sleep(time_left) if time_left > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_time_left(minimum_duration, start_time)
|
||||||
|
minimum_duration - elapsed_time(start_time)
|
||||||
|
end
|
||||||
|
|
||||||
|
def elapsed_time(start_time)
|
||||||
|
Time.now - start_time
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
5
changelogs/unreleased/20956-autostop-frontend.yml
Normal file
5
changelogs/unreleased/20956-autostop-frontend.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Auto stop environments after a certain period
|
||||||
|
merge_request: 20372
|
||||||
|
author:
|
||||||
|
type: added
|
6
changelogs/unreleased/27518-revoke-active-sessions.yml
Normal file
6
changelogs/unreleased/27518-revoke-active-sessions.yml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
title: Restores user's ability to revoke sessions from the active sessions
|
||||||
|
page.
|
||||||
|
merge_request: 17462
|
||||||
|
author: Jesse Hall @jessehall3
|
||||||
|
type: changed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Setup storage for multiple milestones
|
||||||
|
merge_request: 22043
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Allow an upstream pipeline to create a downstream pipeline in the same project
|
||||||
|
merge_request: 20930
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add measurement details for programming languages graph
|
||||||
|
merge_request: 20592
|
||||||
|
author:
|
||||||
|
type: changed
|
5
changelogs/unreleased/notes_api_system_filter.yml
Normal file
5
changelogs/unreleased/notes_api_system_filter.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: 25968-activity-filter-to-notes-api
|
||||||
|
merge_request: 21159
|
||||||
|
author: jhenkens
|
||||||
|
type: added
|
|
@ -0,0 +1,14 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SupportMultipleMilestonesForIssues < ActiveRecord::Migration[5.2]
|
||||||
|
DOWNTIME = false
|
||||||
|
|
||||||
|
def change
|
||||||
|
create_table :issue_milestones, id: false do |t|
|
||||||
|
t.references :issue, foreign_key: { on_delete: :cascade }, index: { unique: true }, null: false
|
||||||
|
t.references :milestone, foreign_key: { on_delete: :cascade }, index: true, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :issue_milestones, [:issue_id, :milestone_id], unique: true
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,14 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SupportMultipleMilestonesForMergeRequests < ActiveRecord::Migration[5.2]
|
||||||
|
DOWNTIME = false
|
||||||
|
|
||||||
|
def change
|
||||||
|
create_table :merge_request_milestones, id: false do |t|
|
||||||
|
t.references :merge_request, foreign_key: { on_delete: :cascade }, index: { unique: true }, null: false
|
||||||
|
t.references :milestone, foreign_key: { on_delete: :cascade }, index: true, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :merge_request_milestones, [:merge_request_id, :milestone_id], name: 'index_mrs_milestones_on_mr_id_and_milestone_id', unique: true
|
||||||
|
end
|
||||||
|
end
|
20
db/schema.rb
20
db/schema.rb
|
@ -2099,6 +2099,14 @@ ActiveRecord::Schema.define(version: 2019_12_29_140154) do
|
||||||
t.index ["issue_id"], name: "index_issue_metrics"
|
t.index ["issue_id"], name: "index_issue_metrics"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "issue_milestones", id: false, force: :cascade do |t|
|
||||||
|
t.bigint "issue_id", null: false
|
||||||
|
t.bigint "milestone_id", null: false
|
||||||
|
t.index ["issue_id", "milestone_id"], name: "index_issue_milestones_on_issue_id_and_milestone_id", unique: true
|
||||||
|
t.index ["issue_id"], name: "index_issue_milestones_on_issue_id", unique: true
|
||||||
|
t.index ["milestone_id"], name: "index_issue_milestones_on_milestone_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "issue_tracker_data", force: :cascade do |t|
|
create_table "issue_tracker_data", force: :cascade do |t|
|
||||||
t.integer "service_id", null: false
|
t.integer "service_id", null: false
|
||||||
t.datetime_with_timezone "created_at", null: false
|
t.datetime_with_timezone "created_at", null: false
|
||||||
|
@ -2486,6 +2494,14 @@ ActiveRecord::Schema.define(version: 2019_12_29_140154) do
|
||||||
t.index ["pipeline_id"], name: "index_merge_request_metrics_on_pipeline_id"
|
t.index ["pipeline_id"], name: "index_merge_request_metrics_on_pipeline_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "merge_request_milestones", id: false, force: :cascade do |t|
|
||||||
|
t.bigint "merge_request_id", null: false
|
||||||
|
t.bigint "milestone_id", null: false
|
||||||
|
t.index ["merge_request_id", "milestone_id"], name: "index_mrs_milestones_on_mr_id_and_milestone_id", unique: true
|
||||||
|
t.index ["merge_request_id"], name: "index_merge_request_milestones_on_merge_request_id", unique: true
|
||||||
|
t.index ["milestone_id"], name: "index_merge_request_milestones_on_milestone_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "merge_request_user_mentions", force: :cascade do |t|
|
create_table "merge_request_user_mentions", force: :cascade do |t|
|
||||||
t.integer "merge_request_id", null: false
|
t.integer "merge_request_id", null: false
|
||||||
t.integer "note_id"
|
t.integer "note_id"
|
||||||
|
@ -4595,6 +4611,8 @@ ActiveRecord::Schema.define(version: 2019_12_29_140154) do
|
||||||
add_foreign_key "issue_links", "issues", column: "source_id", name: "fk_c900194ff2", on_delete: :cascade
|
add_foreign_key "issue_links", "issues", column: "source_id", name: "fk_c900194ff2", on_delete: :cascade
|
||||||
add_foreign_key "issue_links", "issues", column: "target_id", name: "fk_e71bb44f1f", on_delete: :cascade
|
add_foreign_key "issue_links", "issues", column: "target_id", name: "fk_e71bb44f1f", on_delete: :cascade
|
||||||
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
|
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
|
||||||
|
add_foreign_key "issue_milestones", "issues", on_delete: :cascade
|
||||||
|
add_foreign_key "issue_milestones", "milestones", on_delete: :cascade
|
||||||
add_foreign_key "issue_tracker_data", "services", on_delete: :cascade
|
add_foreign_key "issue_tracker_data", "services", on_delete: :cascade
|
||||||
add_foreign_key "issue_user_mentions", "issues", on_delete: :cascade
|
add_foreign_key "issue_user_mentions", "issues", on_delete: :cascade
|
||||||
add_foreign_key "issue_user_mentions", "notes", on_delete: :cascade
|
add_foreign_key "issue_user_mentions", "notes", on_delete: :cascade
|
||||||
|
@ -4638,6 +4656,8 @@ ActiveRecord::Schema.define(version: 2019_12_29_140154) do
|
||||||
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
|
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
|
||||||
add_foreign_key "merge_request_metrics", "users", column: "latest_closed_by_id", name: "fk_ae440388cc", on_delete: :nullify
|
add_foreign_key "merge_request_metrics", "users", column: "latest_closed_by_id", name: "fk_ae440388cc", on_delete: :nullify
|
||||||
add_foreign_key "merge_request_metrics", "users", column: "merged_by_id", name: "fk_7f28d925f3", on_delete: :nullify
|
add_foreign_key "merge_request_metrics", "users", column: "merged_by_id", name: "fk_7f28d925f3", on_delete: :nullify
|
||||||
|
add_foreign_key "merge_request_milestones", "merge_requests", on_delete: :cascade
|
||||||
|
add_foreign_key "merge_request_milestones", "milestones", on_delete: :cascade
|
||||||
add_foreign_key "merge_request_user_mentions", "merge_requests", on_delete: :cascade
|
add_foreign_key "merge_request_user_mentions", "merge_requests", on_delete: :cascade
|
||||||
add_foreign_key "merge_request_user_mentions", "notes", on_delete: :cascade
|
add_foreign_key "merge_request_user_mentions", "notes", on_delete: :cascade
|
||||||
add_foreign_key "merge_requests", "ci_pipelines", column: "head_pipeline_id", name: "fk_fd82eae0b9", on_delete: :nullify
|
add_foreign_key "merge_requests", "ci_pipelines", column: "head_pipeline_id", name: "fk_fd82eae0b9", on_delete: :nullify
|
||||||
|
|
|
@ -2665,7 +2665,7 @@ the currently running/pending `deploy-to-production` job is finished. As a resul
|
||||||
you can ensure that concurrent deployments will never happen to the production environment.
|
you can ensure that concurrent deployments will never happen to the production environment.
|
||||||
|
|
||||||
There can be multiple `resource_group`s defined per environment. A good use case for this
|
There can be multiple `resource_group`s defined per environment. A good use case for this
|
||||||
is when deploying to physical devices. You may have more than one phyisical device, and each
|
is when deploying to physical devices. You may have more than one physical device, and each
|
||||||
one can be deployed to, but there can be only one deployment per device at any given time.
|
one can be deployed to, but there can be only one deployment per device at any given time.
|
||||||
|
|
||||||
### `include`
|
### `include`
|
||||||
|
|
|
@ -24,6 +24,11 @@ review the sessions, and revoke any you don't recognize.
|
||||||
GitLab allows users to have up to 100 active sessions at once. If the number of active sessions
|
GitLab allows users to have up to 100 active sessions at once. If the number of active sessions
|
||||||
exceeds 100, the oldest ones are deleted.
|
exceeds 100, the oldest ones are deleted.
|
||||||
|
|
||||||
|
## Revoking a session
|
||||||
|
|
||||||
|
1. Use the previous steps to navigate to **Active Sessions**.
|
||||||
|
1. Click on **Revoke** besides a session. The current session cannot be revoked, as this would sign you out of GitLab.
|
||||||
|
|
||||||
<!-- ## Troubleshooting
|
<!-- ## Troubleshooting
|
||||||
|
|
||||||
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
|
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 22 KiB |
|
@ -24,6 +24,8 @@ module API
|
||||||
desc: 'Return notes ordered by `created_at` or `updated_at` fields.'
|
desc: 'Return notes ordered by `created_at` or `updated_at` fields.'
|
||||||
optional :sort, type: String, values: %w[asc desc], default: 'desc',
|
optional :sort, type: String, values: %w[asc desc], default: 'desc',
|
||||||
desc: 'Return notes sorted in `asc` or `desc` order.'
|
desc: 'Return notes sorted in `asc` or `desc` order.'
|
||||||
|
optional :activity_filter, type: String, values: UserPreference::NOTES_FILTERS.stringify_keys.keys, default: 'all_notes',
|
||||||
|
desc: 'The type of notables which are returned.'
|
||||||
use :pagination
|
use :pagination
|
||||||
end
|
end
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
|
@ -35,7 +37,8 @@ module API
|
||||||
# at the DB query level (which we cannot in that case), the current
|
# at the DB query level (which we cannot in that case), the current
|
||||||
# page can have less elements than :per_page even if
|
# page can have less elements than :per_page even if
|
||||||
# there's more than one page.
|
# there's more than one page.
|
||||||
raw_notes = noteable.notes.with_metadata.reorder(order_options_with_tie_breaker)
|
notes_filter = UserPreference::NOTES_FILTERS[params[:activity_filter].to_sym]
|
||||||
|
raw_notes = noteable.notes.with_metadata.with_notes_filter(notes_filter).reorder(order_options_with_tie_breaker)
|
||||||
|
|
||||||
# paginate() only works with a relation. This could lead to a
|
# paginate() only works with a relation. This could lead to a
|
||||||
# mismatch between the pagination headers info and the actual notes
|
# mismatch between the pagination headers info and the actual notes
|
||||||
|
|
|
@ -10,7 +10,7 @@ module Gitlab
|
||||||
:trigger_request, :schedule, :merge_request, :external_pull_request,
|
:trigger_request, :schedule, :merge_request, :external_pull_request,
|
||||||
:ignore_skip_ci, :save_incompleted,
|
:ignore_skip_ci, :save_incompleted,
|
||||||
:seeds_block, :variables_attributes, :push_options,
|
:seeds_block, :variables_attributes, :push_options,
|
||||||
:chat_data, :allow_mirror_update,
|
:chat_data, :allow_mirror_update, :bridge,
|
||||||
# These attributes are set by Chains during processing:
|
# These attributes are set by Chains during processing:
|
||||||
:config_content, :config_processor, :stage_seeds
|
:config_content, :config_processor, :stage_seeds
|
||||||
) do
|
) do
|
||||||
|
|
|
@ -9,7 +9,7 @@ module Gitlab
|
||||||
include Chain::Helpers
|
include Chain::Helpers
|
||||||
|
|
||||||
SOURCES = [
|
SOURCES = [
|
||||||
Gitlab::Ci::Pipeline::Chain::Config::Content::Runtime,
|
Gitlab::Ci::Pipeline::Chain::Config::Content::Bridge,
|
||||||
Gitlab::Ci::Pipeline::Chain::Config::Content::Repository,
|
Gitlab::Ci::Pipeline::Chain::Config::Content::Repository,
|
||||||
Gitlab::Ci::Pipeline::Chain::Config::Content::ExternalProject,
|
Gitlab::Ci::Pipeline::Chain::Config::Content::ExternalProject,
|
||||||
Gitlab::Ci::Pipeline::Chain::Config::Content::Remote,
|
Gitlab::Ci::Pipeline::Chain::Config::Content::Remote,
|
||||||
|
@ -17,7 +17,7 @@ module Gitlab
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
LEGACY_SOURCES = [
|
LEGACY_SOURCES = [
|
||||||
Gitlab::Ci::Pipeline::Chain::Config::Content::Runtime,
|
Gitlab::Ci::Pipeline::Chain::Config::Content::Bridge,
|
||||||
Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyRepository,
|
Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyRepository,
|
||||||
Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyAutoDevops
|
Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyAutoDevops
|
||||||
].freeze
|
].freeze
|
||||||
|
|
25
lib/gitlab/ci/pipeline/chain/config/content/bridge.rb
Normal file
25
lib/gitlab/ci/pipeline/chain/config/content/bridge.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Ci
|
||||||
|
module Pipeline
|
||||||
|
module Chain
|
||||||
|
module Config
|
||||||
|
class Content
|
||||||
|
class Bridge < Source
|
||||||
|
def content
|
||||||
|
return unless @command.bridge
|
||||||
|
|
||||||
|
@command.bridge.yaml_for_downstream
|
||||||
|
end
|
||||||
|
|
||||||
|
def source
|
||||||
|
:bridge_source
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,30 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Gitlab
|
|
||||||
module Ci
|
|
||||||
module Pipeline
|
|
||||||
module Chain
|
|
||||||
module Config
|
|
||||||
class Content
|
|
||||||
class Runtime < Source
|
|
||||||
def content
|
|
||||||
@command.config_content
|
|
||||||
end
|
|
||||||
|
|
||||||
def source
|
|
||||||
# The only case when this source is used is when the config content
|
|
||||||
# is passed in as parameter to Ci::CreatePipelineService.
|
|
||||||
# This would only occur with parent/child pipelines which is being
|
|
||||||
# implemented.
|
|
||||||
# TODO: change source to return :runtime_source
|
|
||||||
# https://gitlab.com/gitlab-org/gitlab/merge_requests/21041
|
|
||||||
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -24,6 +24,8 @@ tree:
|
||||||
- milestone:
|
- milestone:
|
||||||
- events:
|
- events:
|
||||||
- :push_event_payload
|
- :push_event_payload
|
||||||
|
- issue_milestones:
|
||||||
|
- :milestone
|
||||||
- resource_label_events:
|
- resource_label_events:
|
||||||
- label:
|
- label:
|
||||||
- :priorities
|
- :priorities
|
||||||
|
@ -57,6 +59,8 @@ tree:
|
||||||
- milestone:
|
- milestone:
|
||||||
- events:
|
- events:
|
||||||
- :push_event_payload
|
- :push_event_payload
|
||||||
|
- merge_request_milestones:
|
||||||
|
- :milestone
|
||||||
- resource_label_events:
|
- resource_label_events:
|
||||||
- label:
|
- label:
|
||||||
- :priorities
|
- :priorities
|
||||||
|
@ -202,6 +206,12 @@ excluded_attributes:
|
||||||
- :latest_merge_request_diff_id
|
- :latest_merge_request_diff_id
|
||||||
- :head_pipeline_id
|
- :head_pipeline_id
|
||||||
- :state_id
|
- :state_id
|
||||||
|
issue_milestones:
|
||||||
|
- :milestone_id
|
||||||
|
- :issue_id
|
||||||
|
merge_request_milestones:
|
||||||
|
- :milestone_id
|
||||||
|
- :merge_request_id
|
||||||
award_emoji:
|
award_emoji:
|
||||||
- :awardable_id
|
- :awardable_id
|
||||||
statuses:
|
statuses:
|
||||||
|
|
|
@ -2152,6 +2152,9 @@ msgstr ""
|
||||||
msgid "Are you sure? Removing this GPG key does not affect already signed commits."
|
msgid "Are you sure? Removing this GPG key does not affect already signed commits."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Are you sure? The device will be signed out of GitLab."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Are you sure? This will invalidate your registered applications and U2F devices."
|
msgid "Are you sure? This will invalidate your registered applications and U2F devices."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -6816,6 +6819,9 @@ msgstr ""
|
||||||
msgid "EnvironmentsDashboard|The environments dashboard provides a summary of each project's environments' status, including pipeline and alert statuses."
|
msgid "EnvironmentsDashboard|The environments dashboard provides a summary of each project's environments' status, including pipeline and alert statuses."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Environments|An error occurred while canceling the auto stop, please try again"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Environments|An error occurred while fetching the environments."
|
msgid "Environments|An error occurred while fetching the environments."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -6834,6 +6840,12 @@ msgstr ""
|
||||||
msgid "Environments|Are you sure you want to stop this environment?"
|
msgid "Environments|Are you sure you want to stop this environment?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Environments|Auto stop in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Environments|Auto stops %{auto_stop_time}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Environments|Commit"
|
msgid "Environments|Commit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -11121,6 +11133,9 @@ msgstr ""
|
||||||
msgid "May"
|
msgid "May"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Measured in bytes of code. Excludes generated and vendored code."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Median"
|
msgid "Median"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -13329,6 +13344,9 @@ msgstr ""
|
||||||
msgid "Prevent approval of merge requests by merge request committers"
|
msgid "Prevent approval of merge requests by merge request committers"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prevent environment from auto-stopping"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Preview"
|
msgid "Preview"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -72,3 +72,4 @@ Disallow: /*/*/protected_branches
|
||||||
Disallow: /*/*/uploads/
|
Disallow: /*/*/uploads/
|
||||||
Disallow: /*/-/group_members
|
Disallow: /*/-/group_members
|
||||||
Disallow: /*/project_members
|
Disallow: /*/project_members
|
||||||
|
Disallow: /groups/*/-/analytics
|
||||||
|
|
|
@ -84,4 +84,31 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
|
||||||
expect(page).not_to have_content('Chrome on Windows')
|
expect(page).not_to have_content('Chrome on Windows')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'User can revoke a session', :js, :redis_session_store do
|
||||||
|
Capybara::Session.new(:session1)
|
||||||
|
Capybara::Session.new(:session2)
|
||||||
|
|
||||||
|
# set an additional session in another browser
|
||||||
|
using_session :session2 do
|
||||||
|
gitlab_sign_in(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
using_session :session1 do
|
||||||
|
gitlab_sign_in(user)
|
||||||
|
visit profile_active_sessions_path
|
||||||
|
|
||||||
|
expect(page).to have_link('Revoke', count: 1)
|
||||||
|
|
||||||
|
accept_confirm { click_on 'Revoke' }
|
||||||
|
|
||||||
|
expect(page).not_to have_link('Revoke')
|
||||||
|
end
|
||||||
|
|
||||||
|
using_session :session2 do
|
||||||
|
visit profile_active_sessions_path
|
||||||
|
|
||||||
|
expect(page).to have_content('You need to sign in or sign up before continuing.')
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,6 +12,10 @@ describe 'Environment' do
|
||||||
project.add_role(user, role)
|
project.add_role(user, role)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def auto_stop_button_selector
|
||||||
|
%q{button[title="Prevent environment from auto-stopping"]}
|
||||||
|
end
|
||||||
|
|
||||||
describe 'environment details page' do
|
describe 'environment details page' do
|
||||||
let!(:environment) { create(:environment, project: project) }
|
let!(:environment) { create(:environment, project: project) }
|
||||||
let!(:permissions) { }
|
let!(:permissions) { }
|
||||||
|
@ -27,6 +31,40 @@ describe 'Environment' do
|
||||||
expect(page).to have_content(environment.name)
|
expect(page).to have_content(environment.name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'without auto-stop' do
|
||||||
|
it 'does not show auto-stop text' do
|
||||||
|
expect(page).not_to have_content('Auto stops')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not show auto-stop button' do
|
||||||
|
expect(page).not_to have_selector(auto_stop_button_selector)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with auto-stop' do
|
||||||
|
let!(:environment) { create(:environment, :will_auto_stop, name: 'staging', project: project) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
visit_environment(environment)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows auto stop info' do
|
||||||
|
expect(page).to have_content('Auto stops')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows auto stop button' do
|
||||||
|
expect(page).to have_selector(auto_stop_button_selector)
|
||||||
|
expect(page.find(auto_stop_button_selector).find(:xpath, '..')['action']).to have_content(cancel_auto_stop_project_environment_path(environment.project, environment))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows user to cancel auto stop', :js do
|
||||||
|
page.find(auto_stop_button_selector).click
|
||||||
|
wait_for_all_requests
|
||||||
|
expect(page).to have_content('Auto stop successfully canceled.')
|
||||||
|
expect(page).not_to have_selector(auto_stop_button_selector)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'without deployments' do
|
context 'without deployments' do
|
||||||
it 'does not show deployments' do
|
it 'does not show deployments' do
|
||||||
expect(page).to have_content('You don\'t have any deployments right now.')
|
expect(page).to have_content('You don\'t have any deployments right now.')
|
||||||
|
|
|
@ -64,6 +64,19 @@ describe PipelinesFinder do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when project has child pipelines' do
|
||||||
|
let!(:parent_pipeline) { create(:ci_pipeline, project: project) }
|
||||||
|
let!(:child_pipeline) { create(:ci_pipeline, project: project, source: :parent_pipeline) }
|
||||||
|
|
||||||
|
let!(:pipeline_source) do
|
||||||
|
create(:ci_sources_pipeline, pipeline: child_pipeline, source_pipeline: parent_pipeline)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filters out child pipelines and show only the parents' do
|
||||||
|
is_expected.to eq([parent_pipeline])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
HasStatus::AVAILABLE_STATUSES.each do |target|
|
HasStatus::AVAILABLE_STATUSES.each do |target|
|
||||||
context "when status is #{target}" do
|
context "when status is #{target}" do
|
||||||
let(:params) { { status: target } }
|
let(:params) { { status: target } }
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { mount } from '@vue/test-utils';
|
import { mount } from '@vue/test-utils';
|
||||||
import { format } from 'timeago.js';
|
import { format } from 'timeago.js';
|
||||||
import EnvironmentItem from '~/environments/components/environment_item.vue';
|
import EnvironmentItem from '~/environments/components/environment_item.vue';
|
||||||
|
import PinComponent from '~/environments/components/environment_pin.vue';
|
||||||
|
|
||||||
import { environment, folder, tableData } from './mock_data';
|
import { environment, folder, tableData } from './mock_data';
|
||||||
|
|
||||||
describe('Environment item', () => {
|
describe('Environment item', () => {
|
||||||
|
@ -26,6 +28,8 @@ describe('Environment item', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const findAutoStop = () => wrapper.find('.js-auto-stop');
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.destroy();
|
wrapper.destroy();
|
||||||
});
|
});
|
||||||
|
@ -77,6 +81,79 @@ describe('Environment item', () => {
|
||||||
expect(wrapper.find('.js-commit-component')).toBeDefined();
|
expect(wrapper.find('.js-commit-component')).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Without auto-stop date', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
factory({
|
||||||
|
propsData: {
|
||||||
|
model: environment,
|
||||||
|
canReadEnvironment: true,
|
||||||
|
tableData,
|
||||||
|
shouldShowAutoStopDate: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render a date', () => {
|
||||||
|
expect(findAutoStop().exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render the suto-stop button', () => {
|
||||||
|
expect(wrapper.find(PinComponent).exists()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('With auto-stop date', () => {
|
||||||
|
describe('in the future', () => {
|
||||||
|
const futureDate = new Date(Date.now() + 100000);
|
||||||
|
beforeEach(() => {
|
||||||
|
factory({
|
||||||
|
propsData: {
|
||||||
|
model: {
|
||||||
|
...environment,
|
||||||
|
auto_stop_at: futureDate,
|
||||||
|
},
|
||||||
|
canReadEnvironment: true,
|
||||||
|
tableData,
|
||||||
|
shouldShowAutoStopDate: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the date', () => {
|
||||||
|
expect(findAutoStop().text()).toContain(format(futureDate));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the auto-stop button', () => {
|
||||||
|
expect(wrapper.find(PinComponent).exists()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('in the past', () => {
|
||||||
|
const pastDate = new Date(Date.now() - 100000);
|
||||||
|
beforeEach(() => {
|
||||||
|
factory({
|
||||||
|
propsData: {
|
||||||
|
model: {
|
||||||
|
...environment,
|
||||||
|
auto_stop_at: pastDate,
|
||||||
|
},
|
||||||
|
canReadEnvironment: true,
|
||||||
|
tableData,
|
||||||
|
shouldShowAutoStopDate: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render a date', () => {
|
||||||
|
expect(findAutoStop().exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render the suto-stop button', () => {
|
||||||
|
expect(wrapper.find(PinComponent).exists()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('With manual actions', () => {
|
describe('With manual actions', () => {
|
||||||
|
|
46
spec/frontend/environments/environment_pin_spec.js
Normal file
46
spec/frontend/environments/environment_pin_spec.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { GlButton } from '@gitlab/ui';
|
||||||
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
|
import eventHub from '~/environments/event_hub';
|
||||||
|
import PinComponent from '~/environments/components/environment_pin.vue';
|
||||||
|
|
||||||
|
describe('Pin Component', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
const factory = (options = {}) => {
|
||||||
|
// This destroys any wrappers created before a nested call to factory reassigns it
|
||||||
|
if (wrapper && wrapper.destroy) {
|
||||||
|
wrapper.destroy();
|
||||||
|
}
|
||||||
|
wrapper = shallowMount(PinComponent, {
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const autoStopUrl = '/root/auto-stop-env-test/-/environments/38/cancel_auto_stop';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
factory({
|
||||||
|
propsData: {
|
||||||
|
autoStopUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the component with thumbtack icon', () => {
|
||||||
|
expect(wrapper.find(Icon).props('name')).toBe('thumbtack');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit onPinClick when clicked', () => {
|
||||||
|
const eventHubSpy = jest.spyOn(eventHub, '$emit');
|
||||||
|
const button = wrapper.find(GlButton);
|
||||||
|
|
||||||
|
button.vm.$emit('click');
|
||||||
|
|
||||||
|
expect(eventHubSpy).toHaveBeenCalledWith('cancelAutoStop', autoStopUrl);
|
||||||
|
});
|
||||||
|
});
|
|
@ -63,6 +63,7 @@ const environment = {
|
||||||
log_path: 'root/ci-folders/environments/31/logs',
|
log_path: 'root/ci-folders/environments/31/logs',
|
||||||
created_at: '2016-11-07T11:11:16.525Z',
|
created_at: '2016-11-07T11:11:16.525Z',
|
||||||
updated_at: '2016-11-10T15:55:58.778Z',
|
updated_at: '2016-11-10T15:55:58.778Z',
|
||||||
|
auto_stop_at: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const folder = {
|
const folder = {
|
||||||
|
@ -98,6 +99,10 @@ const tableData = {
|
||||||
title: 'Updated',
|
title: 'Updated',
|
||||||
spacing: 'section-10',
|
spacing: 'section-10',
|
||||||
},
|
},
|
||||||
|
autoStop: {
|
||||||
|
title: 'Auto stop in',
|
||||||
|
spacing: 'section-5',
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
spacing: 'section-25',
|
spacing: 'section-25',
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,6 +15,42 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
|
||||||
stub_feature_flags(ci_root_config_content: false)
|
stub_feature_flags(ci_root_config_content: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when bridge job is passed in as parameter' do
|
||||||
|
let(:ci_config_path) { nil }
|
||||||
|
let(:bridge) { create(:ci_bridge) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
command.bridge = bridge
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when bridge job has downstream yaml' do
|
||||||
|
before do
|
||||||
|
allow(bridge).to receive(:yaml_for_downstream).and_return('the-yaml')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the content already available in command' do
|
||||||
|
subject.perform!
|
||||||
|
|
||||||
|
expect(pipeline.config_source).to eq 'bridge_source'
|
||||||
|
expect(command.config_content).to eq 'the-yaml'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when bridge job does not have downstream yaml' do
|
||||||
|
before do
|
||||||
|
allow(bridge).to receive(:yaml_for_downstream).and_return(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the next available source' do
|
||||||
|
subject.perform!
|
||||||
|
|
||||||
|
expect(pipeline.config_source).to eq 'auto_devops_source'
|
||||||
|
template = Gitlab::Template::GitlabCiYmlTemplate.find('Beta/Auto-DevOps')
|
||||||
|
expect(command.config_content).to eq(template.content)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when config is defined in a custom path in the repository' do
|
context 'when config is defined in a custom path in the repository' do
|
||||||
let(:ci_config_path) { 'path/to/config.yml' }
|
let(:ci_config_path) { 'path/to/config.yml' }
|
||||||
|
|
||||||
|
@ -135,6 +171,23 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when bridge job is passed in as parameter' do
|
||||||
|
let(:ci_config_path) { nil }
|
||||||
|
let(:bridge) { create(:ci_bridge) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
command.bridge = bridge
|
||||||
|
allow(bridge).to receive(:yaml_for_downstream).and_return('the-yaml')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the content already available in command' do
|
||||||
|
subject.perform!
|
||||||
|
|
||||||
|
expect(pipeline.config_source).to eq 'bridge_source'
|
||||||
|
expect(command.config_content).to eq 'the-yaml'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when config is defined in a custom path in the repository' do
|
context 'when config is defined in a custom path in the repository' do
|
||||||
let(:ci_config_path) { 'path/to/config.yml' }
|
let(:ci_config_path) { 'path/to/config.yml' }
|
||||||
let(:config_content_result) do
|
let(:config_content_result) do
|
||||||
|
|
|
@ -6,6 +6,8 @@ issues:
|
||||||
- assignees
|
- assignees
|
||||||
- updated_by
|
- updated_by
|
||||||
- milestone
|
- milestone
|
||||||
|
- issue_milestones
|
||||||
|
- milestones
|
||||||
- notes
|
- notes
|
||||||
- resource_label_events
|
- resource_label_events
|
||||||
- resource_weight_events
|
- resource_weight_events
|
||||||
|
@ -78,6 +80,8 @@ milestone:
|
||||||
- boards
|
- boards
|
||||||
- milestone_releases
|
- milestone_releases
|
||||||
- releases
|
- releases
|
||||||
|
- issue_milestones
|
||||||
|
- merge_request_milestones
|
||||||
snippets:
|
snippets:
|
||||||
- author
|
- author
|
||||||
- project
|
- project
|
||||||
|
@ -106,6 +110,8 @@ merge_requests:
|
||||||
- assignee
|
- assignee
|
||||||
- updated_by
|
- updated_by
|
||||||
- milestone
|
- milestone
|
||||||
|
- merge_request_milestones
|
||||||
|
- milestones
|
||||||
- notes
|
- notes
|
||||||
- resource_label_events
|
- resource_label_events
|
||||||
- label_links
|
- label_links
|
||||||
|
@ -146,6 +152,12 @@ merge_requests:
|
||||||
- deployment_merge_requests
|
- deployment_merge_requests
|
||||||
- deployments
|
- deployments
|
||||||
- user_mentions
|
- user_mentions
|
||||||
|
issue_milestones:
|
||||||
|
- milestone
|
||||||
|
- issue
|
||||||
|
merge_request_milestones:
|
||||||
|
- milestone
|
||||||
|
- merge_request
|
||||||
external_pull_requests:
|
external_pull_requests:
|
||||||
- project
|
- project
|
||||||
merge_request_diff:
|
merge_request_diff:
|
||||||
|
@ -189,6 +201,8 @@ ci_pipelines:
|
||||||
- sourced_pipelines
|
- sourced_pipelines
|
||||||
- triggered_by_pipeline
|
- triggered_by_pipeline
|
||||||
- triggered_pipelines
|
- triggered_pipelines
|
||||||
|
- child_pipelines
|
||||||
|
- parent_pipeline
|
||||||
- downstream_bridges
|
- downstream_bridges
|
||||||
- job_artifacts
|
- job_artifacts
|
||||||
- vulnerabilities_occurrence_pipelines
|
- vulnerabilities_occurrence_pipelines
|
||||||
|
|
|
@ -44,6 +44,19 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#public_id' do
|
||||||
|
it 'returns an encrypted, url-encoded session id' do
|
||||||
|
original_session_id = "!*'();:@&\n=+$,/?%abcd#123[4567]8"
|
||||||
|
active_session = ActiveSession.new(session_id: original_session_id)
|
||||||
|
encrypted_encoded_id = active_session.public_id
|
||||||
|
|
||||||
|
encrypted_id = CGI.unescape(encrypted_encoded_id)
|
||||||
|
derived_session_id = Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_id)
|
||||||
|
|
||||||
|
expect(original_session_id).to eq derived_session_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '.list' do
|
describe '.list' do
|
||||||
it 'returns all sessions by user' do
|
it 'returns all sessions by user' do
|
||||||
Gitlab::Redis::SharedState.with do |redis|
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
|
@ -173,8 +186,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
|
||||||
device_name: 'iPhone 6',
|
device_name: 'iPhone 6',
|
||||||
device_type: 'smartphone',
|
device_type: 'smartphone',
|
||||||
created_at: Time.zone.parse('2018-03-12 09:06'),
|
created_at: Time.zone.parse('2018-03-12 09:06'),
|
||||||
updated_at: Time.zone.parse('2018-03-12 09:06'),
|
updated_at: Time.zone.parse('2018-03-12 09:06')
|
||||||
session_id: '6919a6f1bb119dd7396fadc38fd18d0d'
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -244,6 +256,40 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.destroy_with_public_id' do
|
||||||
|
it 'receives a user and public id and destroys the associated session' do
|
||||||
|
ActiveSession.set(user, request)
|
||||||
|
session = ActiveSession.list(user).first
|
||||||
|
|
||||||
|
ActiveSession.destroy_with_public_id(user, session.public_id)
|
||||||
|
|
||||||
|
total_sessions = ActiveSession.list(user).count
|
||||||
|
expect(total_sessions).to eq 0
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles invalid input for public id' do
|
||||||
|
expect do
|
||||||
|
ActiveSession.destroy_with_public_id(user, nil)
|
||||||
|
end.not_to raise_error
|
||||||
|
|
||||||
|
expect do
|
||||||
|
ActiveSession.destroy_with_public_id(user, "")
|
||||||
|
end.not_to raise_error
|
||||||
|
|
||||||
|
expect do
|
||||||
|
ActiveSession.destroy_with_public_id(user, "aaaaaaaa")
|
||||||
|
end.not_to raise_error
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not attempt to destroy session when given invalid input for public id' do
|
||||||
|
expect(ActiveSession).not_to receive(:destroy)
|
||||||
|
|
||||||
|
ActiveSession.destroy_with_public_id(user, nil)
|
||||||
|
ActiveSession.destroy_with_public_id(user, "")
|
||||||
|
ActiveSession.destroy_with_public_id(user, "aaaaaaaa")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '.cleanup' do
|
describe '.cleanup' do
|
||||||
before do
|
before do
|
||||||
stub_const("ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS", 5)
|
stub_const("ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS", 5)
|
||||||
|
|
|
@ -2716,4 +2716,114 @@ describe Ci::Pipeline, :mailer do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#parent_pipeline' do
|
||||||
|
let(:project) { create(:project) }
|
||||||
|
let(:pipeline) { create(:ci_pipeline, project: project) }
|
||||||
|
|
||||||
|
context 'when pipeline is triggered by a pipeline from the same project' do
|
||||||
|
let(:upstream_pipeline) { create(:ci_pipeline, project: pipeline.project) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:ci_sources_pipeline,
|
||||||
|
source_pipeline: upstream_pipeline,
|
||||||
|
source_project: project,
|
||||||
|
pipeline: pipeline,
|
||||||
|
project: project)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the parent pipeline' do
|
||||||
|
expect(pipeline.parent_pipeline).to eq(upstream_pipeline)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is child' do
|
||||||
|
expect(pipeline).to be_child
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when pipeline is triggered by a pipeline from another project' do
|
||||||
|
let(:upstream_pipeline) { create(:ci_pipeline) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:ci_sources_pipeline,
|
||||||
|
source_pipeline: upstream_pipeline,
|
||||||
|
source_project: upstream_pipeline.project,
|
||||||
|
pipeline: pipeline,
|
||||||
|
project: project)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nil' do
|
||||||
|
expect(pipeline.parent_pipeline).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is not child' do
|
||||||
|
expect(pipeline).not_to be_child
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when pipeline is not triggered by a pipeline' do
|
||||||
|
it 'returns nil' do
|
||||||
|
expect(pipeline.parent_pipeline).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is not child' do
|
||||||
|
expect(pipeline).not_to be_child
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#child_pipelines' do
|
||||||
|
let(:project) { create(:project) }
|
||||||
|
let(:pipeline) { create(:ci_pipeline, project: project) }
|
||||||
|
|
||||||
|
context 'when pipeline triggered other pipelines on same project' do
|
||||||
|
let(:downstream_pipeline) { create(:ci_pipeline, project: pipeline.project) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:ci_sources_pipeline,
|
||||||
|
source_pipeline: pipeline,
|
||||||
|
source_project: pipeline.project,
|
||||||
|
pipeline: downstream_pipeline,
|
||||||
|
project: pipeline.project)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the child pipelines' do
|
||||||
|
expect(pipeline.child_pipelines).to eq [downstream_pipeline]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is parent' do
|
||||||
|
expect(pipeline).to be_parent
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when pipeline triggered other pipelines on another project' do
|
||||||
|
let(:downstream_pipeline) { create(:ci_pipeline) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:ci_sources_pipeline,
|
||||||
|
source_pipeline: pipeline,
|
||||||
|
source_project: pipeline.project,
|
||||||
|
pipeline: downstream_pipeline,
|
||||||
|
project: downstream_pipeline.project)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns empty array' do
|
||||||
|
expect(pipeline.child_pipelines).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is not parent' do
|
||||||
|
expect(pipeline).not_to be_parent
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when pipeline did not trigger any pipelines' do
|
||||||
|
it 'returns empty array' do
|
||||||
|
expect(pipeline.child_pipelines).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is not parent' do
|
||||||
|
expect(pipeline).not_to be_parent
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -53,43 +53,6 @@ describe Issuable do
|
||||||
it_behaves_like 'validates description length with custom validation'
|
it_behaves_like 'validates description length with custom validation'
|
||||||
it_behaves_like 'truncates the description to its allowed maximum length on import'
|
it_behaves_like 'truncates the description to its allowed maximum length on import'
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'milestone' do
|
|
||||||
let(:project) { create(:project) }
|
|
||||||
let(:milestone_id) { create(:milestone, project: project).id }
|
|
||||||
let(:params) do
|
|
||||||
{
|
|
||||||
title: 'something',
|
|
||||||
project: project,
|
|
||||||
author: build(:user),
|
|
||||||
milestone_id: milestone_id
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
subject { issuable_class.new(params) }
|
|
||||||
|
|
||||||
context 'with correct params' do
|
|
||||||
it { is_expected.to be_valid }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with empty string milestone' do
|
|
||||||
let(:milestone_id) { '' }
|
|
||||||
|
|
||||||
it { is_expected.to be_valid }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with nil milestone id' do
|
|
||||||
let(:milestone_id) { nil }
|
|
||||||
|
|
||||||
it { is_expected.to be_valid }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with a milestone id from another project' do
|
|
||||||
let(:milestone_id) { create(:milestone).id }
|
|
||||||
|
|
||||||
it { is_expected.to be_invalid }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "Scope" do
|
describe "Scope" do
|
||||||
|
@ -141,48 +104,6 @@ describe Issuable do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#milestone_available?' do
|
|
||||||
let(:group) { create(:group) }
|
|
||||||
let(:project) { create(:project, group: group) }
|
|
||||||
let(:issue) { create(:issue, project: project) }
|
|
||||||
|
|
||||||
def build_issuable(milestone_id)
|
|
||||||
issuable_class.new(project: project, milestone_id: milestone_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true with a milestone from the issue project' do
|
|
||||||
milestone = create(:milestone, project: project)
|
|
||||||
|
|
||||||
expect(build_issuable(milestone.id).milestone_available?).to be_truthy
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true with a milestone from the issue project group' do
|
|
||||||
milestone = create(:milestone, group: group)
|
|
||||||
|
|
||||||
expect(build_issuable(milestone.id).milestone_available?).to be_truthy
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true with a milestone from the the parent of the issue project group' do
|
|
||||||
parent = create(:group)
|
|
||||||
group.update(parent: parent)
|
|
||||||
milestone = create(:milestone, group: parent)
|
|
||||||
|
|
||||||
expect(build_issuable(milestone.id).milestone_available?).to be_truthy
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false with a milestone from another project' do
|
|
||||||
milestone = create(:milestone)
|
|
||||||
|
|
||||||
expect(build_issuable(milestone.id).milestone_available?).to be_falsey
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false with a milestone from another group' do
|
|
||||||
milestone = create(:milestone, group: create(:group))
|
|
||||||
|
|
||||||
expect(build_issuable(milestone.id).milestone_available?).to be_falsey
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe ".search" do
|
describe ".search" do
|
||||||
let!(:searchable_issue) { create(:issue, title: "Searchable awesome issue") }
|
let!(:searchable_issue) { create(:issue, title: "Searchable awesome issue") }
|
||||||
let!(:searchable_issue2) { create(:issue, title: 'Aw') }
|
let!(:searchable_issue2) { create(:issue, title: 'Aw') }
|
||||||
|
@ -809,27 +730,6 @@ describe Issuable do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#supports_milestone?' do
|
|
||||||
let(:group) { create(:group) }
|
|
||||||
let(:project) { create(:project, group: group) }
|
|
||||||
|
|
||||||
context "for issues" do
|
|
||||||
let(:issue) { build(:issue, project: project) }
|
|
||||||
|
|
||||||
it 'returns true' do
|
|
||||||
expect(issue.supports_milestone?).to be_truthy
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "for merge requests" do
|
|
||||||
let(:merge_request) { build(:merge_request, target_project: project, source_project: project) }
|
|
||||||
|
|
||||||
it 'returns true' do
|
|
||||||
expect(merge_request.supports_milestone?).to be_truthy
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#matches_cross_reference_regex?' do
|
describe '#matches_cross_reference_regex?' do
|
||||||
context "issue description with long path string" do
|
context "issue description with long path string" do
|
||||||
let(:mentionable) { build(:issue, description: "/a" * 50000) }
|
let(:mentionable) { build(:issue, description: "/a" * 50000) }
|
||||||
|
@ -854,91 +754,4 @@ describe Issuable do
|
||||||
it_behaves_like 'matches_cross_reference_regex? fails fast'
|
it_behaves_like 'matches_cross_reference_regex? fails fast'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'release scopes' do
|
|
||||||
let_it_be(:project) { create(:project) }
|
|
||||||
let(:forked_project) { fork_project(project) }
|
|
||||||
|
|
||||||
let_it_be(:release_1) { create(:release, tag: 'v1.0', project: project) }
|
|
||||||
let_it_be(:release_2) { create(:release, tag: 'v2.0', project: project) }
|
|
||||||
let_it_be(:release_3) { create(:release, tag: 'v3.0', project: project) }
|
|
||||||
let_it_be(:release_4) { create(:release, tag: 'v4.0', project: project) }
|
|
||||||
|
|
||||||
let_it_be(:milestone_1) { create(:milestone, releases: [release_1], title: 'm1', project: project) }
|
|
||||||
let_it_be(:milestone_2) { create(:milestone, releases: [release_1, release_2], title: 'm2', project: project) }
|
|
||||||
let_it_be(:milestone_3) { create(:milestone, releases: [release_2, release_4], title: 'm3', project: project) }
|
|
||||||
let_it_be(:milestone_4) { create(:milestone, releases: [release_3], title: 'm4', project: project) }
|
|
||||||
let_it_be(:milestone_5) { create(:milestone, releases: [release_3], title: 'm5', project: project) }
|
|
||||||
let_it_be(:milestone_6) { create(:milestone, title: 'm6', project: project) }
|
|
||||||
|
|
||||||
let_it_be(:issue_1) { create(:issue, milestone: milestone_1, project: project) }
|
|
||||||
let_it_be(:issue_2) { create(:issue, milestone: milestone_1, project: project) }
|
|
||||||
let_it_be(:issue_3) { create(:issue, milestone: milestone_2, project: project) }
|
|
||||||
let_it_be(:issue_4) { create(:issue, milestone: milestone_5, project: project) }
|
|
||||||
let_it_be(:issue_5) { create(:issue, milestone: milestone_6, project: project) }
|
|
||||||
let_it_be(:issue_6) { create(:issue, project: project) }
|
|
||||||
|
|
||||||
let(:mr_1) { create(:merge_request, milestone: milestone_1, target_project: project, source_project: project) }
|
|
||||||
let(:mr_2) { create(:merge_request, milestone: milestone_3, target_project: project, source_project: forked_project) }
|
|
||||||
let(:mr_3) { create(:merge_request, source_project: project) }
|
|
||||||
|
|
||||||
let_it_be(:issue_items) { Issue.all }
|
|
||||||
let(:mr_items) { MergeRequest.all }
|
|
||||||
|
|
||||||
describe '#without_release' do
|
|
||||||
it 'returns the issues or mrs not tied to any milestone and the ones tied to milestone with no release' do
|
|
||||||
expect(issue_items.without_release).to contain_exactly(issue_5, issue_6)
|
|
||||||
expect(mr_items.without_release).to contain_exactly(mr_3)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#any_release' do
|
|
||||||
it 'returns all issues or all mrs tied to a release' do
|
|
||||||
expect(issue_items.any_release).to contain_exactly(issue_1, issue_2, issue_3, issue_4)
|
|
||||||
expect(mr_items.any_release).to contain_exactly(mr_1, mr_2)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#with_release' do
|
|
||||||
it 'returns the issues tied to a specfic release' do
|
|
||||||
expect(issue_items.with_release('v1.0', project.id)).to contain_exactly(issue_1, issue_2, issue_3)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns the mrs tied to a specific release' do
|
|
||||||
expect(mr_items.with_release('v1.0', project.id)).to contain_exactly(mr_1)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when a release has a milestone with one issue and another one with no issue' do
|
|
||||||
it 'returns that one issue' do
|
|
||||||
expect(issue_items.with_release('v2.0', project.id)).to contain_exactly(issue_3)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the milestone with no issue is added as a filter' do
|
|
||||||
it 'returns an empty list' do
|
|
||||||
expect(issue_items.with_release('v2.0', project.id).with_milestone('m3')).to be_empty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the milestone with the issue is added as a filter' do
|
|
||||||
it 'returns this issue' do
|
|
||||||
expect(issue_items.with_release('v2.0', project.id).with_milestone('m2')).to contain_exactly(issue_3)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when there is no issue or mr under a specific release' do
|
|
||||||
it 'returns no issue or no mr' do
|
|
||||||
expect(issue_items.with_release('v4.0', project.id)).to be_empty
|
|
||||||
expect(mr_items.with_release('v4.0', project.id)).to be_empty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when a non-existent release tag is passed in' do
|
|
||||||
it 'returns no issue or no mr' do
|
|
||||||
expect(issue_items.with_release('v999.0', project.id)).to be_empty
|
|
||||||
expect(mr_items.with_release('v999.0', project.id)).to be_empty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
243
spec/models/concerns/milestoneable_spec.rb
Normal file
243
spec/models/concerns/milestoneable_spec.rb
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Milestoneable do
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let(:milestone) { create(:milestone, project: project) }
|
||||||
|
|
||||||
|
shared_examples_for 'an object that can be assigned a milestone' do
|
||||||
|
describe 'Validation' do
|
||||||
|
describe 'milestone' do
|
||||||
|
let(:project) { create(:project, :repository) }
|
||||||
|
let(:milestone_id) { milestone.id }
|
||||||
|
|
||||||
|
subject { milestoneable_class.new(params) }
|
||||||
|
|
||||||
|
context 'with correct params' do
|
||||||
|
it { is_expected.to be_valid }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with empty string milestone' do
|
||||||
|
let(:milestone_id) { '' }
|
||||||
|
|
||||||
|
it { is_expected.to be_valid }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with nil milestone id' do
|
||||||
|
let(:milestone_id) { nil }
|
||||||
|
|
||||||
|
it { is_expected.to be_valid }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a milestone id from another project' do
|
||||||
|
let(:milestone_id) { create(:milestone).id }
|
||||||
|
|
||||||
|
it { is_expected.to be_invalid }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when valid and saving' do
|
||||||
|
it 'copies the value to the new milestones relationship' do
|
||||||
|
subject.save!
|
||||||
|
|
||||||
|
expect(subject.milestones).to match_array([milestone])
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with old values in milestones relationship' do
|
||||||
|
let(:old_milestone) { create(:milestone, project: project) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject.milestone = old_milestone
|
||||||
|
subject.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'replaces old values' do
|
||||||
|
expect(subject.milestones).to match_array([old_milestone])
|
||||||
|
|
||||||
|
subject.milestone = milestone
|
||||||
|
subject.save!
|
||||||
|
|
||||||
|
expect(subject.milestones).to match_array([milestone])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can nullify the milestone' do
|
||||||
|
expect(subject.milestones).to match_array([old_milestone])
|
||||||
|
|
||||||
|
subject.milestone = nil
|
||||||
|
subject.save!
|
||||||
|
|
||||||
|
expect(subject.milestones).to match_array([])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#milestone_available?' do
|
||||||
|
let(:group) { create(:group) }
|
||||||
|
let(:project) { create(:project, group: group) }
|
||||||
|
let(:issue) { create(:issue, project: project) }
|
||||||
|
|
||||||
|
def build_milestoneable(milestone_id)
|
||||||
|
milestoneable_class.new(project: project, milestone_id: milestone_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true with a milestone from the issue project' do
|
||||||
|
milestone = create(:milestone, project: project)
|
||||||
|
|
||||||
|
expect(build_milestoneable(milestone.id).milestone_available?).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true with a milestone from the issue project group' do
|
||||||
|
milestone = create(:milestone, group: group)
|
||||||
|
|
||||||
|
expect(build_milestoneable(milestone.id).milestone_available?).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true with a milestone from the the parent of the issue project group' do
|
||||||
|
parent = create(:group)
|
||||||
|
group.update(parent: parent)
|
||||||
|
milestone = create(:milestone, group: parent)
|
||||||
|
|
||||||
|
expect(build_milestoneable(milestone.id).milestone_available?).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false with a milestone from another project' do
|
||||||
|
milestone = create(:milestone)
|
||||||
|
|
||||||
|
expect(build_milestoneable(milestone.id).milestone_available?).to be_falsey
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false with a milestone from another group' do
|
||||||
|
milestone = create(:milestone, group: create(:group))
|
||||||
|
|
||||||
|
expect(build_milestoneable(milestone.id).milestone_available?).to be_falsey
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#supports_milestone?' do
|
||||||
|
let(:group) { create(:group) }
|
||||||
|
let(:project) { create(:project, group: group) }
|
||||||
|
|
||||||
|
context "for issues" do
|
||||||
|
let(:issue) { build(:issue, project: project) }
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(issue.supports_milestone?).to be_truthy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "for merge requests" do
|
||||||
|
let(:merge_request) { build(:merge_request, target_project: project, source_project: project) }
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(merge_request.supports_milestone?).to be_truthy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'release scopes' do
|
||||||
|
let_it_be(:project) { create(:project) }
|
||||||
|
|
||||||
|
let_it_be(:release_1) { create(:release, tag: 'v1.0', project: project) }
|
||||||
|
let_it_be(:release_2) { create(:release, tag: 'v2.0', project: project) }
|
||||||
|
let_it_be(:release_3) { create(:release, tag: 'v3.0', project: project) }
|
||||||
|
let_it_be(:release_4) { create(:release, tag: 'v4.0', project: project) }
|
||||||
|
|
||||||
|
let_it_be(:milestone_1) { create(:milestone, releases: [release_1], title: 'm1', project: project) }
|
||||||
|
let_it_be(:milestone_2) { create(:milestone, releases: [release_1, release_2], title: 'm2', project: project) }
|
||||||
|
let_it_be(:milestone_3) { create(:milestone, releases: [release_2, release_4], title: 'm3', project: project) }
|
||||||
|
let_it_be(:milestone_4) { create(:milestone, releases: [release_3], title: 'm4', project: project) }
|
||||||
|
let_it_be(:milestone_5) { create(:milestone, releases: [release_3], title: 'm5', project: project) }
|
||||||
|
let_it_be(:milestone_6) { create(:milestone, title: 'm6', project: project) }
|
||||||
|
|
||||||
|
let_it_be(:issue_1) { create(:issue, milestone: milestone_1, project: project) }
|
||||||
|
let_it_be(:issue_2) { create(:issue, milestone: milestone_1, project: project) }
|
||||||
|
let_it_be(:issue_3) { create(:issue, milestone: milestone_2, project: project) }
|
||||||
|
let_it_be(:issue_4) { create(:issue, milestone: milestone_5, project: project) }
|
||||||
|
let_it_be(:issue_5) { create(:issue, milestone: milestone_6, project: project) }
|
||||||
|
let_it_be(:issue_6) { create(:issue, project: project) }
|
||||||
|
|
||||||
|
let_it_be(:items) { Issue.all }
|
||||||
|
|
||||||
|
describe '#without_release' do
|
||||||
|
it 'returns the issues not tied to any milestone and the ones tied to milestone with no release' do
|
||||||
|
expect(items.without_release).to contain_exactly(issue_5, issue_6)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#any_release' do
|
||||||
|
it 'returns all issues tied to a release' do
|
||||||
|
expect(items.any_release).to contain_exactly(issue_1, issue_2, issue_3, issue_4)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#with_release' do
|
||||||
|
it 'returns the issues tied a specfic release' do
|
||||||
|
expect(items.with_release('v1.0', project.id)).to contain_exactly(issue_1, issue_2, issue_3)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a release has a milestone with one issue and another one with no issue' do
|
||||||
|
it 'returns that one issue' do
|
||||||
|
expect(items.with_release('v2.0', project.id)).to contain_exactly(issue_3)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the milestone with no issue is added as a filter' do
|
||||||
|
it 'returns an empty list' do
|
||||||
|
expect(items.with_release('v2.0', project.id).with_milestone('m3')).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the milestone with the issue is added as a filter' do
|
||||||
|
it 'returns this issue' do
|
||||||
|
expect(items.with_release('v2.0', project.id).with_milestone('m2')).to contain_exactly(issue_3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there is no issue under a specific release' do
|
||||||
|
it 'returns no issue' do
|
||||||
|
expect(items.with_release('v4.0', project.id)).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a non-existent release tag is passed in' do
|
||||||
|
it 'returns no issue' do
|
||||||
|
expect(items.with_release('v999.0', project.id)).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'Issues' do
|
||||||
|
let(:milestoneable_class) { Issue }
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
title: 'something',
|
||||||
|
project: project,
|
||||||
|
author: user,
|
||||||
|
milestone_id: milestone_id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'an object that can be assigned a milestone'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'MergeRequests' do
|
||||||
|
let(:milestoneable_class) { MergeRequest }
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
title: 'something',
|
||||||
|
source_project: project,
|
||||||
|
target_project: project,
|
||||||
|
source_branch: 'feature',
|
||||||
|
target_branch: 'master',
|
||||||
|
author: user,
|
||||||
|
milestone_id: milestone_id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'an object that can be assigned a milestone'
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,6 +5,12 @@ require 'spec_helper'
|
||||||
describe UserPreference do
|
describe UserPreference do
|
||||||
let(:user_preference) { create(:user_preference) }
|
let(:user_preference) { create(:user_preference) }
|
||||||
|
|
||||||
|
describe 'notes filters global keys' do
|
||||||
|
it 'contains expected values' do
|
||||||
|
expect(UserPreference::NOTES_FILTERS.keys).to match_array([:all_notes, :only_comments, :only_activity])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#set_notes_filter' do
|
describe '#set_notes_filter' do
|
||||||
let(:issuable) { build_stubbed(:issue) }
|
let(:issuable) { build_stubbed(:issue) }
|
||||||
|
|
||||||
|
|
|
@ -101,6 +101,75 @@ describe API::Notes do
|
||||||
expect(json_response.first['body']).to eq(cross_reference_note.note)
|
expect(json_response.first['body']).to eq(cross_reference_note.note)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "activity filters" do
|
||||||
|
let!(:user_reference_note) do
|
||||||
|
create :note,
|
||||||
|
noteable: ext_issue, project: ext_proj,
|
||||||
|
note: "Hello there general!",
|
||||||
|
system: false
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:test_url) {"/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes"}
|
||||||
|
|
||||||
|
shared_examples 'a notes request' do
|
||||||
|
it 'is a note array response' do
|
||||||
|
expect(response).to have_gitlab_http_status(200)
|
||||||
|
expect(response).to include_pagination_headers
|
||||||
|
expect(json_response).to be_an Array
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when not provided" do
|
||||||
|
let(:count) { 2 }
|
||||||
|
|
||||||
|
before do
|
||||||
|
get api(test_url, private_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'a notes request'
|
||||||
|
|
||||||
|
it 'returns all the notes' do
|
||||||
|
expect(json_response.count).to eq(count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when all_notes provided" do
|
||||||
|
let(:count) { 2 }
|
||||||
|
|
||||||
|
before do
|
||||||
|
get api(test_url + "?activity_filter=all_notes", private_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'a notes request'
|
||||||
|
|
||||||
|
it 'returns all the notes' do
|
||||||
|
expect(json_response.count).to eq(count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when provided" do
|
||||||
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
where(:filter, :count, :system_notable) do
|
||||||
|
"only_comments" | 1 | false
|
||||||
|
"only_activity" | 1 | true
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
before do
|
||||||
|
get api(test_url + "?activity_filter=#{filter}", private_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'a notes request'
|
||||||
|
|
||||||
|
it "properly filters the returned notables" do
|
||||||
|
expect(json_response.count).to eq(count)
|
||||||
|
expect(json_response.first["system"]).to be system_notable
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET /projects/:id/noteable/:noteable_id/notes/:note_id" do
|
describe "GET /projects/:id/noteable/:noteable_id/notes/:note_id" do
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Ci::CreatePipelineService do
|
||||||
|
let_it_be(:project) { create(:project, :repository) }
|
||||||
|
let_it_be(:user) { create(:admin) }
|
||||||
|
let(:ref) { 'refs/heads/master' }
|
||||||
|
let(:service) { described_class.new(project, user, { ref: ref }) }
|
||||||
|
|
||||||
|
context 'custom config content' do
|
||||||
|
let(:bridge) do
|
||||||
|
double(:bridge, yaml_for_downstream: <<~YML
|
||||||
|
rspec:
|
||||||
|
script: rspec
|
||||||
|
custom:
|
||||||
|
script: custom
|
||||||
|
YML
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { service.execute(:push, bridge: bridge) }
|
||||||
|
|
||||||
|
it 'creates a pipeline using the content passed in as param' do
|
||||||
|
expect(subject).to be_persisted
|
||||||
|
expect(subject.builds.map(&:name)).to eq %w[rspec custom]
|
||||||
|
expect(subject.config_source).to eq 'bridge_source'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,116 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Expects `worker_class` to be defined
|
||||||
|
shared_examples_for 'reenqueuer' do
|
||||||
|
subject(:job) { worker_class.new }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(job).to receive(:sleep) # faster tests
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'implements lease_timeout' do
|
||||||
|
expect(job.lease_timeout).to be_a(ActiveSupport::Duration)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
it 'tries to obtain a lease' do
|
||||||
|
expect_to_obtain_exclusive_lease(job.lease_key)
|
||||||
|
|
||||||
|
job.perform
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Example usage:
|
||||||
|
#
|
||||||
|
# it_behaves_like 'it is rate limited to 1 call per', 5.seconds do
|
||||||
|
# subject { described_class.new }
|
||||||
|
# let(:rate_limited_method) { subject.perform }
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
shared_examples_for 'it is rate limited to 1 call per' do |minimum_duration|
|
||||||
|
before do
|
||||||
|
# Allow Timecop freeze and travel without the block form
|
||||||
|
Timecop.safe_mode = false
|
||||||
|
Timecop.freeze
|
||||||
|
|
||||||
|
time_travel_during_rate_limited_method(actual_duration)
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
Timecop.return
|
||||||
|
Timecop.safe_mode = true
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the work finishes in 0 seconds' do
|
||||||
|
let(:actual_duration) { 0 }
|
||||||
|
|
||||||
|
it 'sleeps exactly the minimum duration' do
|
||||||
|
expect(subject).to receive(:sleep).with(a_value_within(0.01).of(minimum_duration))
|
||||||
|
|
||||||
|
rate_limited_method
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the work finishes in 10% of minimum duration' do
|
||||||
|
let(:actual_duration) { 0.1 * minimum_duration }
|
||||||
|
|
||||||
|
it 'sleeps 90% of minimum duration' do
|
||||||
|
expect(subject).to receive(:sleep).with(a_value_within(0.01).of(0.9 * minimum_duration))
|
||||||
|
|
||||||
|
rate_limited_method
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the work finishes in 90% of minimum duration' do
|
||||||
|
let(:actual_duration) { 0.9 * minimum_duration }
|
||||||
|
|
||||||
|
it 'sleeps 10% of minimum duration' do
|
||||||
|
expect(subject).to receive(:sleep).with(a_value_within(0.01).of(0.1 * minimum_duration))
|
||||||
|
|
||||||
|
rate_limited_method
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the work finishes exactly at minimum duration' do
|
||||||
|
let(:actual_duration) { minimum_duration }
|
||||||
|
|
||||||
|
it 'does not sleep' do
|
||||||
|
expect(subject).not_to receive(:sleep)
|
||||||
|
|
||||||
|
rate_limited_method
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the work takes 10% longer than minimum duration' do
|
||||||
|
let(:actual_duration) { 1.1 * minimum_duration }
|
||||||
|
|
||||||
|
it 'does not sleep' do
|
||||||
|
expect(subject).not_to receive(:sleep)
|
||||||
|
|
||||||
|
rate_limited_method
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the work takes twice as long as minimum duration' do
|
||||||
|
let(:actual_duration) { 2 * minimum_duration }
|
||||||
|
|
||||||
|
it 'does not sleep' do
|
||||||
|
expect(subject).not_to receive(:sleep)
|
||||||
|
|
||||||
|
rate_limited_method
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def time_travel_during_rate_limited_method(actual_duration)
|
||||||
|
# Save the original implementation of ensure_minimum_duration
|
||||||
|
original_ensure_minimum_duration = subject.method(:ensure_minimum_duration)
|
||||||
|
|
||||||
|
allow(subject).to receive(:ensure_minimum_duration) do |minimum_duration, &block|
|
||||||
|
original_ensure_minimum_duration.call(minimum_duration) do
|
||||||
|
# Time travel inside the block inside ensure_minimum_duration
|
||||||
|
Timecop.travel(actual_duration) if actual_duration && actual_duration > 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
179
spec/workers/concerns/reenqueuer_spec.rb
Normal file
179
spec/workers/concerns/reenqueuer_spec.rb
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Reenqueuer do
|
||||||
|
include ExclusiveLeaseHelpers
|
||||||
|
|
||||||
|
let_it_be(:worker_class) do
|
||||||
|
Class.new do
|
||||||
|
def self.name
|
||||||
|
'Gitlab::Foo::Bar::DummyWorker'
|
||||||
|
end
|
||||||
|
|
||||||
|
include ApplicationWorker
|
||||||
|
prepend Reenqueuer
|
||||||
|
|
||||||
|
attr_reader :performed_args
|
||||||
|
|
||||||
|
def perform(*args)
|
||||||
|
@performed_args = args
|
||||||
|
|
||||||
|
success? # for stubbing
|
||||||
|
end
|
||||||
|
|
||||||
|
def success?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def lease_timeout
|
||||||
|
30.seconds
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
subject(:job) { worker_class.new }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(job).to receive(:sleep) # faster tests
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'reenqueuer'
|
||||||
|
|
||||||
|
it_behaves_like 'it is rate limited to 1 call per', 5.seconds do
|
||||||
|
let(:rate_limited_method) { subject.perform }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'disables Sidekiq retries' do
|
||||||
|
expect(job.sidekiq_options_hash).to include('retry' => false)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#perform', :clean_gitlab_redis_shared_state do
|
||||||
|
let(:arbitrary_args) { [:foo, 'bar', { a: 1 }] }
|
||||||
|
|
||||||
|
context 'when the lease is available' do
|
||||||
|
it 'does perform' do
|
||||||
|
job.perform(*arbitrary_args)
|
||||||
|
|
||||||
|
expect(job.performed_args).to eq(arbitrary_args)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the lease is taken' do
|
||||||
|
before do
|
||||||
|
stub_exclusive_lease_taken(job.lease_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not perform' do
|
||||||
|
job.perform(*arbitrary_args)
|
||||||
|
|
||||||
|
expect(job.performed_args).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when #perform returns truthy' do
|
||||||
|
before do
|
||||||
|
allow(job).to receive(:success?).and_return(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'reenqueues the worker' do
|
||||||
|
expect(worker_class).to receive(:perform_async)
|
||||||
|
|
||||||
|
job.perform
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when #perform returns falsey' do
|
||||||
|
it 'does not reenqueue the worker' do
|
||||||
|
expect(worker_class).not_to receive(:perform_async)
|
||||||
|
|
||||||
|
job.perform
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Reenqueuer::ReenqueuerSleeper do
|
||||||
|
let_it_be(:dummy_class) do
|
||||||
|
Class.new do
|
||||||
|
include Reenqueuer::ReenqueuerSleeper
|
||||||
|
|
||||||
|
def rate_limited_method
|
||||||
|
ensure_minimum_duration(11.seconds) do
|
||||||
|
# do work
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
subject(:dummy) { dummy_class.new }
|
||||||
|
|
||||||
|
# Test that rate_limited_method is rate limited by ensure_minimum_duration
|
||||||
|
it_behaves_like 'it is rate limited to 1 call per', 11.seconds do
|
||||||
|
let(:rate_limited_method) { dummy.rate_limited_method }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Test ensure_minimum_duration more directly
|
||||||
|
describe '#ensure_minimum_duration' do
|
||||||
|
around do |example|
|
||||||
|
# Allow Timecop.travel without the block form
|
||||||
|
Timecop.safe_mode = false
|
||||||
|
|
||||||
|
Timecop.freeze do
|
||||||
|
example.run
|
||||||
|
end
|
||||||
|
|
||||||
|
Timecop.safe_mode = true
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:minimum_duration) { 4.seconds }
|
||||||
|
|
||||||
|
context 'when the block completes well before the minimum duration' do
|
||||||
|
let(:time_left) { 3.seconds }
|
||||||
|
|
||||||
|
it 'sleeps until the minimum duration' do
|
||||||
|
expect(dummy).to receive(:sleep).with(a_value_within(0.01).of(time_left))
|
||||||
|
|
||||||
|
dummy.ensure_minimum_duration(minimum_duration) do
|
||||||
|
Timecop.travel(minimum_duration - time_left)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the block completes just before the minimum duration' do
|
||||||
|
let(:time_left) { 0.1.seconds }
|
||||||
|
|
||||||
|
it 'sleeps until the minimum duration' do
|
||||||
|
expect(dummy).to receive(:sleep).with(a_value_within(0.01).of(time_left))
|
||||||
|
|
||||||
|
dummy.ensure_minimum_duration(minimum_duration) do
|
||||||
|
Timecop.travel(minimum_duration - time_left)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the block completes just after the minimum duration' do
|
||||||
|
let(:time_over) { 0.1.seconds }
|
||||||
|
|
||||||
|
it 'does not sleep' do
|
||||||
|
expect(dummy).not_to receive(:sleep)
|
||||||
|
|
||||||
|
dummy.ensure_minimum_duration(minimum_duration) do
|
||||||
|
Timecop.travel(minimum_duration + time_over)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the block completes well after the minimum duration' do
|
||||||
|
let(:time_over) { 10.seconds }
|
||||||
|
|
||||||
|
it 'does not sleep' do
|
||||||
|
expect(dummy).not_to receive(:sleep)
|
||||||
|
|
||||||
|
dummy.ensure_minimum_duration(minimum_duration) do
|
||||||
|
Timecop.travel(minimum_duration + time_over)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue