Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-08-18 18:10:32 +00:00
parent 133b8a64b6
commit bced7d8bcc
85 changed files with 523 additions and 2705 deletions

View File

@ -6,7 +6,7 @@ workflow:
include:
- project: gitlab-org/quality/pipeline-common
ref: 7.2.3
ref: 7.5.0
file:
- /ci/base.gitlab-ci.yml
- /ci/knapsack-report.yml

View File

@ -1 +1 @@
0183d30e998cfab0b4f93fd1aa60200fba8e3771
46892cb4fa5b0e86b7e86fbb7acda5effac412d6

View File

@ -1,262 +0,0 @@
import $ from 'jquery';
import SecretValues from '~/behaviors/secret_values';
import CreateItemDropdown from '~/create_item_dropdown';
import { parseBoolean } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
const ALL_ENVIRONMENTS_STRING = s__('CiVariable|All environments');
function createEnvironmentItem(value) {
return {
title: value === '*' ? ALL_ENVIRONMENTS_STRING : value,
id: value,
text: value === '*' ? s__('CiVariable|* (All environments)') : value,
};
}
export default class VariableList {
constructor({ container, formField, maskableRegex }) {
this.$container = $(container);
this.formField = formField;
this.maskableRegex = new RegExp(maskableRegex);
this.environmentDropdownMap = new WeakMap();
this.inputMap = {
id: {
selector: '.js-ci-variable-input-id',
default: '',
},
variable_type: {
selector: '.js-ci-variable-input-variable-type',
default: 'env_var',
},
key: {
selector: '.js-ci-variable-input-key',
default: '',
},
secret_value: {
selector: '.js-ci-variable-input-value',
default: '',
},
protected: {
selector: '.js-ci-variable-input-protected',
// use `attr` instead of `data` as we don't want the value to be
// converted. we need the value as a string.
default: $('.js-ci-variable-input-protected').attr('data-default'),
},
masked: {
selector: '.js-ci-variable-input-masked',
// use `attr` instead of `data` as we don't want the value to be
// converted. we need the value as a string.
default: $('.js-ci-variable-input-masked').attr('data-default'),
},
environment_scope: {
// We can't use a `.js-` class here because
// deprecated_jquery_dropdown replaces the <input> and doesn't copy over the class
// See https://gitlab.com/gitlab-org/gitlab-foss/issues/42458
selector: `input[name="${this.formField}[variables_attributes][][environment_scope]"]`,
default: '*',
},
_destroy: {
selector: '.js-ci-variable-input-destroy',
default: '',
},
};
this.secretValues = new SecretValues({
container: this.$container[0],
valueSelector: '.js-row:not(:last-child) .js-secret-value',
placeholderSelector: '.js-row:not(:last-child) .js-secret-value-placeholder',
});
}
init() {
this.bindEvents();
this.secretValues.init();
}
bindEvents() {
this.$container.find('.js-row').each((index, rowEl) => {
this.initRow(rowEl);
});
this.$container.on('click', '.js-row-remove-button', (e) => {
e.preventDefault();
this.removeRow($(e.currentTarget).closest('.js-row'));
});
const inputSelector = Object.keys(this.inputMap)
.map((name) => this.inputMap[name].selector)
.join(',');
// Remove any empty rows except the last row
this.$container.on('blur', inputSelector, (e) => {
const $row = $(e.currentTarget).closest('.js-row');
if ($row.is(':not(:last-child)') && !this.checkIfRowTouched($row)) {
this.removeRow($row);
}
});
this.$container.on('input trigger-change', inputSelector, (e) => {
// Always make sure there is an empty last row
const $lastRow = this.$container.find('.js-row').last();
if (this.checkIfRowTouched($lastRow)) {
this.insertRow($lastRow);
}
// If masked, validate value against regex
this.validateMaskability($(e.currentTarget).closest('.js-row'));
});
}
initRow(rowEl) {
const $row = $(rowEl);
// Reset the resizable textarea
$row.find(this.inputMap.secret_value.selector).css('height', '');
const $environmentSelect = $row.find('.js-variable-environment-toggle');
if ($environmentSelect.length) {
const createItemDropdown = new CreateItemDropdown({
$dropdown: $environmentSelect,
defaultToggleLabel: ALL_ENVIRONMENTS_STRING,
fieldName: `${this.formField}[variables_attributes][][environment_scope]`,
getData: (term, callback) => callback(this.getEnvironmentValues()),
createNewItemFromValue: createEnvironmentItem,
onSelect: () => {
// Refresh the other dropdowns in the variable list
// so they have the new value we just picked
this.refreshDropdownData();
$row.find(this.inputMap.environment_scope.selector).trigger('trigger-change');
},
});
// Clear out any data that might have been left-over from the row clone
createItemDropdown.clearDropdown();
this.environmentDropdownMap.set($row[0], createItemDropdown);
}
}
insertRow($row) {
const $rowClone = $row.clone();
$rowClone.removeAttr('data-is-persisted');
// Reset the inputs to their defaults
Object.keys(this.inputMap).forEach((name) => {
const entry = this.inputMap[name];
$rowClone.find(entry.selector).val(entry.default);
});
// Close any dropdowns
$rowClone.find('.dropdown-menu.show').each((index, $dropdown) => {
$dropdown.classList.remove('show');
});
this.initRow($rowClone);
$row.after($rowClone);
}
removeRow(row) {
const $row = $(row);
const isPersisted = parseBoolean($row.attr('data-is-persisted'));
if (isPersisted) {
$row.hide();
$row
// eslint-disable-next-line no-underscore-dangle
.find(this.inputMap._destroy.selector)
.val(true);
} else {
$row.remove();
}
// Refresh the other dropdowns in the variable list
// so any value with the variable deleted is gone
this.refreshDropdownData();
}
checkIfRowTouched($row) {
return Object.keys(this.inputMap).some((name) => {
// Row should not qualify as touched if only switches have been touched
if (['protected', 'masked'].includes(name)) return false;
const entry = this.inputMap[name];
const $el = $row.find(entry.selector);
return $el.length && $el.val() !== entry.default;
});
}
validateMaskability($row) {
const invalidInputClass = 'gl-field-error-outline';
const variableValue = $row.find(this.inputMap.secret_value.selector).val();
const isValueMaskable = this.maskableRegex.test(variableValue) || variableValue === '';
const isMaskedChecked = $row.find(this.inputMap.masked.selector).val() === 'true';
// Show a validation error if the user wants to mask an unmaskable variable value
$row
.find(this.inputMap.secret_value.selector)
.toggleClass(invalidInputClass, isMaskedChecked && !isValueMaskable);
$row
.find('.js-secret-value-placeholder')
.toggleClass(invalidInputClass, isMaskedChecked && !isValueMaskable);
$row.find('.masking-validation-error').toggle(isMaskedChecked && !isValueMaskable);
}
toggleEnableRow(isEnabled = true) {
this.$container.find(this.inputMap.key.selector).attr('disabled', !isEnabled);
this.$container.find('.js-row-remove-button').attr('disabled', !isEnabled);
}
hideValues() {
this.secretValues.updateDom(false);
}
getAllData() {
// Ignore the last empty row because we don't want to try persist
// a blank variable and run into validation problems.
const validRows = this.$container.find('.js-row').toArray().slice(0, -1);
return validRows.map((rowEl) => {
const resultant = {};
Object.keys(this.inputMap).forEach((name) => {
const entry = this.inputMap[name];
const $input = $(rowEl).find(entry.selector);
if ($input.length) {
resultant[name] = $input.val();
}
});
return resultant;
});
}
getEnvironmentValues() {
const valueMap = this.$container
.find(this.inputMap.environment_scope.selector)
.toArray()
.reduce(
(prevValueMap, envInput) => ({
...prevValueMap,
[envInput.value]: envInput.value,
}),
{},
);
return Object.keys(valueMap).map(createEnvironmentItem);
}
refreshDropdownData() {
this.$container.find('.js-row').each((index, rowEl) => {
const environmentDropdown = this.environmentDropdownMap.get(rowEl);
if (environmentDropdown) {
environmentDropdown.refreshData();
}
});
}
}

View File

@ -1,25 +0,0 @@
import $ from 'jquery';
import VariableList from './ci_variable_list';
// Used for the variable list on scheduled pipeline edit page
export default function setupNativeFormVariableList({ container, formField = 'variables' }) {
const $container = $(container);
const variableList = new VariableList({
container: $container,
formField,
});
variableList.init();
// Clear out the names in the empty last row so it
// doesn't get submitted and throw validation errors
$container.closest('form').on('submit trigger-submit', () => {
const $lastRow = $container.find('.js-row').last();
const isTouched = variableList.checkIfRowTouched($lastRow);
if (!isTouched) {
$lastRow.find('input, textarea').attr('name', '');
$lastRow.find('select').attr('name', '');
}
});
}

View File

@ -81,7 +81,7 @@ export default {
this.description = schedule.description;
this.cron = schedule.cron;
this.cronTimezone = schedule.cronTimezone;
this.scheduleRef = schedule.ref;
this.scheduleRef = schedule.ref || this.defaultBranch;
this.variables = variables.map((variable) => {
return {
id: variable.id,

View File

@ -27,10 +27,13 @@ export default {
</script>
<template>
<div>
<gl-icon :name="iconName" />
<div data-testid="pipeline-schedule-target">
<span v-if="refPath">
<gl-icon :name="iconName" />
<gl-link :href="refPath" class="gl-text-gray-900">{{ refDisplay }}</gl-link>
</span>
<span v-else>
{{ s__('PipelineSchedules|None') }}
</span>
</div>
</template>

View File

@ -1,50 +0,0 @@
<script>
import { GlModal } from '@gitlab/ui';
import { __, s__ } from '~/locale';
export default {
components: {
GlModal,
},
props: {
ownershipUrl: {
type: String,
required: true,
},
},
modalId: 'pipeline-take-ownership-modal',
i18n: {
takeOwnership: s__('PipelineSchedules|Take ownership'),
ownershipMessage: s__(
'PipelineSchedules|Only the owner of a pipeline schedule can make changes to it. Do you want to take ownership of this schedule?',
),
cancelLabel: __('Cancel'),
},
computed: {
actionCancel() {
return { text: this.$options.i18n.cancelLabel };
},
actionPrimary() {
return {
text: this.$options.i18n.takeOwnership,
attributes: {
variant: 'confirm',
category: 'primary',
href: this.ownershipUrl,
'data-method': 'post',
},
};
},
},
};
</script>
<template>
<gl-modal
:modal-id="$options.modalId"
:action-primary="actionPrimary"
:action-cancel="actionCancel"
:title="$options.i18n.takeOwnership"
>
<p>{{ $options.i18n.ownershipMessage }}</p>
</gl-modal>
</template>

View File

@ -0,0 +1,25 @@
<script>
import { EVENT_UPDATED_I18N } from '../../constants';
import { getValueByEventTarget } from '../../utils';
import ContributionEventBase from './contribution_event_base.vue';
export default {
name: 'ContributionEventUpdated',
components: { ContributionEventBase },
props: {
event: {
type: Object,
required: true,
},
},
computed: {
message() {
return getValueByEventTarget(EVENT_UPDATED_I18N, this.event);
},
},
};
</script>
<template>
<contribution-event-base :event="event" :message="message" icon-name="pencil" />
</template>

View File

@ -12,6 +12,7 @@ import {
EVENT_TYPE_CLOSED,
EVENT_TYPE_REOPENED,
EVENT_TYPE_COMMENTED,
EVENT_TYPE_UPDATED,
} from '../constants';
import ContributionEventApproved from './contribution_event/contribution_event_approved.vue';
import ContributionEventExpired from './contribution_event/contribution_event_expired.vue';
@ -24,6 +25,7 @@ import ContributionEventCreated from './contribution_event/contribution_event_cr
import ContributionEventClosed from './contribution_event/contribution_event_closed.vue';
import ContributionEventReopened from './contribution_event/contribution_event_reopened.vue';
import ContributionEventCommented from './contribution_event/contribution_event_commented.vue';
import ContributionEventUpdated from './contribution_event/contribution_event_updated.vue';
export default {
props: {
@ -151,6 +153,9 @@ export default {
case EVENT_TYPE_COMMENTED:
return ContributionEventCommented;
case EVENT_TYPE_UPDATED:
return ContributionEventUpdated;
default:
return EmptyComponent;
}

View File

@ -119,14 +119,24 @@ export const EVENT_COMMENTED_I18N = Object.freeze({
[COMMIT_NOTEABLE_TYPE]: s__(
'ContributionEvent|Commented on commit %{noteableLink} in %{resourceParentLink}.',
),
fallback: s__('ContributionEvent|Commented on %{noteableLink}.'),
[TYPE_FALLBACK]: s__('ContributionEvent|Commented on %{noteableLink}.'),
});
export const EVENT_COMMENTED_SNIPPET_I18N = Object.freeze({
[RESOURCE_PARENT_TYPE_PROJECT]: s__(
'ContributionEvent|Commented on snippet %{noteableLink} in %{resourceParentLink}.',
),
fallback: s__('ContributionEvent|Commented on snippet %{noteableLink}.'),
[TYPE_FALLBACK]: s__('ContributionEvent|Commented on snippet %{noteableLink}.'),
});
export const EVENT_UPDATED_I18N = Object.freeze({
[TARGET_TYPE_DESIGN]: s__(
'ContributionEvent|Updated design %{targetLink} in %{resourceParentLink}.',
),
[TARGET_TYPE_WIKI]: s__(
'ContributionEvent|Updated wiki page %{targetLink} in %{resourceParentLink}.',
),
[TYPE_FALLBACK]: s__('ContributionEvent|Updated resource.'),
});
export const EVENT_CLOSED_ICONS = Object.freeze({

View File

@ -87,7 +87,7 @@ export default {
</script>
<template>
<gl-disclosure-dropdown
:text="title"
:toggle-text="title"
:title="title"
:loading="isLoading"
:aria-label="title"

View File

@ -36,7 +36,7 @@ export default {
<deployment-status-link :deployment-job="item.job" :status="item.status" />
</template>
<template #cell(id)="{ item }">
<strong>{{ item.id }}</strong>
<strong data-testid="deployment-id">{{ item.id }}</strong>
</template>
<template #cell(triggerer)="{ item }">
<deployment-triggerer :triggerer="item.triggerer" />

View File

@ -59,9 +59,6 @@ export const initHeader = () => {
};
export const initPage = async () => {
if (!gon.features.environmentDetailsVue) {
return null;
}
const EnvironmentsDetailPageModule = await import('./environment_details/index.vue');
const EnvironmentsDetailPage = EnvironmentsDetailPageModule.default;
const dataElement = document.getElementById('environments-detail-view');

View File

@ -85,7 +85,7 @@ export default {
<template #cell(stage)="{ item }">
<div class="gl-text-truncate">
<span data-testid="job-stage-name">{{ item.stage.name }}</span>
<span v-if="item.stage" data-testid="job-stage-name">{{ item.stage.name }}</span>
</div>
</template>

View File

@ -1,8 +1,3 @@
import initPipelineSchedulesFormApp from '~/ci/pipeline_schedules/mount_pipeline_schedules_form_app';
import initForm from '../shared/init_form';
if (gon.features?.pipelineSchedulesVue) {
initPipelineSchedulesFormApp('#pipeline-schedules-form-edit', true);
} else {
initForm();
}
initPipelineSchedulesFormApp('#pipeline-schedules-form-edit', true);

View File

@ -1,7 +1,5 @@
import Vue from 'vue';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import initPipelineSchedulesApp from '~/ci/pipeline_schedules/mount_pipeline_schedules_app';
import PipelineSchedulesTakeOwnershipModalLegacy from '~/ci/pipeline_schedules/components/take_ownership_modal_legacy.vue';
import PipelineSchedulesCallout from '../shared/components/pipeline_schedules_callout.vue';
function initPipelineSchedulesCallout() {
@ -27,49 +25,5 @@ function initPipelineSchedulesCallout() {
});
}
// TODO: move take ownership feature into new Vue app
// located in directory app/assets/javascripts/pipeline_schedules/components
function initTakeownershipModal() {
const modalId = 'pipeline-take-ownership-modal';
const buttonSelector = 'js-take-ownership-button';
const el = document.getElementById(modalId);
const takeOwnershipButtons = document.querySelectorAll(`.${buttonSelector}`);
if (!el) {
return;
}
// eslint-disable-next-line no-new
new Vue({
el,
data() {
return {
url: '',
};
},
mounted() {
takeOwnershipButtons.forEach((button) => {
button.addEventListener('click', () => {
const { url } = button.dataset;
this.url = url;
this.$root.$emit(BV_SHOW_MODAL, modalId, `.${buttonSelector}`);
});
});
},
render(createElement) {
return createElement(PipelineSchedulesTakeOwnershipModalLegacy, {
props: {
ownershipUrl: this.url,
},
});
},
});
}
if (gon.features?.pipelineSchedulesVue) {
initPipelineSchedulesApp();
} else {
initPipelineSchedulesCallout();
initTakeownershipModal();
}
initPipelineSchedulesApp();
initPipelineSchedulesCallout();

View File

@ -1,8 +1,3 @@
import initPipelineSchedulesFormApp from '~/ci/pipeline_schedules/mount_pipeline_schedules_form_app';
import initForm from '../shared/init_form';
if (gon.features?.pipelineSchedulesVue) {
initPipelineSchedulesFormApp('#pipeline-schedules-form-new');
} else {
initForm();
}
initPipelineSchedulesFormApp('#pipeline-schedules-form-new');

View File

@ -1,94 +0,0 @@
import $ from 'jquery';
import Vue from 'vue';
import { __ } from '~/locale';
import RefSelector from '~/ref/components/ref_selector.vue';
import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
import setupNativeFormVariableList from '~/ci/ci_variable_list/native_form_variable_list';
import GlFieldErrors from '~/gl_field_errors';
import Translate from '~/vue_shared/translate';
import { initTimezoneDropdown } from '../../../profiles/init_timezone_dropdown';
import IntervalPatternInput from './components/interval_pattern_input.vue';
Vue.use(Translate);
function initIntervalPatternInput() {
const intervalPatternMount = document.getElementById('interval-pattern-input');
const initialCronInterval = intervalPatternMount?.dataset?.initialInterval;
const dailyLimit = intervalPatternMount.dataset?.dailyLimit;
return new Vue({
el: intervalPatternMount,
components: {
IntervalPatternInput,
},
render(createElement) {
return createElement('interval-pattern-input', {
props: {
initialCronInterval,
dailyLimit,
},
});
},
});
}
function getEnabledRefTypes() {
return [REF_TYPE_BRANCHES, REF_TYPE_TAGS];
}
function initTargetRefDropdown() {
const $refField = document.getElementById('schedule_ref');
const el = document.querySelector('.js-target-ref-dropdown');
const { projectId, defaultBranch } = el.dataset;
if (!$refField.value) {
$refField.value = defaultBranch;
}
const refDropdown = new Vue({
el,
render(h) {
return h(RefSelector, {
props: {
enabledRefTypes: getEnabledRefTypes(),
projectId,
value: $refField.value,
useSymbolicRefNames: true,
translations: {
dropdownHeader: __('Select target branch or tag'),
},
},
class: 'gl-w-full',
});
},
});
refDropdown.$children[0].$on('input', (newRef) => {
$refField.value = newRef;
});
return refDropdown;
}
export default () => {
/* Most of the form is written in haml, but for fields with more complex behaviors,
* you should mount individual Vue components here. If at some point components need
* to share state, it may make sense to refactor the whole form to Vue */
initIntervalPatternInput();
// Initialize non-Vue JS components in the form
const formElement = document.getElementById('new-pipeline-schedule-form');
gl.pipelineScheduleFieldErrors = new GlFieldErrors(formElement);
initTargetRefDropdown();
setupNativeFormVariableList({
container: $('.js-ci-variable-list-section'),
formField: 'schedule',
});
};
initTimezoneDropdown();

View File

@ -149,7 +149,6 @@ export default {
id="custom-email-form-forwarding"
ref="service-desk-incoming-email"
type="text"
data-testid="custom-email-form-forwarding"
:aria-label="$options.I18N_FORM_FORWARDING_LABEL"
:value="incomingEmail"
:disabled="true"
@ -167,7 +166,6 @@ export default {
<gl-form-group
:label="$options.I18N_FORM_CUSTOM_EMAIL_LABEL"
label-for="custom-email-form-custom-email"
data-testid="form-group-custom-email"
:invalid-feedback="$options.I18N_FORM_INVALID_FEEDBACK_CUSTOM_EMAIL"
class="gl-mt-3"
:description="$options.I18N_FORM_CUSTOM_EMAIL_DESCRIPTION"
@ -191,7 +189,6 @@ export default {
<gl-form-group
:label="$options.I18N_FORM_SMTP_ADDRESS_LABEL"
label-for="custom-email-form-smtp-address"
data-testid="form-group-smtp-address"
:invalid-feedback="$options.I18N_FORM_INVALID_FEEDBACK_SMTP_ADDRESS"
class="gl-mt-3"
>

View File

@ -1,82 +0,0 @@
@import 'mixins_and_variables_and_functions';
.ci-variable-list {
margin-left: 0;
margin-bottom: 0;
padding-left: 0;
list-style: none;
clear: both;
}
.ci-variable-row {
display: flex;
align-items: flex-start;
@include media-breakpoint-down(xs) {
align-items: flex-end;
}
&:not(:last-child) {
margin-bottom: $gl-btn-padding;
@include media-breakpoint-down(xs) {
margin-bottom: 3 * $gl-btn-padding;
}
}
&:last-child {
.ci-variable-body-item:last-child {
margin-right: $ci-variable-remove-button-width;
@include media-breakpoint-down(xs) {
margin-right: 0;
}
}
.ci-variable-row-remove-button {
display: none;
}
@include media-breakpoint-down(xs) {
.ci-variable-row-body {
margin-right: $ci-variable-remove-button-width;
}
}
}
}
.ci-variable-row-body {
display: flex;
align-items: flex-start;
width: 100%;
padding-bottom: $gl-padding;
@include media-breakpoint-down(xs) {
display: block;
}
}
.ci-variable-body-item {
flex: 1;
&:not(:last-child) {
margin-right: $gl-btn-padding;
@include media-breakpoint-down(xs) {
margin-right: 0;
margin-bottom: $gl-btn-padding;
}
}
}
.pipeline-schedule-form {
.gl-field-error {
margin: 10px 0 0;
}
}
.pipeline-schedule-table-row {
a {
color: var(--gl-text-color, $gl-text-color);
}
}

View File

@ -8,10 +8,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
layout 'project'
before_action only: [:show] do
push_frontend_feature_flag(:environment_details_vue, @project)
end
before_action only: [:index, :edit, :new] do
push_frontend_feature_flag(:flux_resource_for_environment)
end

View File

@ -9,7 +9,6 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
before_action :authorize_create_pipeline_schedule!, only: [:new, :create]
before_action :authorize_update_pipeline_schedule!, only: [:edit, :update]
before_action :authorize_admin_pipeline_schedule!, only: [:take_ownership, :destroy]
before_action :push_schedule_feature_flag, only: [:index, :new, :edit]
feature_category :continuous_integration
urgency :low
@ -120,8 +119,4 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
def authorize_admin_pipeline_schedule!
return access_denied! unless can?(current_user, :admin_pipeline_schedule, schedule)
end
def push_schedule_feature_flag
push_frontend_feature_flag(:pipeline_schedules_vue, @project)
end
end

View File

@ -68,7 +68,10 @@ module Types
null: false, description: 'Timestamp of when the pipeline schedule was last updated.'
def ref_path
::Gitlab::Routing.url_helpers.project_commits_path(object.project, object.ref_for_display)
ref_for_display = object.ref_for_display
return unless ref_for_display
::Gitlab::Routing.url_helpers.project_commits_path(object.project, ref_for_display)
end
def edit_path

View File

@ -39,7 +39,12 @@ module Commits
Gitlab::Git::PreReceiveError,
Gitlab::Git::CommandError => ex
Gitlab::ErrorTracking.log_exception(ex)
error(ex.message)
if Feature.enabled?(:errors_utf_8_encoding)
error(Gitlab::EncodingHelper.encode_utf8_no_detect(ex.message))
else
error(ex.message)
end
end
private

View File

@ -1,36 +0,0 @@
- form_field = local_assigns.fetch(:form_field, nil)
- variable = local_assigns.fetch(:variable, nil)
- id = variable&.id
- variable_type = variable&.variable_type
- key = variable&.key
- value = variable&.value
- id_input_name = "#{form_field}[variables_attributes][][id]"
- destroy_input_name = "#{form_field}[variables_attributes][][_destroy]"
- variable_type_input_name = "#{form_field}[variables_attributes][][variable_type]"
- key_input_name = "#{form_field}[variables_attributes][][key]"
- value_input_name = "#{form_field}[variables_attributes][][secret_value]"
%li.js-row.ci-variable-row{ data: { is_persisted: "#{!id.nil?}" } }
.ci-variable-row-body.border-bottom
%input.js-ci-variable-input-id{ type: "hidden", name: id_input_name, value: id }
%input.js-ci-variable-input-destroy{ type: "hidden", name: destroy_input_name }
%select.js-ci-variable-input-variable-type.ci-variable-body-item.form-control.select-control.custom-select.table-section.section-15{ name: variable_type_input_name }
= options_for_select(ci_variable_type_options, variable_type)
%input.js-ci-variable-input-key.ci-variable-body-item.form-control.gl-form-input.table-section.section-15{ type: "text",
name: key_input_name,
value: key,
placeholder: s_('CiVariables|Input variable key') }
.ci-variable-body-item.gl-show-field-errors.table-section.section-15.border-top-0.p-0
.form-control.js-secret-value-placeholder.overflow-hidden{ class: ('hide' unless id) }
= '*' * 17
%textarea.js-ci-variable-input-value.js-secret-value.form-control.gl-form-input{ class: ('hide' if id),
rows: 1,
name: value_input_name,
placeholder: s_('CiVariables|Input variable value') }
= value
%p.masking-validation-error.gl-field-error.hide
= s_("CiVariables|Cannot use Masked Variable with current value")
= link_to sprite_icon('question-o'), help_page_path('ci/variables/index', anchor: 'mask-a-cicd-variable'), target: '_blank', rel: 'noopener noreferrer'
= render Pajamas::ButtonComponent.new(icon: 'close', button_options: { class: 'js-row-remove-button ci-variable-row-remove-button table-section', 'aria-label': s_('CiVariables|Remove variable row') })

View File

@ -1,17 +0,0 @@
.table-mobile-content
.branch-commit.cgray
- if deployment.ref
%span.icon-container.gl-display-inline-block
= deployment.tag? ? sprite_icon('tag', css_class: 'sprite') : sprite_icon('fork', css_class: 'sprite')
= link_to deployment.ref, project_ref_path(@project, deployment.ref), class: "ref-name"
.icon-container.commit-icon
= custom_icon("icon_commit")
= link_to deployment.short_sha, project_commit_path(@project, deployment.sha), class: "commit-sha mr-0"
%p.commit-title.flex-truncate-parent
%span.flex-truncate-child
- if commit_title = deployment.commit_title
= author_avatar(deployment.commit, size: 20)
= link_to_markdown commit_title, project_commit_path(@project, deployment.sha), class: "commit-row-message cgray"
- else
= _("Can't find HEAD commit for this branch")

View File

@ -1,49 +0,0 @@
.gl-responsive-table-row.deployment{ role: 'row' }
.table-section.section-15{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("Status")
.table-mobile-content
= render_deployment_status(deployment)
.table-section.section-10{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("ID")
%strong.table-mobile-content{ data: { testid: 'deployment-id' } } ##{deployment.iid}
.table-section.section-10{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("Triggerer")
.table-mobile-content
- if deployment.deployed_by
= user_avatar(user: deployment.deployed_by, size: 26, css_class: "mr-0 float-none")
.table-section.section-25{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("Commit")
= render 'projects/deployments/commit', deployment: deployment
.table-section.section-10.build-column{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("Job")
- if deployment.deployable
.table-mobile-content
.flex-truncate-parent
.flex-truncate-child.has-tooltip.gl-white-space-normal.gl-md-white-space-nowrap{ :title => "#{deployment.deployable.name} (##{deployment.deployable.id})", data: { container: 'body' } }
= link_to deployment_path(deployment), class: 'build-link' do
#{deployment.deployable.name} (##{deployment.deployable.id})
- else
= gl_badge_tag s_('Deployment|API'), { variant: :info }, { class: 'gl-cursor-help', data: { toggle: 'tooltip' }, title: s_('Deployment|This deployment was created using the API') }
.table-section.section-10{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("Created")
%span.table-mobile-content.flex-truncate-parent
%span.flex-truncate-child
= time_ago_with_tooltip(deployment.created_at)
.table-section.section-10{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("Deployed")
- if deployment.deployed_at
%span.table-mobile-content.flex-truncate-parent
%span.flex-truncate-child
= time_ago_with_tooltip(deployment.deployed_at)
.table-section.section-10.table-button-footer{ role: 'gridcell' }
.btn-group.table-action-buttons
= render 'projects/deployments/actions', deployment: deployment
= render 'projects/deployments/rollback', deployment: deployment
= render_if_exists 'projects/deployments/approvals', deployment: deployment

View File

@ -1,4 +0,0 @@
- if deployment.deployable && can?(current_user, :play_job, deployment.deployable)
- tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment')
- icon = deployment.last? ? 'repeat' : 'redo'
= render Pajamas::ButtonComponent.new(icon: icon, button_options: { title: tooltip, class: 'js-confirm-rollback-modal-button has-tooltip', data: { environment_name: @environment.name, commit_short_sha: deployment.short_sha, commit_url: project_commit_path(@project, deployment.sha), is_last_deployment: deployment.last?.to_s, retry_path: retry_project_job_path(@environment.project, deployment.deployable) } })

View File

@ -8,33 +8,4 @@
#environments-detail-view{ data: { details: environments_detail_data_json(current_user, @project, @environment) } }
#environments-detail-view-header
- if Feature.enabled?(:environment_details_vue, @project)
#environment_details_page
- else
.environments-container
- if @deployments.blank?
.empty-state
.text-content
%h4.state-title
= _("You don't have any deployments right now.")
%p
= html_escape(_("Define environments in the deploy stage(s) in %{code_open}.gitlab-ci.yml%{code_close} to track deployments here.")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
.text-center
= render Pajamas::ButtonComponent.new(variant: :confirm, href: help_page_path("ci/environments/index.md")) do
= _('Read more')
- else
.table-holder.gl-overflow-visible
.ci-table.environments{ role: 'grid' }
.gl-responsive-table-row.table-row-header{ role: 'row' }
.table-section.section-15{ role: 'columnheader' }= _('Status')
.table-section.section-10{ role: 'columnheader' }= _('ID')
.table-section.section-10{ role: 'columnheader' }= _('Triggerer')
.table-section.section-25{ role: 'columnheader' }= _('Commit')
.table-section.section-10{ role: 'columnheader' }= _('Job')
.table-section.section-10{ role: 'columnheader' }= _('Created')
.table-section.section-10{ role: 'columnheader' }= _('Deployed')
= render @deployments
= paginate @deployments, theme: 'gitlab'
#environment_details_page

View File

@ -1,43 +0,0 @@
= gitlab_ui_form_for [@project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "js-pipeline-schedule-form pipeline-schedule-form" } do |f|
= form_errors(@schedule)
.form-group.row
.col-md-9
= f.label :description, _('Description'), class: 'label-bold'
= f.text_field :description, class: 'form-control gl-form-input', required: true, autofocus: true, placeholder: s_('PipelineSchedules|Provide a short description for this pipeline')
.form-group.row
.col-md-9
= f.label :cron, _('Interval Pattern'), class: 'label-bold'
#interval-pattern-input{ data: { initial_interval: @schedule.cron, daily_limit: @schedule.daily_limit } }
.form-group.row
.col-md-9{ data: { testid: 'schedule-timezone' } }
= f.label :cron_timezone, _("Cron Timezone")
.js-timezone-dropdown{ data: { timezone_data: timezone_data.to_json, initial_value: @schedule.cron_timezone, name: 'schedule[cron_timezone]' } }
.form-group.row
.col-md-9
= f.label :ref, _('Target branch or tag'), class: 'label-bold'
%div{ data: { testid: 'schedule-target-ref' } }
.js-target-ref-dropdown{ data: { project_id: @project.id, default_branch: @project.default_branch } }
= f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true
.form-group.row.js-ci-variable-list-section
.col-md-9
%label.label-bold
#{ s_('PipelineSchedules|Variables') }
%ul.ci-variable-list
- @schedule.variables.each do |variable|
= render 'ci/variables/variable_row', form_field: 'schedule', variable: variable
= render 'ci/variables/variable_row', form_field: 'schedule'
- if @schedule.variables.size > 0
= render Pajamas::ButtonComponent.new(category: :secondary, variant: :confirm, button_options: { class: 'gl-mt-3 js-secret-value-reveal-button', data: { secret_reveal_status: "#{@schedule.variables.size == 0}" }}) do
- if @schedule.variables.size == 0
= n_('Hide value', 'Hide values', @schedule.variables.size)
- else
= n_('Reveal value', 'Reveal values', @schedule.variables.size)
.form-group.row
.col-md-9
= f.label :active, s_('PipelineSchedules|Activated'), class: 'label-bold'
%div
= f.gitlab_ui_checkbox_component :active, _('Active'), checkbox_options: { value: @schedule.active, required: false }
.footer-block
= f.submit _('Save pipeline schedule'), pajamas_button: true
= link_button_to _('Cancel'), pipeline_schedules_path(@project)

View File

@ -1,45 +0,0 @@
- if pipeline_schedule
%tr.pipeline-schedule-table-row
%td{ role: 'cell', data: { label: _('Description') } }
%div
= pipeline_schedule.description
%td.branch-name-cell.gl-text-truncate{ role: 'cell', data: { label: s_("PipelineSchedules|Target") } }
%div
- if pipeline_schedule.for_tag?
= sprite_icon('tag', size: 12, css_class: 'gl-vertical-align-middle!')
- else
= sprite_icon('fork', size: 12, css_class: 'gl-vertical-align-middle!')
- if pipeline_schedule.ref.present?
= link_to pipeline_schedule.ref_for_display, project_ref_path(@project, pipeline_schedule.ref_for_display), class: "ref-name"
%td{ role: 'cell', data: { label: _("Last Pipeline") } }
%div
- if pipeline_schedule.last_pipeline
.status-icon-container{ class: "ci-status-icon-#{pipeline_schedule.last_pipeline.status}" }
= link_to project_pipeline_path(@project, pipeline_schedule.last_pipeline.id) do
= ci_icon_for_status(pipeline_schedule.last_pipeline.status)
%span.gl-text-blue-500! ##{pipeline_schedule.last_pipeline.id}
- else
= s_("PipelineSchedules|None")
%td.gl-text-gray-500{ role: 'cell', data: { label: s_("PipelineSchedules|Next Run") }, 'data-testid': 'next-run-cell' }
%div
- if pipeline_schedule.active? && pipeline_schedule.next_run_at
= time_ago_with_tooltip(pipeline_schedule.real_next_run)
- else
= s_("PipelineSchedules|Inactive")
%td{ role: 'cell', data: { label: _("Owner") } }
%div
- if pipeline_schedule.owner
= render Pajamas::AvatarComponent.new(pipeline_schedule.owner, size: 24, class: "gl-mr-2")
= link_to user_path(pipeline_schedule.owner) do
= pipeline_schedule.owner&.name
%td{ role: 'cell', data: { label: _('Actions') } }
.float-right.btn-group
- if can?(current_user, :play_pipeline_schedule, pipeline_schedule)
= link_button_to nil, play_pipeline_schedule_path(pipeline_schedule), method: :post, title: _('Play'), icon: 'play'
- if can?(current_user, :admin_pipeline_schedule, pipeline_schedule) && pipeline_schedule.owner != current_user
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-take-ownership-button has-tooltip', title: s_('PipelineSchedule|Take ownership to edit'), data: { url: take_ownership_pipeline_schedule_path(pipeline_schedule) } }) do
= s_('PipelineSchedules|Take ownership')
- if can?(current_user, :update_pipeline_schedule, pipeline_schedule)
= link_button_to nil, edit_pipeline_schedule_path(pipeline_schedule), title: _('Edit'), icon: 'pencil'
- if can?(current_user, :admin_pipeline_schedule, pipeline_schedule)
= link_button_to nil, pipeline_schedule_path(pipeline_schedule), title: _('Delete'), method: :delete, aria: { label: _('Delete pipeline schedule') }, data: { confirm: _("Are you sure you want to delete this pipeline schedule?"), confirm_btn_variant: 'danger' }, variant: :danger, icon: 'remove'

View File

@ -1,12 +0,0 @@
.table-holder
%table.table.ci-table.responsive-table.b-table.gl-table.b-table-stacked-md{ role: 'table' }
%thead{ role: 'rowgroup' }
%tr{ role: 'row' }
%th.table-th-transparent.border-bottom{ role: 'cell', style: 'width: 34%' }= _("Description")
%th.table-th-transparent.border-bottom{ role: 'cell' }= s_("PipelineSchedules|Target")
%th.table-th-transparent.border-bottom{ role: 'cell' }= _("Last Pipeline")
%th.table-th-transparent.border-bottom{ role: 'cell' }= s_("PipelineSchedules|Next Run")
%th.table-th-transparent.border-bottom{ role: 'cell' }= _("Owner")
%th.table-th-transparent.border-bottom{ role: 'cell' }
%tbody{ role: 'rowgroup' }
= render partial: "pipeline_schedule", collection: @schedules

View File

@ -1,12 +0,0 @@
= gl_tabs_nav({ class: 'gl-display-flex gl-flex-grow-1 gl-border-0' }) do
= gl_tab_link_to schedule_path_proc.call(nil), { item_active: active_when(scope.nil?) } do
= s_("PipelineSchedules|All")
= gl_tab_counter_badge(number_with_delimiter(all_schedules.count(:id)), { class: 'js-totalbuilds-count' })
= gl_tab_link_to schedule_path_proc.call('active'), { item_active: active_when(scope == 'active') } do
= s_("PipelineSchedules|Active")
= gl_tab_counter_badge(number_with_delimiter(all_schedules.active.count(:id)))
= gl_tab_link_to schedule_path_proc.call('inactive'), { item_active: active_when(scope == 'inactive') } do
= s_("PipelineSchedules|Inactive")
= gl_tab_counter_badge(number_with_delimiter(all_schedules.inactive.count(:id)))

View File

@ -1,12 +1,8 @@
- add_to_breadcrumbs _("Schedules"), pipeline_schedules_path(@project)
- breadcrumb_title "##{@schedule.id}"
- page_title _("Edit"), @schedule.description, _("Pipeline Schedule")
- add_page_specific_style 'page_bundles/pipeline_schedules'
%h1.page-title.gl-font-size-h-display
= _("Edit Pipeline Schedule")
- if Feature.enabled?(:pipeline_schedules_vue, @project)
#pipeline-schedules-form-edit{ data: js_pipeline_schedules_form_data(@project, @schedule) }
- else
= render "form"
#pipeline-schedules-form-edit{ data: js_pipeline_schedules_form_data(@project, @schedule) }

View File

@ -1,28 +1,6 @@
- breadcrumb_title _("Schedules")
- page_title _("Pipeline Schedules")
- add_page_specific_style 'page_bundles/pipeline_schedules'
- add_page_specific_style 'page_bundles/ci_status'
- add_page_specific_style 'page_bundles/merge_request'
#pipeline-schedules-callout{ data: { docs_url: help_page_path('ci/pipelines/schedules'), illustration_url: image_path('illustrations/pipeline_schedule_callout.svg') } }
- if Feature.enabled?(:pipeline_schedules_vue, @project)
#pipeline-schedules-app{ data: { full_path: @project.full_path, pipelines_path: project_pipelines_path(@project), new_schedule_path: new_project_pipeline_schedule_path(@project) } }
- else
.top-area
- schedule_path_proc = ->(scope) { pipeline_schedules_path(@project, scope: scope) }
= render "tabs", schedule_path_proc: schedule_path_proc, all_schedules: @all_schedules, scope: @scope
- if can?(current_user, :create_pipeline_schedule, @project)
.nav-controls
= link_button_to new_project_pipeline_schedule_path(@project), variant: :confirm do
= _('New schedule')
- if @schedules.present?
%ul.content-list
= render partial: "table"
- else
.nothing-here-block
= _("No schedules")
#pipeline-take-ownership-modal
#pipeline-schedules-app{ data: { full_path: @project.full_path, pipelines_path: project_pipelines_path(@project), new_schedule_path: new_project_pipeline_schedule_path(@project) } }

View File

@ -1,14 +1,10 @@
- breadcrumb_title _('Schedules')
- @breadcrumb_link = namespace_project_pipeline_schedules_path(@project.namespace, @project)
- page_title _("New Pipeline Schedule")
- add_page_specific_style 'page_bundles/pipeline_schedules'
- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
%h1.page-title.gl-font-size-h-display
= _("Schedule a new pipeline")
- if Feature.enabled?(:pipeline_schedules_vue, @project)
#pipeline-schedules-form-new{ data: js_pipeline_schedules_form_data(@project, @schedule) }
- else
= render "form"
#pipeline-schedules-form-new{ data: js_pipeline_schedules_form_data(@project, @schedule) }

View File

@ -1,8 +0,0 @@
---
name: environment_details_vue
introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105527"
rollout_issue_url: "https://gitlab.com/gitlab-org/gitlab/-/issues/384914"
milestone: '15.7'
type: development
group: group::configure
default_enabled: true

View File

@ -1,8 +1,8 @@
---
name: pipeline_schedules_vue
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/99155
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/375139
milestone: '15.5'
name: errors_utf_8_encoding
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129217
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/422061
milestone: '16.4'
type: development
group: group::pipeline execution
group: group::source code
default_enabled: false

View File

@ -1,8 +1,8 @@
---
name: explain_vulnerability_vertex
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118754
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/409182
milestone: '16.0'
name: explain_vulnerability_anthropic
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128118
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/420608
milestone: '16.4'
type: development
group: group::threat insights
default_enabled: false

View File

@ -1,30 +1,11 @@
# frozen_string_literal: true
class RebalancePartitionIdCiPipeline < Gitlab::Database::Migration[2.1]
MIGRATION = 'RebalancePartitionId'
DELAY_INTERVAL = 2.minutes
TABLE = :ci_pipelines
BATCH_SIZE = 2_000
SUB_BATCH_SIZE = 200
restrict_gitlab_migration gitlab_schema: :gitlab_ci
def up
return unless Gitlab.com?
queue_batched_background_migration(
MIGRATION,
TABLE,
:id,
job_interval: DELAY_INTERVAL,
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
# no-op
end
def down
return unless Gitlab.com?
delete_batched_background_migration(MIGRATION, TABLE, :id, [])
# no-op
end
end

View File

@ -1,30 +1,11 @@
# frozen_string_literal: true
class RebalancePartitionIdCiBuild < Gitlab::Database::Migration[2.1]
MIGRATION = 'RebalancePartitionId'
DELAY_INTERVAL = 2.minutes.freeze
TABLE = :ci_builds
BATCH_SIZE = 5_000
SUB_BATCH_SIZE = 500
restrict_gitlab_migration gitlab_schema: :gitlab_ci
def up
return unless Gitlab.com?
queue_batched_background_migration(
MIGRATION,
TABLE,
:id,
job_interval: DELAY_INTERVAL,
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
# no-op
end
def down
return unless Gitlab.com?
delete_batched_background_migration(MIGRATION, TABLE, :id, [])
# no-op
end
end

View File

@ -1,30 +1,11 @@
# frozen_string_literal: true
class FixPartitionIdsForCiPipelineVariable < Gitlab::Database::Migration[2.1]
MIGRATION = 'RebalancePartitionId'
DELAY_INTERVAL = 2.minutes.freeze
TABLE = :ci_pipeline_variables
BATCH_SIZE = 2_000
SUB_BATCH_SIZE = 200
restrict_gitlab_migration gitlab_schema: :gitlab_ci
def up
return unless Gitlab.com?
queue_batched_background_migration(
MIGRATION,
TABLE,
:id,
job_interval: DELAY_INTERVAL,
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
# no-op
end
def down
return unless Gitlab.com?
delete_batched_background_migration(MIGRATION, TABLE, :id, [])
# no-op
end
end

View File

@ -1,30 +1,11 @@
# frozen_string_literal: true
class FixPartitionIdsForCiJobArtifact < Gitlab::Database::Migration[2.1]
MIGRATION = 'RebalancePartitionId'
DELAY_INTERVAL = 2.minutes
TABLE = :ci_job_artifacts
BATCH_SIZE = 5_000
SUB_BATCH_SIZE = 500
restrict_gitlab_migration gitlab_schema: :gitlab_ci
def up
return unless Gitlab.com?
queue_batched_background_migration(
MIGRATION,
TABLE,
:id,
job_interval: DELAY_INTERVAL,
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
# no-op
end
def down
return unless Gitlab.com?
delete_batched_background_migration(MIGRATION, TABLE, :id, [])
# no-op
end
end

View File

@ -1,30 +1,11 @@
# frozen_string_literal: true
class FixPartitionIdsForCiStage < Gitlab::Database::Migration[2.1]
MIGRATION = 'RebalancePartitionId'
DELAY_INTERVAL = 2.minutes.freeze
TABLE = :ci_stages
BATCH_SIZE = 3_000
SUB_BATCH_SIZE = 300
restrict_gitlab_migration gitlab_schema: :gitlab_ci
def up
return unless Gitlab.com?
queue_batched_background_migration(
MIGRATION,
TABLE,
:id,
job_interval: DELAY_INTERVAL,
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
# no-op
end
def down
return unless Gitlab.com?
delete_batched_background_migration(MIGRATION, TABLE, :id, [])
# no-op
end
end

View File

@ -1,30 +1,11 @@
# frozen_string_literal: true
class FixPartitionIdsForCiBuildReportResult < Gitlab::Database::Migration[2.1]
MIGRATION = 'RebalancePartitionId'
DELAY_INTERVAL = 2.minutes.freeze
TABLE = :ci_build_report_results
BATCH_SIZE = 2_000
SUB_BATCH_SIZE = 200
restrict_gitlab_migration gitlab_schema: :gitlab_ci
def up
return unless Gitlab.com?
queue_batched_background_migration(
MIGRATION,
TABLE,
:build_id,
job_interval: DELAY_INTERVAL,
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
# no-op
end
def down
return unless Gitlab.com?
delete_batched_background_migration(MIGRATION, TABLE, :build_id, [])
# no-op
end
end

View File

@ -1,30 +1,11 @@
# frozen_string_literal: true
class FixPartitionIdsForCiBuildTraceMetadata < Gitlab::Database::Migration[2.1]
MIGRATION = 'RebalancePartitionId'
DELAY_INTERVAL = 2.minutes.freeze
TABLE = :ci_build_trace_metadata
BATCH_SIZE = 3_000
SUB_BATCH_SIZE = 300
restrict_gitlab_migration gitlab_schema: :gitlab_ci
def up
return unless Gitlab.com?
queue_batched_background_migration(
MIGRATION,
TABLE,
:build_id,
job_interval: DELAY_INTERVAL,
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
# no-op
end
def down
return unless Gitlab.com?
delete_batched_background_migration(MIGRATION, TABLE, :build_id, [])
# no-op
end
end

View File

@ -1,30 +1,11 @@
# frozen_string_literal: true
class FixPartitionIdsForCiBuildMetadata < Gitlab::Database::Migration[2.1]
MIGRATION = 'RebalancePartitionId'
DELAY_INTERVAL = 2.minutes
TABLE = :p_ci_builds_metadata
BATCH_SIZE = 5_000
SUB_BATCH_SIZE = 500
restrict_gitlab_migration gitlab_schema: :gitlab_ci
def up
return unless Gitlab.com?
queue_batched_background_migration(
MIGRATION,
TABLE,
:id,
job_interval: DELAY_INTERVAL,
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
# no-op
end
def down
return unless Gitlab.com?
delete_batched_background_migration(MIGRATION, TABLE, :id, [])
# no-op
end
end

View File

@ -1,20 +1,8 @@
# frozen_string_literal: true
class FixPartitionIdsForCiJobVariables < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_ci
BATCH_SIZE = 50
def up
return unless Gitlab.com?
define_batchable_model(:ci_job_variables)
.where(partition_id: 101)
.each_batch(of: BATCH_SIZE) do |batch|
batch.update_all(partition_id: 100)
sleep 0.1
end
# no-op
end
def down

View File

@ -1,36 +1,11 @@
# frozen_string_literal: true
class FixPartitionIdsOnCiSourcesPipelines < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_ci
BATCH_SIZE = 50
def up
return unless Gitlab.com?
model = define_batchable_model(:ci_sources_pipelines)
batch_update_records(model, :partition_id, from: 101, to: 100, source_partition_id: 100)
batch_update_records(model, :source_partition_id, from: 101, to: 100)
# no-op
end
def down
# no-op
end
private
def batch_update_records(model, column, from:, to:, **updates)
updates.reverse_merge!(column => to)
model
.where(model.arel_table[column].eq(from))
.each_batch(of: BATCH_SIZE) { |batch| update_records(batch, updates) }
end
def update_records(relation, updates)
relation.update_all(updates)
sleep 0.1
end
end

View File

@ -34,7 +34,7 @@ The following API resources are available in the project context:
| [Conan distributions](packages/conan.md) | `/projects/:id/packages/conan` (also available standalone) |
| [Debian distributions](packages/debian_project_distributions.md) | `/projects/:id/debian_distributions` (also available for groups) |
| [Debian packages](packages/debian.md) | `/projects/:id/packages/debian` (also available for groups) |
| [Dependencies](dependencies.md) **(ULTIMATE)** | `/projects/:id/dependencies` |
| [Dependencies](dependencies.md) **(ULTIMATE ALL)** | `/projects/:id/dependencies` |
| [Deploy keys](deploy_keys.md) | `/projects/:id/deploy_keys` (also available standalone) |
| [Deploy tokens](deploy_tokens.md) | `/projects/:id/deploy_tokens` (also available for groups and standalone) |
| [Deployments](deployments.md) | `/projects/:id/deployments` |
@ -82,7 +82,7 @@ The following API resources are available in the project context:
| [Project milestones](milestones.md) | `/projects/:id/milestones` |
| [Project snippets](project_snippets.md) | `/projects/:id/snippets` |
| [Project templates](project_templates.md) | `/projects/:id/templates` |
| [Project vulnerabilities](project_vulnerabilities.md) **(ULTIMATE)** | `/projects/:id/vulnerabilities` |
| [Project vulnerabilities](project_vulnerabilities.md) **(ULTIMATE ALL)** | `/projects/:id/vulnerabilities` |
| [Project wikis](wikis.md) | `/projects/:id/wikis` |
| [Project-level variables](project_level_variables.md) | `/projects/:id/variables` |
| [Projects](projects.md) including setting Webhooks | `/projects`, `/projects/:id/hooks` (also available for users) |
@ -104,9 +104,9 @@ The following API resources are available in the project context:
| [Terraform modules](packages/terraform-modules.md) | `/projects/:id/packages/terraform/modules` (also available standalone) |
| [User-starred metrics dashboards](metrics_user_starred_dashboards.md ) | `/projects/:id/metrics/user_starred_dashboards` |
| [Visual Review discussions](visual_review_discussions.md) **(PREMIUM)** | `/projects/:id/merge_requests/:merge_request_id/visual_review_discussions` |
| [Vulnerabilities](vulnerabilities.md) **(ULTIMATE)** | `/vulnerabilities/:id` |
| [Vulnerability exports](vulnerability_exports.md) **(ULTIMATE)** | `/projects/:id/vulnerability_exports` |
| [Vulnerability findings](vulnerability_findings.md) **(ULTIMATE)** | `/projects/:id/vulnerability_findings` |
| [Vulnerabilities](vulnerabilities.md) **(ULTIMATE ALL)** | `/vulnerabilities/:id` |
| [Vulnerability exports](vulnerability_exports.md) **(ULTIMATE ALL)** | `/projects/:id/vulnerability_exports` |
| [Vulnerability findings](vulnerability_findings.md) **(ULTIMATE ALL)** | `/projects/:id/vulnerability_findings` |
## Group resources

View File

@ -314,7 +314,7 @@ Parameters:
| `include_subgroups` | boolean | no | Include projects in subgroups of this group. Default is `false` |
| `min_access_level` | integer | no | Limit to projects where current user has at least this [role (`access_level`)](members.md#roles) |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (administrators only) |
| `with_security_reports` **(ULTIMATE)** | boolean | no | Return only projects that have security reports artifacts present in any of their builds. This means "projects with security reports enabled". Default is `false` |
| `with_security_reports` **(ULTIMATE ALL)** | boolean | no | Return only projects that have security reports artifacts present in any of their builds. This means "projects with security reports enabled". Default is `false` |
1. Order by similarity: Orders the results by a similarity score calculated from the provided `search`
URL parameter. When using `order_by=similarity`, the `sort` parameter is ignored. When the `search`
@ -992,11 +992,11 @@ PUT /groups/:id
| `membership_lock` **(PREMIUM)** | boolean | no | Users cannot be added to projects in this group. |
| `prevent_forking_outside_group` **(PREMIUM)** | boolean | no | When enabled, users can **not** fork projects from this group to external namespaces. |
| `shared_runners_minutes_limit` **(PREMIUM SELF)** | integer | no | Can be set by administrators only. Maximum number of monthly compute minutes for this group. Can be `nil` (default; inherit system default), `0` (unlimited), or `> 0`. |
| `unique_project_download_limit` **(ULTIMATE)** | integer | no | Maximum number of unique projects a user can download in the specified time period before they are banned. Available only on top-level groups. Default: 0, Maximum: 10,000. |
| `unique_project_download_limit_interval_in_seconds` **(ULTIMATE)** | integer | no | Time period during which a user can download a maximum amount of projects before they are banned. Available only on top-level groups. Default: 0, Maximum: 864,000 seconds (10 days). |
| `unique_project_download_limit_allowlist` **(ULTIMATE)** | array of strings | no | List of usernames excluded from the unique project download limit. Available only on top-level groups. Default: `[]`, Maximum: 100 usernames. |
| `unique_project_download_limit_alertlist` **(ULTIMATE)** | array of integers | no | List of user IDs that are emailed when the unique project download limit is exceeded. Available only on top-level groups. Default: `[]`, Maximum: 100 user IDs. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110201) in GitLab 15.9. |
| `auto_ban_user_on_excessive_projects_download` **(ULTIMATE)** | boolean | no | When enabled, users are automatically banned from the group when they download more than the maximum number of unique projects specified by `unique_project_download_limit` and `unique_project_download_limit_interval_in_seconds`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/94159) in GitLab 15.4. |
| `unique_project_download_limit` **(ULTIMATE ALL)** | integer | no | Maximum number of unique projects a user can download in the specified time period before they are banned. Available only on top-level groups. Default: 0, Maximum: 10,000. |
| `unique_project_download_limit_interval_in_seconds` **(ULTIMATE ALL)** | integer | no | Time period during which a user can download a maximum amount of projects before they are banned. Available only on top-level groups. Default: 0, Maximum: 864,000 seconds (10 days). |
| `unique_project_download_limit_allowlist` **(ULTIMATE ALL)** | array of strings | no | List of usernames excluded from the unique project download limit. Available only on top-level groups. Default: `[]`, Maximum: 100 usernames. |
| `unique_project_download_limit_alertlist` **(ULTIMATE ALL)** | array of integers | no | List of user IDs that are emailed when the unique project download limit is exceeded. Available only on top-level groups. Default: `[]`, Maximum: 100 user IDs. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110201) in GitLab 15.9. |
| `auto_ban_user_on_excessive_projects_download` **(ULTIMATE ALL)** | boolean | no | When enabled, users are automatically banned from the group when they download more than the maximum number of unique projects specified by `unique_project_download_limit` and `unique_project_download_limit_interval_in_seconds`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/94159) in GitLab 15.4. |
| `ip_restriction_ranges` **(PREMIUM)** | string | no | Comma-separated list of IP addresses or subnet masks to restrict group access. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/351493) in GitLab 15.4. |
| `wiki_access_level` **(PREMIUM)** | string | no | The wiki access level. Can be `disabled`, `private`, or `enabled`. |

View File

@ -65,7 +65,7 @@ GET /issues?state=opened
| `created_before` | datetime | no | Return issues created on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
| `due_date` | string | no | Return issues that have no due date, are overdue, or whose due date is this week, this month, or between two weeks ago and next month. Accepts: `0` (no due date), `any`, `today`, `tomorrow`, `overdue`, `week`, `month`, `next_month_and_previous_two_weeks`. |
| `epic_id` **(PREMIUM)** | integer | no | Return issues associated with the given epic ID. `None` returns issues that are not associated with an epic. `Any` returns issues that are associated with an epic. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46887) in GitLab 13.6)_
| `health_status` **(ULTIMATE)** | string | no | Return issues with the specified `health_status`. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/370721) in GitLab 15.4)._ In [GitLab 15.5 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/370721), `None` returns issues with no health status assigned, and `Any` returns issues with a health status assigned.
| `health_status` **(ULTIMATE ALL)** | string | no | Return issues with the specified `health_status`. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/370721) in GitLab 15.4)._ In [GitLab 15.5 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/370721), `None` returns issues with no health status assigned, and `Any` returns issues with a health status assigned.
| `iids[]` | integer array | no | Return only the issues having the given `iid` |
| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` |
| `issue_type` | string | no | Filter to a given type of issue. One of `issue`, `incident`, or `test_case`. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/260375) in GitLab 13.12)_ |

View File

@ -573,7 +573,7 @@ PUT /projects/:id/members/:user_id
| `user_id` | integer | yes | The user ID of the member |
| `access_level` | integer | yes | A valid access level |
| `expires_at` | string | no | A date string in the format `YEAR-MONTH-DAY` |
| `member_role_id` | integer | no | The ID of a member role **(ULTIMATE)** |
| `member_role_id` | integer | no | The ID of a member role **(ULTIMATE ALL)** |
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/:id/members/:user_id?access_level=40"

View File

@ -38,7 +38,7 @@ If the `custom` level is used, specific email events can be controlled. Availabl
- `success_pipeline`
- `moved_project`
- `merge_when_pipeline_succeeds`
- `new_epic` **(ULTIMATE)**
- `new_epic` **(ULTIMATE ALL)**
## Global notification settings
@ -94,7 +94,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab
| `success_pipeline` | boolean | no | Enable/disable this notification |
| `moved_project` | boolean | no | Enable/disable this notification ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30371) in GitLab 13.3) |
| `merge_when_pipeline_succeeds` | boolean | no | Enable/disable this notification ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/244840) in GitLab 13.9) |
| `new_epic` | boolean | no | Enable/disable this notification ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5863) in GitLab 11.3) **(ULTIMATE)** |
| `new_epic` | boolean | no | Enable/disable this notification ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5863) in GitLab 11.3) **(ULTIMATE ALL)** |
Example response:
@ -166,7 +166,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab
| `success_pipeline` | boolean | no | Enable/disable this notification |
| `moved_project` | boolean | no | Enable/disable this notification ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30371) in GitLab 13.3) |
| `merge_when_pipeline_succeeds` | boolean | no | Enable/disable this notification ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/244840) in GitLab 13.9) |
| `new_epic` | boolean | no | Enable/disable this notification ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5863) in GitLab 11.3) **(ULTIMATE)** |
| `new_epic` | boolean | no | Enable/disable this notification ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5863) in GitLab 11.3) **(ULTIMATE ALL)** |
Example responses:

View File

@ -1495,7 +1495,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your-token>" \
| `name` | string | **{check-circle}** Yes (if `path` isn't provided) | The name of the new project. Equals path if not provided. |
| `path` | string | **{check-circle}** Yes (if `name` isn't provided) | Repository name for new project. Generated based on name if not provided (generated as lowercase with dashes). Starting with GitLab 14.9, path must not start or end with a special character and must not contain consecutive special characters. |
| `allow_merge_on_skipped_pipeline` | boolean | **{dotted-circle}** No | Set whether or not merge requests can be merged with skipped jobs. |
| `only_allow_merge_if_all_status_checks_passed` **(ULTIMATE)** | boolean | **{dotted-circle}** No | Indicates that merges of merge requests should be blocked unless all status checks have passed. Defaults to false. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/369859) in GitLab 15.5 with feature flag `only_allow_merge_if_all_status_checks_passed` disabled by default. |
| `only_allow_merge_if_all_status_checks_passed` **(ULTIMATE ALL)** | boolean | **{dotted-circle}** No | Indicates that merges of merge requests should be blocked unless all status checks have passed. Defaults to false. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/369859) in GitLab 15.5 with feature flag `only_allow_merge_if_all_status_checks_passed` disabled by default. |
| `analytics_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private` or `enabled` |
| `approvals_before_merge` **(PREMIUM)** | integer | **{dotted-circle}** No | How many approvers should approve merge requests by default. To configure approval rules, see [Merge request approvals API](merge_request_approvals.md). [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/353097) in GitLab 16.0. |
| `auto_cancel_pending_pipelines` | string | **{dotted-circle}** No | Auto-cancel pending pipelines. This action toggles between an enabled state and a disabled state; it is not a boolean. |
@ -1583,7 +1583,7 @@ POST /projects/user/:user_id
| `user_id` | integer | **{check-circle}** Yes | The user ID of the project owner. |
| `name` | string | **{check-circle}** Yes | The name of the new project. |
| `allow_merge_on_skipped_pipeline` | boolean | **{dotted-circle}** No | Set whether or not merge requests can be merged with skipped jobs. |
| `only_allow_merge_if_all_status_checks_passed` **(ULTIMATE)** | boolean | **{dotted-circle}** No | Indicates that merges of merge requests should be blocked unless all status checks have passed. Defaults to false. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/369859) in GitLab 15.5 with feature flag `only_allow_merge_if_all_status_checks_passed` disabled by default. |
| `only_allow_merge_if_all_status_checks_passed` **(ULTIMATE ALL)** | boolean | **{dotted-circle}** No | Indicates that merges of merge requests should be blocked unless all status checks have passed. Defaults to false. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/369859) in GitLab 15.5 with feature flag `only_allow_merge_if_all_status_checks_passed` disabled by default. |
| `analytics_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private` or `enabled` |
| `approvals_before_merge` **(PREMIUM)** | integer | **{dotted-circle}** No | How many approvers should approve merge requests by default. [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/353097) in GitLab 16.0. To configure approval rules, see [Merge request approvals API](merge_request_approvals.md). |
| `auto_cancel_pending_pipelines` | string | **{dotted-circle}** No | Auto-cancel pending pipelines. This action toggles between an enabled state and a disabled state; it is not a boolean. |
@ -1684,7 +1684,7 @@ Supported attributes:
| `id` | integer or string | **{check-circle}** Yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
| `allow_merge_on_skipped_pipeline` | boolean | **{dotted-circle}** No | Set whether or not merge requests can be merged with skipped jobs. |
| `allow_pipeline_trigger_approve_deployment` **(PREMIUM)** | boolean | **{dotted-circle}** No | Set whether or not a pipeline triggerer is allowed to approve deployments. |
| `only_allow_merge_if_all_status_checks_passed` **(ULTIMATE)** | boolean | **{dotted-circle}** No | Indicates that merges of merge requests should be blocked unless all status checks have passed. Defaults to false.<br/><br/>[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/369859) in GitLab 15.5 with feature flag `only_allow_merge_if_all_status_checks_passed` disabled by default. The feature flag was enabled by default in GitLab 15.9. |
| `only_allow_merge_if_all_status_checks_passed` **(ULTIMATE ALL)** | boolean | **{dotted-circle}** No | Indicates that merges of merge requests should be blocked unless all status checks have passed. Defaults to false.<br/><br/>[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/369859) in GitLab 15.5 with feature flag `only_allow_merge_if_all_status_checks_passed` disabled by default. The feature flag was enabled by default in GitLab 15.9. |
| `analytics_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private` or `enabled` |
| `approvals_before_merge` **(PREMIUM)** | integer | **{dotted-circle}** No | How many approvers should approve merge requests by default. [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/353097) in GitLab 16.0. To configure approval rules, see [Merge request approvals API](merge_request_approvals.md). |
| `auto_cancel_pending_pipelines` | string | **{dotted-circle}** No | Auto-cancel pending pipelines. This action toggles between an enabled state and a disabled state; it is not a boolean. |

View File

@ -73,7 +73,7 @@ of possible security breaches in our code:
Remember to run
[SAST](../../user/application_security/sast/index.md) and [Dependency Scanning](../../user/application_security/dependency_scanning/index.md)
**(ULTIMATE)** on your project (or at least the
**(ULTIMATE ALL)** on your project (or at least the
[`gosec` analyzer](https://gitlab.com/gitlab-org/security-products/analyzers/gosec)),
and to follow our [Security requirements](../code_review.md#security).

View File

@ -94,7 +94,7 @@ The following events are supported for integrations:
| [Pipeline event](../../user/project/integrations/webhook_events.md#pipeline-events) | | `pipeline` | A pipeline status changes.
| [Push event](../../user/project/integrations/webhook_events.md#push-events) | ✓ | `push` | A push is made to the repository.
| [Tag push event](../../user/project/integrations/webhook_events.md#tag-events) | ✓ | `tag_push` | New tags are pushed to the repository.
| Vulnerability event **(ULTIMATE)** | | `vulnerability` | A new, unique vulnerability is recorded.
| Vulnerability event **(ULTIMATE ALL)** | | `vulnerability` | A new, unique vulnerability is recorded.
| [Wiki page event](../../user/project/integrations/webhook_events.md#wiki-page-events) | ✓ | `wiki_page` | A wiki page is created or updated.
#### Event examples

View File

@ -64,7 +64,7 @@ content directly. Instead, it enhances the results with additional properties, i
- CWEs.
- Location tracking fields.
- A means of identifying false positives or insignificant findings. **(ULTIMATE)**
- A means of identifying false positives or insignificant findings. **(ULTIMATE ALL)**
## Transition to Semgrep-based scanning

View File

@ -1,21 +0,0 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# This rebalances partition_id to fix invalid records in production
class RebalancePartitionId < BatchedMigrationJob
INVALID_PARTITION_ID = 101
VALID_PARTITION_ID = 100
scope_to ->(relation) { relation.where(partition_id: INVALID_PARTITION_ID) }
operation_name :update_all
feature_category :continuous_integration
def perform
each_sub_batch do |sub_batch|
sub_batch.update_all(partition_id: VALID_PARTITION_ID)
end
end
end
end
end

View File

@ -45,6 +45,11 @@ msgstr ""
msgid " and %{sliced}"
msgstr ""
msgid " except branch:"
msgid_plural " except branches:"
msgstr[0] ""
msgstr[1] ""
msgid " or "
msgstr ""
@ -1438,6 +1443,9 @@ msgstr ""
msgid "+ %{count} more"
msgstr ""
msgid "+ %{hiddenBranchesLength} more"
msgstr ""
msgid "+ %{moreCount} more"
msgstr ""
@ -6215,9 +6223,6 @@ msgstr ""
msgid "Are you sure you want to delete this label?"
msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr ""
msgid "Are you sure you want to delete this pipeline? Doing so will expire all pipeline caches and delete all related objects, such as builds, logs, artifacts, and triggers. This action cannot be undone."
msgstr ""
@ -10132,9 +10137,6 @@ msgstr ""
msgid "CiVariables|Cancel"
msgstr ""
msgid "CiVariables|Cannot use Masked Variable with current value"
msgstr ""
msgid "CiVariables|Delete variable"
msgstr ""
@ -10195,9 +10197,6 @@ msgstr ""
msgid "CiVariables|Remove variable"
msgstr ""
msgid "CiVariables|Remove variable row"
msgstr ""
msgid "CiVariables|Run job"
msgstr ""
@ -10240,9 +10239,6 @@ msgstr ""
msgid "CiVariables|You have reached the maximum number of variables available. To add new variables, you must reduce the number of defined variables."
msgstr ""
msgid "CiVariable|* (All environments)"
msgstr ""
msgid "CiVariable|All environments"
msgstr ""
@ -13218,6 +13214,15 @@ msgstr ""
msgid "ContributionEvent|Reopened test case %{targetLink} in %{resourceParentLink}."
msgstr ""
msgid "ContributionEvent|Updated design %{targetLink} in %{resourceParentLink}."
msgstr ""
msgid "ContributionEvent|Updated resource."
msgstr ""
msgid "ContributionEvent|Updated wiki page %{targetLink} in %{resourceParentLink}."
msgstr ""
msgid "ContributionEvent|…and %{count} more commits. %{linkStart}Compare%{linkEnd}."
msgstr ""
@ -14142,9 +14147,6 @@ msgstr ""
msgid "Crm|Organization has been updated."
msgstr ""
msgid "Cron Timezone"
msgstr ""
msgid "Cron time zone"
msgstr ""
@ -15478,9 +15480,6 @@ msgstr ""
msgid "Define custom rules for what constitutes spam, independent of Akismet"
msgstr ""
msgid "Define environments in the deploy stage(s) in %{code_open}.gitlab-ci.yml%{code_close} to track deployments here."
msgstr ""
msgid "Define how approval rules are applied to merge requests."
msgstr ""
@ -15603,9 +15602,6 @@ msgstr ""
msgid "Delete pipeline"
msgstr ""
msgid "Delete pipeline schedule"
msgstr ""
msgid "Delete project"
msgstr ""
@ -16344,9 +16340,6 @@ msgstr ""
msgid "Deployments|You don't have any deployments right now."
msgstr ""
msgid "Deployment|API"
msgstr ""
msgid "Deployment|Cancelled"
msgstr ""
@ -16395,9 +16388,6 @@ msgstr ""
msgid "Deployment|Sync status is unknown. %{linkStart}How do I configure Flux for my deployment?%{linkEnd}"
msgstr ""
msgid "Deployment|This deployment was created using the API"
msgstr ""
msgid "Deployment|Triggerer"
msgstr ""
@ -25565,9 +25555,6 @@ msgstr ""
msgid "Interval"
msgstr ""
msgid "Interval Pattern"
msgstr ""
msgid "Introducing Your DevOps Reports"
msgstr ""
@ -27563,9 +27550,6 @@ msgstr ""
msgid "Last Name"
msgstr ""
msgid "Last Pipeline"
msgstr ""
msgid "Last Seen"
msgstr ""
@ -31180,9 +31164,6 @@ msgstr ""
msgid "New runners registration token has been generated!"
msgstr ""
msgid "New schedule"
msgstr ""
msgid "New snippet"
msgstr ""
@ -31501,9 +31482,6 @@ msgstr ""
msgid "No runner executable"
msgstr ""
msgid "No schedules"
msgstr ""
msgid "No service accounts"
msgstr ""
@ -34295,9 +34273,6 @@ msgstr ""
msgid "PipelineSchedules|A scheduled pipeline starts automatically at regular intervals, like daily or weekly. The pipeline: "
msgstr ""
msgid "PipelineSchedules|Activated"
msgstr ""
msgid "PipelineSchedules|Active"
msgstr ""
@ -34412,12 +34387,6 @@ msgstr ""
msgid "PipelineSchedules|There was a problem taking ownership of the pipeline schedule."
msgstr ""
msgid "PipelineSchedules|Variables"
msgstr ""
msgid "PipelineSchedule|Take ownership to edit"
msgstr ""
msgid "PipelineSource|API"
msgstr ""
@ -41379,9 +41348,6 @@ msgstr ""
msgid "Save password"
msgstr ""
msgid "Save pipeline schedule"
msgstr ""
msgid "Saving"
msgstr ""
@ -42214,6 +42180,12 @@ msgstr ""
msgid "SecurityOrchestration|%{agent} for %{namespaces}"
msgstr ""
msgid "SecurityOrchestration|%{branchName}"
msgstr ""
msgid "SecurityOrchestration|%{branchName} (in %{codeStart}%{fullPath}%{codeEnd})"
msgstr ""
msgid "SecurityOrchestration|%{cadence} on %{branches}"
msgstr ""
@ -42361,6 +42333,9 @@ msgstr ""
msgid "SecurityOrchestration|Groups"
msgstr ""
msgid "SecurityOrchestration|Hide extra branches"
msgstr ""
msgid "SecurityOrchestration|If any scanner finds a newly detected critical vulnerability in an open merge request targeting the master branch, then require two approvals from any member of App security."
msgstr ""
@ -42645,13 +42620,13 @@ msgstr ""
msgid "SecurityOrchestration|Vulnerability age requires previously existing vulnerability states (detected, confirmed, resolved, or dismissed)"
msgstr ""
msgid "SecurityOrchestration|When %{scanners} %{vulnerabilitiesAllowed} %{vulnerability} in an open merge request %{targeting}%{branches}%{criteriaApply}"
msgid "SecurityOrchestration|When %{scanners} %{vulnerabilitiesAllowed} %{vulnerability} in an open merge request %{targeting}%{branches}%{branchExceptionsString}%{criteriaApply}"
msgstr ""
msgid "SecurityOrchestration|When license scanner finds any license except %{licenses}%{detection} in an open merge request %{targeting}%{branches}."
msgid "SecurityOrchestration|When license scanner finds any license except %{licenses}%{detection} in an open merge request %{targeting}%{branches}%{branchExceptionsString}"
msgstr ""
msgid "SecurityOrchestration|When license scanner finds any license matching %{licenses}%{detection} in an open merge request %{targeting}%{branches}."
msgid "SecurityOrchestration|When license scanner finds any license matching %{licenses}%{detection} in an open merge request %{targeting}%{branches}%{branchExceptionsString}"
msgstr ""
msgid "SecurityOrchestration|When this policy is enabled, protected branches cannot be unprotected and users will not be allowed to force push to protected branches."
@ -46639,9 +46614,6 @@ msgstr ""
msgid "Target branch"
msgstr ""
msgid "Target branch or tag"
msgstr ""
msgid "Target branch: %{target_branch}"
msgstr ""
@ -54317,9 +54289,6 @@ msgstr ""
msgid "You don't have any authorized applications."
msgstr ""
msgid "You don't have any deployments right now."
msgstr ""
msgid "You don't have any open merge requests"
msgstr ""

View File

@ -11,7 +11,7 @@ module Gitlab
link :upgrade_to_ultimate
# Subscription details
strong :subscription_header
h5 :subscription_header
button :refresh_seats
# Usage

View File

@ -10,24 +10,19 @@ RSpec.describe 'Environment', feature_category: :groups_and_projects do
before do
sign_in(user)
project.add_role(user, role)
stub_feature_flags(environment_details_vue: false)
end
def auto_stop_button_selector
%q{button[title="Prevent environment from auto-stopping"]}
end
describe 'environment details page vue' do
describe 'environment details page', :js do
let_it_be(:environment) { create(:environment, project: project) }
let!(:permissions) {}
let!(:deployment) {}
let!(:action) {}
let!(:cluster) {}
before do
stub_feature_flags(environment_details_vue: true)
end
context 'with auto-stop' do
let_it_be(:environment) { create(:environment, :will_auto_stop, name: 'staging', project: project) }
@ -35,122 +30,16 @@ RSpec.describe 'Environment', feature_category: :groups_and_projects do
visit_environment(environment)
end
it 'shows auto stop info', :js do
it 'shows auto stop info' do
expect(page).to have_content('Auto stops')
end
it 'shows auto stop button', :js do
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
before do
visit_environment(environment)
end
it 'does not show deployments', :js do
expect(page).to have_content('You don\'t have any deployments right now.')
end
end
context 'with deployments' do
before do
visit_environment(environment)
end
context 'when there is a successful deployment' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, :success, pipeline: pipeline) }
let(:deployment) do
create(:deployment, :success, environment: environment, deployable: build)
end
it 'does show deployments', :js do
wait_for_requests
expect(page).to have_link("#{build.name} (##{build.id})")
end
end
context 'when there is a failed deployment' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
let(:deployment) do
create(:deployment, :failed, environment: environment, deployable: build)
end
it 'does show deployments', :js do
wait_for_requests
expect(page).to have_link("#{build.name} (##{build.id})")
end
end
context 'with related deployable present' do
let_it_be(:previous_pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:previous_build) do
create(:ci_build, :success, pipeline: previous_pipeline, environment: environment.name)
end
let_it_be(:previous_deployment) do
create(:deployment, :success, environment: environment, deployable: previous_build)
end
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:build) { create(:ci_build, pipeline: pipeline, environment: environment.name) }
let_it_be(:deployment) do
create(:deployment, :success, environment: environment, deployable: build)
end
before do
visit_environment(environment)
end
it 'shows deployment information and buttons', :js do
wait_for_requests
expect(page).to have_button('Re-deploy to environment')
expect(page).to have_button('Rollback environment')
expect(page).to have_link("#{build.name} (##{build.id})")
end
end
end
end
describe 'environment details page' do
let_it_be(:environment) { create(:environment, project: project) }
let!(:permissions) {}
let!(:deployment) {}
let!(:action) {}
let!(:cluster) {}
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', :js do
expect(page).to have_content('Auto stops')
end
it 'shows auto stop button', :js 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
it 'allows user to cancel auto stop' do
page.find(auto_stop_button_selector).click
wait_for_all_requests
expect(page).to have_content('Auto stop successfully canceled.')
@ -208,10 +97,6 @@ RSpec.describe 'Environment', feature_category: :groups_and_projects do
it 'does show deployments' do
expect(page).to have_link("#{build.name} (##{build.id})")
end
it 'shows a tooltip on the job name' do
expect(page).to have_css("[title=\"#{build.name} (##{build.id})\"].has-tooltip")
end
end
context 'when there is a failed deployment' do
@ -227,26 +112,6 @@ RSpec.describe 'Environment', feature_category: :groups_and_projects do
end
end
context 'with many deployments' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
let!(:second) { create(:deployment, environment: environment, deployable: build, status: :success, finished_at: Time.current) }
let!(:first) { create(:deployment, environment: environment, deployable: build, status: :running) }
let!(:last) { create(:deployment, environment: environment, deployable: build, status: :success, finished_at: 2.days.ago) }
let!(:third) { create(:deployment, environment: environment, deployable: build, status: :canceled, finished_at: 1.day.ago) }
before do
visit_environment(environment)
end
it 'shows all of them in ordered way' do
ids = find_all('[data-testid="deployment-id"]').map { |e| e.text }
expected_ordered_ids = [first, second, third, last].map { |d| "##{d.iid}" }
expect(ids).to eq(expected_ordered_ids)
end
end
context 'with upcoming deployments' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
@ -265,7 +130,7 @@ RSpec.describe 'Environment', feature_category: :groups_and_projects do
# See https://gitlab.com/gitlab-org/gitlab/-/issues/350618 for more information.
it 'shows upcoming deployments in unordered way' do
displayed_ids = find_all('[data-testid="deployment-id"]').map { |e| e.text }
internal_ids = [runnind_deployment_1, runnind_deployment_2, success_without_finished_at].map { |d| "##{d.iid}" }
internal_ids = [runnind_deployment_1, runnind_deployment_2, success_without_finished_at].map { |d| d.iid.to_s }
expect(displayed_ids).to match_array(internal_ids)
end
end
@ -309,20 +174,19 @@ RSpec.describe 'Environment', feature_category: :groups_and_projects do
end
it 'does show a play button' do
expect(page).to have_link(action.name)
expect(page).to have_button(action.name, visible: :all)
end
it 'does allow to play manual action', :js do
it 'does allow to play manual action' do
expect(action).to be_manual
find('button.dropdown').click
click_button('Deploy to...')
expect { click_link(action.name) }
expect { click_button(action.name) }
.not_to change { Ci::Pipeline.count }
wait_for_all_requests
expect(page).to have_content(action.name)
expect(action.reload).to be_pending
end
end
@ -347,38 +211,6 @@ RSpec.describe 'Environment', feature_category: :groups_and_projects do
end
end
context 'with terminal' do
context 'when user configured kubernetes from CI/CD > Clusters' do
let!(:cluster) do
create(:cluster, :project, :provided_by_gcp, projects: [project])
end
context 'for project maintainer' do
let(:role) { :maintainer }
context 'web terminal', :js do
before do
# Stub #terminals as it causes js-enabled feature specs to
# render the page incorrectly
#
# In EE we have to stub EE::Environment since it overwrites
# the "terminals" method.
allow_next_instance_of(Gitlab.ee? ? EE::Environment : Environment) do |instance|
allow(instance).to receive(:terminals) { nil }
end
visit terminal_project_environment_path(project, environment)
end
it 'displays a web terminal' do
expect(page).to have_selector('#terminal')
expect(page).to have_link(nil, href: environment.external_url)
end
end
end
end
end
context 'when environment is available' do
context 'with stop action' do
let(:build) { create(:ci_build, :success, pipeline: pipeline, environment: environment.name) }
@ -446,6 +278,8 @@ RSpec.describe 'Environment', feature_category: :groups_and_projects do
visit folder_project_environments_path(project, id: 'staging-1.0')
end
wait_for_requests
expect(reqs.first.status_code).to eq(200)
expect(page).to have_content('Environments / staging-1.0')
end

View File

@ -12,389 +12,301 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :groups_and_projects
let!(:user) { create(:user) }
let!(:maintainer) { create(:user) }
context 'with pipeline_schedules_vue feature flag turned off' do
context 'logged in as the pipeline schedule owner' do
before do
stub_feature_flags(pipeline_schedules_vue: false)
project.add_developer(user)
pipeline_schedule.update!(owner: user)
gitlab_sign_in(user)
end
context 'logged in as the pipeline schedule owner' do
describe 'GET /projects/pipeline_schedules' do
before do
project.add_developer(user)
pipeline_schedule.update!(owner: user)
gitlab_sign_in(user)
visit_pipelines_schedules
end
describe 'GET /projects/pipeline_schedules' do
before do
visit_pipelines_schedules
end
it 'edits the pipeline' do
page.find('[data-testid="edit-pipeline-schedule-btn"]').click
it 'edits the pipeline' do
page.within('.pipeline-schedule-table-row') do
click_link 'Edit'
end
expect(page).to have_content(s_('PipelineSchedules|Edit pipeline schedule'))
end
end
expect(page).to have_content('Edit Pipeline Schedule')
end
describe 'PATCH /projects/pipelines_schedules/:id/edit' do
before do
edit_pipeline_schedule
end
describe 'PATCH /projects/pipelines_schedules/:id/edit' do
it 'displays existing properties' do
description = find_field('schedule-description').value
expect(description).to eq('pipeline schedule')
expect(page).to have_button('master')
expect(page).to have_button(_('Select timezone'))
end
it 'edits the scheduled pipeline' do
fill_in 'schedule-description', with: 'my brand new description'
save_pipeline_schedule
expect(page).to have_content('my brand new description')
end
context 'when ref is nil' do
before do
pipeline_schedule.update_attribute(:ref, nil)
edit_pipeline_schedule
end
it 'displays existing properties' do
description = find_field('schedule_description').value
expect(description).to eq('pipeline schedule')
expect(page).to have_button('master')
expect(page).to have_button('Select timezone')
end
it 'edits the scheduled pipeline' do
fill_in 'schedule_description', with: 'my brand new description'
save_pipeline_schedule
expect(page).to have_content('my brand new description')
end
context 'when ref is nil' do
before do
pipeline_schedule.update_attribute(:ref, nil)
edit_pipeline_schedule
end
it 'shows the pipeline schedule with default ref' do
page.within('[data-testid="schedule-target-ref"]') do
expect(first('.gl-button-text').text).to eq('master')
end
end
end
context 'when ref is empty' do
before do
pipeline_schedule.update_attribute(:ref, '')
edit_pipeline_schedule
end
it 'shows the pipeline schedule with default ref' do
page.within('[data-testid="schedule-target-ref"]') do
expect(first('.gl-button-text').text).to eq('master')
end
it 'shows the pipeline schedule with default ref' do
page.within('#schedule-target-branch-tag') do
expect(first('.gl-button-text').text).to eq('master')
end
end
end
end
context 'logged in as a project maintainer' do
before do
project.add_maintainer(user)
gitlab_sign_in(user)
end
describe 'GET /projects/pipeline_schedules' do
context 'when ref is empty' do
before do
visit_pipelines_schedules
pipeline_schedule.update_attribute(:ref, '')
edit_pipeline_schedule
end
describe 'The view' do
it 'displays the required information description' do
page.within('.pipeline-schedule-table-row') do
expect(page).to have_content('pipeline schedule')
expect(find("[data-testid='next-run-cell'] time")['title'])
.to include(pipeline_schedule.real_next_run.strftime('%b %-d, %Y'))
expect(page).to have_link('master')
expect(page).to have_link("##{pipeline.id}")
end
end
it 'creates a new scheduled pipeline' do
click_link 'New schedule'
expect(page).to have_content('Schedule a new pipeline')
end
it 'changes ownership of the pipeline' do
click_button 'Take ownership'
page.within('#pipeline-take-ownership-modal') do
click_link 'Take ownership'
end
page.within('.pipeline-schedule-table-row') do
expect(page).not_to have_content('No owner')
expect(page).to have_link('Sidney Jones')
end
end
it 'deletes the pipeline' do
click_link 'Delete'
accept_gl_confirm(button_text: 'Delete pipeline schedule')
expect(page).not_to have_css(".pipeline-schedule-table-row")
end
end
context 'when ref is nil' do
before do
pipeline_schedule.update_attribute(:ref, nil)
visit_pipelines_schedules
end
it 'shows a list of the pipeline schedules with empty ref column' do
expect(first('.branch-name-cell').text).to eq('')
end
end
context 'when ref is empty' do
before do
pipeline_schedule.update_attribute(:ref, '')
visit_pipelines_schedules
end
it 'shows a list of the pipeline schedules with empty ref column' do
expect(first('.branch-name-cell').text).to eq('')
end
end
end
describe 'POST /projects/pipeline_schedules/new' do
before do
visit_new_pipeline_schedule
end
it 'sets defaults for timezone and target branch' do
expect(page).to have_button('master')
expect(page).to have_button('Select timezone')
end
it 'creates a new scheduled pipeline' do
fill_in_schedule_form
save_pipeline_schedule
expect(page).to have_content('my fancy description')
end
it 'prevents an invalid form from being submitted' do
save_pipeline_schedule
expect(page).to have_content('This field is required')
end
end
context 'when user creates a new pipeline schedule with variables' do
before do
visit_pipelines_schedules
click_link 'New schedule'
fill_in_schedule_form
all('[name="schedule[variables_attributes][][key]"]')[0].set('AAA')
all('[name="schedule[variables_attributes][][secret_value]"]')[0].set('AAA123')
all('[name="schedule[variables_attributes][][key]"]')[1].set('BBB')
all('[name="schedule[variables_attributes][][secret_value]"]')[1].set('BBB123')
save_pipeline_schedule
end
it 'user sees the new variable in edit window', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/397040' do
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
page.within('.ci-variable-list') do
expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('AAA')
expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('AAA123')
expect(find(".ci-variable-row:nth-child(2) .js-ci-variable-input-key").value).to eq('BBB')
expect(find(".ci-variable-row:nth-child(2) .js-ci-variable-input-value", visible: false).value).to eq('BBB123')
end
end
end
context 'when user edits a variable of a pipeline schedule' do
before do
create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
create(:ci_pipeline_schedule_variable, key: 'AAA', value: 'AAA123', pipeline_schedule: pipeline_schedule)
end
visit_pipelines_schedules
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
find('.js-ci-variable-list-section .js-secret-value-reveal-button').click
first('.js-ci-variable-input-key').set('foo')
first('.js-ci-variable-input-value').set('bar')
click_button 'Save pipeline schedule'
end
it 'user sees the updated variable in edit window' do
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
page.within('.ci-variable-list') do
expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('foo')
expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('bar')
end
end
end
context 'when user removes a variable of a pipeline schedule' do
before do
create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
create(:ci_pipeline_schedule_variable, key: 'AAA', value: 'AAA123', pipeline_schedule: pipeline_schedule)
end
visit_pipelines_schedules
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
find('.ci-variable-list .ci-variable-row-remove-button').click
click_button 'Save pipeline schedule'
end
it 'user does not see the removed variable in edit window' do
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
page.within('.ci-variable-list') do
expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('')
expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('')
end
end
end
context 'when active is true and next_run_at is NULL' do
before do
create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
pipeline_schedule.update_attribute(:next_run_at, nil) # Consequently next_run_at will be nil
end
end
it 'user edit and recover the problematic pipeline schedule' do
visit_pipelines_schedules
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
fill_in 'schedule_cron', with: '* 1 2 3 4'
click_button 'Save pipeline schedule'
page.within('.pipeline-schedule-table-row:nth-child(1)') do
expect(page).to have_css("[data-testid='next-run-cell'] time")
end
end
end
end
context 'logged in as non-member' do
before do
gitlab_sign_in(user)
end
describe 'GET /projects/pipeline_schedules' do
before do
visit_pipelines_schedules
end
describe 'The view' do
it 'does not show create schedule button' do
expect(page).not_to have_link('New schedule')
end
end
end
end
context 'not logged in' do
describe 'GET /projects/pipeline_schedules' do
before do
visit_pipelines_schedules
end
describe 'The view' do
it 'does not show create schedule button' do
expect(page).not_to have_link('New schedule')
it 'shows the pipeline schedule with default ref' do
page.within('#schedule-target-branch-tag') do
expect(first('.gl-button-text').text).to eq('master')
end
end
end
end
end
context 'with pipeline_schedules_vue feature flag turned on' do
context 'logged in as a project maintainer' do
context 'logged in as a project maintainer' do
before do
project.add_maintainer(user)
pipeline_schedule.update!(owner: maintainer)
gitlab_sign_in(user)
end
describe 'GET /projects/pipeline_schedules' do
before do
project.add_maintainer(maintainer)
pipeline_schedule.update!(owner: user)
gitlab_sign_in(maintainer)
visit_pipelines_schedules
wait_for_requests
end
describe 'GET /projects/pipeline_schedules' do
before do
visit_pipelines_schedules
wait_for_requests
describe 'The view' do
it 'displays the required information description' do
page.within('[data-testid="pipeline-schedule-table-row"]') do
expect(page).to have_content('pipeline schedule')
expect(find('[data-testid="next-run-cell"] time')['title'])
.to include(pipeline_schedule.real_next_run.strftime('%b %-d, %Y'))
expect(page).to have_link('master')
expect(find("[data-testid='last-pipeline-status'] a")['href']).to include(pipeline.id.to_s)
end
end
describe 'The view' do
it 'displays the required information description' do
page.within('[data-testid="pipeline-schedule-table-row"]') do
expect(page).to have_content('pipeline schedule')
expect(find("[data-testid='next-run-cell'] time")['title'])
.to include(pipeline_schedule.real_next_run.strftime('%b %-d, %Y'))
expect(page).to have_link('master')
expect(find("[data-testid='last-pipeline-status'] a")['href']).to include(pipeline.id.to_s)
end
end
it 'creates a new scheduled pipeline' do
click_link 'New schedule'
it 'changes ownership of the pipeline' do
click_button 'Take ownership'
expect(page).to have_content('Schedule a new pipeline')
end
page.within('#pipeline-take-ownership-modal') do
click_button 'Take ownership'
it 'changes ownership of the pipeline' do
find("[data-testid='take-ownership-pipeline-schedule-btn']").click
wait_for_requests
end
page.within('[data-testid="pipeline-schedule-table-row"]') do
expect(page).not_to have_content('No owner')
expect(page).to have_link('Sidney Jones')
end
end
it 'runs the pipeline' do
click_button 'Run pipeline schedule'
page.within('#pipeline-take-ownership-modal') do
click_button s_('PipelineSchedules|Take ownership')
wait_for_requests
expect(page).to have_content("Successfully scheduled a pipeline to run. Go to the Pipelines page for details.")
end
it 'deletes the pipeline' do
click_button 'Delete pipeline schedule'
accept_gl_confirm(button_text: 'Delete pipeline schedule')
expect(page).not_to have_css('[data-testid="pipeline-schedule-table-row"]')
page.within('[data-testid="pipeline-schedule-table-row"]') do
expect(page).not_to have_content('No owner')
expect(page).to have_link('Sidney Jones')
end
end
it 'deletes the pipeline' do
page.within('[data-testid="pipeline-schedule-table-row"]') do
click_button s_('PipelineSchedules|Delete pipeline schedule')
end
accept_gl_confirm(button_text: s_('PipelineSchedules|Delete pipeline schedule'))
expect(page).not_to have_css('[data-testid="pipeline-schedule-table-row"]')
end
end
context 'when ref is nil' do
before do
pipeline_schedule.update_attribute(:ref, nil)
visit_pipelines_schedules
wait_for_requests
end
it 'shows a list of the pipeline schedules with empty ref column' do
target = find('[data-testid="pipeline-schedule-target"]')
page.within('[data-testid="pipeline-schedule-table-row"]') do
expect(target.text).to eq(s_('PipelineSchedules|None'))
end
end
end
context 'when ref is empty' do
before do
pipeline_schedule.update_attribute(:ref, '')
visit_pipelines_schedules
wait_for_requests
end
it 'shows a list of the pipeline schedules with empty ref column' do
target = find('[data-testid="pipeline-schedule-target"]')
expect(target.text).to eq(s_('PipelineSchedules|None'))
end
end
end
context 'logged in as non-member' do
describe 'POST /projects/pipeline_schedules/new' do
before do
gitlab_sign_in(user)
visit_new_pipeline_schedule
end
describe 'GET /projects/pipeline_schedules' do
before do
visit_pipelines_schedules
it 'sets defaults for timezone and target branch' do
expect(page).to have_button('master')
expect(page).to have_button('Select timezone')
end
wait_for_requests
end
it 'creates a new scheduled pipeline' do
fill_in_schedule_form
create_pipeline_schedule
describe 'The view' do
it 'does not show create schedule button' do
expect(page).not_to have_link('New schedule')
end
expect(page).to have_content('my fancy description')
end
it 'prevents an invalid form from being submitted' do
create_pipeline_schedule
expect(page).to have_content("Cron timezone can't be blank")
end
end
context 'when user creates a new pipeline schedule with variables' do
before do
visit_pipelines_schedules
click_link 'New schedule'
fill_in_schedule_form
all('[name="schedule[variables_attributes][][key]"]')[0].set('AAA')
all('[name="schedule[variables_attributes][][secret_value]"]')[0].set('AAA123')
all('[name="schedule[variables_attributes][][key]"]')[1].set('BBB')
all('[name="schedule[variables_attributes][][secret_value]"]')[1].set('BBB123')
create_pipeline_schedule
end
it 'user sees the new variable in edit window', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/397040' do
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
page.within('.ci-variable-list') do
expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('AAA')
expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('AAA123')
expect(find(".ci-variable-row:nth-child(2) .js-ci-variable-input-key").value).to eq('BBB')
expect(find(".ci-variable-row:nth-child(2) .js-ci-variable-input-value", visible: false).value).to eq('BBB123')
end
end
end
context 'not logged in' do
describe 'GET /projects/pipeline_schedules' do
before do
visit_pipelines_schedules
wait_for_requests
context 'when user edits a variable of a pipeline schedule' do
before do
create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
create(:ci_pipeline_schedule_variable, key: 'AAA', value: 'AAA123', pipeline_schedule: pipeline_schedule)
end
describe 'The view' do
it 'does not show create schedule button' do
expect(page).not_to have_link('New schedule')
end
visit_pipelines_schedules
first('[data-testid="edit-pipeline-schedule-btn"]').click
click_button _('Reveal values')
first('[data-testid="pipeline-form-ci-variable-key"]').set('foo')
first('[data-testid="pipeline-form-ci-variable-value"]').set('bar')
save_pipeline_schedule
end
it 'user sees the updated variable' do
first('[data-testid="edit-pipeline-schedule-btn"]').click
expect(first('[data-testid="pipeline-form-ci-variable-key"]').value).to eq('foo')
expect(first('[data-testid="pipeline-form-ci-variable-value"]').value).to eq('')
click_button _('Reveal values')
expect(first('[data-testid="pipeline-form-ci-variable-value"]').value).to eq('bar')
end
end
context 'when user removes a variable of a pipeline schedule' do
before do
create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
create(:ci_pipeline_schedule_variable, key: 'AAA', value: 'AAA123', pipeline_schedule: pipeline_schedule)
end
visit_pipelines_schedules
first('[data-testid="edit-pipeline-schedule-btn"]').click
find('[data-testid="remove-ci-variable-row"]').click
save_pipeline_schedule
end
it 'user does not see the removed variable in edit window' do
first('[data-testid="edit-pipeline-schedule-btn"]').click
expect(first('[data-testid="pipeline-form-ci-variable-key"]').value).to eq('')
expect(first('[data-testid="pipeline-form-ci-variable-value"]').value).to eq('')
end
end
context 'when active is true and next_run_at is NULL' do
before do
create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
pipeline_schedule.update_attribute(:next_run_at, nil) # Consequently next_run_at will be nil
end
end
it 'user edit and recover the problematic pipeline schedule' do
visit_pipelines_schedules
first('[data-testid="edit-pipeline-schedule-btn"]').click
fill_in 'schedule_cron', with: '* 1 2 3 4'
save_pipeline_schedule
page.within(first('[data-testid="pipeline-schedule-table-row"]')) do
expect(page).to have_css("[data-testid='next-run-cell'] time")
end
end
end
end
context 'logged in as non-member' do
before do
gitlab_sign_in(user)
end
describe 'GET /projects/pipeline_schedules' do
before do
visit_pipelines_schedules
end
describe 'The view' do
it 'does not show create schedule button' do
expect(page).not_to have_link('New schedule')
end
end
end
end
context 'not logged in' do
describe 'GET /projects/pipeline_schedules' do
before do
visit_pipelines_schedules
end
describe 'The view' do
it 'does not show create schedule button' do
expect(page).not_to have_link('New schedule')
end
end
end
@ -413,7 +325,7 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :groups_and_projects
end
def select_timezone
find('[data-testid="schedule-timezone"] .gl-new-dropdown-toggle').click
find('#schedule-timezone .gl-new-dropdown-toggle').click
find("li", text: "Arizona").click
end
@ -421,12 +333,16 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :groups_and_projects
click_button 'master'
end
def create_pipeline_schedule
click_button s_('PipelineSchedules|Create pipeline schedule')
end
def save_pipeline_schedule
click_button 'Save pipeline schedule'
click_button s_('PipelineSchedules|Edit pipeline schedule')
end
def fill_in_schedule_form
fill_in 'schedule_description', with: 'my fancy description'
fill_in 'schedule-description', with: 'my fancy description'
fill_in 'schedule_cron', with: '* 1 2 3 4'
select_timezone

View File

@ -843,12 +843,10 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
end
it 'displays the PipelineSchedule in an inactive state' do
stub_feature_flags(pipeline_schedules_vue: false)
visit project_pipeline_schedules_path(project)
page.click_link('Inactive')
expect(page).to have_selector('table.ci-table > tbody > tr > td', text: 'blocked user schedule')
expect(page).to have_selector('[data-testid="pipeline-schedule-description"]', text: 'blocked user schedule')
end
it 'does not create a new Pipeline', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/408215' do

View File

@ -1,161 +0,0 @@
import $ from 'jquery';
import htmlPipelineSchedulesEdit from 'test_fixtures/pipeline_schedules/edit.html';
import htmlPipelineSchedulesEditWithVariables from 'test_fixtures/pipeline_schedules/edit_with_variables.html';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import VariableList from '~/ci/ci_variable_list/ci_variable_list';
const HIDE_CLASS = 'hide';
describe('VariableList', () => {
let $wrapper;
let variableList;
describe('with only key/value inputs', () => {
describe('with no variables', () => {
beforeEach(() => {
setHTMLFixture(htmlPipelineSchedulesEdit);
$wrapper = $('.js-ci-variable-list-section');
variableList = new VariableList({
container: $wrapper,
formField: 'schedule',
});
variableList.init();
});
afterEach(() => {
resetHTMLFixture();
});
it('should remove the row when clicking the remove button', () => {
$wrapper.find('.js-row-remove-button').trigger('click');
expect($wrapper.find('.js-row').length).toBe(0);
});
it('should add another row when editing the last rows key input', () => {
const $row = $wrapper.find('.js-row');
$row.find('.js-ci-variable-input-key').val('foo').trigger('input');
expect($wrapper.find('.js-row').length).toBe(2);
// Check for the correct default in the new row
const $keyInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-key');
expect($keyInput.val()).toBe('');
});
it('should add another row when editing the last rows value textarea', () => {
const $row = $wrapper.find('.js-row');
$row.find('.js-ci-variable-input-value').val('foo').trigger('input');
expect($wrapper.find('.js-row').length).toBe(2);
// Check for the correct default in the new row
const $valueInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-key');
expect($valueInput.val()).toBe('');
});
it('should remove empty row after blurring', () => {
const $row = $wrapper.find('.js-row');
$row.find('.js-ci-variable-input-key').val('foo').trigger('input');
expect($wrapper.find('.js-row').length).toBe(2);
$row.find('.js-ci-variable-input-key').val('').trigger('input').trigger('blur');
expect($wrapper.find('.js-row').length).toBe(1);
});
});
describe('with persisted variables', () => {
beforeEach(() => {
setHTMLFixture(htmlPipelineSchedulesEditWithVariables);
$wrapper = $('.js-ci-variable-list-section');
variableList = new VariableList({
container: $wrapper,
formField: 'schedule',
});
variableList.init();
});
afterEach(() => {
resetHTMLFixture();
});
it('should have "Reveal values" button initially when there are already variables', () => {
expect($wrapper.find('.js-secret-value-reveal-button').text()).toBe('Reveal values');
});
it('should reveal hidden values', () => {
const $row = $wrapper.find('.js-row:first-child');
const $inputValue = $row.find('.js-ci-variable-input-value');
const $placeholder = $row.find('.js-secret-value-placeholder');
expect($placeholder.hasClass(HIDE_CLASS)).toBe(false);
expect($inputValue.hasClass(HIDE_CLASS)).toBe(true);
// Reveal values
$wrapper.find('.js-secret-value-reveal-button').click();
expect($placeholder.hasClass(HIDE_CLASS)).toBe(true);
expect($inputValue.hasClass(HIDE_CLASS)).toBe(false);
});
});
});
describe('toggleEnableRow method', () => {
beforeEach(() => {
setHTMLFixture(htmlPipelineSchedulesEditWithVariables);
$wrapper = $('.js-ci-variable-list-section');
variableList = new VariableList({
container: $wrapper,
formField: 'variables',
});
variableList.init();
});
afterEach(() => {
resetHTMLFixture();
});
it('should disable all key inputs', () => {
expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3);
variableList.toggleEnableRow(false);
expect($wrapper.find('.js-ci-variable-input-key[disabled]').length).toBe(3);
});
it('should disable all remove buttons', () => {
expect($wrapper.find('.js-row-remove-button:not([disabled])').length).toBe(3);
variableList.toggleEnableRow(false);
expect($wrapper.find('.js-row-remove-button[disabled]').length).toBe(3);
});
it('should enable all remove buttons', () => {
variableList.toggleEnableRow(false);
expect($wrapper.find('.js-row-remove-button[disabled]').length).toBe(3);
variableList.toggleEnableRow(true);
expect($wrapper.find('.js-row-remove-button:not([disabled])').length).toBe(3);
});
it('should enable all key inputs', () => {
variableList.toggleEnableRow(false);
expect($wrapper.find('.js-ci-variable-input-key[disabled]').length).toBe(3);
variableList.toggleEnableRow(true);
expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3);
});
});
});

View File

@ -1,41 +0,0 @@
import $ from 'jquery';
import htmlPipelineSchedulesEdit from 'test_fixtures/pipeline_schedules/edit.html';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import setupNativeFormVariableList from '~/ci/ci_variable_list/native_form_variable_list';
describe('NativeFormVariableList', () => {
let $wrapper;
beforeEach(() => {
setHTMLFixture(htmlPipelineSchedulesEdit);
$wrapper = $('.js-ci-variable-list-section');
setupNativeFormVariableList({
container: $wrapper,
formField: 'schedule',
});
});
afterEach(() => {
resetHTMLFixture();
});
describe('onFormSubmit', () => {
it('should clear out the `name` attribute on the inputs for the last empty row on form submission (avoid BE validation)', () => {
const $row = $wrapper.find('.js-row');
expect($row.find('.js-ci-variable-input-key').attr('name')).toBe(
'schedule[variables_attributes][][key]',
);
expect($row.find('.js-ci-variable-input-value').attr('name')).toBe(
'schedule[variables_attributes][][secret_value]',
);
$wrapper.closest('form').trigger('trigger-submit');
expect($row.find('.js-ci-variable-input-key').attr('name')).toBe('');
expect($row.find('.js-ci-variable-input-value').attr('name')).toBe('');
});
});
});

View File

@ -1,5 +1,6 @@
import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { s__ } from '~/locale';
import PipelineScheduleTarget from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue';
import { mockPipelineScheduleNodes } from '../../../mock_data';
@ -20,18 +21,35 @@ describe('Pipeline schedule target', () => {
const findIcon = () => wrapper.findComponent(GlIcon);
const findLink = () => wrapper.findComponent(GlLink);
const findTarget = () => wrapper.findComponent('[data-testid="pipeline-schedule-target"]');
beforeEach(() => {
createComponent();
describe('with ref', () => {
beforeEach(() => {
createComponent();
});
it('displays icon', () => {
expect(findIcon().exists()).toBe(true);
expect(findIcon().props('name')).toBe('fork');
});
it('displays ref link', () => {
expect(findLink().attributes('href')).toBe(defaultProps.schedule.refPath);
expect(findLink().text()).toBe(defaultProps.schedule.refForDisplay);
});
});
it('displays icon', () => {
expect(findIcon().exists()).toBe(true);
expect(findIcon().props('name')).toBe('fork');
});
describe('without refPath', () => {
beforeEach(() => {
createComponent({
schedule: { ...mockPipelineScheduleNodes[0], refPath: null, refForDisplay: null },
});
});
it('displays ref link', () => {
expect(findLink().attributes('href')).toBe(defaultProps.schedule.refPath);
expect(findLink().text()).toBe(defaultProps.schedule.refForDisplay);
it('displays none for the target', () => {
expect(findIcon().exists()).toBe(false);
expect(findLink().exists()).toBe(false);
expect(findTarget().text()).toBe(s__('PipelineSchedules|None'));
});
});
});

View File

@ -1,42 +0,0 @@
import { GlModal } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TakeOwnershipModalLegacy from '~/ci/pipeline_schedules/components/take_ownership_modal_legacy.vue';
describe('Take ownership modal', () => {
let wrapper;
const url = `/root/job-log-tester/-/pipeline_schedules/3/take_ownership`;
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(TakeOwnershipModalLegacy, {
propsData: {
ownershipUrl: url,
...props,
},
});
};
const findModal = () => wrapper.findComponent(GlModal);
beforeEach(() => {
createComponent();
});
it('has a primary action set to a url and a post data-method', () => {
const actionPrimary = findModal().props('actionPrimary');
expect(actionPrimary.attributes).toEqual(
expect.objectContaining({
category: 'primary',
variant: 'confirm',
href: url,
'data-method': 'post',
}),
);
});
it('shows a take ownership message', () => {
expect(findModal().text()).toBe(
'Only the owner of a pipeline schedule can make changes to it. Do you want to take ownership of this schedule?',
);
});
});

View File

@ -0,0 +1,31 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContributionEventUpdated from '~/contribution_events/components/contribution_event/contribution_event_updated.vue';
import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
import { eventDesignUpdated, eventWikiPageUpdated } from '../../utils';
describe('ContributionEventUpdated', () => {
let wrapper;
const createComponent = ({ propsData }) => {
wrapper = shallowMountExtended(ContributionEventUpdated, {
propsData,
});
};
describe.each`
event | expectedMessage
${eventDesignUpdated()} | ${'Updated design %{targetLink} in %{resourceParentLink}.'}
${eventWikiPageUpdated()} | ${'Updated wiki page %{targetLink} in %{resourceParentLink}.'}
${{ target: { type: 'unsupported type' } }} | ${'Updated resource.'}
`('when event target type is $event.target.type', ({ event, expectedMessage }) => {
it('renders `ContributionEventBase` with correct props', () => {
createComponent({ propsData: { event } });
expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({
event,
message: expectedMessage,
iconName: 'pencil',
});
});
});
});

View File

@ -11,6 +11,7 @@ import ContributionEventCreated from '~/contribution_events/components/contribut
import ContributionEventClosed from '~/contribution_events/components/contribution_event/contribution_event_closed.vue';
import ContributionEventReopened from '~/contribution_events/components/contribution_event/contribution_event_reopened.vue';
import ContributionEventCommented from '~/contribution_events/components/contribution_event/contribution_event_commented.vue';
import ContributionEventUpdated from '~/contribution_events/components/contribution_event/contribution_event_updated.vue';
import {
eventApproved,
eventExpired,
@ -23,6 +24,7 @@ import {
eventClosed,
eventReopened,
eventCommented,
eventUpdated,
} from '../utils';
describe('ContributionEvents', () => {
@ -43,6 +45,7 @@ describe('ContributionEvents', () => {
eventClosed(),
eventReopened(),
eventCommented(),
eventUpdated(),
],
},
});
@ -61,6 +64,7 @@ describe('ContributionEvents', () => {
${ContributionEventClosed} | ${eventClosed()}
${ContributionEventReopened} | ${eventReopened()}
${ContributionEventCommented} | ${eventCommented()}
${ContributionEventUpdated} | ${eventUpdated()}
`(
'renders `$expectedComponent.name` component and passes expected event',
({ expectedComponent, expectedEvent }) => {

View File

@ -10,6 +10,7 @@ import {
EVENT_TYPE_CLOSED,
EVENT_TYPE_REOPENED,
EVENT_TYPE_COMMENTED,
EVENT_TYPE_UPDATED,
PUSH_EVENT_REF_TYPE_BRANCH,
PUSH_EVENT_REF_TYPE_TAG,
EVENT_TYPE_CREATED,
@ -32,22 +33,12 @@ import {
COMMIT_NOTEABLE_TYPE,
} from '~/notes/constants';
// Private finders
const findEventByAction = (action) => () => events.find((event) => event.action === action);
const findEventByActionAndTargetType = (action, targetType) => () =>
events.find((event) => event.action === action && event.target?.type === targetType);
const findEventByActionAndIssueType = (action, issueType) => () =>
events.find((event) => event.action === action && event.target.issue_type === issueType);
export const eventApproved = findEventByAction(EVENT_TYPE_APPROVED);
export const eventExpired = findEventByAction(EVENT_TYPE_EXPIRED);
export const eventJoined = findEventByAction(EVENT_TYPE_JOINED);
export const eventLeft = findEventByAction(EVENT_TYPE_LEFT);
export const eventMerged = findEventByAction(EVENT_TYPE_MERGED);
const findPushEvent = ({
isNew = false,
isRemoved = false,
@ -62,6 +53,43 @@ const findPushEvent = ({
ref.type === refType &&
commit.count === commitCount,
);
const findEventByActionAndNoteableType = (action, noteableType) => () =>
events.find((event) => event.action === action && event.noteable?.type === noteableType);
const findCommentedSnippet = (resourceParentType) => () =>
events.find(
(event) =>
event.action === EVENT_TYPE_COMMENTED &&
event.noteable?.type === SNIPPET_NOTEABLE_TYPE &&
event.resource_parent?.type === resourceParentType,
);
const findUpdatedEvent = (targetType) =>
findEventByActionAndTargetType(EVENT_TYPE_UPDATED, targetType);
// Finders that are used by EE
export const findCreatedEvent = (targetType) =>
findEventByActionAndTargetType(EVENT_TYPE_CREATED, targetType);
export const findWorkItemCreatedEvent = (issueType) =>
findEventByActionAndIssueType(EVENT_TYPE_CREATED, issueType);
export const findClosedEvent = (targetType) =>
findEventByActionAndTargetType(EVENT_TYPE_CREATED, targetType);
export const findWorkItemClosedEvent = (issueType) =>
findEventByActionAndIssueType(EVENT_TYPE_CLOSED, issueType);
export const findReopenedEvent = (targetType) =>
findEventByActionAndTargetType(EVENT_TYPE_REOPENED, targetType);
export const findWorkItemReopenedEvent = (issueType) =>
findEventByActionAndIssueType(EVENT_TYPE_REOPENED, issueType);
export const findCommentedEvent = (noteableType) =>
findEventByActionAndNoteableType(EVENT_TYPE_COMMENTED, noteableType);
export const eventApproved = findEventByAction(EVENT_TYPE_APPROVED);
export const eventExpired = findEventByAction(EVENT_TYPE_EXPIRED);
export const eventJoined = findEventByAction(EVENT_TYPE_JOINED);
export const eventLeft = findEventByAction(EVENT_TYPE_LEFT);
export const eventMerged = findEventByAction(EVENT_TYPE_MERGED);
export const eventPushedNewBranch = findPushEvent({ isNew: true });
export const eventPushedNewTag = findPushEvent({ isNew: true, refType: PUSH_EVENT_REF_TYPE_TAG });
@ -77,12 +105,6 @@ export const eventBulkPushedBranch = findPushEvent({ commitCount: 5 });
export const eventPrivate = () => ({ ...events[0], action: EVENT_TYPE_PRIVATE });
export const eventCreated = findEventByAction(EVENT_TYPE_CREATED);
export const findCreatedEvent = (targetType) =>
findEventByActionAndTargetType(EVENT_TYPE_CREATED, targetType);
export const findWorkItemCreatedEvent = (issueType) =>
findEventByActionAndIssueType(EVENT_TYPE_CREATED, issueType);
export const eventProjectCreated = findCreatedEvent(undefined);
export const eventMilestoneCreated = findCreatedEvent(TARGET_TYPE_MILESTONE);
export const eventIssueCreated = findCreatedEvent(TARGET_TYPE_ISSUE);
@ -93,12 +115,6 @@ export const eventTaskCreated = findWorkItemCreatedEvent(WORK_ITEM_ISSUE_TYPE_TA
export const eventIncidentCreated = findWorkItemCreatedEvent(WORK_ITEM_ISSUE_TYPE_INCIDENT);
export const eventClosed = findEventByAction(EVENT_TYPE_CLOSED);
export const findClosedEvent = (targetType) =>
findEventByActionAndTargetType(EVENT_TYPE_CREATED, targetType);
export const findWorkItemClosedEvent = (issueType) =>
findEventByActionAndIssueType(EVENT_TYPE_CLOSED, issueType);
export const eventMilestoneClosed = findClosedEvent(TARGET_TYPE_MILESTONE);
export const eventIssueClosed = findClosedEvent(TARGET_TYPE_ISSUE);
export const eventMergeRequestClosed = findClosedEvent(TARGET_TYPE_MERGE_REQUEST);
@ -108,12 +124,6 @@ export const eventTaskClosed = findWorkItemClosedEvent(WORK_ITEM_ISSUE_TYPE_TASK
export const eventIncidentClosed = findWorkItemClosedEvent(WORK_ITEM_ISSUE_TYPE_INCIDENT);
export const eventReopened = findEventByAction(EVENT_TYPE_REOPENED);
export const findReopenedEvent = (targetType) =>
findEventByActionAndTargetType(EVENT_TYPE_REOPENED, targetType);
export const findWorkItemReopenedEvent = (issueType) =>
findEventByActionAndIssueType(EVENT_TYPE_REOPENED, issueType);
export const eventMilestoneReopened = findReopenedEvent(TARGET_TYPE_MILESTONE);
export const eventMergeRequestReopened = findReopenedEvent(TARGET_TYPE_MERGE_REQUEST);
export const eventWikiPageReopened = findReopenedEvent(TARGET_TYPE_WIKI);
@ -123,19 +133,6 @@ export const eventTaskReopened = findWorkItemReopenedEvent(WORK_ITEM_ISSUE_TYPE_
export const eventIncidentReopened = findWorkItemReopenedEvent(WORK_ITEM_ISSUE_TYPE_INCIDENT);
export const eventCommented = findEventByAction(EVENT_TYPE_COMMENTED);
const findEventByActionAndNoteableType = (action, noteableType) => () =>
events.find((event) => event.action === action && event.noteable?.type === noteableType);
export const findCommentedEvent = (noteableType) =>
findEventByActionAndNoteableType(EVENT_TYPE_COMMENTED, noteableType);
export const findCommentedSnippet = (resourceParentType) => () =>
events.find(
(event) =>
event.action === EVENT_TYPE_COMMENTED &&
event.noteable?.type === SNIPPET_NOTEABLE_TYPE &&
event.resource_parent?.type === resourceParentType,
);
export const eventCommentedIssue = findCommentedEvent(ISSUE_NOTEABLE_TYPE);
export const eventCommentedMergeRequest = findCommentedEvent(MERGE_REQUEST_NOTEABLE_TYPE);
export const eventCommentedSnippet = findCommentedEvent(SNIPPET_NOTEABLE_TYPE);
@ -153,3 +150,7 @@ export const eventCommentedCommit = () => ({
first_line_in_markdown: '\u003cp\u003eMy title 9\u003c/p\u003e',
},
});
export const eventUpdated = findEventByAction(EVENT_TYPE_UPDATED);
export const eventDesignUpdated = findUpdatedEvent(TARGET_TYPE_DESIGN);
export const eventWikiPageUpdated = findUpdatedEvent(TARGET_TYPE_WIKI);

View File

@ -16,35 +16,6 @@ RSpec.describe 'Pipeline schedules (JavaScript fixtures)' do
let!(:pipeline_schedule_variable1) { create(:ci_pipeline_schedule_variable, key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule_populated) }
let!(:pipeline_schedule_variable2) { create(:ci_pipeline_schedule_variable, key: 'bar', value: 'barvalue', pipeline_schedule: pipeline_schedule_populated) }
describe Projects::PipelineSchedulesController, type: :controller do
render_views
before do
sign_in(user)
stub_feature_flags(pipeline_schedules_vue: false)
end
it 'pipeline_schedules/edit.html' do
get :edit, params: {
namespace_id: project.namespace.to_param,
project_id: project,
id: pipeline_schedule.id
}
expect(response).to be_successful
end
it 'pipeline_schedules/edit_with_variables.html' do
get :edit, params: {
namespace_id: project.namespace.to_param,
project_id: project,
id: pipeline_schedule_populated.id
}
expect(response).to be_successful
end
end
describe GraphQL::Query, type: :request do
before do
pipeline_schedule.pipelines << build(:ci_pipeline, project: project)

View File

@ -62,6 +62,15 @@ describe('Jobs Table', () => {
});
expect(findAllCoverageJobs()).toHaveLength(jobsThatHaveCoverage.length);
});
describe('when stage of a job is missing', () => {
it('shows no stage', () => {
const stagelessJob = { ...mockJobsNodes[0], stage: null };
createComponent({ jobs: [stagelessJob] });
expect(findJobStage().exists()).toBe(false);
});
});
});
describe('regular user', () => {

View File

@ -1,46 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::RebalancePartitionId,
:migration,
schema: 20230125093723,
feature_category: :continuous_integration do
let(:ci_builds_table) { table(:ci_builds, database: :ci) }
let(:ci_pipelines_table) { table(:ci_pipelines, database: :ci) }
let!(:valid_ci_pipeline) { ci_pipelines_table.create!(id: 1, partition_id: 100) }
let!(:invalid_ci_pipeline) { ci_pipelines_table.create!(id: 2, partition_id: 101) }
describe '#perform' do
using RSpec::Parameterized::TableSyntax
where(:table_name, :invalid_record, :valid_record) do
:ci_pipelines | invalid_ci_pipeline | valid_ci_pipeline
end
subject(:perform) do
described_class.new(
start_id: 1,
end_id: 2,
batch_table: table_name,
batch_column: :id,
sub_batch_size: 1,
pause_ms: 0,
connection: Ci::ApplicationRecord.connection
).perform
end
shared_examples 'fix invalid records' do
it 'rebalances partition_id to 100 when partition_id is 101' do
expect { perform }
.to change { invalid_record.reload.partition_id }.from(101).to(100)
.and not_change { valid_record.reload.partition_id }
end
end
with_them do
it_behaves_like 'fix invalid records'
end
end
end

View File

@ -1,58 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe RebalancePartitionIdCiPipeline, migration: :gitlab_ci, feature_category: :continuous_integration do
let(:migration) { described_class::MIGRATION }
context 'when on sass' do
before do
allow(Gitlab).to receive(:com?).and_return(true)
end
describe '#up' do
it 'schedules background jobs for each batch of ci_builds' do
migrate!
expect(migration).to have_scheduled_batched_migration(
gitlab_schema: :gitlab_ci,
table_name: :ci_pipelines,
column_name: :id,
interval: described_class::DELAY_INTERVAL,
batch_size: described_class::BATCH_SIZE,
sub_batch_size: described_class::SUB_BATCH_SIZE
)
end
end
describe '#down' do
it 'deletes all batched migration records' do
migrate!
schema_migrate_down!
expect(migration).not_to have_scheduled_batched_migration
end
end
end
context 'when on self-managed instance' do
let(:migration) { described_class.new }
describe '#up' do
it 'does not schedule background job' do
expect(migration).not_to receive(:queue_batched_background_migration)
migration.up
end
end
describe '#down' do
it 'does not delete background job' do
expect(migration).not_to receive(:delete_batched_background_migration)
migration.down
end
end
end
end

View File

@ -1,58 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe RebalancePartitionIdCiBuild, migration: :gitlab_ci, feature_category: :continuous_integration do
let(:migration) { described_class::MIGRATION }
context 'when on sass' do
before do
allow(Gitlab).to receive(:com?).and_return(true)
end
describe '#up' do
it 'schedules background jobs for each batch of ci_builds' do
migrate!
expect(migration).to have_scheduled_batched_migration(
gitlab_schema: :gitlab_ci,
table_name: :ci_builds,
column_name: :id,
interval: described_class::DELAY_INTERVAL,
batch_size: described_class::BATCH_SIZE,
sub_batch_size: described_class::SUB_BATCH_SIZE
)
end
end
describe '#down' do
it 'deletes all batched migration records' do
migrate!
schema_migrate_down!
expect(migration).not_to have_scheduled_batched_migration
end
end
end
context 'when on self-managed instance' do
let(:migration) { described_class.new }
describe '#up' do
it 'does not schedule background job' do
expect(migration).not_to receive(:queue_batched_background_migration)
migration.up
end
end
describe '#down' do
it 'does not delete background job' do
expect(migration).not_to receive(:delete_batched_background_migration)
migration.down
end
end
end
end

View File

@ -1,58 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe FixPartitionIdsForCiPipelineVariable, migration: :gitlab_ci, feature_category: :continuous_integration do
let(:migration) { described_class::MIGRATION }
context 'when on saas' do
before do
allow(Gitlab).to receive(:com?).and_return(true)
end
describe '#up' do
it 'schedules background jobs for each batch of ci_pipeline_variables' do
migrate!
expect(migration).to have_scheduled_batched_migration(
gitlab_schema: :gitlab_ci,
table_name: :ci_pipeline_variables,
column_name: :id,
interval: described_class::DELAY_INTERVAL,
batch_size: described_class::BATCH_SIZE,
sub_batch_size: described_class::SUB_BATCH_SIZE
)
end
end
describe '#down' do
it 'deletes all batched migration records' do
migrate!
schema_migrate_down!
expect(migration).not_to have_scheduled_batched_migration
end
end
end
context 'when on self-managed instance' do
let(:migration) { described_class.new }
describe '#up' do
it 'does not schedule background job' do
expect(migration).not_to receive(:queue_batched_background_migration)
migration.up
end
end
describe '#down' do
it 'does not delete background job' do
expect(migration).not_to receive(:delete_batched_background_migration)
migration.down
end
end
end
end

View File

@ -1,58 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe FixPartitionIdsForCiJobArtifact, migration: :gitlab_ci, feature_category: :continuous_integration do
let(:migration) { described_class::MIGRATION }
context 'when on saas' do
before do
allow(Gitlab).to receive(:com?).and_return(true)
end
describe '#up' do
it 'schedules background jobs for each batch of ci_job_artifacts' do
migrate!
expect(migration).to have_scheduled_batched_migration(
gitlab_schema: :gitlab_ci,
table_name: :ci_job_artifacts,
column_name: :id,
interval: described_class::DELAY_INTERVAL,
batch_size: described_class::BATCH_SIZE,
sub_batch_size: described_class::SUB_BATCH_SIZE
)
end
end
describe '#down' do
it 'deletes all batched migration records' do
migrate!
schema_migrate_down!
expect(migration).not_to have_scheduled_batched_migration
end
end
end
context 'when on self-managed instance' do
let(:migration) { described_class.new }
describe '#up' do
it 'does not schedule background job' do
expect(migration).not_to receive(:queue_batched_background_migration)
migration.up
end
end
describe '#down' do
it 'does not delete background job' do
expect(migration).not_to receive(:delete_batched_background_migration)
migration.down
end
end
end
end

View File

@ -1,58 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe FixPartitionIdsForCiStage, migration: :gitlab_ci, feature_category: :continuous_integration do
let(:migration) { described_class::MIGRATION }
context 'when on saas' do
before do
allow(Gitlab).to receive(:com?).and_return(true)
end
describe '#up' do
it 'schedules background jobs for each batch of ci_stages' do
migrate!
expect(migration).to have_scheduled_batched_migration(
gitlab_schema: :gitlab_ci,
table_name: :ci_stages,
column_name: :id,
interval: described_class::DELAY_INTERVAL,
batch_size: described_class::BATCH_SIZE,
sub_batch_size: described_class::SUB_BATCH_SIZE
)
end
end
describe '#down' do
it 'deletes all batched migration records' do
migrate!
schema_migrate_down!
expect(migration).not_to have_scheduled_batched_migration
end
end
end
context 'when on self-managed instance' do
let(:migration) { described_class.new }
describe '#up' do
it 'does not schedule background job' do
expect(migration).not_to receive(:queue_batched_background_migration)
migration.up
end
end
describe '#down' do
it 'does not delete background job' do
expect(migration).not_to receive(:delete_batched_background_migration)
migration.down
end
end
end
end

View File

@ -1,60 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe FixPartitionIdsForCiBuildReportResult,
migration: :gitlab_ci,
feature_category: :continuous_integration do
let(:migration) { described_class::MIGRATION }
context 'when on saas' do
before do
allow(Gitlab).to receive(:com?).and_return(true)
end
describe '#up' do
it 'schedules background jobs for each batch of ci_build_report_results' do
migrate!
expect(migration).to have_scheduled_batched_migration(
gitlab_schema: :gitlab_ci,
table_name: :ci_build_report_results,
column_name: :build_id,
interval: described_class::DELAY_INTERVAL,
batch_size: described_class::BATCH_SIZE,
sub_batch_size: described_class::SUB_BATCH_SIZE
)
end
end
describe '#down' do
it 'deletes all batched migration records' do
migrate!
schema_migrate_down!
expect(migration).not_to have_scheduled_batched_migration
end
end
end
context 'when on self-managed instance' do
let(:migration) { described_class.new }
describe '#up' do
it 'does not schedule background job' do
expect(migration).not_to receive(:queue_batched_background_migration)
migration.up
end
end
describe '#down' do
it 'does not delete background job' do
expect(migration).not_to receive(:delete_batched_background_migration)
migration.down
end
end
end
end

View File

@ -1,60 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe FixPartitionIdsForCiBuildTraceMetadata,
migration: :gitlab_ci,
feature_category: :continuous_integration do
let(:migration) { described_class::MIGRATION }
context 'when on saas' do
before do
allow(Gitlab).to receive(:com?).and_return(true)
end
describe '#up' do
it 'schedules background jobs for each batch of ci_build_trace_metadata' do
migrate!
expect(migration).to have_scheduled_batched_migration(
gitlab_schema: :gitlab_ci,
table_name: :ci_build_trace_metadata,
column_name: :build_id,
interval: described_class::DELAY_INTERVAL,
batch_size: described_class::BATCH_SIZE,
sub_batch_size: described_class::SUB_BATCH_SIZE
)
end
end
describe '#down' do
it 'deletes all batched migration records' do
migrate!
schema_migrate_down!
expect(migration).not_to have_scheduled_batched_migration
end
end
end
context 'when on self-managed instance' do
let(:migration) { described_class.new }
describe '#up' do
it 'does not schedule background job' do
expect(migration).not_to receive(:queue_batched_background_migration)
migration.up
end
end
describe '#down' do
it 'does not delete background job' do
expect(migration).not_to receive(:delete_batched_background_migration)
migration.down
end
end
end
end

View File

@ -1,60 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe FixPartitionIdsForCiBuildMetadata,
migration: :gitlab_ci,
feature_category: :continuous_integration do
let(:migration) { described_class::MIGRATION }
context 'when on saas' do
before do
allow(Gitlab).to receive(:com?).and_return(true)
end
describe '#up' do
it 'schedules background jobs for each batch of p_ci_builds_metadata' do
migrate!
expect(migration).to have_scheduled_batched_migration(
gitlab_schema: :gitlab_ci,
table_name: :p_ci_builds_metadata,
column_name: :id,
interval: described_class::DELAY_INTERVAL,
batch_size: described_class::BATCH_SIZE,
sub_batch_size: described_class::SUB_BATCH_SIZE
)
end
end
describe '#down' do
it 'deletes all batched migration records' do
migrate!
schema_migrate_down!
expect(migration).not_to have_scheduled_batched_migration
end
end
end
context 'when on self-managed instance' do
let(:migration) { described_class.new }
describe '#up' do
it 'does not schedule background job' do
expect(migration).not_to receive(:queue_batched_background_migration)
migration.up
end
end
describe '#down' do
it 'does not delete background job' do
expect(migration).not_to receive(:delete_batched_background_migration)
migration.down
end
end
end
end

View File

@ -1,51 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe FixPartitionIdsForCiJobVariables, migration: :gitlab_ci, feature_category: :continuous_integration do
let(:builds) { table(:ci_builds, database: :ci) }
let(:job_variables) { table(:ci_job_variables, database: :ci) }
let(:connection) { job_variables.connection }
around do |example|
connection.execute "ALTER TABLE #{job_variables.quoted_table_name} DISABLE TRIGGER ALL;"
example.run
ensure
connection.execute "ALTER TABLE #{job_variables.quoted_table_name} ENABLE TRIGGER ALL;"
end
before do
job = builds.create!(partition_id: 100)
job_variables.insert_all!([
{ job_id: job.id, partition_id: 100, key: 'variable-100' },
{ job_id: job.id, partition_id: 101, key: 'variable-101' }
])
end
describe '#up', :aggregate_failures do
context 'when on sass' do
before do
allow(Gitlab).to receive(:com?).and_return(true)
end
it 'fixes partition_id' do
expect { migrate! }.not_to raise_error
expect(job_variables.where(partition_id: 100).count).to eq(2)
expect(job_variables.where(partition_id: 101).count).to eq(0)
end
end
context 'when on self managed' do
it 'does not change partition_id' do
expect { migrate! }.not_to raise_error
expect(job_variables.where(partition_id: 100).count).to eq(1)
expect(job_variables.where(partition_id: 101).count).to eq(1)
end
end
end
end

View File

@ -1,45 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe FixPartitionIdsOnCiSourcesPipelines, migration: :gitlab_ci, feature_category: :continuous_integration do
let(:sources_pipelines) { table(:ci_sources_pipelines, database: :ci) }
before do
sources_pipelines.insert_all!([
{ partition_id: 100, source_partition_id: 100 },
{ partition_id: 100, source_partition_id: 101 },
{ partition_id: 101, source_partition_id: 100 },
{ partition_id: 101, source_partition_id: 101 }
])
end
describe '#up' do
context 'when on sass' do
before do
allow(Gitlab).to receive(:com?).and_return(true)
end
it 'fixes partition_id and source_partition_id' do
expect { migrate! }.not_to raise_error
expect(sources_pipelines.where(partition_id: 100).count).to eq(4)
expect(sources_pipelines.where(partition_id: 101).count).to eq(0)
expect(sources_pipelines.where(source_partition_id: 100).count).to eq(4)
expect(sources_pipelines.where(source_partition_id: 101).count).to eq(0)
end
end
context 'when on self managed' do
it 'does not change partition_id or source_partition_id' do
expect { migrate! }.not_to raise_error
expect(sources_pipelines.where(partition_id: 100).count).to eq(2)
expect(sources_pipelines.where(partition_id: 100).count).to eq(2)
expect(sources_pipelines.where(source_partition_id: 101).count).to eq(2)
expect(sources_pipelines.where(source_partition_id: 101).count).to eq(2)
end
end
end
end

View File

@ -1,49 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'projects/pipeline_schedules/_pipeline_schedule' do
let(:owner) { create(:user) }
let(:maintainer) { create(:user) }
let(:project) { create(:project) }
let(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project) }
before do
assign(:project, project)
allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:pipeline_schedule).and_return(pipeline_schedule)
allow(view).to receive(:can?).and_return(true)
end
context 'taking ownership of schedule' do
context 'when non-owner is signed in' do
let(:user) { maintainer }
before do
allow(view).to receive(:can?).with(maintainer, :admin_pipeline_schedule, pipeline_schedule).and_return(true)
end
it 'non-owner can take ownership of pipeline' do
render
expect(rendered).to have_button('Take ownership')
end
end
context 'when owner is signed in' do
let(:user) { owner }
before do
allow(view).to receive(:can?).with(owner, :admin_pipeline_schedule, pipeline_schedule).and_return(false)
end
it 'owner cannot take ownership of pipeline' do
render
expect(rendered).not_to have_button('Take ownership')
end
end
end
end