Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
8d15913bc4
commit
b46d41d54b
91 changed files with 802 additions and 192 deletions
|
@ -1 +1 @@
|
|||
40fae4205d3ad62ca9341620146486bee8d31b28
|
||||
d924490032231edb9452acdaca7d8e4747cf6ab4
|
||||
|
|
|
@ -870,6 +870,14 @@ const Api = {
|
|||
return axios.put(url, freezePeriod);
|
||||
},
|
||||
|
||||
deleteFreezePeriod(id, freezePeriodId) {
|
||||
const url = Api.buildUrl(this.freezePeriodPath)
|
||||
.replace(':id', encodeURIComponent(id))
|
||||
.replace(':freeze_period_id', encodeURIComponent(freezePeriodId));
|
||||
|
||||
return axios.delete(url);
|
||||
},
|
||||
|
||||
trackRedisCounterEvent(event) {
|
||||
if (!gon.features?.usageDataApi) {
|
||||
return null;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlTable, GlButton, GlModalDirective, GlSprintf } from '@gitlab/ui';
|
||||
import { GlTable, GlButton, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
|
@ -21,21 +21,42 @@ export default {
|
|||
key: 'edit',
|
||||
label: s__('DeployFreeze|Edit'),
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: s__('DeployFreeze|Delete'),
|
||||
},
|
||||
],
|
||||
translations: {
|
||||
addDeployFreeze: s__('DeployFreeze|Add deploy freeze'),
|
||||
deleteDeployFreezeTitle: s__('DeployFreeze|Delete deploy freeze?'),
|
||||
deleteDeployFreezeMessage: s__(
|
||||
'DeployFreeze|Deploy freeze from %{start} to %{end} in %{timezone} will be removed. Are you sure?',
|
||||
),
|
||||
emptyStateText: s__(
|
||||
'DeployFreeze|No deploy freezes exist for this project. To add one, select %{strongStart}Add deploy freeze%{strongEnd}',
|
||||
),
|
||||
},
|
||||
modal: {
|
||||
id: 'deleteFreezePeriodModal',
|
||||
actionPrimary: {
|
||||
text: s__('DeployFreeze|Delete freeze period'),
|
||||
attributes: { variant: 'danger', 'data-testid': 'modal-confirm' },
|
||||
},
|
||||
},
|
||||
components: {
|
||||
GlTable,
|
||||
GlButton,
|
||||
GlModal,
|
||||
GlSprintf,
|
||||
},
|
||||
directives: {
|
||||
GlModal: GlModalDirective,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
freezePeriodToDelete: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['freezePeriods']),
|
||||
tableIsNotEmpty() {
|
||||
|
@ -46,7 +67,14 @@ export default {
|
|||
this.fetchFreezePeriods();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchFreezePeriods', 'setFreezePeriod']),
|
||||
...mapActions(['fetchFreezePeriods', 'setFreezePeriod', 'deleteFreezePeriod']),
|
||||
handleDeleteFreezePeriod(freezePeriod) {
|
||||
this.freezePeriodToDelete = freezePeriod;
|
||||
},
|
||||
confirmDeleteFreezePeriod() {
|
||||
this.deleteFreezePeriod(this.freezePeriodToDelete);
|
||||
this.freezePeriodToDelete = null;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -72,6 +100,18 @@ export default {
|
|||
@click="setFreezePeriod(item)"
|
||||
/>
|
||||
</template>
|
||||
<template #cell(delete)="{ item }">
|
||||
<gl-button
|
||||
v-gl-modal="$options.modal.id"
|
||||
category="secondary"
|
||||
variant="danger"
|
||||
icon="remove"
|
||||
:aria-label="$options.modal.actionPrimary.text"
|
||||
:loading="item.isDeleting"
|
||||
data-testid="delete-deploy-freeze"
|
||||
@click="handleDeleteFreezePeriod(item)"
|
||||
/>
|
||||
</template>
|
||||
<template #empty>
|
||||
<p data-testid="empty-freeze-periods" class="gl-text-center text-plain">
|
||||
<gl-sprintf :message="$options.translations.emptyStateText">
|
||||
|
@ -90,5 +130,24 @@ export default {
|
|||
>
|
||||
{{ $options.translations.addDeployFreeze }}
|
||||
</gl-button>
|
||||
<gl-modal
|
||||
:title="$options.translations.deleteDeployFreezeTitle"
|
||||
:modal-id="$options.modal.id"
|
||||
:action-primary="$options.modal.actionPrimary"
|
||||
static
|
||||
@primary="confirmDeleteFreezePeriod"
|
||||
>
|
||||
<template v-if="freezePeriodToDelete">
|
||||
<gl-sprintf :message="$options.translations.deleteDeployFreezeMessage">
|
||||
<template #start>
|
||||
<code>{{ freezePeriodToDelete.freezeStart }}</code>
|
||||
</template>
|
||||
<template #end>
|
||||
<code>{{ freezePeriodToDelete.freezeEnd }}</code>
|
||||
</template>
|
||||
<template #timezone>{{ freezePeriodToDelete.cronTimezone.formattedTimezone }}</template>
|
||||
</gl-sprintf>
|
||||
</template>
|
||||
</gl-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -52,6 +52,22 @@ export const updateFreezePeriod = (store) =>
|
|||
}),
|
||||
);
|
||||
|
||||
export const deleteFreezePeriod = ({ state, commit }, { id }) => {
|
||||
commit(types.REQUEST_DELETE_FREEZE_PERIOD, id);
|
||||
|
||||
return Api.deleteFreezePeriod(state.projectId, id)
|
||||
.then(() => commit(types.RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS, id))
|
||||
.catch((e) => {
|
||||
createFlash({
|
||||
message: __('Error: Unable to delete deploy freeze'),
|
||||
});
|
||||
commit(types.RECEIVE_DELETE_FREEZE_PERIOD_ERROR, id);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[gitlab] Unable to delete deploy freeze:', e);
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchFreezePeriods = ({ commit, state }) => {
|
||||
commit(types.REQUEST_FREEZE_PERIODS);
|
||||
|
||||
|
|
|
@ -10,4 +10,8 @@ export const SET_SELECTED_ID = 'SET_SELECTED_ID';
|
|||
export const SET_FREEZE_START_CRON = 'SET_FREEZE_START_CRON';
|
||||
export const SET_FREEZE_END_CRON = 'SET_FREEZE_END_CRON';
|
||||
|
||||
export const REQUEST_DELETE_FREEZE_PERIOD = 'REQUEST_DELETE_FREEZE_PERIOD';
|
||||
export const RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS = 'RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS';
|
||||
export const RECEIVE_DELETE_FREEZE_PERIOD_ERROR = 'RECEIVE_DELETE_FREEZE_PERIOD_ERROR';
|
||||
|
||||
export const RESET_MODAL = 'RESET_MODAL';
|
||||
|
|
|
@ -1,15 +1,28 @@
|
|||
import Vue from 'vue';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import { secondsToHours } from '~/lib/utils/datetime_utility';
|
||||
import * as types from './mutation_types';
|
||||
|
||||
const formatTimezoneName = (freezePeriod, timezoneList) =>
|
||||
convertObjectPropsToCamelCase({
|
||||
const formatTimezoneName = (freezePeriod, timezoneList) => {
|
||||
const tz = timezoneList.find((timezone) => timezone.identifier === freezePeriod.cron_timezone);
|
||||
return convertObjectPropsToCamelCase({
|
||||
...freezePeriod,
|
||||
cron_timezone: {
|
||||
formattedTimezone: timezoneList.find((tz) => tz.identifier === freezePeriod.cron_timezone)
|
||||
?.name,
|
||||
formattedTimezone: tz && `[UTC ${secondsToHours(tz.offset)}] ${tz.name}`,
|
||||
identifier: freezePeriod.cron_timezone,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const setFreezePeriodIsDeleting = (state, id, isDeleting) => {
|
||||
const freezePeriod = state.freezePeriods.find((f) => f.id === id);
|
||||
|
||||
if (!freezePeriod) {
|
||||
return;
|
||||
}
|
||||
|
||||
Vue.set(freezePeriod, 'isDeleting', isDeleting);
|
||||
};
|
||||
|
||||
export default {
|
||||
[types.REQUEST_FREEZE_PERIODS](state) {
|
||||
|
@ -53,6 +66,18 @@ export default {
|
|||
state.selectedId = id;
|
||||
},
|
||||
|
||||
[types.REQUEST_DELETE_FREEZE_PERIOD](state, id) {
|
||||
setFreezePeriodIsDeleting(state, id, true);
|
||||
},
|
||||
|
||||
[types.RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS](state, id) {
|
||||
state.freezePeriods = state.freezePeriods.filter((f) => f.id !== id);
|
||||
},
|
||||
|
||||
[types.RECEIVE_DELETE_FREEZE_PERIOD_ERROR](state, id) {
|
||||
setFreezePeriodIsDeleting(state, id, false);
|
||||
},
|
||||
|
||||
[types.RESET_MODAL](state) {
|
||||
state.freezeStartCron = '';
|
||||
state.freezeEndCron = '';
|
||||
|
|
|
@ -22,10 +22,6 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
canReadEnvironment: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onChangePage(page) {
|
||||
|
@ -42,7 +38,7 @@ export default {
|
|||
<slot name="empty-state"></slot>
|
||||
|
||||
<div v-if="!isLoading && environments.length > 0" class="table-holder">
|
||||
<environment-table :environments="environments" :can-read-environment="canReadEnvironment" />
|
||||
<environment-table :environments="environments" />
|
||||
|
||||
<table-pagination
|
||||
v-if="pagination && pagination.totalPages > 1"
|
||||
|
|
|
@ -48,12 +48,6 @@ export default {
|
|||
mixins: [timeagoMixin],
|
||||
|
||||
props: {
|
||||
canReadEnvironment: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
|
||||
model: {
|
||||
type: Object,
|
||||
required: true,
|
||||
|
@ -790,14 +784,14 @@ export default {
|
|||
/>
|
||||
|
||||
<external-url-component
|
||||
v-if="externalURL && canReadEnvironment"
|
||||
v-if="externalURL"
|
||||
:external-url="externalURL"
|
||||
data-track-action="click_button"
|
||||
data-track-label="environment_url"
|
||||
/>
|
||||
|
||||
<monitoring-button-component
|
||||
v-if="monitoringUrl && canReadEnvironment"
|
||||
v-if="monitoringUrl"
|
||||
:monitoring-url="monitoringUrl"
|
||||
data-track-action="click_button"
|
||||
data-track-label="environment_monitoring"
|
||||
|
|
|
@ -52,10 +52,6 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
canReadEnvironment: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
newEnvironmentPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -210,7 +206,6 @@ export default {
|
|||
:is-loading="isLoading"
|
||||
:environments="state.environments"
|
||||
:pagination="state.paginationInformation"
|
||||
:can-read-environment="canReadEnvironment"
|
||||
@onChangePage="onChangePage"
|
||||
>
|
||||
<template v-if="!isLoading && state.environments.length === 0" #empty-state>
|
||||
|
|
|
@ -27,10 +27,6 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
canReadEnvironment: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
canAdminEnvironment: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
|
@ -84,7 +80,7 @@ export default {
|
|||
return this.environment.isAvailable && Boolean(this.environment.autoStopAt);
|
||||
},
|
||||
shouldShowExternalUrlButton() {
|
||||
return this.canReadEnvironment && Boolean(this.environment.externalUrl);
|
||||
return Boolean(this.environment.externalUrl);
|
||||
},
|
||||
shouldShowStopButton() {
|
||||
return this.canStopEnvironment && this.environment.isAvailable;
|
||||
|
@ -138,7 +134,7 @@ export default {
|
|||
>{{ $options.i18n.externalButtonText }}</gl-button
|
||||
>
|
||||
<gl-button
|
||||
v-if="canReadEnvironment"
|
||||
v-if="shouldShowExternalUrlButton"
|
||||
data-testid="metrics-button"
|
||||
:href="metricsPath"
|
||||
:title="$options.i18n.metricsButtonTitle"
|
||||
|
|
|
@ -23,11 +23,6 @@ export default {
|
|||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
canReadEnvironment: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -155,7 +150,6 @@ export default {
|
|||
<environment-item
|
||||
:key="`environment-item-${i}`"
|
||||
:model="model"
|
||||
:can-read-environment="canReadEnvironment"
|
||||
:table-data="tableData"
|
||||
data-qa-selector="environment_item"
|
||||
/>
|
||||
|
@ -191,7 +185,6 @@ export default {
|
|||
<environment-item
|
||||
:key="`environment-row-${i}-${index}`"
|
||||
:model="child"
|
||||
:can-read-environment="canReadEnvironment"
|
||||
:table-data="tableData"
|
||||
data-qa-selector="environment_item"
|
||||
/>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import { parseBoolean } from '../../lib/utils/common_utils';
|
||||
import Translate from '../../vue_shared/translate';
|
||||
import environmentsFolderApp from './environments_folder_view.vue';
|
||||
|
||||
|
@ -31,7 +30,6 @@ export default () => {
|
|||
endpoint: environmentsData.environmentsDataEndpoint,
|
||||
folderName: environmentsData.environmentsDataFolderName,
|
||||
cssContainerClass: environmentsData.cssClass,
|
||||
canReadEnvironment: parseBoolean(environmentsData.environmentsDataCanReadEnvironment),
|
||||
};
|
||||
},
|
||||
render(createElement) {
|
||||
|
@ -40,7 +38,6 @@ export default () => {
|
|||
endpoint: this.endpoint,
|
||||
folderName: this.folderName,
|
||||
cssContainerClass: this.cssContainerClass,
|
||||
canReadEnvironment: this.canReadEnvironment,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -30,10 +30,6 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
canReadEnvironment: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
successCallback(resp) {
|
||||
|
@ -72,7 +68,6 @@ export default {
|
|||
:is-loading="isLoading"
|
||||
:environments="state.environments"
|
||||
:pagination="state.paginationInformation"
|
||||
:can-read-environment="canReadEnvironment"
|
||||
@onChangePage="onChangePage"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -32,7 +32,6 @@ export default () => {
|
|||
newEnvironmentPath: environmentsData.newEnvironmentPath,
|
||||
helpPagePath: environmentsData.helpPagePath,
|
||||
canCreateEnvironment: parseBoolean(environmentsData.canCreateEnvironment),
|
||||
canReadEnvironment: parseBoolean(environmentsData.canReadEnvironment),
|
||||
};
|
||||
},
|
||||
render(createElement) {
|
||||
|
@ -42,7 +41,6 @@ export default () => {
|
|||
newEnvironmentPath: this.newEnvironmentPath,
|
||||
helpPagePath: this.helpPagePath,
|
||||
canCreateEnvironment: this.canCreateEnvironment,
|
||||
canReadEnvironment: this.canReadEnvironment,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -36,7 +36,6 @@ export const initHeader = () => {
|
|||
environment: this.environment,
|
||||
canDestroyEnvironment: dataset.canDestroyEnvironment,
|
||||
canUpdateEnvironment: dataset.canUpdateEnvironment,
|
||||
canReadEnvironment: dataset.canReadEnvironment,
|
||||
canStopEnvironment: dataset.canStopEnvironment,
|
||||
canAdminEnvironment: dataset.canAdminEnvironment,
|
||||
cancelAutoStopPath: dataset.environmentCancelAutoStopPath,
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="gl-text-center gl-p-7 gl-bg-gray-50">
|
||||
<img :src="url" :alt="alt" data-testid="image" />
|
||||
</div>
|
||||
</template>
|
|
@ -6,6 +6,8 @@ export const loadViewer = (type) => {
|
|||
return () => import(/* webpackChunkName: 'blob_text_viewer' */ './text_viewer.vue');
|
||||
case 'download':
|
||||
return () => import(/* webpackChunkName: 'blob_download_viewer' */ './download_viewer.vue');
|
||||
case 'image':
|
||||
return () => import(/* webpackChunkName: 'blob_image_viewer' */ './image_viewer.vue');
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -23,5 +25,9 @@ export const viewerProps = (type, blob) => {
|
|||
filePath: blob.rawPath,
|
||||
fileSize: blob.rawSize,
|
||||
},
|
||||
image: {
|
||||
url: blob.rawPath,
|
||||
alt: blob.name,
|
||||
},
|
||||
}[type];
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@ export default (search = '') => {
|
|||
const highlightLineClass = 'hll';
|
||||
const contentBody = document.getElementById('content-body');
|
||||
const searchTerm = search.toLowerCase();
|
||||
const blobs = contentBody.querySelectorAll('.blob-result');
|
||||
const blobs = contentBody.querySelectorAll('.js-blob-result');
|
||||
|
||||
blobs.forEach((blob) => {
|
||||
const lines = blob.querySelectorAll('.line');
|
||||
|
|
|
@ -27,9 +27,7 @@ module Boards
|
|||
list_service = Boards::Issues::ListService.new(board_parent, current_user, filter_params)
|
||||
issues = issues_from(list_service)
|
||||
|
||||
if Gitlab::Database.read_write? && !board.disabled_for?(current_user)
|
||||
Issue.move_nulls_to_end(issues)
|
||||
end
|
||||
::Boards::Issues::ListService.initialize_relative_positions(board, current_user, issues)
|
||||
|
||||
render_issues(issues, list_service.metadata)
|
||||
end
|
||||
|
|
|
@ -21,9 +21,10 @@ class RegistrationsController < Devise::RegistrationsController
|
|||
|
||||
def create
|
||||
set_user_state
|
||||
accept_pending_invitations
|
||||
|
||||
super do |new_user|
|
||||
accept_pending_invitations if new_user.persisted?
|
||||
|
||||
persist_accepted_terms_if_required(new_user)
|
||||
set_role_required(new_user)
|
||||
|
||||
|
|
|
@ -15,8 +15,11 @@ module Resolvers
|
|||
def resolve(**args)
|
||||
filter_params = item_filters(args[:filters]).merge(board_id: list.board.id, id: list.id)
|
||||
service = ::Boards::Issues::ListService.new(list.board.resource_parent, context[:current_user], filter_params)
|
||||
pagination_connections = Gitlab::Graphql::Pagination::Keyset::Connection.new(service.execute)
|
||||
|
||||
service.execute
|
||||
::Boards::Issues::ListService.initialize_relative_positions(list.board, current_user, pagination_connections.items)
|
||||
|
||||
pagination_connections
|
||||
end
|
||||
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/235681
|
||||
|
|
|
@ -52,15 +52,22 @@ module Types
|
|||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def jobs_for_pipeline(pipeline, stage_ids, include_needs)
|
||||
builds_results = pipeline.latest_builds.where(stage_id: stage_ids).preload(:job_artifacts, :project)
|
||||
bridges_results = pipeline.bridges.where(stage_id: stage_ids).preload(:project)
|
||||
builds_results = builds_results.preload(:needs) if include_needs
|
||||
bridges_results = bridges_results.preload(:needs) if include_needs
|
||||
commit_status_results = pipeline.latest_statuses.where(stage_id: stage_ids)
|
||||
jobs = pipeline.statuses.latest.where(stage_id: stage_ids)
|
||||
|
||||
results = builds_results | bridges_results | commit_status_results
|
||||
common_relations = [:project]
|
||||
common_relations << :needs if include_needs
|
||||
|
||||
results.group_by(&:stage_id)
|
||||
preloaders = {
|
||||
::Ci::Build => [:metadata, :job_artifacts],
|
||||
::Ci::Bridge => [:metadata, :downstream_pipeline],
|
||||
::GenericCommitStatus => []
|
||||
}
|
||||
|
||||
preloaders.each do |klass, relations|
|
||||
ActiveRecord::Associations::Preloader.new.preload(jobs.select { |job| job.is_a?(klass) }, relations + common_relations)
|
||||
end
|
||||
|
||||
jobs.group_by(&:stage_id)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
|
|
@ -73,7 +73,6 @@ module EnvironmentHelper
|
|||
external_url: environment.external_url,
|
||||
can_update_environment: can?(current_user, :update_environment, environment),
|
||||
can_destroy_environment: can_destroy_environment?(environment),
|
||||
can_read_environment: can?(current_user, :read_environment, environment),
|
||||
can_stop_environment: can?(current_user, :stop_environment, environment),
|
||||
can_admin_environment: can?(current_user, :admin_environment, project),
|
||||
environment_metrics_path: environment_metrics_path(environment),
|
||||
|
|
|
@ -66,6 +66,7 @@ module Ci
|
|||
has_many :processables, class_name: 'Ci::Processable', foreign_key: :commit_id, inverse_of: :pipeline
|
||||
has_many :bridges, class_name: 'Ci::Bridge', foreign_key: :commit_id, inverse_of: :pipeline
|
||||
has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
|
||||
has_many :generic_commit_statuses, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'GenericCommitStatus'
|
||||
has_many :job_artifacts, through: :builds
|
||||
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :variables, class_name: 'Ci::PipelineVariable'
|
||||
|
|
|
@ -14,12 +14,10 @@ module CronSchedulable
|
|||
# The `next_run_at` column is set to the actual execution date of worker that
|
||||
# triggers the schedule. This way, a schedule like `*/1 * * * *` won't be triggered
|
||||
# in a short interval when the worker runs irregularly by Sidekiq Memory Killer.
|
||||
def calculate_next_run_at
|
||||
now = Time.zone.now
|
||||
def calculate_next_run_at(start_time = Time.zone.now)
|
||||
ideal_next_run = ideal_next_run_from(start_time)
|
||||
|
||||
ideal_next_run = ideal_next_run_from(now)
|
||||
|
||||
if ideal_next_run == cron_worker_next_run_from(now)
|
||||
if ideal_next_run == cron_worker_next_run_from(start_time)
|
||||
ideal_next_run
|
||||
else
|
||||
cron_worker_next_run_from(ideal_next_run)
|
||||
|
|
|
@ -278,12 +278,14 @@ class Member < ApplicationRecord
|
|||
|
||||
def accept_invite!(new_user)
|
||||
return false unless invite?
|
||||
return false unless new_user
|
||||
|
||||
self.user = new_user
|
||||
return false unless self.user.save
|
||||
|
||||
self.invite_token = nil
|
||||
self.invite_accepted_at = Time.current.utc
|
||||
|
||||
self.user = new_user
|
||||
|
||||
saved = self.save
|
||||
|
||||
after_accept_invite if saved
|
||||
|
|
|
@ -582,7 +582,7 @@ class Note < ApplicationRecord
|
|||
end
|
||||
|
||||
def post_processed_cache_key
|
||||
cache_key_items = [cache_key, author.cache_key]
|
||||
cache_key_items = [cache_key, author&.cache_key]
|
||||
cache_key_items << Digest::SHA1.hexdigest(redacted_note_html) if redacted_note_html.present?
|
||||
|
||||
cache_key_items.join(':')
|
||||
|
|
|
@ -12,11 +12,11 @@ module Ci
|
|||
erased_by.name if erased_by_user?
|
||||
end
|
||||
|
||||
def status_title
|
||||
def status_title(status = detailed_status)
|
||||
if auto_canceled?
|
||||
"Job is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}"
|
||||
else
|
||||
tooltip_for_badge
|
||||
tooltip_for_badge(status)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -41,8 +41,8 @@ module Ci
|
|||
|
||||
private
|
||||
|
||||
def tooltip_for_badge
|
||||
detailed_status.badge_tooltip.capitalize
|
||||
def tooltip_for_badge(status)
|
||||
status.badge_tooltip.capitalize
|
||||
end
|
||||
|
||||
def detailed_status
|
||||
|
|
|
@ -15,18 +15,23 @@ module Ci
|
|||
private
|
||||
|
||||
def preload_statuses(statuses)
|
||||
loaded_statuses = statuses.load
|
||||
statuses.tap do |statuses|
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
ActiveRecord::Associations::Preloader.new.preload(preloadable_statuses(loaded_statuses), %w[pipeline tags job_artifacts_archive metadata])
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
end
|
||||
end
|
||||
common_relations = [:pipeline]
|
||||
|
||||
def preloadable_statuses(statuses)
|
||||
statuses.reject do |status|
|
||||
status.instance_of?(::GenericCommitStatus) || status.instance_of?(::Ci::Bridge)
|
||||
preloaders = {
|
||||
::Ci::Build => [:metadata, :tags, :job_artifacts_archive],
|
||||
::Ci::Bridge => [:metadata, :downstream_pipeline],
|
||||
::GenericCommitStatus => []
|
||||
}
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
preloaders.each do |klass, relations|
|
||||
ActiveRecord::Associations::Preloader
|
||||
.new
|
||||
.preload(statuses.select { |job| job.is_a?(klass) }, relations + common_relations)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
statuses
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,6 +9,14 @@ module Boards
|
|||
IssuesFinder.valid_params
|
||||
end
|
||||
|
||||
# It is a class method because we cannot apply it
|
||||
# prior to knowing how many items should be fetched for a list.
|
||||
def self.initialize_relative_positions(board, current_user, issues)
|
||||
if Gitlab::Database.read_write? && !board.disabled_for?(current_user)
|
||||
Issue.move_nulls_to_end(issues)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def order(items)
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Dast profile schedule cadence schema",
|
||||
"type": "object",
|
||||
"anyOf": [
|
||||
{
|
||||
"properties": {
|
||||
"unit": { "enum": ["day"] },
|
||||
"duration": { "enum": [1] }
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"unit": { "enum": ["week"] },
|
||||
"duration": { "enum": [1] }
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"unit": { "enum": ["month"] },
|
||||
"duration": { "enum": [1, 3 ,6] }
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"unit": { "enum": ["year"] },
|
||||
"duration": { "enum": [1] }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -52,7 +52,7 @@
|
|||
.float-right
|
||||
= form_for project.runner_projects.new, url: admin_namespace_project_runner_projects_path(project.namespace, project), method: :post do |f|
|
||||
= f.hidden_field :runner_id, value: @runner.id
|
||||
= f.submit _('Enable'), class: 'gl-button btn btn-sm'
|
||||
= f.submit _('Enable'), class: 'gl-button btn btn-sm', data: { confirm: (s_('Runners|You are about to change this instance runner to a project runner. This operation is not reversible. Are you sure you want to continue?') if @runner.instance_type?) }
|
||||
= paginate_without_count @projects
|
||||
|
||||
.col-md-6
|
||||
|
|
|
@ -7,10 +7,14 @@
|
|||
- pipeline_link = local_assigns.fetch(:pipeline_link, false)
|
||||
- stage = local_assigns.fetch(:stage, false)
|
||||
- allow_retry = local_assigns.fetch(:allow_retry, false)
|
||||
-# This prevents initializing another Ci::Status object where 'status' is used
|
||||
- status = job.detailed_status(current_user)
|
||||
|
||||
%tr.build.commit{ class: ('retried' if retried) }
|
||||
%td.status
|
||||
= render "ci/status/badge", status: job.detailed_status(current_user), title: job.status_title
|
||||
-# Sending 'status' prevents calling the user relation inside the presenter, generating N+1,
|
||||
-# see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68743
|
||||
= render "ci/status/badge", status: status, title: job.status_title(status)
|
||||
|
||||
%td
|
||||
- if can?(current_user, :read_build, job)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.blob-result.gl-mt-3.gl-mb-5{ data: { qa_selector: 'result_item_content' } }
|
||||
.js-blob-result.gl-mt-3.gl-mb-5{ data: { qa_selector: 'result_item_content' } }
|
||||
.file-holder.file-holder-top-border
|
||||
.js-file-title.file-title{ data: { qa_selector: 'file_title_content' } }
|
||||
= link_to blob_link, data: {track_event: 'click_text', track_label: 'blob_path', track_property: 'search_result'} do
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: method_instrumentation_disable_initialization
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69091
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339665
|
||||
milestone: '14.3'
|
||||
type: development
|
||||
group: group::memory
|
||||
default_enabled: false
|
|
@ -178,27 +178,33 @@ if Gitlab::Metrics.enabled? && !Rails.env.test? && !(Rails.env.development? && d
|
|||
ActiveRecord::Querying.public_instance_methods(false).map(&:to_s)
|
||||
)
|
||||
|
||||
Gitlab::Metrics::Instrumentation
|
||||
.instrument_class_hierarchy(ActiveRecord::Base) do |klass, method|
|
||||
# Instrumenting the ApplicationSetting class can lead to an infinite
|
||||
# loop. Since the data is cached any way we don't really need to
|
||||
# instrument it.
|
||||
if klass == ApplicationSetting
|
||||
false
|
||||
else
|
||||
loc = method.source_location
|
||||
# We are removing the Instrumentation module entirely in steps.
|
||||
# More in https://gitlab.com/gitlab-org/gitlab/-/issues/217978.
|
||||
unless ::Feature.enabled?(:method_instrumentation_disable_initialization)
|
||||
Gitlab::Metrics::Instrumentation
|
||||
.instrument_class_hierarchy(ActiveRecord::Base) do |klass, method|
|
||||
# Instrumenting the ApplicationSetting class can lead to an infinite
|
||||
# loop. Since the data is cached any way we don't really need to
|
||||
# instrument it.
|
||||
if klass == ApplicationSetting
|
||||
false
|
||||
else
|
||||
loc = method.source_location
|
||||
|
||||
loc && loc[0].start_with?(models_path) && method.source =~ regex
|
||||
loc && loc[0].start_with?(models_path) && method.source =~ regex
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Ability is in app/models, is not an ActiveRecord model, but should still
|
||||
# be instrumented.
|
||||
Gitlab::Metrics::Instrumentation.instrument_methods(Ability)
|
||||
# Ability is in app/models, is not an ActiveRecord model, but should still
|
||||
# be instrumented.
|
||||
Gitlab::Metrics::Instrumentation.instrument_methods(Ability)
|
||||
end
|
||||
end
|
||||
|
||||
Gitlab::Metrics::Instrumentation.configure do |config|
|
||||
instrument_classes(config)
|
||||
unless ::Feature.enabled?(:method_instrumentation_disable_initialization)
|
||||
Gitlab::Metrics::Instrumentation.configure do |config|
|
||||
instrument_classes(config)
|
||||
end
|
||||
end
|
||||
|
||||
GC::Profiler.enable
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddCadenceToDastProfileSchedules < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_column :dast_profile_schedules, :cadence, :jsonb, null: false, default: {}
|
||||
end
|
||||
end
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTimezoneToDastProfileSchedules < ActiveRecord::Migration[6.1]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
# We disable these cops here because adding the column is safe. The table does not
|
||||
# have any data in it as it's behind a feature flag.
|
||||
# rubocop: disable Rails/NotNullColumn
|
||||
def up
|
||||
execute('DELETE FROM dast_profile_schedules')
|
||||
|
||||
unless column_exists?(:dast_profile_schedules, :timezone)
|
||||
add_column :dast_profile_schedules, :timezone, :text, null: false
|
||||
end
|
||||
|
||||
add_text_limit :dast_profile_schedules, :timezone, 255
|
||||
end
|
||||
|
||||
def down
|
||||
return unless column_exists?(:dast_profile_schedules, :timezone)
|
||||
|
||||
remove_column :dast_profile_schedules, :timezone
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddStartsAtToDastProfileSchedules < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_column :dast_profile_schedules, :starts_at, :datetime_with_timezone, null: false, default: -> { 'NOW()' }
|
||||
end
|
||||
end
|
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
|
||||
# for more information on how to write migrations for GitLab.
|
||||
|
||||
class AddUniqueIndexOnDastProfileToDastProfileSchedules < ActiveRecord::Migration[6.1]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
INDEX_NAME = 'index_dast_profile_schedules_on_dast_profile_id'
|
||||
TABLE = :dast_profile_schedules
|
||||
# We disable these cops here because changing this index is safe. The table does not
|
||||
# have any data in it as it's behind a feature flag.
|
||||
# rubocop: disable Migration/AddIndex
|
||||
# rubocop: disable Migration/RemoveIndex
|
||||
def up
|
||||
execute('DELETE FROM dast_profile_schedules')
|
||||
|
||||
if index_exists_by_name?(TABLE, INDEX_NAME)
|
||||
remove_index TABLE, :dast_profile_id, name: INDEX_NAME
|
||||
end
|
||||
|
||||
unless index_exists_by_name?(TABLE, INDEX_NAME)
|
||||
add_index TABLE, :dast_profile_id, unique: true, name: INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
execute('DELETE FROM dast_profile_schedules')
|
||||
|
||||
if index_exists_by_name?(TABLE, INDEX_NAME)
|
||||
remove_index TABLE, :dast_profile_id, name: INDEX_NAME
|
||||
end
|
||||
|
||||
unless index_exists_by_name?(TABLE, INDEX_NAME)
|
||||
add_index TABLE, :dast_profile_id
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
|
||||
# for more information on how to write migrations for GitLab.
|
||||
|
||||
class RemoveProjectProfileCompoundIndexFromDastProfileSchedules < ActiveRecord::Migration[6.1]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
TABLE = :dast_profile_schedules
|
||||
INDEX_NAME = 'index_dast_profile_schedules_on_project_id_and_dast_profile_id'
|
||||
# We disable these cops here because changing this index is safe. The table does not
|
||||
# have any data in it as it's behind a feature flag.
|
||||
# rubocop: disable Migration/AddIndex
|
||||
# rubocop: disable Migration/RemoveIndex
|
||||
def up
|
||||
execute('DELETE FROM dast_profile_schedules')
|
||||
|
||||
if index_exists_by_name?(TABLE, INDEX_NAME)
|
||||
remove_index TABLE, %i[project_id dast_profile_id], name: INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
execute('DELETE FROM dast_profile_schedules')
|
||||
|
||||
unless index_exists_by_name?(TABLE, INDEX_NAME)
|
||||
add_index TABLE, %i[project_id dast_profile_id], unique: true, name: INDEX_NAME
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
|
||||
# for more information on how to write migrations for GitLab.
|
||||
|
||||
class AddIndexProjectIdOnDastProfileSchedule < ActiveRecord::Migration[6.1]
|
||||
# We disable these cops here because changing this index is safe. The table does not
|
||||
# have any data in it as it's behind a feature flag.
|
||||
# rubocop: disable Migration/AddIndex
|
||||
def change
|
||||
add_index :dast_profile_schedules, :project_id
|
||||
end
|
||||
end
|
1
db/schema_migrations/20210807101446
Normal file
1
db/schema_migrations/20210807101446
Normal file
|
@ -0,0 +1 @@
|
|||
30e1463616c60b92afb28bbb76e3c55830a385af6df0e60e16ed96d9e75943b9
|
1
db/schema_migrations/20210807101621
Normal file
1
db/schema_migrations/20210807101621
Normal file
|
@ -0,0 +1 @@
|
|||
7e9b39914ade766357751953a4981225dbae7e5d371d4824af61b01af70f46ae
|
1
db/schema_migrations/20210807102004
Normal file
1
db/schema_migrations/20210807102004
Normal file
|
@ -0,0 +1 @@
|
|||
a2454f9fca3b1cedf7a0f2288b69abe799fe1f9ff4e2fe26d2cadfdddea73a83
|
1
db/schema_migrations/20210816095826
Normal file
1
db/schema_migrations/20210816095826
Normal file
|
@ -0,0 +1 @@
|
|||
d1ad234656f49861d2ca7694d23116e930bba597fca32b1015db698cc23bdc1c
|
1
db/schema_migrations/20210818061156
Normal file
1
db/schema_migrations/20210818061156
Normal file
|
@ -0,0 +1 @@
|
|||
23becdc9ad558882f4ce42e76391cdc2f760322a09c998082465fcb6d29dfeb5
|
1
db/schema_migrations/20210818115613
Normal file
1
db/schema_migrations/20210818115613
Normal file
|
@ -0,0 +1 @@
|
|||
9c5114dac05e90c15567bb3274f20f03a82f9e4d73d5c72d89c26bc9d742cc35
|
|
@ -12089,7 +12089,11 @@ CREATE TABLE dast_profile_schedules (
|
|||
updated_at timestamp with time zone NOT NULL,
|
||||
active boolean DEFAULT true NOT NULL,
|
||||
cron text NOT NULL,
|
||||
CONSTRAINT check_86531ea73f CHECK ((char_length(cron) <= 255))
|
||||
cadence jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
timezone text NOT NULL,
|
||||
starts_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT check_86531ea73f CHECK ((char_length(cron) <= 255)),
|
||||
CONSTRAINT check_be4d1c3af1 CHECK ((char_length(timezone) <= 255))
|
||||
);
|
||||
|
||||
COMMENT ON TABLE dast_profile_schedules IS '{"owner":"group::dynamic analysis","description":"Scheduling for scans using DAST Profiles"}';
|
||||
|
@ -23812,9 +23816,9 @@ CREATE UNIQUE INDEX index_daily_build_group_report_results_unique_columns ON ci_
|
|||
|
||||
CREATE INDEX index_dast_profile_schedules_active_next_run_at ON dast_profile_schedules USING btree (active, next_run_at);
|
||||
|
||||
CREATE INDEX index_dast_profile_schedules_on_dast_profile_id ON dast_profile_schedules USING btree (dast_profile_id);
|
||||
CREATE UNIQUE INDEX index_dast_profile_schedules_on_dast_profile_id ON dast_profile_schedules USING btree (dast_profile_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_dast_profile_schedules_on_project_id_and_dast_profile_id ON dast_profile_schedules USING btree (project_id, dast_profile_id);
|
||||
CREATE INDEX index_dast_profile_schedules_on_project_id ON dast_profile_schedules USING btree (project_id);
|
||||
|
||||
CREATE INDEX index_dast_profile_schedules_on_user_id ON dast_profile_schedules USING btree (user_id);
|
||||
|
||||
|
|
|
@ -95,7 +95,7 @@ GitLab provides a series of [CI templates that you can include in your project](
|
|||
To automate deployments of your application to your [Amazon Elastic Container Service](https://aws.amazon.com/ecs/) (AWS ECS)
|
||||
cluster, you can `include` the `AWS/Deploy-ECS.gitlab-ci.yml` template in your `.gitlab-ci.yml` file.
|
||||
|
||||
GitLab also provides [Docker images](https://gitlab.com/gitlab-org/cloud-deploy/-/tree/master/aws) that can be used in your `gitlab-ci.yml` file to simplify working with AWS:
|
||||
GitLab also provides [Docker images](https://gitlab.com/gitlab-org/cloud-deploy/-/tree/master/aws) that can be used in your `.gitlab-ci.yml` file to simplify working with AWS:
|
||||
|
||||
- Use `registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest` to use AWS CLI commands.
|
||||
- Use `registry.gitlab.com/gitlab-org/cloud-deploy/aws-ecs:latest` to deploy your application to AWS ECS.
|
||||
|
|
|
@ -136,10 +136,10 @@ connect the CD project to your development projects by using [multi-project pipe
|
|||
|
||||
A `.gitlab-ci.yml` may contain rules to deploy an application to the production server. This
|
||||
deployment usually runs automatically after pushing a merge request. To prevent developers from
|
||||
changing the `gitlab-ci.yml`, you can define it in a different repository. The configuration can
|
||||
changing the `.gitlab-ci.yml`, you can define it in a different repository. The configuration can
|
||||
reference a file in another project with a completely different set of permissions (similar to
|
||||
[separating a project for deployments](#separate-project-for-deployments)).
|
||||
In this scenario, the `gitlab-ci.yml` is publicly accessible, but can only be edited by users with
|
||||
In this scenario, the `.gitlab-ci.yml` is publicly accessible, but can only be edited by users with
|
||||
appropriate permissions in the other project.
|
||||
|
||||
For more information, see [Custom CI/CD configuration path](../pipelines/settings.md#specify-a-custom-cicd-configuration-file).
|
||||
|
|
|
@ -135,5 +135,5 @@ to switch to a different deployment. Both deployments are running in parallel, a
|
|||
can be switched to at any time.
|
||||
|
||||
An [example deployable application](https://gitlab.com/gl-release/blue-green-example)
|
||||
is available, with a [`gitlab-ci.yml` CI/CD configuration file](https://gitlab.com/gl-release/blue-green-example/blob/master/.gitlab-ci.yml)
|
||||
is available, with a [`.gitlab-ci.yml` CI/CD configuration file](https://gitlab.com/gl-release/blue-green-example/blob/master/.gitlab-ci.yml)
|
||||
that demonstrates blue-green deployments.
|
||||
|
|
|
@ -177,7 +177,7 @@ You can find the play button in the pipelines, environments, deployments, and jo
|
|||
|
||||
If you are deploying to a [Kubernetes cluster](../../user/project/clusters/index.md)
|
||||
associated with your project, you can configure these deployments from your
|
||||
`gitlab-ci.yml` file.
|
||||
`.gitlab-ci.yml` file.
|
||||
|
||||
NOTE:
|
||||
Kubernetes configuration isn't supported for Kubernetes clusters that are
|
||||
|
|
|
@ -251,7 +251,7 @@ To protect a group-level environment:
|
|||
|
||||
1. Make sure your environments have the correct
|
||||
[`deployment_tier`](index.md#deployment-tier-of-environments) defined in
|
||||
`gitlab-ci.yml`.
|
||||
`.gitlab-ci.yml`.
|
||||
1. Configure the group-level protected environments via the
|
||||
[REST API](../../api/group_protected_environments.md).
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ reflected in the CI lint. It displays the same results as the existing [CI Lint
|
|||
> - [Moved to **CI/CD > Editor**](https://gitlab.com/gitlab-org/gitlab/-/issues/263141) in GitLab 13.7.
|
||||
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/290117) in GitLab 13.12.
|
||||
|
||||
To view a visualization of your `gitlab-ci.yml` configuration, in your project,
|
||||
To view a visualization of your `.gitlab-ci.yml` configuration, in your project,
|
||||
go to **CI/CD > Editor**, and then select the **Visualize** tab. The
|
||||
visualization shows all stages and jobs. Any [`needs`](../yaml/index.md#needs)
|
||||
relationships are displayed as lines connecting jobs together, showing the
|
||||
|
|
|
@ -29,7 +29,7 @@ with your editor of choice.
|
|||
### Verify syntax with CI Lint tool
|
||||
|
||||
The [CI Lint tool](lint.md) is a simple way to ensure the syntax of a CI/CD configuration
|
||||
file is correct. Paste in full `gitlab-ci.yml` files or individual jobs configuration,
|
||||
file is correct. Paste in full `.gitlab-ci.yml` files or individual jobs configuration,
|
||||
to verify the basic syntax.
|
||||
|
||||
When a `.gitlab-ci.yml` file is present in a project, you can also use the CI Lint
|
||||
|
@ -49,7 +49,7 @@ and check if their values are what you expect.
|
|||
|
||||
## GitLab CI/CD documentation
|
||||
|
||||
The [complete `gitlab-ci.yml` reference](yaml/index.md) contains a full list of
|
||||
The [complete `.gitlab-ci.yml` reference](yaml/index.md) contains a full list of
|
||||
every keyword you may need to use to configure your pipelines.
|
||||
|
||||
You can also look at a large number of pipeline configuration [examples](examples/index.md)
|
||||
|
|
|
@ -386,7 +386,7 @@ does not block triggered pipelines.
|
|||
> [Moved](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/42861) to GitLab Free in 11.4.
|
||||
|
||||
Use `include` to include external YAML files in your CI/CD configuration.
|
||||
You can break down one long `gitlab-ci.yml` file into multiple files to increase readability,
|
||||
You can break down one long `.gitlab-ci.yml` file into multiple files to increase readability,
|
||||
or reduce duplication of the same configuration in multiple places.
|
||||
|
||||
You can also store template files in a central repository and `include` them in projects.
|
||||
|
@ -4483,7 +4483,7 @@ deploy_review_job:
|
|||
|
||||
You can use only integers and strings for the variable's name and value.
|
||||
|
||||
If you define a variable at the top level of the `gitlab-ci.yml` file, it is global,
|
||||
If you define a variable at the top level of the `.gitlab-ci.yml` file, it is global,
|
||||
meaning it applies to all jobs. If you define a variable in a job, it's available
|
||||
to that job only.
|
||||
|
||||
|
|
|
@ -98,6 +98,7 @@ EE: true
|
|||
database records created during Cycle Analytics model spec."
|
||||
- _Any_ contribution from a community member, no matter how small, **may** have
|
||||
a changelog entry regardless of these guidelines if the contributor wants one.
|
||||
- Any [GLEX experiment](experiment_guide/gitlab_experiment.md) changes **should not** have a changelog entry.
|
||||
- [Removing](feature_flags/#changelog) a feature flag, when the new code is retained.
|
||||
|
||||
## Writing good changelog entries
|
||||
|
|
|
@ -74,7 +74,7 @@ If your application utilizes Docker containers you have another option for deplo
|
|||
After your Docker build job completes and your image is added to your container registry, you can use the image as a
|
||||
[service](../../../ci/services/index.md).
|
||||
|
||||
By using service definitions in your `gitlab-ci.yml`, you can scan services with the DAST analyzer.
|
||||
By using service definitions in your `.gitlab-ci.yml`, you can scan services with the DAST analyzer.
|
||||
|
||||
```yaml
|
||||
stages:
|
||||
|
@ -1307,9 +1307,9 @@ dast:
|
|||
By default, DAST downloads all artifacts defined by previous jobs in the pipeline. If
|
||||
your DAST job does not rely on `environment_url.txt` to define the URL under test or any other files created
|
||||
in previous jobs, we recommend you don't download artifacts. To avoid downloading
|
||||
artifacts, add the following to your `gitlab-ci.yml` file:
|
||||
artifacts, add the following to your `.gitlab-ci.yml` file:
|
||||
|
||||
```json
|
||||
```yaml
|
||||
dast:
|
||||
dependencies: []
|
||||
```
|
||||
|
|
|
@ -111,7 +111,7 @@ example of such a transfer:
|
|||
GitLab provides a [vendored template](../../../ci/yaml/index.md#includetemplate)
|
||||
to ease this process.
|
||||
|
||||
This template should be used in a new, empty project, with a `gitlab-ci.yml` file containing:
|
||||
This template should be used in a new, empty project, with a `.gitlab-ci.yml` file containing:
|
||||
|
||||
```yaml
|
||||
include:
|
||||
|
|
|
@ -316,7 +316,7 @@ The optional `runtime` parameter can refer to one of the following runtime alias
|
|||
| `openfaas/classic/python3` | OpenFaaS |
|
||||
| `openfaas/classic/ruby` | OpenFaaS |
|
||||
|
||||
After the `gitlab-ci.yml` template has been added and the `serverless.yml` file
|
||||
After the `.gitlab-ci.yml` template has been added and the `serverless.yml` file
|
||||
has been created, pushing a commit to your project results in a CI pipeline
|
||||
being executed which deploys each function as a Knative service. After the
|
||||
deploy stage has finished, additional details for the function display
|
||||
|
|
|
@ -129,7 +129,7 @@ The `source` is ignored if the path does not follow this pattern. The parser ass
|
|||
|
||||
### JavaScript example
|
||||
|
||||
The following [`gitlab-ci.yml`](../../../ci/yaml/index.md) example uses [Mocha](https://mochajs.org/)
|
||||
The following [`.gitlab-ci.yml`](../../../ci/yaml/index.md) example uses [Mocha](https://mochajs.org/)
|
||||
JavaScript testing and [nyc](https://github.com/istanbuljs/nyc) coverage-tooling to
|
||||
generate the coverage artifact:
|
||||
|
||||
|
@ -147,7 +147,7 @@ test:
|
|||
|
||||
#### Maven example
|
||||
|
||||
The following [`gitlab-ci.yml`](../../../ci/yaml/index.md) example for Java or Kotlin uses [Maven](https://maven.apache.org/)
|
||||
The following [`.gitlab-ci.yml`](../../../ci/yaml/index.md) example for Java or Kotlin uses [Maven](https://maven.apache.org/)
|
||||
to build the project and [JaCoCo](https://www.eclemma.org/jacoco/) coverage-tooling to
|
||||
generate the coverage artifact.
|
||||
You can check the [Docker image configuration and scripts](https://gitlab.com/haynes/jacoco2cobertura) if you want to build your own image.
|
||||
|
@ -185,7 +185,7 @@ coverage-jdk11:
|
|||
|
||||
#### Gradle example
|
||||
|
||||
The following [`gitlab-ci.yml`](../../../ci/yaml/index.md) example for Java or Kotlin uses [Gradle](https://gradle.org/)
|
||||
The following [`.gitlab-ci.yml`](../../../ci/yaml/index.md) example for Java or Kotlin uses [Gradle](https://gradle.org/)
|
||||
to build the project and [JaCoCo](https://www.eclemma.org/jacoco/) coverage-tooling to
|
||||
generate the coverage artifact.
|
||||
You can check the [Docker image configuration and scripts](https://gitlab.com/haynes/jacoco2cobertura) if you want to build your own image.
|
||||
|
@ -223,7 +223,7 @@ coverage-jdk11:
|
|||
|
||||
### Python example
|
||||
|
||||
The following [`gitlab-ci.yml`](../../../ci/yaml/index.md) example for Python uses [pytest-cov](https://pytest-cov.readthedocs.io/) to collect test coverage data and [coverage.py](https://coverage.readthedocs.io/) to convert the report to use full relative paths.
|
||||
The following [`.gitlab-ci.yml`](../../../ci/yaml/index.md) example for Python uses [pytest-cov](https://pytest-cov.readthedocs.io/) to collect test coverage data and [coverage.py](https://coverage.readthedocs.io/) to convert the report to use full relative paths.
|
||||
The information isn't displayed without the conversion.
|
||||
|
||||
This example assumes that the code for your package is in `src/` and your tests are in `tests.py`:
|
||||
|
@ -243,7 +243,7 @@ run tests:
|
|||
|
||||
### C/C++ example
|
||||
|
||||
The following [`gitlab-ci.yml`](../../../ci/yaml/index.md) example for C/C++ with
|
||||
The following [`.gitlab-ci.yml`](../../../ci/yaml/index.md) example for C/C++ with
|
||||
`gcc` or `g++` as the compiler uses [`gcovr`](https://gcovr.com/en/stable/) to generate the coverage
|
||||
output file in Cobertura XML format.
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ To create a GitLab Pages website:
|
|||
|
||||
| Document | Description |
|
||||
| -------- | ----------- |
|
||||
| [Create a `gitlab-ci.yml` file from scratch](getting_started/pages_from_scratch.md) | Add a Pages site to an existing project. Learn how to create and configure your own CI file. |
|
||||
| [Create a `.gitlab-ci.yml` file from scratch](getting_started/pages_from_scratch.md) | Add a Pages site to an existing project. Learn how to create and configure your own CI file. |
|
||||
| [Use a `.gitlab-ci.yml` template](getting_started/pages_ci_cd_template.md) | Add a Pages site to an existing project. Use a pre-populated CI template file. |
|
||||
| [Fork a sample project](getting_started/pages_forked_sample_project.md) | Create a new project with Pages already configured by forking a sample project. |
|
||||
| [Use a project template](getting_started/pages_new_project_template.md) | Create a new project with Pages already configured by using a template. |
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 16 KiB |
BIN
doc/user/project/releases/img/deploy_freeze_v14_3.png
Normal file
BIN
doc/user/project/releases/img/deploy_freeze_v14_3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
|
@ -186,7 +186,8 @@ To subscribe to notifications for releases:
|
|||
|
||||
## Prevent unintentional releases by setting a deploy freeze
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29382) in GitLab 13.0.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29382) in GitLab 13.0.
|
||||
> - The ability to delete freeze periods through the UI was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/212451) in GitLab 14.3.
|
||||
|
||||
Prevent unintended production releases during a period of time you specify by
|
||||
setting a [*deploy freeze* period](../../../ci/environments/deployment_safety.md).
|
||||
|
@ -199,7 +200,7 @@ If the job that's executing is within a freeze period, GitLab CI/CD creates an e
|
|||
variable named `$CI_DEPLOY_FREEZE`.
|
||||
|
||||
To prevent the deployment job from executing, create a `rules` entry in your
|
||||
`gitlab-ci.yml`, for example:
|
||||
`.gitlab-ci.yml`, for example:
|
||||
|
||||
```yaml
|
||||
deploy_to_production:
|
||||
|
@ -219,11 +220,8 @@ To set a deploy freeze window in the UI, complete these steps:
|
|||
1. Click **Add deploy freeze** to open the deploy freeze modal.
|
||||
1. Enter the start time, end time, and timezone of the desired deploy freeze period.
|
||||
1. Click **Add deploy freeze** in the modal.
|
||||
1. After the deploy freeze is saved, you can edit it by selecting the edit button (**{pencil}**).
|
||||
![Deploy freeze modal for setting a deploy freeze period](img/deploy_freeze_v13_10.png)
|
||||
|
||||
WARNING:
|
||||
To delete a deploy freeze, use the [Freeze Periods API](../../../api/freeze_periods.md).
|
||||
1. After the deploy freeze is saved, you can edit it by selecting the edit button (**{pencil}**) and remove it by selecting the delete button (**{remove}**).
|
||||
![Deploy freeze modal for setting a deploy freeze period](img/deploy_freeze_v14_3.png)
|
||||
|
||||
If a project contains multiple freeze periods, all periods apply. If they overlap, the freeze covers the
|
||||
complete overlapping period.
|
||||
|
|
|
@ -6,8 +6,40 @@ module Gitlab
|
|||
VALID_SYNTAX_SAMPLE_TIME_ZONE = 'UTC'
|
||||
VALID_SYNTAX_SAMPLE_CRON = '* * * * *'
|
||||
|
||||
def self.parse_natural(expression, cron_timezone = 'UTC')
|
||||
new(Fugit::Nat.parse(expression)&.original, cron_timezone)
|
||||
class << self
|
||||
def parse_natural(expression, cron_timezone = 'UTC')
|
||||
new(Fugit::Nat.parse(expression)&.original, cron_timezone)
|
||||
end
|
||||
|
||||
# This method generates compatible expressions that can be
|
||||
# parsed by Fugit::Nat.parse to generate a cron line.
|
||||
# It takes start date of the cron and cadence in the following format:
|
||||
# cadence = {
|
||||
# unit: 'day/week/month/year'
|
||||
# duration: 1
|
||||
# }
|
||||
def parse_natural_with_timestamp(starts_at, cadence)
|
||||
case cadence[:unit]
|
||||
when 'day' # Currently supports only 'every 1 day'.
|
||||
"#{starts_at.min} #{starts_at.hour} * * *"
|
||||
when 'week' # Currently supports only 'every 1 week'.
|
||||
"#{starts_at.min} #{starts_at.hour} * * #{starts_at.wday}"
|
||||
when 'month'
|
||||
unless [1, 3, 6, 12].include?(cadence[:duration])
|
||||
raise NotImplementedError, "The cadence #{cadence} is not supported"
|
||||
end
|
||||
|
||||
"#{starts_at.min} #{starts_at.hour} #{starts_at.mday} #{fall_in_months(cadence[:duration], starts_at)} *"
|
||||
when 'year' # Currently supports only 'every 1 year'.
|
||||
"#{starts_at.min} #{starts_at.hour} #{starts_at.mday} #{starts_at.month} *"
|
||||
else
|
||||
raise NotImplementedError, "The cadence unit #{cadence[:unit]} is not implemented"
|
||||
end
|
||||
end
|
||||
|
||||
def fall_in_months(offset, start_date)
|
||||
(1..(12 / offset)).map { |i| start_date.next_month(offset * i).month }.join(',')
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(cron, cron_timezone = 'UTC')
|
||||
|
|
|
@ -11068,6 +11068,18 @@ msgstr ""
|
|||
msgid "DeployFreeze|Add deploy freeze"
|
||||
msgstr ""
|
||||
|
||||
msgid "DeployFreeze|Delete"
|
||||
msgstr ""
|
||||
|
||||
msgid "DeployFreeze|Delete deploy freeze?"
|
||||
msgstr ""
|
||||
|
||||
msgid "DeployFreeze|Delete freeze period"
|
||||
msgstr ""
|
||||
|
||||
msgid "DeployFreeze|Deploy freeze from %{start} to %{end} in %{timezone} will be removed. Are you sure?"
|
||||
msgstr ""
|
||||
|
||||
msgid "DeployFreeze|Edit"
|
||||
msgstr ""
|
||||
|
||||
|
@ -13262,6 +13274,9 @@ msgstr ""
|
|||
msgid "Error: Unable to create deploy freeze"
|
||||
msgstr ""
|
||||
|
||||
msgid "Error: Unable to delete deploy freeze"
|
||||
msgstr ""
|
||||
|
||||
msgid "Error: Unable to find AWS role for current user"
|
||||
msgstr ""
|
||||
|
||||
|
@ -28991,6 +29006,9 @@ msgstr ""
|
|||
msgid "Runners|Windows 2019 Shell with manual scaling and optional scheduling. Non-spot. Default choice for Windows Shell executor."
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|You are about to change this instance runner to a project runner. This operation is not reversible. Are you sure you want to continue?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|You can set up a specific runner to be used by multiple projects but you cannot make this a shared runner."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -311,23 +311,42 @@ RSpec.describe Projects::PipelinesController do
|
|||
|
||||
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
|
||||
def create_build_with_artifacts(stage, stage_idx, name)
|
||||
create(:ci_build, :artifacts, :tags, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name)
|
||||
def create_build_with_artifacts(stage, stage_idx, name, status)
|
||||
create(:ci_build, :artifacts, :tags, status, user: user, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name)
|
||||
end
|
||||
|
||||
def create_bridge(stage, stage_idx, name, status)
|
||||
create(:ci_bridge, status, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name)
|
||||
end
|
||||
|
||||
before do
|
||||
create_build_with_artifacts('build', 0, 'job1')
|
||||
create_build_with_artifacts('build', 0, 'job2')
|
||||
create_build_with_artifacts('build', 0, 'job1', :failed)
|
||||
create_build_with_artifacts('build', 0, 'job2', :running)
|
||||
create_build_with_artifacts('build', 0, 'job3', :pending)
|
||||
create_bridge('deploy', 1, 'deploy-a', :failed)
|
||||
create_bridge('deploy', 1, 'deploy-b', :created)
|
||||
end
|
||||
|
||||
it 'avoids N+1 database queries', :request_store do
|
||||
control_count = ActiveRecord::QueryRecorder.new { get_pipeline_html }.count
|
||||
it 'avoids N+1 database queries', :request_store, :use_sql_query_cache do
|
||||
# warm up
|
||||
get_pipeline_html
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
|
||||
create_build_with_artifacts('build', 0, 'job3')
|
||||
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
|
||||
get_pipeline_html
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
expect { get_pipeline_html }.not_to exceed_query_limit(control_count)
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
create_build_with_artifacts('build', 0, 'job4', :failed)
|
||||
create_build_with_artifacts('build', 0, 'job5', :running)
|
||||
create_build_with_artifacts('build', 0, 'job6', :pending)
|
||||
create_bridge('deploy', 1, 'deploy-c', :failed)
|
||||
create_bridge('deploy', 1, 'deploy-d', :created)
|
||||
|
||||
expect do
|
||||
get_pipeline_html
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end.not_to exceed_all_query_limit(control)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -283,6 +283,26 @@ RSpec.describe RegistrationsController do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when the registration fails' do
|
||||
let_it_be(:member) { create(:project_member, :invited) }
|
||||
let_it_be(:missing_user_params) do
|
||||
{ username: '', email: member.invite_email, password: 'Any_password' }
|
||||
end
|
||||
|
||||
let_it_be(:user_params) { { user: missing_user_params } }
|
||||
|
||||
let(:session_params) { { invite_email: member.invite_email } }
|
||||
|
||||
subject { post(:create, params: user_params, session: session_params) }
|
||||
|
||||
it 'does not delete the invitation or register the new user' do
|
||||
subject
|
||||
|
||||
expect(member.invite_token).not_to be_nil
|
||||
expect(controller.current_user).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when soft email confirmation is enabled' do
|
||||
before do
|
||||
stub_feature_flags(soft_email_confirmation: true)
|
||||
|
|
|
@ -189,6 +189,16 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
|
|||
end
|
||||
|
||||
context 'email confirmation enabled' do
|
||||
context 'when user is not valid in sign up form' do
|
||||
let(:new_user) { build_stubbed(:user, first_name: '', last_name: '') }
|
||||
|
||||
it 'fails sign up and redirects back to sign up', :aggregate_failures do
|
||||
expect { fill_in_sign_up_form(new_user) }.not_to change { User.count }
|
||||
expect(page).to have_content('prohibited this user from being saved')
|
||||
expect(current_path).to eq(user_registration_path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invite email acceptance', :snowplow do
|
||||
it 'tracks the accepted invite' do
|
||||
fill_in_sign_up_form(new_user)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { GlModal } from '@gitlab/ui';
|
||||
import { createLocalVue, mount } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import DeployFreezeTable from '~/deploy_freeze/components/deploy_freeze_table.vue';
|
||||
|
@ -29,6 +30,8 @@ describe('Deploy freeze table', () => {
|
|||
const findAddDeployFreezeButton = () => wrapper.find('[data-testid="add-deploy-freeze"]');
|
||||
const findEditDeployFreezeButton = () => wrapper.find('[data-testid="edit-deploy-freeze"]');
|
||||
const findDeployFreezeTable = () => wrapper.find('[data-testid="deploy-freeze-table"]');
|
||||
const findDeleteDeployFreezeButton = () => wrapper.find('[data-testid="delete-deploy-freeze"]');
|
||||
const findDeleteDeployFreezeModal = () => wrapper.findComponent(GlModal);
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
|
@ -73,6 +76,29 @@ describe('Deploy freeze table', () => {
|
|||
store.state.freezePeriods[0],
|
||||
);
|
||||
});
|
||||
|
||||
it('displays delete deploy freeze button', () => {
|
||||
expect(findDeleteDeployFreezeButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('confirms a user wants to delete a deploy freeze', async () => {
|
||||
const [{ freezeStart, freezeEnd, cronTimezone }] = store.state.freezePeriods;
|
||||
await findDeleteDeployFreezeButton().trigger('click');
|
||||
const modal = findDeleteDeployFreezeModal();
|
||||
expect(modal.text()).toContain(
|
||||
`Deploy freeze from ${freezeStart} to ${freezeEnd} in ${cronTimezone.formattedTimezone} will be removed.`,
|
||||
);
|
||||
});
|
||||
|
||||
it('deletes the freeze period on confirmation', async () => {
|
||||
await findDeleteDeployFreezeButton().trigger('click');
|
||||
const modal = findDeleteDeployFreezeModal();
|
||||
modal.vm.$emit('primary');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
'deleteFreezePeriod',
|
||||
store.state.freezePeriods[0],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ jest.mock('~/api.js');
|
|||
jest.mock('~/flash.js');
|
||||
|
||||
describe('deploy freeze store actions', () => {
|
||||
const freezePeriodFixture = freezePeriodsFixture[0];
|
||||
let mock;
|
||||
let state;
|
||||
|
||||
|
@ -24,6 +25,7 @@ describe('deploy freeze store actions', () => {
|
|||
Api.freezePeriods.mockResolvedValue({ data: freezePeriodsFixture });
|
||||
Api.createFreezePeriod.mockResolvedValue();
|
||||
Api.updateFreezePeriod.mockResolvedValue();
|
||||
Api.deleteFreezePeriod.mockResolvedValue();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -195,4 +197,46 @@ describe('deploy freeze store actions', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteFreezePeriod', () => {
|
||||
it('dispatch correct actions on deleting a freeze period', () => {
|
||||
testAction(
|
||||
actions.deleteFreezePeriod,
|
||||
freezePeriodFixture,
|
||||
state,
|
||||
[
|
||||
{ type: 'REQUEST_DELETE_FREEZE_PERIOD', payload: freezePeriodFixture.id },
|
||||
{ type: 'RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS', payload: freezePeriodFixture.id },
|
||||
],
|
||||
[],
|
||||
() =>
|
||||
expect(Api.deleteFreezePeriod).toHaveBeenCalledWith(
|
||||
state.projectId,
|
||||
freezePeriodFixture.id,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should show flash error and set error in state on delete failure', () => {
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
const error = new Error();
|
||||
Api.deleteFreezePeriod.mockRejectedValue(error);
|
||||
|
||||
testAction(
|
||||
actions.deleteFreezePeriod,
|
||||
freezePeriodFixture,
|
||||
state,
|
||||
[
|
||||
{ type: 'REQUEST_DELETE_FREEZE_PERIOD', payload: freezePeriodFixture.id },
|
||||
{ type: 'RECEIVE_DELETE_FREEZE_PERIOD_ERROR', payload: freezePeriodFixture.id },
|
||||
],
|
||||
[],
|
||||
() => {
|
||||
expect(createFlash).toHaveBeenCalled();
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith('[gitlab] Unable to delete deploy freeze:', error);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,9 +28,9 @@ describe('Deploy freeze mutations', () => {
|
|||
describe('RECEIVE_FREEZE_PERIODS_SUCCESS', () => {
|
||||
it('should set freeze periods and format timezones from identifiers to names', () => {
|
||||
const timezoneNames = {
|
||||
'Europe/Berlin': 'Berlin',
|
||||
'Etc/UTC': 'UTC',
|
||||
'America/New_York': 'Eastern Time (US & Canada)',
|
||||
'Europe/Berlin': '[UTC 2] Berlin',
|
||||
'Etc/UTC': '[UTC 0] UTC',
|
||||
'America/New_York': '[UTC -4] Eastern Time (US & Canada)',
|
||||
};
|
||||
|
||||
mutations[types.RECEIVE_FREEZE_PERIODS_SUCCESS](stateCopy, freezePeriodsFixture);
|
||||
|
|
|
@ -31,7 +31,6 @@ describe('Environment item', () => {
|
|||
factory({
|
||||
propsData: {
|
||||
model: environment,
|
||||
canReadEnvironment: true,
|
||||
tableData,
|
||||
},
|
||||
});
|
||||
|
@ -135,7 +134,6 @@ describe('Environment item', () => {
|
|||
factory({
|
||||
propsData: {
|
||||
model: environmentWithoutDeployable,
|
||||
canReadEnvironment: true,
|
||||
tableData,
|
||||
},
|
||||
});
|
||||
|
@ -161,7 +159,6 @@ describe('Environment item', () => {
|
|||
factory({
|
||||
propsData: {
|
||||
model: environmentWithoutUpcomingDeployment,
|
||||
canReadEnvironment: true,
|
||||
tableData,
|
||||
},
|
||||
});
|
||||
|
@ -177,7 +174,6 @@ describe('Environment item', () => {
|
|||
factory({
|
||||
propsData: {
|
||||
model: environment,
|
||||
canReadEnvironment: true,
|
||||
tableData,
|
||||
shouldShowAutoStopDate: true,
|
||||
},
|
||||
|
@ -205,7 +201,6 @@ describe('Environment item', () => {
|
|||
...environment,
|
||||
auto_stop_at: futureDate,
|
||||
},
|
||||
canReadEnvironment: true,
|
||||
tableData,
|
||||
shouldShowAutoStopDate: true,
|
||||
},
|
||||
|
@ -241,7 +236,6 @@ describe('Environment item', () => {
|
|||
...environment,
|
||||
auto_stop_at: pastDate,
|
||||
},
|
||||
canReadEnvironment: true,
|
||||
tableData,
|
||||
shouldShowAutoStopDate: true,
|
||||
},
|
||||
|
@ -360,7 +354,6 @@ describe('Environment item', () => {
|
|||
factory({
|
||||
propsData: {
|
||||
model: folder,
|
||||
canReadEnvironment: true,
|
||||
tableData,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -28,7 +28,6 @@ describe('Environment table', () => {
|
|||
factory({
|
||||
propsData: {
|
||||
environments: [folder],
|
||||
canReadEnvironment: true,
|
||||
...eeOnlyProps,
|
||||
},
|
||||
});
|
||||
|
@ -50,7 +49,6 @@ describe('Environment table', () => {
|
|||
await factory({
|
||||
propsData: {
|
||||
environments: [mockItem],
|
||||
canReadEnvironment: true,
|
||||
userCalloutsPath: '/callouts',
|
||||
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
|
||||
helpCanaryDeploymentsPath: 'help/canary-deployments',
|
||||
|
@ -78,7 +76,6 @@ describe('Environment table', () => {
|
|||
propsData: {
|
||||
environments: [mockItem],
|
||||
canCreateDeployment: false,
|
||||
canReadEnvironment: true,
|
||||
userCalloutsPath: '/callouts',
|
||||
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
|
||||
helpCanaryDeploymentsPath: 'help/canary-deployments',
|
||||
|
@ -114,7 +111,6 @@ describe('Environment table', () => {
|
|||
propsData: {
|
||||
environments: [mockItem],
|
||||
canCreateDeployment: false,
|
||||
canReadEnvironment: true,
|
||||
userCalloutsPath: '/callouts',
|
||||
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
|
||||
helpCanaryDeploymentsPath: 'help/canary-deployments',
|
||||
|
@ -151,7 +147,6 @@ describe('Environment table', () => {
|
|||
factory({
|
||||
propsData: {
|
||||
environments: [mockItem],
|
||||
canReadEnvironment: true,
|
||||
userCalloutsPath: '/callouts',
|
||||
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
|
||||
helpCanaryDeploymentsPath: 'help/canary-deployments',
|
||||
|
@ -179,7 +174,6 @@ describe('Environment table', () => {
|
|||
propsData: {
|
||||
environments: [mockItem],
|
||||
canCreateDeployment: false,
|
||||
canReadEnvironment: true,
|
||||
userCalloutsPath: '/callouts',
|
||||
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
|
||||
helpCanaryDeploymentsPath: 'help/canary-deployments',
|
||||
|
@ -230,7 +224,6 @@ describe('Environment table', () => {
|
|||
factory({
|
||||
propsData: {
|
||||
environments: mockItems,
|
||||
canReadEnvironment: true,
|
||||
...eeOnlyProps,
|
||||
},
|
||||
});
|
||||
|
@ -296,7 +289,6 @@ describe('Environment table', () => {
|
|||
factory({
|
||||
propsData: {
|
||||
environments: mockItems,
|
||||
canReadEnvironment: true,
|
||||
...eeOnlyProps,
|
||||
},
|
||||
});
|
||||
|
@ -335,7 +327,6 @@ describe('Environment table', () => {
|
|||
factory({
|
||||
propsData: {
|
||||
environments: mockItems,
|
||||
canReadEnvironment: true,
|
||||
...eeOnlyProps,
|
||||
},
|
||||
});
|
||||
|
@ -364,7 +355,6 @@ describe('Environment table', () => {
|
|||
factory({
|
||||
propsData: {
|
||||
environments: mockItems,
|
||||
canReadEnvironment: true,
|
||||
...eeOnlyProps,
|
||||
},
|
||||
});
|
||||
|
@ -415,7 +405,6 @@ describe('Environment table', () => {
|
|||
factory({
|
||||
propsData: {
|
||||
environments: mockItems,
|
||||
canReadEnvironment: true,
|
||||
...eeOnlyProps,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -20,7 +20,6 @@ describe('Environment', () => {
|
|||
const mockData = {
|
||||
endpoint: 'environments.json',
|
||||
canCreateEnvironment: true,
|
||||
canReadEnvironment: true,
|
||||
newEnvironmentPath: 'environments/new',
|
||||
helpPagePath: 'help',
|
||||
userCalloutsPath: '/callouts',
|
||||
|
|
|
@ -44,7 +44,6 @@ describe('Environments detail header component', () => {
|
|||
TimeAgo,
|
||||
},
|
||||
propsData: {
|
||||
canReadEnvironment: false,
|
||||
canAdminEnvironment: false,
|
||||
canUpdateEnvironment: false,
|
||||
canStopEnvironment: false,
|
||||
|
@ -60,7 +59,7 @@ describe('Environments detail header component', () => {
|
|||
|
||||
describe('default state with minimal access', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({ props: { environment: createEnvironment() } });
|
||||
createWrapper({ props: { environment: createEnvironment({ externalUrl: null }) } });
|
||||
});
|
||||
|
||||
it('displays the environment name', () => {
|
||||
|
@ -164,7 +163,6 @@ describe('Environments detail header component', () => {
|
|||
createWrapper({
|
||||
props: {
|
||||
environment: createEnvironment({ hasTerminals: true, externalUrl }),
|
||||
canReadEnvironment: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -178,8 +176,7 @@ describe('Environments detail header component', () => {
|
|||
beforeEach(() => {
|
||||
createWrapper({
|
||||
props: {
|
||||
environment: createEnvironment(),
|
||||
canReadEnvironment: true,
|
||||
environment: createEnvironment({ metricsUrl: 'my metrics url' }),
|
||||
metricsPath,
|
||||
},
|
||||
});
|
||||
|
@ -195,7 +192,6 @@ describe('Environments detail header component', () => {
|
|||
createWrapper({
|
||||
props: {
|
||||
environment: createEnvironment(),
|
||||
canReadEnvironment: true,
|
||||
canAdminEnvironment: true,
|
||||
canStopEnvironment: true,
|
||||
canUpdateEnvironment: true,
|
||||
|
|
|
@ -11,7 +11,6 @@ describe('Environments Folder View', () => {
|
|||
const mockData = {
|
||||
endpoint: 'environments.json',
|
||||
folderName: 'review',
|
||||
canReadEnvironment: true,
|
||||
cssContainerClass: 'container',
|
||||
userCalloutsPath: '/callouts',
|
||||
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
|
||||
|
|
|
@ -14,7 +14,6 @@ describe('Environments Folder View', () => {
|
|||
const mockData = {
|
||||
endpoint: 'environments.json',
|
||||
folderName: 'review',
|
||||
canReadEnvironment: true,
|
||||
cssContainerClass: 'container',
|
||||
userCalloutsPath: '/callouts',
|
||||
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import ImageViewer from '~/repository/components/blob_viewers/image_viewer.vue';
|
||||
|
||||
describe('Image Viewer', () => {
|
||||
let wrapper;
|
||||
|
||||
const propsData = {
|
||||
url: 'some/image.png',
|
||||
alt: 'image.png',
|
||||
};
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = shallowMount(ImageViewer, { propsData });
|
||||
};
|
||||
|
||||
const findImage = () => wrapper.find('[data-testid="image"]');
|
||||
|
||||
it('renders a Source Editor component', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findImage().exists()).toBe(true);
|
||||
expect(findImage().attributes('src')).toBe(propsData.url);
|
||||
expect(findImage().attributes('alt')).toBe(propsData.alt);
|
||||
});
|
||||
});
|
|
@ -9,6 +9,6 @@ describe('search/highlight_blob_search_result', () => {
|
|||
it('highlights lines with search term occurrence', () => {
|
||||
setHighlightClass(searchKeyword);
|
||||
|
||||
expect(document.querySelectorAll('.blob-result .hll').length).toBe(4);
|
||||
expect(document.querySelectorAll('.js-blob-result .hll').length).toBe(4);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,18 +20,20 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
|
|||
let!(:issue1) { create(:issue, project: project, labels: [label], relative_position: 10) }
|
||||
let!(:issue2) { create(:issue, project: project, labels: [label, label2], relative_position: 12) }
|
||||
let!(:issue3) { create(:issue, project: project, labels: [label, label3], relative_position: 10) }
|
||||
let!(:issue4) { create(:issue, project: project, labels: [label], relative_position: nil) }
|
||||
|
||||
it 'returns the issues in the correct order' do
|
||||
it 'returns issues in the correct order with non-nil relative positions', :aggregate_failures do
|
||||
# by relative_position and then ID
|
||||
issues = resolve_board_list_issues
|
||||
result = resolve_board_list_issues
|
||||
|
||||
expect(issues.map(&:id)).to eq [issue3.id, issue1.id, issue2.id]
|
||||
expect(result.map(&:id)).to eq [issue3.id, issue1.id, issue2.id, issue4.id]
|
||||
expect(result.map(&:relative_position)).not_to include(nil)
|
||||
end
|
||||
|
||||
it 'finds only issues matching filters' do
|
||||
result = resolve_board_list_issues(args: { filters: { label_name: [label.title], not: { label_name: [label2.title] } } })
|
||||
|
||||
expect(result).to match_array([issue1, issue3])
|
||||
expect(result).to match_array([issue1, issue3, issue4])
|
||||
end
|
||||
|
||||
it 'finds only issues matching search param' do
|
||||
|
@ -49,7 +51,7 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
|
|||
it 'accepts assignee wildcard id NONE' do
|
||||
result = resolve_board_list_issues(args: { filters: { assignee_wildcard_id: 'NONE' } })
|
||||
|
||||
expect(result).to match_array([issue1, issue2, issue3])
|
||||
expect(result).to match_array([issue1, issue2, issue3, issue4])
|
||||
end
|
||||
|
||||
it 'accepts assignee wildcard id ANY' do
|
||||
|
@ -89,6 +91,6 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
|
|||
end
|
||||
|
||||
def resolve_board_list_issues(args: {}, current_user: user)
|
||||
resolve(described_class, obj: list, args: args, ctx: { current_user: current_user })
|
||||
resolve(described_class, obj: list, args: args, ctx: { current_user: current_user }).items
|
||||
end
|
||||
end
|
||||
|
|
|
@ -43,7 +43,6 @@ RSpec.describe EnvironmentHelper do
|
|||
external_url: environment.external_url,
|
||||
can_update_environment: true,
|
||||
can_destroy_environment: true,
|
||||
can_read_environment: true,
|
||||
can_stop_environment: true,
|
||||
can_admin_environment: true,
|
||||
environment_metrics_path: environment_metrics_path(environment),
|
||||
|
|
|
@ -297,4 +297,65 @@ RSpec.describe Gitlab::Ci::CronParser do
|
|||
it { is_expected.to eq(true) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.parse_natural', :aggregate_failures do
|
||||
let(:cron_line) { described_class.parse_natural_with_timestamp(time, { unit: 'day', duration: 1 }) }
|
||||
let(:time) { Time.parse('Mon, 30 Aug 2021 06:29:44.067132000 UTC +00:00') }
|
||||
let(:hours) { Fugit::Cron.parse(cron_line).hours }
|
||||
let(:minutes) { Fugit::Cron.parse(cron_line).minutes }
|
||||
let(:weekdays) { Fugit::Cron.parse(cron_line).weekdays.first }
|
||||
let(:months) { Fugit::Cron.parse(cron_line).months }
|
||||
|
||||
context 'when repeat cycle is day' do
|
||||
it 'generates daily cron expression', :aggregate_failures do
|
||||
expect(hours).to include time.hour
|
||||
expect(minutes).to include time.min
|
||||
end
|
||||
end
|
||||
|
||||
context 'when repeat cycle is week' do
|
||||
let(:cron_line) { described_class.parse_natural_with_timestamp(time, { unit: 'week', duration: 1 }) }
|
||||
|
||||
it 'generates weekly cron expression', :aggregate_failures do
|
||||
expect(hours).to include time.hour
|
||||
expect(minutes).to include time.min
|
||||
expect(weekdays).to include time.wday
|
||||
end
|
||||
end
|
||||
|
||||
context 'when repeat cycle is month' do
|
||||
let(:cron_line) { described_class.parse_natural_with_timestamp(time, { unit: 'month', duration: 3 }) }
|
||||
|
||||
it 'generates monthly cron expression', :aggregate_failures do
|
||||
expect(minutes).to include time.min
|
||||
expect(months).to include time.month
|
||||
end
|
||||
|
||||
context 'when an unsupported duration is specified' do
|
||||
subject { described_class.parse_natural_with_timestamp(time, { unit: 'month', duration: 7 }) }
|
||||
|
||||
it 'raises an exception' do
|
||||
expect { subject }.to raise_error(NotImplementedError, 'The cadence {:unit=>"month", :duration=>7} is not supported')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when repeat cycle is year' do
|
||||
let(:cron_line) { described_class.parse_natural_with_timestamp(time, { unit: 'year', duration: 1 }) }
|
||||
|
||||
it 'generates yearly cron expression', :aggregate_failures do
|
||||
expect(hours).to include time.hour
|
||||
expect(minutes).to include time.min
|
||||
expect(months).to include time.month
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the repeat cycle is not implemented' do
|
||||
subject { described_class.parse_natural_with_timestamp(time, { unit: 'quarterly', duration: 1 }) }
|
||||
|
||||
it 'raises an exception' do
|
||||
expect { subject }.to raise_error(NotImplementedError, 'The cadence unit quarterly is not implemented')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -224,6 +224,7 @@ ci_pipelines:
|
|||
- builds
|
||||
- bridges
|
||||
- processables
|
||||
- generic_commit_statuses
|
||||
- trigger_requests
|
||||
- variables
|
||||
- auto_canceled_by
|
||||
|
|
|
@ -645,6 +645,16 @@ RSpec.describe Member do
|
|||
|
||||
expect(user.authorized_projects.reload).to include(project)
|
||||
end
|
||||
|
||||
it 'does not accept the invite if saving a new user fails' do
|
||||
invalid_user = User.new(first_name: '', last_name: '')
|
||||
|
||||
member.accept_invite! invalid_user
|
||||
|
||||
expect(member.invite_accepted_at).to be_nil
|
||||
expect(member.invite_token).not_to be_nil
|
||||
expect_any_instance_of(Member).not_to receive(:after_accept_invite)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#decline_invite!" do
|
||||
|
|
|
@ -1576,6 +1576,14 @@ RSpec.describe Note do
|
|||
expect(note.post_processed_cache_key).to eq("#{note.cache_key}:#{note.author.cache_key}")
|
||||
end
|
||||
|
||||
context 'when note has no author' do
|
||||
let(:note) { build(:note, author: nil) }
|
||||
|
||||
it 'returns cache key only' do
|
||||
expect(note.post_processed_cache_key).to eq("#{note.cache_key}:")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when note has redacted_note_html' do
|
||||
let(:redacted_note_html) { 'redacted note html' }
|
||||
|
||||
|
|
|
@ -48,13 +48,18 @@ RSpec.describe 'get board lists' do
|
|||
issues_data.map { |i| i['title'] }
|
||||
end
|
||||
|
||||
def issue_relative_positions
|
||||
issues_data.map { |i| i['relativePosition'] }
|
||||
end
|
||||
|
||||
shared_examples 'group and project board list issues query' do
|
||||
let!(:board) { create(:board, resource_parent: board_parent) }
|
||||
let!(:label_list) { create(:list, board: board, label: label, position: 10) }
|
||||
let!(:issue1) { create(:issue, project: issue_project, labels: [label, label2], relative_position: 9) }
|
||||
let!(:issue2) { create(:issue, project: issue_project, labels: [label, label2], relative_position: 2) }
|
||||
let!(:issue3) { create(:issue, project: issue_project, labels: [label], relative_position: 9) }
|
||||
let!(:issue4) { create(:issue, project: issue_project, labels: [label2], relative_position: 432) }
|
||||
let!(:issue3) { create(:issue, project: issue_project, labels: [label, label2], relative_position: nil) }
|
||||
let!(:issue4) { create(:issue, project: issue_project, labels: [label], relative_position: 9) }
|
||||
let!(:issue5) { create(:issue, project: issue_project, labels: [label2], relative_position: 432) }
|
||||
|
||||
context 'when the user does not have access to the board' do
|
||||
it 'returns nil' do
|
||||
|
@ -69,10 +74,11 @@ RSpec.describe 'get board lists' do
|
|||
board_parent.add_reporter(user)
|
||||
end
|
||||
|
||||
it 'can access the issues' do
|
||||
it 'can access the issues', :aggregate_failures do
|
||||
post_graphql(query("id: \"#{global_id_of(label_list)}\""), current_user: user)
|
||||
|
||||
expect(issue_titles).to eq([issue2.title, issue1.title])
|
||||
expect(issue_titles).to eq([issue2.title, issue1.title, issue3.title])
|
||||
expect(issue_relative_positions).not_to include(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -311,6 +311,10 @@ RSpec.describe 'getting pipeline information nested in a project' do
|
|||
end
|
||||
|
||||
it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do
|
||||
# create extra statuses
|
||||
create(:generic_commit_status, :pending, name: 'generic-build-a', pipeline: pipeline, stage_idx: 0, stage: 'build')
|
||||
create(:ci_bridge, :failed, name: 'deploy-a', pipeline: pipeline, stage_idx: 2, stage: 'deploy')
|
||||
|
||||
# warm up
|
||||
post_graphql(query, current_user: current_user)
|
||||
|
||||
|
@ -318,9 +322,11 @@ RSpec.describe 'getting pipeline information nested in a project' do
|
|||
post_graphql(query, current_user: current_user)
|
||||
end
|
||||
|
||||
create(:ci_build, name: 'test-a', pipeline: pipeline, stage_idx: 1, stage: 'test')
|
||||
create(:ci_build, name: 'test-b', pipeline: pipeline, stage_idx: 1, stage: 'test')
|
||||
create(:ci_build, name: 'deploy-a', pipeline: pipeline, stage_idx: 2, stage: 'deploy')
|
||||
create(:generic_commit_status, :pending, name: 'generic-build-b', pipeline: pipeline, stage_idx: 0, stage: 'build')
|
||||
create(:ci_build, :failed, name: 'test-a', pipeline: pipeline, stage_idx: 1, stage: 'test')
|
||||
create(:ci_build, :running, name: 'test-b', pipeline: pipeline, stage_idx: 1, stage: 'test')
|
||||
create(:ci_build, :pending, name: 'deploy-b', pipeline: pipeline, stage_idx: 2, stage: 'deploy')
|
||||
create(:ci_bridge, :failed, name: 'deploy-c', pipeline: pipeline, stage_idx: 2, stage: 'deploy')
|
||||
|
||||
expect do
|
||||
post_graphql(query, current_user: current_user)
|
||||
|
|
|
@ -139,4 +139,51 @@ RSpec.describe Boards::Issues::ListService do
|
|||
end
|
||||
# rubocop: enable RSpec/MultipleMemoizedHelpers
|
||||
end
|
||||
|
||||
describe '.initialize_relative_positions' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project, :empty_repo) }
|
||||
let_it_be(:board) { create(:board, project: project) }
|
||||
let_it_be(:backlog) { create(:backlog_list, board: board) }
|
||||
|
||||
let(:issue) { create(:issue, project: project, relative_position: nil) }
|
||||
|
||||
context "when 'Gitlab::Database::read_write?' is true" do
|
||||
before do
|
||||
allow(Gitlab::Database).to receive(:read_write?).and_return(true)
|
||||
end
|
||||
|
||||
context 'user cannot move issues' do
|
||||
it 'does not initialize the relative positions of issues' do
|
||||
described_class.initialize_relative_positions(board, user, [issue])
|
||||
|
||||
expect(issue.relative_position).to eq nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'user can move issues' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
it 'initializes the relative positions of issues' do
|
||||
described_class.initialize_relative_positions(board, user, [issue])
|
||||
|
||||
expect(issue.relative_position).not_to eq nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when 'Gitlab::Database::read_write?' is false" do
|
||||
before do
|
||||
allow(Gitlab::Database).to receive(:read_write?).and_return(false)
|
||||
end
|
||||
|
||||
it 'does not initialize the relative positions of issues' do
|
||||
described_class.initialize_relative_positions(board, user, [issue])
|
||||
|
||||
expect(issue.relative_position).to eq nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue