Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
cddb9394be
commit
a9abb02902
|
|
@ -3316,7 +3316,6 @@ RSpec/FeatureCategory:
|
|||
- 'spec/models/legacy_diff_discussion_spec.rb'
|
||||
- 'spec/models/legacy_diff_note_spec.rb'
|
||||
- 'spec/models/lfs_download_object_spec.rb'
|
||||
- 'spec/models/lfs_file_lock_spec.rb'
|
||||
- 'spec/models/license_template_spec.rb'
|
||||
- 'spec/models/list_spec.rb'
|
||||
- 'spec/models/list_user_preference_spec.rb'
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
0.0.24
|
||||
0.0.26
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ export default {
|
|||
form: {},
|
||||
errorTitle: null,
|
||||
error: null,
|
||||
pipelineVariables: [],
|
||||
predefinedVariables: null,
|
||||
warnings: [],
|
||||
totalWarnings: 0,
|
||||
|
|
@ -277,6 +278,9 @@ export default {
|
|||
clearTimeout(pollTimeout);
|
||||
this.$apollo.queries.ciConfigVariables.stopPolling();
|
||||
},
|
||||
handleVariablesUpdated(updatedVariables) {
|
||||
this.pipelineVariables = updatedVariables;
|
||||
},
|
||||
populateForm() {
|
||||
this.configVariablesWithDescription = this.predefinedVariables.reduce(
|
||||
(accumulator, { description, key, value, valueOptions }) => {
|
||||
|
|
@ -367,7 +371,9 @@ export default {
|
|||
input: {
|
||||
projectPath: this.projectPath,
|
||||
ref: this.refShortName,
|
||||
variables: filterVariables(this.variables),
|
||||
variables: this.isUsingPipelineInputs
|
||||
? this.pipelineVariables
|
||||
: filterVariables(this.variables),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -488,8 +494,13 @@ export default {
|
|||
<pipeline-variables-form
|
||||
v-if="isUsingPipelineInputs"
|
||||
:default-branch="defaultBranch"
|
||||
:file-params="fileParams"
|
||||
:is-maintainer="isMaintainer"
|
||||
:project-path="projectPath"
|
||||
:ref-param="refParam"
|
||||
:settings-link="settingsLink"
|
||||
:variable-params="variableParams"
|
||||
@variables-updated="handleVariablesUpdated"
|
||||
/>
|
||||
<div v-else>
|
||||
<gl-loading-icon v-if="isLoading" class="gl-mb-5" size="md" />
|
||||
|
|
|
|||
|
|
@ -1,16 +1,68 @@
|
|||
<script>
|
||||
import { GlLoadingIcon, GlFormGroup } from '@gitlab/ui';
|
||||
import { fetchPolicies } from '~/lib/graphql';
|
||||
import {
|
||||
GlIcon,
|
||||
GlButton,
|
||||
GlCollapsibleListbox,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlFormTextarea,
|
||||
GlLink,
|
||||
GlLoadingIcon,
|
||||
GlSprintf,
|
||||
} from '@gitlab/ui';
|
||||
import { uniqueId } from 'lodash';
|
||||
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import { s__ } from '~/locale';
|
||||
import { reportToSentry } from '~/ci/utils';
|
||||
import { fetchPolicies } from '~/lib/graphql';
|
||||
import filterVariables from '../utils/filter_variables';
|
||||
import {
|
||||
CONFIG_VARIABLES_TIMEOUT,
|
||||
CI_VARIABLE_TYPE_FILE,
|
||||
CI_VARIABLE_TYPE_ENV_VAR,
|
||||
} from '../constants';
|
||||
import ciConfigVariablesQuery from '../graphql/queries/ci_config_variables.graphql';
|
||||
import VariableValuesListbox from './variable_values_listbox.vue';
|
||||
|
||||
let pollTimeout;
|
||||
export const POLLING_INTERVAL = 2000;
|
||||
|
||||
export default {
|
||||
name: 'PipelineVariablesForm',
|
||||
formElementClasses: 'gl-basis-1/4 gl-shrink-0 gl-flex-grow-0',
|
||||
learnMorePath: helpPagePath('ci/variables/_index', {
|
||||
anchor: 'cicd-variable-precedence',
|
||||
}),
|
||||
// this height value is used inline on the textarea to match the input field height
|
||||
// it's used to prevent the overwrite if 'gl-h-7' or '!gl-h-7' were used
|
||||
textAreaStyle: { height: '32px' },
|
||||
components: {
|
||||
GlLoadingIcon,
|
||||
GlIcon,
|
||||
GlButton,
|
||||
GlCollapsibleListbox,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlFormTextarea,
|
||||
GlLink,
|
||||
GlLoadingIcon,
|
||||
GlSprintf,
|
||||
VariableValuesListbox,
|
||||
},
|
||||
props: {
|
||||
defaultBranch: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fileParams: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
isMaintainer: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
|
@ -19,16 +71,26 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
defaultBranch: {
|
||||
settingsLink: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
variableParams: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ciConfigVariables: null,
|
||||
configVariablesWithDescription: {},
|
||||
form: {},
|
||||
refValue: {
|
||||
shortName: this.refParam,
|
||||
// this is needed until we add support for ref type in url query strings
|
||||
// ensure default branch is called with full ref on load
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/287815
|
||||
fullName: this.refParam === this.defaultBranch ? `refs/heads/${this.refParam}` : undefined,
|
||||
},
|
||||
};
|
||||
|
|
@ -37,6 +99,9 @@ export default {
|
|||
ciConfigVariables: {
|
||||
fetchPolicy: fetchPolicies.NO_CACHE,
|
||||
query: ciConfigVariablesQuery,
|
||||
skip() {
|
||||
return Object.keys(this.form).includes(this.refFullName);
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.projectPath,
|
||||
|
|
@ -46,21 +111,183 @@ export default {
|
|||
update({ project }) {
|
||||
return project?.ciConfigVariables || [];
|
||||
},
|
||||
result() {
|
||||
// API cache is empty when ciConfigVariables === null, so we need to
|
||||
// poll while cache values are being populated in the backend.
|
||||
// After CONFIG_VARIABLES_TIMEOUT ms have passed, we stop polling
|
||||
// and populate the form regardless.
|
||||
if (this.isFetchingCiConfigVariables && !pollTimeout) {
|
||||
pollTimeout = setTimeout(() => {
|
||||
this.ciConfigVariables = [];
|
||||
this.clearPolling();
|
||||
this.populateForm();
|
||||
}, CONFIG_VARIABLES_TIMEOUT);
|
||||
}
|
||||
|
||||
if (!this.isFetchingCiConfigVariables) {
|
||||
this.clearPolling();
|
||||
this.populateForm();
|
||||
}
|
||||
},
|
||||
error(error) {
|
||||
reportToSentry(this.$options.name, error);
|
||||
},
|
||||
pollInterval: POLLING_INTERVAL,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
descriptions() {
|
||||
return this.form[this.refFullName]?.descriptions ?? {};
|
||||
},
|
||||
isFetchingCiConfigVariables() {
|
||||
return this.ciConfigVariables === null;
|
||||
},
|
||||
isLoading() {
|
||||
return this.$apollo.queries.ciConfigVariables.loading || this.isFetchingCiConfigVariables;
|
||||
},
|
||||
isMobile() {
|
||||
return ['sm', 'xs'].includes(GlBreakpointInstance.getBreakpointSize());
|
||||
},
|
||||
refFullName() {
|
||||
return this.refValue.fullName;
|
||||
},
|
||||
refQueryParam() {
|
||||
return this.refValue.fullName || this.refValue.shortName;
|
||||
return this.refFullName || this.refShortName;
|
||||
},
|
||||
refShortName() {
|
||||
return this.refValue.shortName;
|
||||
},
|
||||
removeButtonCategory() {
|
||||
return this.isMobile ? 'secondary' : 'tertiary';
|
||||
},
|
||||
variables() {
|
||||
return this.form[this.refFullName]?.variables ?? [];
|
||||
},
|
||||
variableTypeListboxItems() {
|
||||
return [
|
||||
{
|
||||
value: CI_VARIABLE_TYPE_ENV_VAR,
|
||||
text: s__('Pipeline|Variable'),
|
||||
},
|
||||
{
|
||||
value: CI_VARIABLE_TYPE_FILE,
|
||||
text: s__('Pipeline|File'),
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
variables: {
|
||||
handler(newVariables) {
|
||||
this.$emit('variables-updated', filterVariables(newVariables));
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addEmptyVariable(refValue) {
|
||||
const { variables } = this.form[refValue];
|
||||
|
||||
const lastVar = variables[variables.length - 1];
|
||||
if (lastVar?.key === '' && lastVar?.value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
variables.push({
|
||||
uniqueId: uniqueId(`var-${refValue}`),
|
||||
variableType: CI_VARIABLE_TYPE_ENV_VAR,
|
||||
key: '',
|
||||
value: '',
|
||||
});
|
||||
},
|
||||
canRemove(index) {
|
||||
return index < this.variables.length - 1;
|
||||
},
|
||||
clearPolling() {
|
||||
clearTimeout(pollTimeout);
|
||||
this.$apollo.queries.ciConfigVariables.stopPolling();
|
||||
},
|
||||
createListItemsFromVariableOptions(key) {
|
||||
return this.configVariablesWithDescription.options[key].map((option) => ({
|
||||
text: option,
|
||||
value: option,
|
||||
}));
|
||||
},
|
||||
getPipelineAriaLabel(index) {
|
||||
return `${s__('Pipeline|Variable')} ${index + 1}`;
|
||||
},
|
||||
populateForm() {
|
||||
this.configVariablesWithDescription = this.ciConfigVariables.reduce(
|
||||
(accumulator, { description, key, value, valueOptions }) => {
|
||||
if (description) {
|
||||
accumulator.descriptions[key] = description;
|
||||
accumulator.values[key] = value;
|
||||
accumulator.options[key] = valueOptions;
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
},
|
||||
{ descriptions: {}, values: {}, options: {} },
|
||||
);
|
||||
|
||||
this.form = {
|
||||
...this.form,
|
||||
[this.refFullName]: {
|
||||
descriptions: this.configVariablesWithDescription.descriptions,
|
||||
variables: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Add default variables from yml
|
||||
this.setVariableParams(
|
||||
this.refFullName,
|
||||
CI_VARIABLE_TYPE_ENV_VAR,
|
||||
this.configVariablesWithDescription.values,
|
||||
);
|
||||
|
||||
// Add/update variables, e.g. from query string
|
||||
if (this.variableParams) {
|
||||
this.setVariableParams(this.refFullName, CI_VARIABLE_TYPE_ENV_VAR, this.variableParams);
|
||||
}
|
||||
|
||||
if (this.fileParams) {
|
||||
this.setVariableParams(this.refFullName, CI_VARIABLE_TYPE_FILE, this.fileParams);
|
||||
}
|
||||
|
||||
// Adds empty var at the end of the form
|
||||
this.addEmptyVariable(this.refFullName);
|
||||
},
|
||||
removeVariable(index) {
|
||||
this.variables.splice(index, 1);
|
||||
},
|
||||
setVariableAttribute(key, attribute, value) {
|
||||
const { variables } = this.form[this.refFullName];
|
||||
const variable = variables.find((v) => v.key === key);
|
||||
variable[attribute] = value;
|
||||
},
|
||||
setVariable(refValue, { type, key, value }) {
|
||||
const { variables } = this.form[refValue];
|
||||
|
||||
const variable = variables.find((v) => v.key === key);
|
||||
if (variable) {
|
||||
variable.variableType = type;
|
||||
variable.value = value;
|
||||
} else {
|
||||
variables.push({
|
||||
uniqueId: uniqueId(`var-${refValue}`),
|
||||
key,
|
||||
value,
|
||||
variableType: type,
|
||||
});
|
||||
}
|
||||
},
|
||||
setVariableParams(refValue, type, paramsObj) {
|
||||
Object.entries(paramsObj).forEach(([key, value]) => {
|
||||
this.setVariable(refValue, { type, key, value });
|
||||
});
|
||||
},
|
||||
shouldShowValuesDropdown(key) {
|
||||
return this.configVariablesWithDescription.options[key]?.length > 1;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -69,8 +296,105 @@ export default {
|
|||
<template>
|
||||
<div>
|
||||
<gl-loading-icon v-if="isLoading" class="gl-mb-5" size="md" />
|
||||
<gl-form-group v-else>
|
||||
<pre>{{ ciConfigVariables }}</pre>
|
||||
<gl-form-group v-else :label="s__('Pipeline|Variables')">
|
||||
<div
|
||||
v-for="(variable, index) in variables"
|
||||
:key="variable.uniqueId"
|
||||
class="gl-mb-4"
|
||||
data-testid="ci-variable-row-container"
|
||||
>
|
||||
<div class="gl-flex gl-flex-col gl-items-stretch gl-gap-4 md:gl-flex-row">
|
||||
<gl-collapsible-listbox
|
||||
:items="variableTypeListboxItems"
|
||||
:selected="variable.variableType"
|
||||
block
|
||||
fluid-width
|
||||
:aria-label="getPipelineAriaLabel(index)"
|
||||
:class="$options.formElementClasses"
|
||||
data-testid="pipeline-form-ci-variable-type"
|
||||
@select="setVariableAttribute(variable.key, 'variableType', $event)"
|
||||
/>
|
||||
<gl-form-input
|
||||
v-model="variable.key"
|
||||
:placeholder="s__('CiVariables|Input variable key')"
|
||||
:class="$options.formElementClasses"
|
||||
data-testid="pipeline-form-ci-variable-key-field"
|
||||
@change="addEmptyVariable(refFullName)"
|
||||
/>
|
||||
<variable-values-listbox
|
||||
v-if="shouldShowValuesDropdown(variable.key)"
|
||||
:items="createListItemsFromVariableOptions(variable.key)"
|
||||
:selected="variable.value"
|
||||
:class="$options.formElementClasses"
|
||||
class="!gl-mr-0 gl-grow"
|
||||
data-testid="pipeline-form-ci-variable-value-dropdown"
|
||||
@select="setVariableAttribute(variable.key, 'value', $event)"
|
||||
/>
|
||||
<gl-form-textarea
|
||||
v-else
|
||||
v-model="variable.value"
|
||||
:placeholder="s__('CiVariables|Input variable value')"
|
||||
:style="$options.textAreaStyle"
|
||||
:no-resize="false"
|
||||
data-testid="pipeline-form-ci-variable-value-field"
|
||||
/>
|
||||
<template v-if="variables.length > 1">
|
||||
<gl-button
|
||||
v-if="canRemove(index)"
|
||||
size="small"
|
||||
class="gl-shrink-0"
|
||||
data-testid="remove-ci-variable-row"
|
||||
:category="removeButtonCategory"
|
||||
:aria-label="s__('CiVariables|Remove variable')"
|
||||
@click="removeVariable(index)"
|
||||
>
|
||||
<gl-icon class="!gl-mr-0" name="remove" />
|
||||
<span class="md:gl-hidden">{{ s__('CiVariables|Remove variable') }}</span>
|
||||
</gl-button>
|
||||
<gl-button
|
||||
v-else
|
||||
class="gl-invisible gl-hidden gl-shrink-0 md:gl-block"
|
||||
icon="remove"
|
||||
:aria-label="s__('CiVariables|Remove variable')"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="descriptions[variable.key]" class="gl-text-subtle">
|
||||
{{ descriptions[variable.key] }}
|
||||
</div>
|
||||
</div>
|
||||
<template #description>
|
||||
<gl-sprintf
|
||||
:message="
|
||||
s__(
|
||||
'Pipeline|Specify variable values to be used in this run. The variables specified in the configuration file as well as %{linkStart}CI/CD settings%{linkEnd} are used by default.',
|
||||
)
|
||||
"
|
||||
>
|
||||
<template #link="{ content }">
|
||||
<gl-link v-if="isMaintainer" :href="settingsLink" data-testid="ci-cd-settings-link">
|
||||
{{ content }}
|
||||
</gl-link>
|
||||
<template v-else>{{ content }}</template>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
<gl-link :href="$options.learnMorePath" target="_blank">
|
||||
{{ __('Learn more') }}
|
||||
</gl-link>
|
||||
<div class="gl-mt-4 gl-text-subtle">
|
||||
<gl-sprintf
|
||||
:message="
|
||||
s__(
|
||||
'CiVariables|Variables specified here are %{boldStart}expanded%{boldEnd} and not %{boldStart}masked.%{boldEnd}',
|
||||
)
|
||||
"
|
||||
>
|
||||
<template #bold="{ content }">
|
||||
<strong>{{ content }}</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
</template>
|
||||
</gl-form-group>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ export default {
|
|||
event: 'change',
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
|
@ -134,6 +139,7 @@ export default {
|
|||
/>
|
||||
<div class="select-wrapper gl-grow">
|
||||
<select
|
||||
:id="id"
|
||||
v-model="internalValue"
|
||||
:disabled="disableSelectInput"
|
||||
class="form-control project-repo-select select-control"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,21 @@
|
|||
<script>
|
||||
import { GlFormGroup } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlFormGroup,
|
||||
},
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
labelFor: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
helpPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
|
@ -26,20 +36,27 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="project-feature-row">
|
||||
<div class="gl-flex">
|
||||
<label v-if="label" class="label-bold !gl-mb-0" :class="{ 'gl-text-disabled': locked }">
|
||||
<gl-form-group :label-for="labelFor" label-class="!gl-pb-1" class="project-feature-row gl-mb-0">
|
||||
<template #label>
|
||||
<span
|
||||
v-if="label"
|
||||
:class="{ 'gl-text-disabled': locked }"
|
||||
data-testid="project-settings-row-label"
|
||||
>
|
||||
{{ label }}
|
||||
</label>
|
||||
</span>
|
||||
<slot name="label-icon"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<span v-if="helpText" class="gl-text-subtle"> {{ helpText }} </span>
|
||||
<span v-if="helpText" class="gl-text-subtle" data-testid="project-settings-row-help-text">
|
||||
{{ helpText }}
|
||||
</span>
|
||||
<span v-if="helpPath"
|
||||
><a :href="helpPath" target="_blank">{{ __('Learn more') }}</a
|
||||
>.</span
|
||||
>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</gl-form-group>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -573,12 +573,14 @@ export default {
|
|||
ref="project-visibility-settings"
|
||||
:help-path="visibilityHelpPath"
|
||||
:label="s__('ProjectSettings|Project visibility')"
|
||||
label-for="project_visibility_level"
|
||||
:help-text="
|
||||
s__('ProjectSettings|Manage who can see the project in the public access directory.')
|
||||
"
|
||||
>
|
||||
<div class="project-feature-controls gl-mx-0 gl-my-3 gl-flex gl-items-center">
|
||||
<gl-form-select
|
||||
id="project_visibility_level"
|
||||
v-model="visibilityLevel"
|
||||
:disabled="!canChangeVisibilityLevel"
|
||||
name="project[visibility_level]"
|
||||
|
|
@ -654,6 +656,7 @@ export default {
|
|||
ref="issues-settings"
|
||||
:help-path="issuesHelpPath"
|
||||
:label="$options.i18n.issuesLabel"
|
||||
label-for="issues_access_level"
|
||||
:help-text="
|
||||
s__(
|
||||
'ProjectSettings|Flexible tool to collaboratively develop ideas and plan work in this project.',
|
||||
|
|
@ -661,6 +664,7 @@ export default {
|
|||
"
|
||||
>
|
||||
<project-feature-setting
|
||||
id="issues_access_level"
|
||||
v-model="issuesAccessLevel"
|
||||
:label="$options.i18n.issuesLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
|
|
@ -687,9 +691,11 @@ export default {
|
|||
<project-setting-row
|
||||
ref="repository-settings"
|
||||
:label="$options.i18n.repositoryLabel"
|
||||
label-for="repository_access_level"
|
||||
:help-text="repositoryHelpText"
|
||||
>
|
||||
<project-feature-setting
|
||||
id="repository_access_level"
|
||||
v-model="repositoryAccessLevel"
|
||||
:label="$options.i18n.repositoryLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
|
|
@ -701,9 +707,11 @@ export default {
|
|||
<project-setting-row
|
||||
ref="merge-request-settings"
|
||||
:label="$options.i18n.mergeRequestsLabel"
|
||||
label-for="merge_requests_access_level"
|
||||
:help-text="s__('ProjectSettings|Submit changes to be merged upstream.')"
|
||||
>
|
||||
<project-feature-setting
|
||||
id="merge_requests_access_level"
|
||||
v-model="mergeRequestsAccessLevel"
|
||||
:label="$options.i18n.mergeRequestsLabel"
|
||||
:options="repoFeatureAccessLevelOptions"
|
||||
|
|
@ -715,9 +723,11 @@ export default {
|
|||
<project-setting-row
|
||||
ref="fork-settings"
|
||||
:label="$options.i18n.forksLabel"
|
||||
label-for="forking_access_level"
|
||||
:help-text="s__('ProjectSettings|Users can copy the repository to a new project.')"
|
||||
>
|
||||
<project-feature-setting
|
||||
id="forking_access_level"
|
||||
v-model="forkingAccessLevel"
|
||||
:label="$options.i18n.forksLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
|
|
@ -764,6 +774,7 @@ export default {
|
|||
<project-setting-row
|
||||
ref="pipeline-settings"
|
||||
:label="$options.i18n.ciCdLabel"
|
||||
label-for="builds_access_level"
|
||||
:help-text="
|
||||
s__(
|
||||
'ProjectSettings|Build, test, and deploy your changes. Does not apply to project integrations.',
|
||||
|
|
@ -771,6 +782,7 @@ export default {
|
|||
"
|
||||
>
|
||||
<project-feature-setting
|
||||
id="builds_access_level"
|
||||
v-model="buildsAccessLevel"
|
||||
:label="$options.i18n.ciCdLabel"
|
||||
:options="repoFeatureAccessLevelOptions"
|
||||
|
|
@ -785,6 +797,7 @@ export default {
|
|||
ref="container-registry-settings"
|
||||
:help-path="registryHelpPath"
|
||||
:label="$options.i18n.containerRegistryLabel"
|
||||
label-for="container_registry_access_level"
|
||||
:help-text="
|
||||
s__('ProjectSettings|Every project can have its own space to store its Docker images')
|
||||
"
|
||||
|
|
@ -803,6 +816,7 @@ export default {
|
|||
</gl-sprintf>
|
||||
</div>
|
||||
<project-feature-setting
|
||||
id="container_registry_access_level"
|
||||
v-model="containerRegistryAccessLevel"
|
||||
:options="featureAccessLevelOptions"
|
||||
:disabled-select-input="isProjectPrivate"
|
||||
|
|
@ -813,9 +827,11 @@ export default {
|
|||
<project-setting-row
|
||||
ref="analytics-settings"
|
||||
:label="$options.i18n.analyticsLabel"
|
||||
label-for="analytics_access_level"
|
||||
:help-text="s__('ProjectSettings|View project analytics.')"
|
||||
>
|
||||
<project-feature-setting
|
||||
id="analytics_access_level"
|
||||
v-model="analyticsAccessLevel"
|
||||
:label="$options.i18n.analyticsLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
|
|
@ -827,9 +843,11 @@ export default {
|
|||
v-if="requirementsAvailable"
|
||||
ref="requirements-settings"
|
||||
:label="$options.i18n.requirementsLabel"
|
||||
label-for="requirements_access_level"
|
||||
:help-text="s__('ProjectSettings|Requirements management system.')"
|
||||
>
|
||||
<project-feature-setting
|
||||
id="requirements_access_level"
|
||||
v-model="requirementsAccessLevel"
|
||||
:label="$options.i18n.requirementsLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
|
|
@ -839,9 +857,11 @@ export default {
|
|||
</project-setting-row>
|
||||
<project-setting-row
|
||||
:label="$options.i18n.securityAndComplianceLabel"
|
||||
label-for="security_and_compliance_access_level"
|
||||
:help-text="s__('ProjectSettings|Security and compliance for this project.')"
|
||||
>
|
||||
<project-feature-setting
|
||||
id="security_and_compliance_access_level"
|
||||
v-model="securityAndComplianceAccessLevel"
|
||||
:label="$options.i18n.securityAndComplianceLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
|
|
@ -852,9 +872,11 @@ export default {
|
|||
<project-setting-row
|
||||
ref="wiki-settings"
|
||||
:label="$options.i18n.wikiLabel"
|
||||
label-for="wiki_access_level"
|
||||
:help-text="s__('ProjectSettings|Pages for project documentation.')"
|
||||
>
|
||||
<project-feature-setting
|
||||
id="wiki_access_level"
|
||||
v-model="wikiAccessLevel"
|
||||
:label="$options.i18n.wikiLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
|
|
@ -865,9 +887,11 @@ export default {
|
|||
<project-setting-row
|
||||
ref="snippet-settings"
|
||||
:label="$options.i18n.snippetsLabel"
|
||||
label-for="snippets_access_level"
|
||||
:help-text="s__('ProjectSettings|Share code with others outside the project.')"
|
||||
>
|
||||
<project-feature-setting
|
||||
id="snippets_access_level"
|
||||
v-model="snippetsAccessLevel"
|
||||
:label="$options.i18n.snippetsLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
|
|
@ -918,10 +942,12 @@ export default {
|
|||
<project-setting-row
|
||||
ref="model-experiments-settings"
|
||||
:label="$options.i18n.modelExperimentsLabel"
|
||||
label-for="model_experiments_access_level"
|
||||
:help-text="$options.i18n.modelExperimentsHelpText"
|
||||
:help-path="$options.modelExperimentsHelpPath"
|
||||
>
|
||||
<project-feature-setting
|
||||
id="model_experiments_access_level"
|
||||
v-model="modelExperimentsAccessLevel"
|
||||
:label="$options.i18n.modelExperimentsLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
|
|
@ -932,10 +958,12 @@ export default {
|
|||
<project-setting-row
|
||||
ref="model-registry-settings"
|
||||
:label="$options.i18n.modelRegistryLabel"
|
||||
label-for="model_registry_access_level"
|
||||
:help-text="$options.i18n.modelRegistryHelpText"
|
||||
:help-path="$options.modelRegistryHelpPath"
|
||||
>
|
||||
<project-feature-setting
|
||||
id="model_registry_access_level"
|
||||
v-model="modelRegistryAccessLevel"
|
||||
:label="$options.i18n.modelRegistryLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
|
|
@ -948,6 +976,7 @@ export default {
|
|||
ref="pages-settings"
|
||||
:help-path="pagesHelpPath"
|
||||
:label="$options.i18n.pagesLabel"
|
||||
label-for="pages_access_level"
|
||||
:help-text="
|
||||
s__(
|
||||
'ProjectSettings|With GitLab Pages you can host your static websites on GitLab. GitLab Pages uses a caching mechanism for efficiency. Your changes may not take effect until that cache is invalidated, which usually takes less than a minute.',
|
||||
|
|
@ -955,6 +984,7 @@ export default {
|
|||
"
|
||||
>
|
||||
<project-feature-setting
|
||||
id="pages_access_level"
|
||||
v-model="pagesAccessLevel"
|
||||
:label="$options.i18n.pagesLabel"
|
||||
:access-control-forced="pagesAccessControlForced"
|
||||
|
|
@ -965,11 +995,13 @@ export default {
|
|||
<project-setting-row
|
||||
ref="monitor-settings"
|
||||
:label="$options.i18n.monitorLabel"
|
||||
label-for="monitor_access_level"
|
||||
:help-text="
|
||||
s__('ProjectSettings|Monitor the health of your project and respond to incidents.')
|
||||
"
|
||||
>
|
||||
<project-feature-setting
|
||||
id="monitor_access_level"
|
||||
v-model="monitorAccessLevel"
|
||||
:label="$options.i18n.monitorLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
|
|
@ -980,10 +1012,12 @@ export default {
|
|||
<project-setting-row
|
||||
ref="environments-settings"
|
||||
:label="$options.i18n.environmentsLabel"
|
||||
label-for="environments_access_level"
|
||||
:help-text="$options.i18n.environmentsHelpText"
|
||||
:help-path="environmentsHelpPath"
|
||||
>
|
||||
<project-feature-setting
|
||||
id="environments_access_level"
|
||||
v-model="environmentsAccessLevel"
|
||||
:label="$options.i18n.environmentsLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
|
|
@ -994,10 +1028,12 @@ export default {
|
|||
<project-setting-row
|
||||
ref="feature-flags-settings"
|
||||
:label="$options.i18n.featureFlagsLabel"
|
||||
label-for="feature_flags_access_level"
|
||||
:help-text="$options.i18n.featureFlagsHelpText"
|
||||
:help-path="featureFlagsHelpPath"
|
||||
>
|
||||
<project-feature-setting
|
||||
id="feature_flags_access_level"
|
||||
v-model="featureFlagsAccessLevel"
|
||||
:label="$options.i18n.featureFlagsLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
|
|
@ -1008,10 +1044,12 @@ export default {
|
|||
<project-setting-row
|
||||
ref="infrastructure-settings"
|
||||
:label="$options.i18n.infrastructureLabel"
|
||||
label-for="infrastructure_access_level"
|
||||
:help-text="$options.i18n.infrastructureHelpText"
|
||||
:help-path="infrastructureHelpPath"
|
||||
>
|
||||
<project-feature-setting
|
||||
id="infrastructure_access_level"
|
||||
v-model="infrastructureAccessLevel"
|
||||
:label="$options.i18n.infrastructureLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
|
|
@ -1022,10 +1060,12 @@ export default {
|
|||
<project-setting-row
|
||||
ref="releases-settings"
|
||||
:label="$options.i18n.releasesLabel"
|
||||
label-for="releases_access_level"
|
||||
:help-text="$options.i18n.releasesHelpText"
|
||||
:help-path="releasesHelpPath"
|
||||
>
|
||||
<project-feature-setting
|
||||
id="releases_access_level"
|
||||
v-model="releasesAccessLevel"
|
||||
:label="$options.i18n.releasesLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ module Projects
|
|||
class TagsController < ::Projects::Registry::ApplicationController
|
||||
include PackagesHelper
|
||||
|
||||
before_action :authorize_destroy_container_image!, only: [:destroy]
|
||||
before_action :authorize_destroy_container_image_tag!, only: [:destroy]
|
||||
|
||||
LIMIT = 15
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ module Mutations
|
|||
LIMIT = 20
|
||||
TOO_MANY_TAGS_ERROR_MESSAGE = "Number of tags is greater than #{LIMIT}"
|
||||
|
||||
authorize :destroy_container_image
|
||||
authorize :destroy_container_image_tag
|
||||
|
||||
argument :id,
|
||||
::Types::GlobalIDType[::ContainerRepository],
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ module Types
|
|||
class ContainerRepositoryTag < BasePermissionType
|
||||
graphql_name 'ContainerRepositoryTagPermissions'
|
||||
|
||||
ability_field :destroy_container_image,
|
||||
ability_field :destroy_container_image_tag,
|
||||
name: 'destroy_container_repository_tag',
|
||||
resolver_method: :destroy_container_image
|
||||
resolver_method: :destroy_container_image_tag
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ class LfsFileLock < ApplicationRecord
|
|||
|
||||
validates :project_id, :user_id, :path, presence: true
|
||||
|
||||
def self.for_path!(path)
|
||||
find_by!(path: path)
|
||||
end
|
||||
|
||||
def can_be_unlocked_by?(current_user, forced = false)
|
||||
return true if current_user.id == user_id
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ module ContainerRegistry
|
|||
condition(:protected_for_delete) { @subject.protected_for_delete?(@user) }
|
||||
|
||||
rule { protected_for_delete }.policy do
|
||||
prevent :destroy_container_image
|
||||
prevent :destroy_container_image_tag
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -565,6 +565,7 @@ class ProjectPolicy < BasePolicy
|
|||
enable :create_container_image
|
||||
enable :update_container_image
|
||||
enable :destroy_container_image
|
||||
enable :destroy_container_image_tag
|
||||
enable :create_environment
|
||||
enable :update_environment
|
||||
enable :destroy_environment
|
||||
|
|
@ -784,6 +785,7 @@ class ProjectPolicy < BasePolicy
|
|||
|
||||
rule { container_registry_disabled }.policy do
|
||||
prevent(*create_read_update_admin_destroy(:container_image))
|
||||
prevent :destroy_container_image_tag
|
||||
end
|
||||
|
||||
rule { anonymous & ~public_project }.prevent_all
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ module Auth
|
|||
:read_container_image,
|
||||
:create_container_image,
|
||||
:destroy_container_image,
|
||||
:destroy_container_image_tag,
|
||||
:update_container_image,
|
||||
:admin_container_image,
|
||||
:build_read_container_image,
|
||||
|
|
|
|||
|
|
@ -34,17 +34,15 @@ module Lfs
|
|||
end
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def lock
|
||||
return @lock if defined?(@lock)
|
||||
|
||||
@lock = if params[:id].present?
|
||||
project.lfs_file_locks.find(params[:id])
|
||||
elsif params[:path].present?
|
||||
project.lfs_file_locks.find_by!(path: params[:path])
|
||||
project.lfs_file_locks.for_path!(params[:path])
|
||||
end
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ module Projects
|
|||
def can_destroy?
|
||||
return true if container_expiration_policy
|
||||
|
||||
can?(current_user, :destroy_container_image, project)
|
||||
can?(current_user, :destroy_container_image_tag, project)
|
||||
end
|
||||
|
||||
def valid_regex?
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ module Projects
|
|||
@container_repository = container_repository
|
||||
|
||||
unless container_expiration_policy?
|
||||
return error('access denied') unless can?(current_user, :destroy_container_image, project)
|
||||
return error('access denied') unless can?(current_user, :destroy_container_image_tag, project)
|
||||
end
|
||||
|
||||
@tag_names = params[:tags]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ module AntiAbuse
|
|||
|
||||
def validate(record)
|
||||
return if record.errors.include?(:email)
|
||||
return unless ::Gitlab::CurrentSettings.enforce_email_subaddress_restrictions
|
||||
|
||||
email = record.email
|
||||
|
||||
|
|
@ -24,14 +25,10 @@ module AntiAbuse
|
|||
private
|
||||
|
||||
def prevent_banned_user_email_reuse?(email)
|
||||
return false unless ::Feature.enabled?(:block_banned_user_normalized_email_reuse, ::Feature.current_request)
|
||||
|
||||
::Users::BannedUser.by_detumbled_email(email).exists?
|
||||
end
|
||||
|
||||
def limit_normalized_email_reuse?(email)
|
||||
return false unless ::Feature.enabled?(:limit_normalized_email_reuse, ::Feature.current_request)
|
||||
|
||||
Email.users_by_detumbled_email_count(email) >= NORMALIZED_EMAIL_ACCOUNT_LIMIT
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
= f.label :pages_extra_deployments_default_expiry_seconds, s_('AdminSettings|Default expiration time for parallel deployments (in seconds)'), class: 'label-bold'
|
||||
= f.number_field :pages_extra_deployments_default_expiry_seconds, class: 'form-control gl-form-input'
|
||||
.form-text.gl-text-subtle
|
||||
- link = link_to('', help_page_path('user/project/pages/_index.md', anchor: 'parallel-deployments'), target: '_blank', rel: 'noopener noreferrer')
|
||||
- link = link_to('', help_page_path('user/project/pages/parallel_deployments.md'), target: '_blank', rel: 'noopener noreferrer')
|
||||
= safe_format(s_('AdminSettings|Set the default time after which parallel deployments expire (0 for unlimited). %{link_start}What are parallel deployments%{link_end}?'), tag_pair(link, :link_start, :link_end))
|
||||
%h5
|
||||
= s_("AdminSettings|Configure Let's Encrypt")
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ module AntiAbuse
|
|||
attr_reader :banned_user
|
||||
|
||||
def ban_users_with_the_same_detumbled_email!
|
||||
return unless Feature.enabled?(:auto_ban_via_detumbled_email, banned_user, type: :gitlab_com_derisk)
|
||||
return unless ::Gitlab::CurrentSettings.enforce_email_subaddress_restrictions
|
||||
|
||||
reason = "User #{banned_user.id} was banned with the same detumbled email address"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
name: auto_ban_via_detumbled_email
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/modelops/anti-abuse/team-tasks/-/issues/814
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/166673
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/491796
|
||||
milestone: '17.5'
|
||||
group: group::anti-abuse
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
name: block_banned_user_normalized_email_reuse
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/modelops/anti-abuse/team-tasks/-/issues/815
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161122
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/474964
|
||||
milestone: '17.3'
|
||||
group: group::anti-abuse
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
name: limit_normalized_email_reuse
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/modelops/anti-abuse/team-tasks/-/issues/812
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/167357
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/495124
|
||||
milestone: '17.6'
|
||||
group: group::anti-abuse
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
||||
|
|
@ -3,7 +3,7 @@ name: autoflow_issue_events_enabled
|
|||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/443486
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161804
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/516169
|
||||
milestone: '17.9'
|
||||
milestone: '17.10'
|
||||
group: group::environments
|
||||
type: wip
|
||||
default_enabled: false
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
name: autoflow_merge_request_events_enabled
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/443486
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/179686
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/516168
|
||||
milestone: '17.10'
|
||||
group: group::environments
|
||||
type: wip
|
||||
default_enabled: false
|
||||
|
|
@ -225,6 +225,16 @@
|
|||
- 1
|
||||
- - cluster_agent
|
||||
- 1
|
||||
- - clusters_agents_auto_flow_merge_requests_closed_event
|
||||
- 1
|
||||
- - clusters_agents_auto_flow_merge_requests_created_event
|
||||
- 1
|
||||
- - clusters_agents_auto_flow_merge_requests_merged_event
|
||||
- 1
|
||||
- - clusters_agents_auto_flow_merge_requests_reopened_event
|
||||
- 1
|
||||
- - clusters_agents_auto_flow_merge_requests_updated_event
|
||||
- 1
|
||||
- - clusters_agents_auto_flow_work_items_closed_event
|
||||
- 1
|
||||
- - clusters_agents_auto_flow_work_items_created_event
|
||||
|
|
|
|||
|
|
@ -23182,7 +23182,7 @@ A tag from a container repository.
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="containerrepositorytagpermissionsdestroycontainerrepositorytag"></a>`destroyContainerRepositoryTag` | [`Boolean!`](#boolean) | If `true`, the user can perform `destroy_container_image` on this resource. |
|
||||
| <a id="containerrepositorytagpermissionsdestroycontainerrepositorytag"></a>`destroyContainerRepositoryTag` | [`Boolean!`](#boolean) | If `true`, the user can perform `destroy_container_image_tag` on this resource. |
|
||||
|
||||
### `ContainerTagsExpirationPolicy`
|
||||
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ If you have Kubernetes clusters connected with GitLab, [upgrade your GitLab agen
|
|||
### Elasticsearch
|
||||
|
||||
Before updating GitLab, confirm advanced search migrations are complete by
|
||||
[checking for pending advanced search migrations](background_migrations.md#check-for-pending-advanced-search-migrations).
|
||||
[checking for pending migrations](background_migrations.md#check-for-pending-migrations).
|
||||
|
||||
After updating GitLab, you may have to upgrade
|
||||
[Elasticsearch if the new version breaks compatibility](../integration/advanced_search/elasticsearch.md#version-requirements).
|
||||
|
|
|
|||
|
|
@ -476,7 +476,9 @@ sudo -u git -H bundle exec rails runner -e production 'puts Gitlab::Database::Ba
|
|||
|
||||
{{< /tabs >}}
|
||||
|
||||
## Check for pending advanced search migrations
|
||||
## Advanced search migrations
|
||||
|
||||
### Check for pending migrations
|
||||
|
||||
{{< details >}}
|
||||
|
||||
|
|
@ -511,3 +513,15 @@ sudo -u git -H bundle exec rake gitlab:elastic:list_pending_migrations
|
|||
{{< /tab >}}
|
||||
|
||||
{{< /tabs >}}
|
||||
|
||||
If you're on a long upgrade path and have many pending migrations, you might want to configure
|
||||
`Requeue indexing workers` and `Number of shards for non-code indexing` to speed up indexing.
|
||||
Another option is to ignore the pending migrations and [reindex the instance](../integration/advanced_search/elasticsearch.md#index-the-instance) after you upgrade GitLab to the target version.
|
||||
You can also disable advanced search during this process with the [`Search with Elasticsearch enabled`](../integration/advanced_search/elasticsearch.md#advanced-search-configuration) setting.
|
||||
|
||||
{{< alert type="warning" >}}
|
||||
|
||||
Indexing large instances comes with risks.
|
||||
For more information, see [index large instances efficiently](../integration/advanced_search/elasticsearch.md#index-large-instances-efficiently).
|
||||
|
||||
{{< /alert >}}
|
||||
|
|
|
|||
|
|
@ -341,12 +341,12 @@ The limit varies depending on your plan and the number of seats in your subscrip
|
|||
|
||||
### Other limits
|
||||
|
||||
| Setting | Default for GitLab.com |
|
||||
|:---------------------------------------------------------------|:-----------------------|
|
||||
| Number of webhooks | 100 for each project, 50 for each group (subgroup webhooks are not counted towards parent group limits ) |
|
||||
| Maximum payload size | 25 MB |
|
||||
| Timeout | 10 seconds |
|
||||
| [Multiple Pages deployments](../project/pages/_index.md#limits) | 100 extra deployments (Premium tier), 500 extra deployments (Ultimate tier) |
|
||||
| Setting | Default for GitLab.com |
|
||||
|:--------------------------------------------------------------------|:-----------------------|
|
||||
| Number of webhooks | 100 for each project, 50 for each group (subgroup webhooks are not counted towards parent group limits ) |
|
||||
| Maximum payload size | 25 MB |
|
||||
| Timeout | 10 seconds |
|
||||
| [Parallel Pages deployments](../project/pages/parallel_deployments.md#limits) | 100 extra deployments (Premium tier), 500 extra deployments (Ultimate tier) |
|
||||
|
||||
For self-managed instance limits, see:
|
||||
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ To improve your security, try these features:
|
|||
| Feature | Tier | Add-on | Offering | Status |
|
||||
| ------- | ---- | ------ | -------- | ------ |
|
||||
| [GitLab Duo Chat](../gitlab_duo_chat/_index.md) | Premium, Ultimate | GitLab Duo Pro or Enterprise | GitLab.com, Self-managed, GitLab Dedicated | General availability |
|
||||
| [GitLab Duo Self-Hosted](../../administration/gitlab_duo_self_hosted/_index.md) | Ultimate | GitLab Duo Enterprise | Self-managed | Beta |
|
||||
| [GitLab Duo Self-Hosted](../../administration/gitlab_duo_self_hosted/_index.md) | Ultimate | GitLab Duo Enterprise | Self-managed | General availability |
|
||||
| [GitLab Duo Workflow](../duo_workflow/_index.md) | Ultimate | - | GitLab.com | Experiment |
|
||||
| [Issue Description Generation](../project/issues/managing_issues.md#populate-an-issue-with-issue-description-generation) | Ultimate | GitLab Duo Enterprise | GitLab.com | Experiment |
|
||||
| [Discussion Summary](../discussions/_index.md#summarize-issue-discussions-with-duo-chat) | Ultimate | GitLab Duo Enterprise | GitLab.com, Self-managed, GitLab Dedicated | General availability |
|
||||
|
|
|
|||
|
|
@ -205,13 +205,6 @@ Prerequisites:
|
|||
|
||||
## Expiring deployments
|
||||
|
||||
{{< details >}}
|
||||
|
||||
- Tier: Premium, Ultimate
|
||||
- Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated
|
||||
|
||||
{{< /details >}}
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/162826) in GitLab 17.4.
|
||||
|
|
@ -233,10 +226,6 @@ deploy-pages:
|
|||
- public
|
||||
```
|
||||
|
||||
By default, [parallel deployments](#parallel-deployments) expire
|
||||
automatically after 24 hours.
|
||||
To disable this behavior, set `pages.expire_in` to `never`.
|
||||
|
||||
Expired deployments are stopped by a cron job that runs every 10 minutes.
|
||||
Stopped deployments are subsequently deleted by another cron job that also
|
||||
runs every 10 minutes. To recover it, follow the steps described in
|
||||
|
|
@ -263,169 +252,6 @@ To recover a stopped deployment that has not yet been deleted:
|
|||
list.
|
||||
1. Expand the deployment you want to recover and select **Restore**.
|
||||
|
||||
## Parallel deployments
|
||||
|
||||
{{< details >}}
|
||||
|
||||
- Tier: Premium, Ultimate
|
||||
|
||||
{{< /details >}}
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129534) in GitLab 16.7 as an [experiment](../../../policy/development_stages_support.md) [with a flag](../../feature_flags.md) named `pages_multiple_versions_setting`. Disabled by default.
|
||||
- [Renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/480195) from "multiple deployments" to "parallel deployments" in GitLab 17.4.
|
||||
- [Enabled on GitLab.com, GitLab Self-Managed, and GitLab Dedicated](https://gitlab.com/gitlab-org/gitlab/-/issues/422145) in GitLab 17.4.
|
||||
- [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/502219) to remove the project setting in GitLab 17.7.
|
||||
- [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/507423) to allow periods in `path_prefix` in GitLab 17.8.
|
||||
- [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/500000) to allow variables when passed to `publish` property in GitLab 17.9.
|
||||
- [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/487161) in GitLab 17.9. Feature flag `pages_multiple_versions_setting` removed.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
Use the [`pages.path_prefix`](../../../ci/yaml/_index.md#pagespagespath_prefix) CI/CD option to configure a prefix for the GitLab Pages URL.
|
||||
A prefix allows you to differentiate between multiple GitLab Pages deployments:
|
||||
|
||||
- Main deployment: a Pages deployment created with a blank `path_prefix`.
|
||||
- Parallel deployment: a Pages deployment created with a non-blank `path_prefix`
|
||||
|
||||
The value of `pages.path_prefix` is:
|
||||
|
||||
- Converted to lowercase.
|
||||
- Shortened to 63 bytes.
|
||||
- Any character except numbers (`0-9`), letters (`a-z`) and periods (`.`) is replaced with a hyphen (`-`).
|
||||
- Leading and trailing hyphens (`-`) and period (`.`) are removed.
|
||||
|
||||
### Example configuration
|
||||
|
||||
Consider a project such as `https://gitlab.example.com/namespace/project`. By default, its main Pages deployment can be accessed through:
|
||||
|
||||
- When using a [unique domain](#unique-domains): `https://project-123456.gitlab.io/`.
|
||||
- When not using a unique domain: `https://namespace.gitlab.io/project`.
|
||||
|
||||
If a `pages.path_prefix` is configured to the project branch names,
|
||||
like `path_prefix = $CI_COMMIT_BRANCH`, and there's a
|
||||
branch named `username/testing_feature`, this parallel Pages deployment would be accessible through:
|
||||
|
||||
- When using a [unique domain](#unique-domains): `https://project-123456.gitlab.io/username-testing-feature`.
|
||||
- When not using a unique domain: `https://namespace.gitlab.io/project/username-testing-feature`.
|
||||
|
||||
### Limits
|
||||
|
||||
The number of parallel deployments is limited by the root-level namespace. For
|
||||
specific limits for:
|
||||
|
||||
- GitLab.com, see [Other limits](../../gitlab_com/_index.md#other-limits).
|
||||
- GitLab Self-Managed, see
|
||||
[Number of parallel Pages deployments](../../../administration/instance_limits.md#number-of-parallel-pages-deployments).
|
||||
|
||||
To immediately reduce the number of active deployments in your namespace,
|
||||
delete some deployments. For more information, see
|
||||
[Delete a deployment](#delete-a-deployment).
|
||||
|
||||
To configure an expiry time to automatically
|
||||
delete older deployments, see
|
||||
[Expiring deployments](#expiring-deployments).
|
||||
|
||||
### Expiration
|
||||
|
||||
By default, parallel deployments expire after 24 hours, after which they are
|
||||
deleted. If you're using a self-hosted instance, your instance admin can
|
||||
[configure a different default duration](../../../administration/pages/_index.md#configure-the-default-expiry-for-parallel-deployments).
|
||||
|
||||
To customize the expiry time, [configure `pages.expire_in`](#expiring-deployments).
|
||||
|
||||
To prevent deployments from automatically expiring, set `pages.expire_in` to
|
||||
`never`.
|
||||
|
||||
### Path clash
|
||||
|
||||
`pages.path_prefix` can take dynamic values from [CI/CD variables](../../../ci/variables/_index.md)
|
||||
that can create pages deployments which could clash with existing paths in your site.
|
||||
For example, given an existing GitLab Pages site with the following paths:
|
||||
|
||||
```plaintext
|
||||
/index.html
|
||||
/documents/index.html
|
||||
```
|
||||
|
||||
If a `pages.path_prefix` is `documents`, that version will override the existing path.
|
||||
In other words, `https://namespace.gitlab.io/project/documents/index.html` will point to the
|
||||
`/index.html` on the `documents` deployment of the site, instead of `documents/index.html` of the
|
||||
`main` deployment of the site.
|
||||
|
||||
Mixing [CI/CD variables](../../../ci/variables/_index.md) with other strings can reduce the path clash
|
||||
possibility. For example:
|
||||
|
||||
```yaml
|
||||
deploy-pages:
|
||||
stage: deploy
|
||||
script:
|
||||
- echo "Pages accessible through ${CI_PAGES_URL}"
|
||||
variables:
|
||||
PAGES_PREFIX: "" # No prefix by default (main)
|
||||
pages: # specifies that this is a Pages job
|
||||
path_prefix: "$PAGES_PREFIX"
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run on default branch (with default PAGES_PREFIX)
|
||||
- if: $CI_COMMIT_BRANCH == "staging" # Run on main (with default PAGES_PREFIX)
|
||||
variables:
|
||||
PAGES_PREFIX: '_stg' # Prefix with _stg for the staging branch
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event" # Conditionally change the prefix for Merge Requests
|
||||
when: manual # Run pages manually on Merge Requests
|
||||
variables:
|
||||
PAGES_PREFIX: 'mr-$CI_MERGE_REQUEST_IID' # Prefix with the mr-<iid>, like `mr-123`
|
||||
```
|
||||
|
||||
Some other examples of mixing [variables](../../../ci/variables/_index.md) with strings for dynamic prefixes:
|
||||
|
||||
- `pages.path_prefix: 'mr-$CI_COMMIT_REF_SLUG'`: Branch or tag name prefixed with `mr-`, like `mr-branch-name`.
|
||||
- `pages.path_prefix: '_${CI_MERGE_REQUEST_IID}_'`: Merge request number
|
||||
prefixed ans suffixed with `_`, like `_123_`.
|
||||
|
||||
The previous YAML example uses [user-defined job names](#user-defined-job-names).
|
||||
|
||||
### Use parallel deployments to create Pages environments
|
||||
|
||||
You can use parallel GitLab Pages deployments to create a new [environment](../../../ci/environments/_index.md).
|
||||
For example:
|
||||
|
||||
```yaml
|
||||
deploy-pages:
|
||||
stage: deploy
|
||||
script:
|
||||
- echo "Pages accessible through ${CI_PAGES_URL}"
|
||||
variables:
|
||||
PAGES_PREFIX: "" # no prefix by default (master)
|
||||
pages: # specifies that this is a Pages job
|
||||
path_prefix: "$PAGES_PREFIX"
|
||||
environment:
|
||||
name: "Pages ${PAGES_PREFIX}"
|
||||
url: $CI_PAGES_URL
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "staging" # ensure to run on master (with default PAGES_PREFIX)
|
||||
variables:
|
||||
PAGES_PREFIX: '_stg' # prefix with _stg for the staging branch
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event" # conditionally change the prefix on Merge Requests
|
||||
when: manual # run pages manually on Merge Requests
|
||||
variables:
|
||||
PAGES_PREFIX: 'mr-$CI_MERGE_REQUEST_IID' # prefix with the mr-<iid>, like `mr-123`
|
||||
```
|
||||
|
||||
With this configuration, users will have the access to each GitLab Pages deployment through the UI.
|
||||
When using [environments](../../../ci/environments/_index.md) for pages, all pages environments are
|
||||
listed on the project environment list.
|
||||
|
||||
You can also [group similar environments](../../../ci/environments/_index.md#group-similar-environments) together.
|
||||
|
||||
The previous YAML example uses [user-defined job names](#user-defined-job-names).
|
||||
|
||||
### Delete a Deployment
|
||||
|
||||
To delete a deployment:
|
||||
|
|
@ -442,11 +268,6 @@ Stopped deployments are deleted by a cron job running every 10 minutes.
|
|||
To restore a stopped deployment that has not been deleted yet, see
|
||||
[Recover a stopped deployment](#recover-a-stopped-deployment).
|
||||
|
||||
#### Auto-clean
|
||||
|
||||
Parallel Pages deployments, created by a merge request with a `path_prefix`, are automatically deleted when the
|
||||
merge request is closed or merged.
|
||||
|
||||
## User-defined job names
|
||||
|
||||
{{< history >}}
|
||||
|
|
@ -493,3 +314,8 @@ deployment is triggered:
|
|||
pages:
|
||||
pages: false
|
||||
```
|
||||
|
||||
## Parallel deployments
|
||||
|
||||
To create multiple deployments for your project at the same time, for example to
|
||||
create review apps, view the documentation on [Parallel Deployments](parallel_deployments.md).
|
||||
|
|
|
|||
|
|
@ -0,0 +1,240 @@
|
|||
---
|
||||
stage: Plan
|
||||
group: Knowledge
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
title: GitLab Pages parallel deployments
|
||||
---
|
||||
|
||||
{{< details >}}
|
||||
|
||||
- Tier: Premium, Ultimate
|
||||
- Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated
|
||||
|
||||
{{< /details >}}
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129534) in GitLab 16.7 as an [experiment](../../../policy/development_stages_support.md) [with a flag](../../feature_flags.md) named `pages_multiple_versions_setting`. Disabled by default.
|
||||
- [Renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/480195) from "multiple deployments" to "parallel deployments" in GitLab 17.4.
|
||||
- [Enabled on GitLab.com, GitLab Self-Managed, and GitLab Dedicated](https://gitlab.com/gitlab-org/gitlab/-/issues/422145) in GitLab 17.4.
|
||||
- [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/502219) to remove the project setting in GitLab 17.7.
|
||||
- [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/507423) to allow periods in `path_prefix` in GitLab 17.8.
|
||||
- [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/500000) to allow variables when passed to `publish` property in GitLab 17.9.
|
||||
- [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/487161) in GitLab 17.9. Feature flag `pages_multiple_versions_setting` removed.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
With parallel deployments, you can publish multiple versions of your [GitLab Pages](_index.md)
|
||||
site at the same time. Each version has its own unique URL based on a path prefix you specify.
|
||||
|
||||
Use parallel deployments to:
|
||||
|
||||
- Enhance your workflow for testing changes in development branches before merging
|
||||
to production.
|
||||
- Share working previews with stakeholders for feedback.
|
||||
- Maintain documentation for multiple software versions simultaneously.
|
||||
- Publish localized content for different audiences.
|
||||
- Create staging environments for review before final publication.
|
||||
|
||||
Each version of your site gets its own URL based on a path prefix that you specify.
|
||||
Control how long these parallel deployments exist.
|
||||
They expire after 24 hours by default, but you can customize this duration to fit your review timeline.
|
||||
|
||||
### Create a parallel deployment
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- The root-level namespace must have available [parallel deployment slots](../../gitlab_com/_index.md#other-limits).
|
||||
|
||||
To create a parallel deployment:
|
||||
|
||||
1. In your `.gitlab-ci.yml` file, add a Pages job with a `path_prefix`:
|
||||
|
||||
```yaml
|
||||
pages:
|
||||
stage: deploy
|
||||
script:
|
||||
- echo "Pages accessible through ${CI_PAGES_URL}/${CI_COMMIT_BRANCH}"
|
||||
pages:
|
||||
path_prefix: "$CI_COMMIT_BRANCH"
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
```
|
||||
|
||||
The `path_prefix` value:
|
||||
|
||||
- Is converted to lowercase.
|
||||
- Can contain numbers (`0-9`), letters (`a-z`), and periods (`.`).
|
||||
- Is replaced with hyphens (`-`) for any other characters.
|
||||
- Cannot start or end with hyphens (`-`) or periods (`.`), so they are removed.
|
||||
- Must be 63 bytes or shorter. Anything longer is trimmed.
|
||||
|
||||
1. Optional. If you want dynamic prefixes, use
|
||||
[CI/CD variables](../../../ci/variables/where_variables_can_be_used.md#gitlab-ciyml-file) in your `path_prefix`.
|
||||
For example:
|
||||
|
||||
```yaml
|
||||
pages:
|
||||
path_prefix: "mr-$CI_MERGE_REQUEST_IID" # Results in paths like mr-123
|
||||
```
|
||||
|
||||
1. Optional. To set an expiry time for the deployment, add `expire_in`:
|
||||
|
||||
```yaml
|
||||
pages:
|
||||
pages:
|
||||
path_prefix: "$CI_COMMIT_BRANCH"
|
||||
expire_in: 1 week
|
||||
```
|
||||
|
||||
By default, parallel deployments [expire](#expiration) after 24 hours.
|
||||
|
||||
1. Commit your changes and push to your repository.
|
||||
|
||||
The deployment is accessible at:
|
||||
|
||||
- With a [unique domain](_index.md#unique-domains): `https://project-123456.gitlab.io/your-prefix-name`.
|
||||
- Without a unique domain: `https://namespace.gitlab.io/project/your-prefix-name`.
|
||||
|
||||
The URL path between the site domain and public directory is determined by the `path_prefix`.
|
||||
For example, if your main deployment has content at `/index.html`, a parallel deployment with prefix
|
||||
`staging` can access that same content at `/staging/index.html`.
|
||||
|
||||
To prevent path clashes, avoid using path prefixes that match the names of existing folders in your site.
|
||||
For more information, see [Path clash](#path-clash).
|
||||
|
||||
### Example configuration
|
||||
|
||||
Consider a project such as `https://gitlab.example.com/namespace/project`. By default, its main Pages deployment can be accessed through:
|
||||
|
||||
- When using a [unique domain](_index.md#unique-domains): `https://project-123456.gitlab.io/`.
|
||||
- When not using a unique domain: `https://namespace.gitlab.io/project`.
|
||||
|
||||
If a `pages.path_prefix` is configured to the project branch names,
|
||||
like `path_prefix = $CI_COMMIT_BRANCH`, and there's a
|
||||
branch named `username/testing_feature`, this parallel Pages deployment would be accessible through:
|
||||
|
||||
- When using a [unique domain](_index.md#unique-domains): `https://project-123456.gitlab.io/username-testing-feature`.
|
||||
- When not using a unique domain: `https://namespace.gitlab.io/project/username-testing-feature`.
|
||||
|
||||
### Limits
|
||||
|
||||
The number of parallel deployments is limited by the root-level namespace. For
|
||||
specific limits for:
|
||||
|
||||
- GitLab.com, see [Other limits](../../gitlab_com/_index.md#other-limits).
|
||||
- GitLab Self-Managed, see
|
||||
[Number of parallel Pages deployments](../../../administration/instance_limits.md#number-of-parallel-pages-deployments).
|
||||
|
||||
To immediately reduce the number of active deployments in your namespace,
|
||||
delete some deployments. For more information, see
|
||||
[Delete a deployment](_index.md#delete-a-deployment).
|
||||
|
||||
To configure an expiry time to automatically
|
||||
delete older deployments, see
|
||||
[Expiring deployments](_index.md#expiring-deployments).
|
||||
|
||||
### Expiration
|
||||
|
||||
By default, parallel deployments [expire](_index.md#expiring-deployments) after 24 hours,
|
||||
after which they are deleted. If you're using a self-hosted instance, your instance admin can
|
||||
[configure a different default duration](../../../administration/pages/_index.md#configure-the-default-expiry-for-parallel-deployments).
|
||||
|
||||
To customize the expiry time, [configure `pages.expire_in`](_index.md#expiring-deployments).
|
||||
|
||||
To prevent deployments from automatically expiring, set `pages.expire_in` to
|
||||
`never`.
|
||||
|
||||
### Path clash
|
||||
|
||||
`pages.path_prefix` can take dynamic values from [CI/CD variables](../../../ci/variables/_index.md)
|
||||
that can create pages deployments which could clash with existing paths in your site.
|
||||
For example, given an existing GitLab Pages site with the following paths:
|
||||
|
||||
```plaintext
|
||||
/index.html
|
||||
/documents/index.html
|
||||
```
|
||||
|
||||
If a `pages.path_prefix` is `documents`, that version overrides the existing path.
|
||||
In other words, `https://namespace.gitlab.io/project/documents/index.html` points to the
|
||||
`/index.html` on the `documents` deployment of the site, instead of `documents/index.html` of the
|
||||
`main` deployment of the site.
|
||||
|
||||
Mixing [CI/CD variables](../../../ci/variables/_index.md) with other strings can reduce the path clash
|
||||
possibility. For example:
|
||||
|
||||
```yaml
|
||||
deploy-pages:
|
||||
stage: deploy
|
||||
script:
|
||||
- echo "Pages accessible through ${CI_PAGES_URL}"
|
||||
variables:
|
||||
PAGES_PREFIX: "" # No prefix by default (main)
|
||||
pages: # specifies that this is a Pages job
|
||||
path_prefix: "$PAGES_PREFIX"
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run on default branch (with default PAGES_PREFIX)
|
||||
- if: $CI_COMMIT_BRANCH == "staging" # Run on main (with default PAGES_PREFIX)
|
||||
variables:
|
||||
PAGES_PREFIX: '_stg' # Prefix with _stg for the staging branch
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event" # Conditionally change the prefix for Merge Requests
|
||||
when: manual # Run pages manually on Merge Requests
|
||||
variables:
|
||||
PAGES_PREFIX: 'mr-$CI_MERGE_REQUEST_IID' # Prefix with the mr-<iid>, like `mr-123`
|
||||
```
|
||||
|
||||
Some other examples of mixing [variables](../../../ci/variables/_index.md) with strings for dynamic prefixes:
|
||||
|
||||
- `pages.path_prefix: 'mr-$CI_COMMIT_REF_SLUG'`: Branch or tag name prefixed with `mr-`, like `mr-branch-name`.
|
||||
- `pages.path_prefix: '_${CI_MERGE_REQUEST_IID}_'`: Merge request number
|
||||
prefixed ans suffixed with `_`, like `_123_`.
|
||||
|
||||
The previous YAML example uses [user-defined job names](_index.md#user-defined-job-names).
|
||||
|
||||
### Use parallel deployments to create Pages environments
|
||||
|
||||
You can use parallel GitLab Pages deployments to create a new [environment](../../../ci/environments/_index.md).
|
||||
For example:
|
||||
|
||||
```yaml
|
||||
deploy-pages:
|
||||
stage: deploy
|
||||
script:
|
||||
- echo "Pages accessible through ${CI_PAGES_URL}"
|
||||
variables:
|
||||
PAGES_PREFIX: "" # no prefix by default (master)
|
||||
pages: # specifies that this is a Pages job
|
||||
path_prefix: "$PAGES_PREFIX"
|
||||
environment:
|
||||
name: "Pages ${PAGES_PREFIX}"
|
||||
url: $CI_PAGES_URL
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "staging" # ensure to run on master (with default PAGES_PREFIX)
|
||||
variables:
|
||||
PAGES_PREFIX: '_stg' # prefix with _stg for the staging branch
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event" # conditionally change the prefix on Merge Requests
|
||||
when: manual # run pages manually on Merge Requests
|
||||
variables:
|
||||
PAGES_PREFIX: 'mr-$CI_MERGE_REQUEST_IID' # prefix with the mr-<iid>, like `mr-123`
|
||||
```
|
||||
|
||||
With this configuration, users will have the access to each GitLab Pages deployment through the UI.
|
||||
When using [environments](../../../ci/environments/_index.md) for pages, all pages environments are
|
||||
listed on the project environment list.
|
||||
|
||||
You can also [group similar environments](../../../ci/environments/_index.md#group-similar-environments) together.
|
||||
|
||||
The previous YAML example uses [user-defined job names](_index.md#user-defined-job-names).
|
||||
|
||||
#### Auto-clean
|
||||
|
||||
Parallel Pages deployments, created by a merge request with a `path_prefix`, are automatically deleted when the
|
||||
merge request is closed or merged.
|
||||
|
|
@ -180,7 +180,7 @@ module API
|
|||
requires :tag_name, type: String, desc: 'The name of the tag'
|
||||
end
|
||||
delete ':id/registry/repositories/:repository_id/tags/:tag_name', requirements: REPOSITORY_ENDPOINT_REQUIREMENTS do
|
||||
authorize_destroy_container_image!
|
||||
authorize_destroy_container_image_tag!
|
||||
|
||||
result = ::Projects::ContainerRepository::DeleteTagsService
|
||||
.new(repository.project, current_user, tags: [declared_params[:tag_name]])
|
||||
|
|
@ -205,8 +205,8 @@ module API
|
|||
authorize! :read_container_image, repository
|
||||
end
|
||||
|
||||
def authorize_destroy_container_image!
|
||||
authorize! :destroy_container_image, repository
|
||||
def authorize_destroy_container_image_tag!
|
||||
authorize! :destroy_container_image_tag, tag
|
||||
end
|
||||
|
||||
def authorize_admin_container_image!
|
||||
|
|
|
|||
|
|
@ -82,6 +82,10 @@ RSpec.describe Projects::Registry::TagsController do
|
|||
end
|
||||
|
||||
describe 'POST destroy' do
|
||||
before do
|
||||
allow(controller).to receive(:authorize_destroy_container_image_tag!).and_call_original
|
||||
end
|
||||
|
||||
context 'when user has access to registry' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
|
|
@ -96,12 +100,16 @@ RSpec.describe Projects::Registry::TagsController do
|
|||
expect_delete_tags(%w[rc1])
|
||||
|
||||
destroy_tag('rc1')
|
||||
|
||||
expect(controller).to have_received(:authorize_destroy_container_image_tag!)
|
||||
end
|
||||
|
||||
it 'makes it possible to delete a tag that ends with a dot' do
|
||||
expect_delete_tags(%w[test.])
|
||||
|
||||
destroy_tag('test.')
|
||||
|
||||
expect(controller).to have_received(:authorize_destroy_container_image_tag!)
|
||||
end
|
||||
|
||||
it 'tracks the event', :snowplow do
|
||||
|
|
@ -114,6 +122,21 @@ RSpec.describe Projects::Registry::TagsController do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when user cannot destroy image tags' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
allow(Ability).to receive(:allowed?).and_call_original
|
||||
allow(Ability).to receive(:allowed?).with(user, :destroy_container_image_tag, project).and_return(false)
|
||||
end
|
||||
|
||||
it 'returns not_found' do
|
||||
destroy_tag('test.')
|
||||
|
||||
expect(controller).to have_received(:authorize_destroy_container_image_tag!)
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def destroy_tag(name)
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ describe('Pipeline New Form', () => {
|
|||
expect(mockCiConfigVariables).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the ciInputsForPipelines flag is enabled', () => {
|
||||
beforeEach(async () => {
|
||||
mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,24 @@
|
|||
import { GlFormGroup, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import Vue from 'vue';
|
||||
import { fetchPolicies } from '~/lib/graphql';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
||||
import { fetchPolicies } from '~/lib/graphql';
|
||||
import { reportToSentry } from '~/ci/utils';
|
||||
import ciConfigVariablesQuery from '~/ci/pipeline_new/graphql/queries/ci_config_variables.graphql';
|
||||
import { VARIABLE_TYPE } from '~/ci/pipeline_new/constants';
|
||||
import PipelineVariablesForm from '~/ci/pipeline_new/components/pipeline_variables_form.vue';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
jest.mock('~/ci/utils');
|
||||
jest.mock('@gitlab/ui/dist/utils', () => ({
|
||||
GlBreakpointInstance: {
|
||||
getBreakpointSize: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('PipelineVariablesForm', () => {
|
||||
let wrapper;
|
||||
|
|
@ -19,16 +26,41 @@ describe('PipelineVariablesForm', () => {
|
|||
let mockCiConfigVariables;
|
||||
|
||||
const defaultProps = {
|
||||
projectPath: 'group/project',
|
||||
defaultBranch: 'main',
|
||||
isMaintainer: true,
|
||||
projectPath: 'group/project',
|
||||
refParam: 'feature',
|
||||
settingsLink: 'link/to/settings',
|
||||
};
|
||||
|
||||
const createComponent = async ({ props = {} } = {}) => {
|
||||
const configVariablesWithOptions = [
|
||||
{
|
||||
key: 'VAR_WITH_OPTIONS',
|
||||
value: 'option1',
|
||||
description: 'Variable with options',
|
||||
valueOptions: ['option1', 'option2', 'option3'],
|
||||
},
|
||||
{
|
||||
key: 'SIMPLE_VAR',
|
||||
value: 'simple-value',
|
||||
description: 'Simple variable',
|
||||
valueOptions: [],
|
||||
},
|
||||
];
|
||||
|
||||
const createComponent = async ({ props = {}, configVariables = [] } = {}) => {
|
||||
mockCiConfigVariables = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
project: {
|
||||
ciConfigVariables: configVariables,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const handlers = [[ciConfigVariablesQuery, mockCiConfigVariables]];
|
||||
mockApollo = createMockApollo(handlers);
|
||||
|
||||
wrapper = shallowMount(PipelineVariablesForm, {
|
||||
wrapper = shallowMountExtended(PipelineVariablesForm, {
|
||||
apolloProvider: mockApollo,
|
||||
propsData: { ...defaultProps, ...props },
|
||||
});
|
||||
|
|
@ -36,8 +68,11 @@ describe('PipelineVariablesForm', () => {
|
|||
await waitForPromises();
|
||||
};
|
||||
|
||||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findForm = () => wrapper.findComponent(GlFormGroup);
|
||||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findVariableRows = () => wrapper.findAllByTestId('ci-variable-row-container');
|
||||
const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key-field');
|
||||
const findRemoveButton = () => wrapper.findByTestId('remove-ci-variable-row');
|
||||
|
||||
beforeEach(() => {
|
||||
mockCiConfigVariables = jest.fn().mockResolvedValue({
|
||||
|
|
@ -65,15 +100,47 @@ describe('PipelineVariablesForm', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('form initialization', () => {
|
||||
it('adds an empty variable row', async () => {
|
||||
await createComponent();
|
||||
|
||||
expect(findVariableRows()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('initializes with variables from config', async () => {
|
||||
await createComponent({ configVariables: configVariablesWithOptions });
|
||||
|
||||
const keyInputs = findKeyInputs();
|
||||
expect(keyInputs.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Check if at least one of the expected variables exists
|
||||
const keys = keyInputs.wrappers.map((w) => w.props('value'));
|
||||
expect(keys.some((key) => ['VAR_WITH_OPTIONS', 'SIMPLE_VAR'].includes(key))).toBe(true);
|
||||
});
|
||||
|
||||
it('initializes with variables from props', async () => {
|
||||
await createComponent({
|
||||
props: {
|
||||
variableParams: { CUSTOM_VAR: 'custom-value' },
|
||||
},
|
||||
});
|
||||
|
||||
const keyInputs = findKeyInputs();
|
||||
expect(keyInputs.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// At least the empty row should exist
|
||||
const emptyRowExists = keyInputs.wrappers.some((w) => w.props('value') === '');
|
||||
expect(emptyRowExists).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('query configuration', () => {
|
||||
it('has correct apollo query configuration', async () => {
|
||||
await createComponent();
|
||||
const { apollo } = wrapper.vm.$options;
|
||||
|
||||
expect(apollo.ciConfigVariables).toMatchObject({
|
||||
fetchPolicy: fetchPolicies.NO_CACHE,
|
||||
query: ciConfigVariablesQuery,
|
||||
});
|
||||
expect(apollo.ciConfigVariables.fetchPolicy).toBe(fetchPolicies.NO_CACHE);
|
||||
expect(apollo.ciConfigVariables.query).toBe(ciConfigVariablesQuery);
|
||||
});
|
||||
|
||||
it('makes query with correct variables', async () => {
|
||||
|
|
@ -86,10 +153,73 @@ describe('PipelineVariablesForm', () => {
|
|||
});
|
||||
|
||||
it('reports to sentry when query fails', async () => {
|
||||
mockCiConfigVariables = jest.fn().mockRejectedValue(new Error('GraphQL error'));
|
||||
|
||||
const error = new Error('GraphQL error');
|
||||
await createComponent();
|
||||
expect(reportToSentry).toHaveBeenCalledWith('PipelineVariablesForm', expect.any(Error));
|
||||
wrapper.vm.$options.apollo.ciConfigVariables.error.call(wrapper.vm, error);
|
||||
|
||||
expect(reportToSentry).toHaveBeenCalledWith('PipelineVariablesForm', error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('variable rows', () => {
|
||||
it('emits variables-updated event when variables change', async () => {
|
||||
await createComponent();
|
||||
|
||||
expect(wrapper.emitted('variables-updated')).toHaveLength(1);
|
||||
|
||||
wrapper.vm.$options.watch.variables.handler.call(wrapper.vm, [
|
||||
{ key: 'TEST_KEY', value: 'test_value', variableType: VARIABLE_TYPE },
|
||||
]);
|
||||
|
||||
expect(wrapper.emitted('variables-updated')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('variable removal', () => {
|
||||
it('shows remove button with correct aria-label', async () => {
|
||||
await createComponent({
|
||||
props: { variableParams: { VAR1: 'value1', VAR2: 'value2' } },
|
||||
});
|
||||
|
||||
expect(findRemoveButton().exists()).toBe(true);
|
||||
expect(findRemoveButton().attributes('aria-label')).toBe('Remove variable');
|
||||
});
|
||||
});
|
||||
|
||||
describe('responsive design', () => {
|
||||
it('uses secondary button category on mobile', async () => {
|
||||
GlBreakpointInstance.getBreakpointSize.mockReturnValue('sm');
|
||||
await createComponent({
|
||||
props: { variableParams: { VAR1: 'value1' } },
|
||||
});
|
||||
|
||||
expect(findRemoveButton().exists()).toBe(true);
|
||||
expect(findRemoveButton().props('category')).toBe('secondary');
|
||||
});
|
||||
|
||||
it('uses tertiary button category on desktop', async () => {
|
||||
GlBreakpointInstance.getBreakpointSize.mockReturnValue('md');
|
||||
await createComponent({
|
||||
props: { variableParams: { VAR1: 'value1' } },
|
||||
});
|
||||
|
||||
expect(findRemoveButton().exists()).toBe(true);
|
||||
expect(findRemoveButton().props('category')).toBe('tertiary');
|
||||
});
|
||||
});
|
||||
|
||||
describe('settings link', () => {
|
||||
it('passes correct props for maintainers', async () => {
|
||||
await createComponent({ props: { isMaintainer: true } });
|
||||
|
||||
expect(wrapper.props('isMaintainer')).toBe(true);
|
||||
expect(wrapper.props('settingsLink')).toBe(defaultProps.settingsLink);
|
||||
});
|
||||
|
||||
it('passes correct props for non-maintainers', async () => {
|
||||
await createComponent({ props: { isMaintainer: false } });
|
||||
|
||||
expect(wrapper.props('isMaintainer')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,14 +1,21 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import { GlIcon, GlFormGroup } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import projectSettingRow from '~/pages/projects/shared/permissions/components/project_setting_row.vue';
|
||||
|
||||
describe('Project Setting Row', () => {
|
||||
let wrapper;
|
||||
|
||||
const findLabel = () => wrapper.findByTestId('project-settings-row-label');
|
||||
const findHelpText = () => wrapper.findByTestId('project-settings-row-help-text');
|
||||
|
||||
const createComponent = (customProps = {}) => {
|
||||
const propsData = { ...customProps };
|
||||
return shallowMount(projectSettingRow, { propsData });
|
||||
return shallowMountExtended(projectSettingRow, {
|
||||
propsData: {
|
||||
...customProps,
|
||||
},
|
||||
stubs: { GlFormGroup },
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -19,25 +26,26 @@ describe('Project Setting Row', () => {
|
|||
wrapper = createComponent({ label: 'Test label' });
|
||||
|
||||
await nextTick();
|
||||
expect(wrapper.find('label').text()).toEqual('Test label');
|
||||
expect(findLabel().text()).toEqual('Test label');
|
||||
});
|
||||
|
||||
it('should hide the label if it is not set', () => {
|
||||
expect(wrapper.find('label').exists()).toBe(false);
|
||||
expect(findLabel().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should apply gl-text-disabled class to label when locked', async () => {
|
||||
wrapper = createComponent({ label: 'Test label', locked: true });
|
||||
|
||||
await nextTick();
|
||||
expect(wrapper.find('label').classes()).toContain('gl-text-disabled');
|
||||
expect(findLabel().classes()).toContain('gl-text-disabled');
|
||||
});
|
||||
|
||||
it('should render default slot content', () => {
|
||||
wrapper = shallowMount(projectSettingRow, {
|
||||
wrapper = shallowMountExtended(projectSettingRow, {
|
||||
slots: {
|
||||
'label-icon': GlIcon,
|
||||
},
|
||||
stubs: { GlFormGroup },
|
||||
});
|
||||
expect(wrapper.findComponent(GlIcon).exists()).toBe(true);
|
||||
});
|
||||
|
|
@ -63,10 +71,10 @@ describe('Project Setting Row', () => {
|
|||
wrapper = createComponent({ helpText: 'Test text' });
|
||||
|
||||
await nextTick();
|
||||
expect(wrapper.find('span').text()).toEqual('Test text');
|
||||
expect(findHelpText().text()).toEqual('Test text');
|
||||
});
|
||||
|
||||
it('should hide the help text if it is set', () => {
|
||||
expect(wrapper.find('span').exists()).toBe(false);
|
||||
expect(findHelpText().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -913,6 +913,7 @@ describe('Settings Panel', () => {
|
|||
helpPath: '/help/user/ai_features',
|
||||
helpText: 'Use AI-powered features in this project.',
|
||||
label: 'GitLab Duo',
|
||||
labelFor: null,
|
||||
locked: false,
|
||||
});
|
||||
});
|
||||
|
|
@ -985,6 +986,7 @@ describe('Settings Panel', () => {
|
|||
helpPath: '/help/user/duo_amazon_q/_index.md',
|
||||
helpText: 'This project can use Amazon Q.',
|
||||
label: 'Amazon Q',
|
||||
labelFor: null,
|
||||
locked: false,
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ RSpec.describe Mutations::ContainerRepositories::DestroyTags, feature_category:
|
|||
let(:id) { repository.to_global_id }
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
specify { expect(described_class).to require_graphql_authorizations(:destroy_container_image) }
|
||||
specify { expect(described_class).to require_graphql_authorizations(:destroy_container_image_tag) }
|
||||
|
||||
describe '#resolve' do
|
||||
let(:tags) { %w[A C D E] }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe LfsFileLock do
|
||||
RSpec.describe LfsFileLock, feature_category: :source_code_management do
|
||||
let_it_be(:lfs_file_lock, reload: true) { create(:lfs_file_lock) }
|
||||
|
||||
subject { lfs_file_lock }
|
||||
|
|
@ -57,4 +57,18 @@ RSpec.describe LfsFileLock do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#for_path!(path)' do
|
||||
context 'when the lfs_file_lock exists' do
|
||||
it 'returns the lfs file lock' do
|
||||
expect(described_class.for_path!(lfs_file_lock.path)).to eq(lfs_file_lock)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the path does not exist' do
|
||||
it 'raises an error' do
|
||||
expect { described_class.for_path!('not_a_real_path.rb') }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9036,6 +9036,10 @@ RSpec.describe User, feature_category: :user_profile do
|
|||
|
||||
subject(:validate) { new_user.validate }
|
||||
|
||||
before do
|
||||
stub_application_setting(enforce_email_subaddress_restrictions: true)
|
||||
end
|
||||
|
||||
shared_examples 'adds a validation error' do |reason|
|
||||
specify do
|
||||
expect(::Gitlab::AppLogger).to receive(:info).with(
|
||||
|
|
@ -9090,9 +9094,9 @@ RSpec.describe User, feature_category: :user_profile do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when the feature flag is disabled' do
|
||||
context 'when the enforce_email_subaddress_restrictions application setting is disabled' do
|
||||
before do
|
||||
stub_feature_flags(limit_normalized_email_reuse: false)
|
||||
stub_application_setting(enforce_email_subaddress_restrictions: false)
|
||||
end
|
||||
|
||||
it 'does not perform the check' do
|
||||
|
|
@ -9158,16 +9162,14 @@ RSpec.describe User, feature_category: :user_profile do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when feature flag is disabled' do
|
||||
context 'when enforce_email_subaddress_restrictions application setting is disabled' do
|
||||
before do
|
||||
stub_feature_flags(block_banned_user_normalized_email_reuse: false)
|
||||
stub_application_setting(enforce_email_subaddress_restrictions: false)
|
||||
end
|
||||
|
||||
it 'does not perform the check' do
|
||||
expect(::Users::BannedUser).not_to receive(:by_detumbled_email)
|
||||
end
|
||||
|
||||
it_behaves_like 'checking normalized email reuse limit'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ RSpec.describe ContainerRegistry::TagPolicy, feature_category: :container_regist
|
|||
|
||||
subject { described_class.new(user, tag) }
|
||||
|
||||
describe 'destroy_container_image' do
|
||||
describe 'destroy_container_image_tag' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
shared_examples 'matching expected result with protection rules' do
|
||||
|
|
@ -23,13 +23,13 @@ RSpec.describe ContainerRegistry::TagPolicy, feature_category: :container_regist
|
|||
allow(tag).to receive(:protection_rule).and_return(protection_rule)
|
||||
end
|
||||
|
||||
it { is_expected.to send(expected_result, :destroy_container_image) }
|
||||
it { is_expected.to send(expected_result, :destroy_container_image_tag) }
|
||||
end
|
||||
|
||||
context 'for admin', :enable_admin_mode do
|
||||
let(:user) { build_stubbed(:admin) }
|
||||
|
||||
it { expect_allowed(:destroy_container_image) }
|
||||
it { expect_allowed(:destroy_container_image_tag) }
|
||||
end
|
||||
|
||||
context 'for owner' do
|
||||
|
|
@ -38,7 +38,7 @@ RSpec.describe ContainerRegistry::TagPolicy, feature_category: :container_regist
|
|||
end
|
||||
|
||||
context 'when tag has no protection rule' do
|
||||
it { expect_allowed(:destroy_container_image) }
|
||||
it { expect_allowed(:destroy_container_image_tag) }
|
||||
end
|
||||
|
||||
context 'when tag has protection rule' do
|
||||
|
|
@ -60,7 +60,7 @@ RSpec.describe ContainerRegistry::TagPolicy, feature_category: :container_regist
|
|||
end
|
||||
|
||||
context 'when tag has no protection rule' do
|
||||
it { expect_allowed(:destroy_container_image) }
|
||||
it { expect_allowed(:destroy_container_image_tag) }
|
||||
end
|
||||
|
||||
context 'when tag has protection rule' do
|
||||
|
|
@ -82,7 +82,7 @@ RSpec.describe ContainerRegistry::TagPolicy, feature_category: :container_regist
|
|||
end
|
||||
|
||||
context 'when tag has no protection rule' do
|
||||
it { expect_allowed(:destroy_container_image) }
|
||||
it { expect_allowed(:destroy_container_image_tag) }
|
||||
end
|
||||
|
||||
context 'when tag has protection rule' do
|
||||
|
|
|
|||
|
|
@ -2977,7 +2977,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
|
|||
|
||||
let(:developer_operations_permissions) do
|
||||
guest_operations_permissions + [
|
||||
:create_container_image, :update_container_image, :destroy_container_image
|
||||
:create_container_image, :update_container_image, :destroy_container_image, :destroy_container_image_tag
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ RSpec.describe RegistrationsController, :with_current_organization, type: :reque
|
|||
|
||||
let!(:banned_user) { create(:user, :banned, email: normalized_email) }
|
||||
|
||||
before do
|
||||
stub_application_setting(enforce_email_subaddress_restrictions: true)
|
||||
end
|
||||
|
||||
it 'renders new action with correct error message', :aggregate_failures do
|
||||
request
|
||||
|
||||
|
|
@ -48,9 +52,9 @@ RSpec.describe RegistrationsController, :with_current_organization, type: :reque
|
|||
expect(response).to render_template(:new)
|
||||
end
|
||||
|
||||
context 'when feature flag is disabled' do
|
||||
context 'when enforce_email_subaddress_restrictions application setting is disabled' do
|
||||
before do
|
||||
stub_feature_flags(block_banned_user_normalized_email_reuse: false)
|
||||
stub_application_setting(enforce_email_subaddress_restrictions: false)
|
||||
end
|
||||
|
||||
it 'does not re-render the form' do
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ RSpec.shared_context 'ProjectPolicy context' do
|
|||
create_commit_status create_container_image create_deployment
|
||||
create_environment create_merge_request_from
|
||||
create_pipeline create_release
|
||||
create_wiki destroy_container_image push_code read_pod_logs
|
||||
create_wiki destroy_container_image destroy_container_image_tag push_code read_pod_logs
|
||||
read_terraform_state resolve_note update_build cancel_build update_commit_status
|
||||
update_container_image update_deployment update_environment
|
||||
update_merge_request update_pipeline update_release destroy_release
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ RSpec.describe AntiAbuse::BanDuplicateUsersWorker, feature_category: :instance_r
|
|||
# The banned user cannot be instantiated as banned because validators prevent users from
|
||||
# being created that have similar characteristics of previously banned users.
|
||||
before do
|
||||
stub_application_setting(enforce_email_subaddress_restrictions: true)
|
||||
banned_user.ban!
|
||||
end
|
||||
|
||||
|
|
@ -44,9 +45,9 @@ RSpec.describe AntiAbuse::BanDuplicateUsersWorker, feature_category: :instance_r
|
|||
|
||||
it_behaves_like 'executing the ban duplicate users worker'
|
||||
|
||||
context 'when the auto_ban_via_detumbled_email feature is disabled' do
|
||||
context 'when the enforce_email_subaddress_restrictions application setting is disabled' do
|
||||
before do
|
||||
stub_feature_flags(auto_ban_via_detumbled_email: false)
|
||||
stub_application_setting(enforce_email_subaddress_restrictions: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'does not ban the duplicate user'
|
||||
|
|
|
|||
Loading…
Reference in New Issue