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_discussion_spec.rb'
 | 
				
			||||||
    - 'spec/models/legacy_diff_note_spec.rb'
 | 
					    - 'spec/models/legacy_diff_note_spec.rb'
 | 
				
			||||||
    - 'spec/models/lfs_download_object_spec.rb'
 | 
					    - 'spec/models/lfs_download_object_spec.rb'
 | 
				
			||||||
    - 'spec/models/lfs_file_lock_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/license_template_spec.rb'
 | 
					    - 'spec/models/license_template_spec.rb'
 | 
				
			||||||
    - 'spec/models/list_spec.rb'
 | 
					    - 'spec/models/list_spec.rb'
 | 
				
			||||||
    - 'spec/models/list_user_preference_spec.rb'
 | 
					    - 'spec/models/list_user_preference_spec.rb'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1 +1 @@
 | 
				
			||||||
0.0.24
 | 
					0.0.26
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -147,6 +147,7 @@ export default {
 | 
				
			||||||
      form: {},
 | 
					      form: {},
 | 
				
			||||||
      errorTitle: null,
 | 
					      errorTitle: null,
 | 
				
			||||||
      error: null,
 | 
					      error: null,
 | 
				
			||||||
 | 
					      pipelineVariables: [],
 | 
				
			||||||
      predefinedVariables: null,
 | 
					      predefinedVariables: null,
 | 
				
			||||||
      warnings: [],
 | 
					      warnings: [],
 | 
				
			||||||
      totalWarnings: 0,
 | 
					      totalWarnings: 0,
 | 
				
			||||||
| 
						 | 
					@ -277,6 +278,9 @@ export default {
 | 
				
			||||||
      clearTimeout(pollTimeout);
 | 
					      clearTimeout(pollTimeout);
 | 
				
			||||||
      this.$apollo.queries.ciConfigVariables.stopPolling();
 | 
					      this.$apollo.queries.ciConfigVariables.stopPolling();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    handleVariablesUpdated(updatedVariables) {
 | 
				
			||||||
 | 
					      this.pipelineVariables = updatedVariables;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    populateForm() {
 | 
					    populateForm() {
 | 
				
			||||||
      this.configVariablesWithDescription = this.predefinedVariables.reduce(
 | 
					      this.configVariablesWithDescription = this.predefinedVariables.reduce(
 | 
				
			||||||
        (accumulator, { description, key, value, valueOptions }) => {
 | 
					        (accumulator, { description, key, value, valueOptions }) => {
 | 
				
			||||||
| 
						 | 
					@ -367,7 +371,9 @@ export default {
 | 
				
			||||||
            input: {
 | 
					            input: {
 | 
				
			||||||
              projectPath: this.projectPath,
 | 
					              projectPath: this.projectPath,
 | 
				
			||||||
              ref: this.refShortName,
 | 
					              ref: this.refShortName,
 | 
				
			||||||
              variables: filterVariables(this.variables),
 | 
					              variables: this.isUsingPipelineInputs
 | 
				
			||||||
 | 
					                ? this.pipelineVariables
 | 
				
			||||||
 | 
					                : filterVariables(this.variables),
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
| 
						 | 
					@ -488,8 +494,13 @@ export default {
 | 
				
			||||||
      <pipeline-variables-form
 | 
					      <pipeline-variables-form
 | 
				
			||||||
        v-if="isUsingPipelineInputs"
 | 
					        v-if="isUsingPipelineInputs"
 | 
				
			||||||
        :default-branch="defaultBranch"
 | 
					        :default-branch="defaultBranch"
 | 
				
			||||||
 | 
					        :file-params="fileParams"
 | 
				
			||||||
 | 
					        :is-maintainer="isMaintainer"
 | 
				
			||||||
        :project-path="projectPath"
 | 
					        :project-path="projectPath"
 | 
				
			||||||
        :ref-param="refParam"
 | 
					        :ref-param="refParam"
 | 
				
			||||||
 | 
					        :settings-link="settingsLink"
 | 
				
			||||||
 | 
					        :variable-params="variableParams"
 | 
				
			||||||
 | 
					        @variables-updated="handleVariablesUpdated"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      <div v-else>
 | 
					      <div v-else>
 | 
				
			||||||
        <gl-loading-icon v-if="isLoading" class="gl-mb-5" size="md" />
 | 
					        <gl-loading-icon v-if="isLoading" class="gl-mb-5" size="md" />
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,16 +1,68 @@
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import { GlLoadingIcon, GlFormGroup } from '@gitlab/ui';
 | 
					import {
 | 
				
			||||||
import { fetchPolicies } from '~/lib/graphql';
 | 
					  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 { 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 ciConfigVariablesQuery from '../graphql/queries/ci_config_variables.graphql';
 | 
				
			||||||
 | 
					import VariableValuesListbox from './variable_values_listbox.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let pollTimeout;
 | 
				
			||||||
 | 
					export const POLLING_INTERVAL = 2000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: 'PipelineVariablesForm',
 | 
					  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: {
 | 
					  components: {
 | 
				
			||||||
    GlLoadingIcon,
 | 
					    GlIcon,
 | 
				
			||||||
 | 
					    GlButton,
 | 
				
			||||||
 | 
					    GlCollapsibleListbox,
 | 
				
			||||||
    GlFormGroup,
 | 
					    GlFormGroup,
 | 
				
			||||||
 | 
					    GlFormInput,
 | 
				
			||||||
 | 
					    GlFormTextarea,
 | 
				
			||||||
 | 
					    GlLink,
 | 
				
			||||||
 | 
					    GlLoadingIcon,
 | 
				
			||||||
 | 
					    GlSprintf,
 | 
				
			||||||
 | 
					    VariableValuesListbox,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
 | 
					    defaultBranch: {
 | 
				
			||||||
 | 
					      type: String,
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    fileParams: {
 | 
				
			||||||
 | 
					      type: Object,
 | 
				
			||||||
 | 
					      required: false,
 | 
				
			||||||
 | 
					      default: () => ({}),
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    isMaintainer: {
 | 
				
			||||||
 | 
					      type: Boolean,
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    projectPath: {
 | 
					    projectPath: {
 | 
				
			||||||
      type: String,
 | 
					      type: String,
 | 
				
			||||||
      required: true,
 | 
					      required: true,
 | 
				
			||||||
| 
						 | 
					@ -19,16 +71,26 @@ export default {
 | 
				
			||||||
      type: String,
 | 
					      type: String,
 | 
				
			||||||
      required: true,
 | 
					      required: true,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    defaultBranch: {
 | 
					    settingsLink: {
 | 
				
			||||||
      type: String,
 | 
					      type: String,
 | 
				
			||||||
      required: true,
 | 
					      required: true,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    variableParams: {
 | 
				
			||||||
 | 
					      type: Object,
 | 
				
			||||||
 | 
					      required: false,
 | 
				
			||||||
 | 
					      default: () => ({}),
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      ciConfigVariables: null,
 | 
					      ciConfigVariables: null,
 | 
				
			||||||
 | 
					      configVariablesWithDescription: {},
 | 
				
			||||||
 | 
					      form: {},
 | 
				
			||||||
      refValue: {
 | 
					      refValue: {
 | 
				
			||||||
        shortName: this.refParam,
 | 
					        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,
 | 
					        fullName: this.refParam === this.defaultBranch ? `refs/heads/${this.refParam}` : undefined,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
| 
						 | 
					@ -37,6 +99,9 @@ export default {
 | 
				
			||||||
    ciConfigVariables: {
 | 
					    ciConfigVariables: {
 | 
				
			||||||
      fetchPolicy: fetchPolicies.NO_CACHE,
 | 
					      fetchPolicy: fetchPolicies.NO_CACHE,
 | 
				
			||||||
      query: ciConfigVariablesQuery,
 | 
					      query: ciConfigVariablesQuery,
 | 
				
			||||||
 | 
					      skip() {
 | 
				
			||||||
 | 
					        return Object.keys(this.form).includes(this.refFullName);
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      variables() {
 | 
					      variables() {
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
          fullPath: this.projectPath,
 | 
					          fullPath: this.projectPath,
 | 
				
			||||||
| 
						 | 
					@ -46,21 +111,183 @@ export default {
 | 
				
			||||||
      update({ project }) {
 | 
					      update({ project }) {
 | 
				
			||||||
        return project?.ciConfigVariables || [];
 | 
					        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) {
 | 
					      error(error) {
 | 
				
			||||||
        reportToSentry(this.$options.name, error);
 | 
					        reportToSentry(this.$options.name, error);
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      pollInterval: POLLING_INTERVAL,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
 | 
					    descriptions() {
 | 
				
			||||||
 | 
					      return this.form[this.refFullName]?.descriptions ?? {};
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    isFetchingCiConfigVariables() {
 | 
					    isFetchingCiConfigVariables() {
 | 
				
			||||||
      return this.ciConfigVariables === null;
 | 
					      return this.ciConfigVariables === null;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    isLoading() {
 | 
					    isLoading() {
 | 
				
			||||||
      return this.$apollo.queries.ciConfigVariables.loading || this.isFetchingCiConfigVariables;
 | 
					      return this.$apollo.queries.ciConfigVariables.loading || this.isFetchingCiConfigVariables;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    isMobile() {
 | 
				
			||||||
 | 
					      return ['sm', 'xs'].includes(GlBreakpointInstance.getBreakpointSize());
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    refFullName() {
 | 
				
			||||||
 | 
					      return this.refValue.fullName;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    refQueryParam() {
 | 
					    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>
 | 
					<template>
 | 
				
			||||||
  <div>
 | 
					  <div>
 | 
				
			||||||
    <gl-loading-icon v-if="isLoading" class="gl-mb-5" size="md" />
 | 
					    <gl-loading-icon v-if="isLoading" class="gl-mb-5" size="md" />
 | 
				
			||||||
    <gl-form-group v-else>
 | 
					    <gl-form-group v-else :label="s__('Pipeline|Variables')">
 | 
				
			||||||
      <pre>{{ ciConfigVariables }}</pre>
 | 
					      <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>
 | 
					    </gl-form-group>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,6 +12,11 @@ export default {
 | 
				
			||||||
    event: 'change',
 | 
					    event: 'change',
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
 | 
					    id: {
 | 
				
			||||||
 | 
					      type: String,
 | 
				
			||||||
 | 
					      required: false,
 | 
				
			||||||
 | 
					      default: '',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    label: {
 | 
					    label: {
 | 
				
			||||||
      type: String,
 | 
					      type: String,
 | 
				
			||||||
      required: false,
 | 
					      required: false,
 | 
				
			||||||
| 
						 | 
					@ -134,6 +139,7 @@ export default {
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
    <div class="select-wrapper gl-grow">
 | 
					    <div class="select-wrapper gl-grow">
 | 
				
			||||||
      <select
 | 
					      <select
 | 
				
			||||||
 | 
					        :id="id"
 | 
				
			||||||
        v-model="internalValue"
 | 
					        v-model="internalValue"
 | 
				
			||||||
        :disabled="disableSelectInput"
 | 
					        :disabled="disableSelectInput"
 | 
				
			||||||
        class="form-control project-repo-select select-control"
 | 
					        class="form-control project-repo-select select-control"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,11 +1,21 @@
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
 | 
					import { GlFormGroup } from '@gitlab/ui';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
 | 
					  components: {
 | 
				
			||||||
 | 
					    GlFormGroup,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
    label: {
 | 
					    label: {
 | 
				
			||||||
      type: String,
 | 
					      type: String,
 | 
				
			||||||
      required: false,
 | 
					      required: false,
 | 
				
			||||||
      default: null,
 | 
					      default: null,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    labelFor: {
 | 
				
			||||||
 | 
					      type: String,
 | 
				
			||||||
 | 
					      required: false,
 | 
				
			||||||
 | 
					      default: null,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    helpPath: {
 | 
					    helpPath: {
 | 
				
			||||||
      type: String,
 | 
					      type: String,
 | 
				
			||||||
      required: false,
 | 
					      required: false,
 | 
				
			||||||
| 
						 | 
					@ -26,20 +36,27 @@ export default {
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="project-feature-row">
 | 
					  <gl-form-group :label-for="labelFor" label-class="!gl-pb-1" class="project-feature-row gl-mb-0">
 | 
				
			||||||
    <div class="gl-flex">
 | 
					    <template #label>
 | 
				
			||||||
      <label v-if="label" class="label-bold !gl-mb-0" :class="{ 'gl-text-disabled': locked }">
 | 
					      <span
 | 
				
			||||||
 | 
					        v-if="label"
 | 
				
			||||||
 | 
					        :class="{ 'gl-text-disabled': locked }"
 | 
				
			||||||
 | 
					        data-testid="project-settings-row-label"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
        {{ label }}
 | 
					        {{ label }}
 | 
				
			||||||
      </label>
 | 
					      </span>
 | 
				
			||||||
      <slot name="label-icon"></slot>
 | 
					      <slot name="label-icon"></slot>
 | 
				
			||||||
    </div>
 | 
					    </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div>
 | 
					    <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"
 | 
					      <span v-if="helpPath"
 | 
				
			||||||
        ><a :href="helpPath" target="_blank">{{ __('Learn more') }}</a
 | 
					        ><a :href="helpPath" target="_blank">{{ __('Learn more') }}</a
 | 
				
			||||||
        >.</span
 | 
					        >.</span
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <slot></slot>
 | 
					    <slot></slot>
 | 
				
			||||||
  </div>
 | 
					  </gl-form-group>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -573,12 +573,14 @@ export default {
 | 
				
			||||||
        ref="project-visibility-settings"
 | 
					        ref="project-visibility-settings"
 | 
				
			||||||
        :help-path="visibilityHelpPath"
 | 
					        :help-path="visibilityHelpPath"
 | 
				
			||||||
        :label="s__('ProjectSettings|Project visibility')"
 | 
					        :label="s__('ProjectSettings|Project visibility')"
 | 
				
			||||||
 | 
					        label-for="project_visibility_level"
 | 
				
			||||||
        :help-text="
 | 
					        :help-text="
 | 
				
			||||||
          s__('ProjectSettings|Manage who can see the project in the public access directory.')
 | 
					          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">
 | 
					        <div class="project-feature-controls gl-mx-0 gl-my-3 gl-flex gl-items-center">
 | 
				
			||||||
          <gl-form-select
 | 
					          <gl-form-select
 | 
				
			||||||
 | 
					            id="project_visibility_level"
 | 
				
			||||||
            v-model="visibilityLevel"
 | 
					            v-model="visibilityLevel"
 | 
				
			||||||
            :disabled="!canChangeVisibilityLevel"
 | 
					            :disabled="!canChangeVisibilityLevel"
 | 
				
			||||||
            name="project[visibility_level]"
 | 
					            name="project[visibility_level]"
 | 
				
			||||||
| 
						 | 
					@ -654,6 +656,7 @@ export default {
 | 
				
			||||||
        ref="issues-settings"
 | 
					        ref="issues-settings"
 | 
				
			||||||
        :help-path="issuesHelpPath"
 | 
					        :help-path="issuesHelpPath"
 | 
				
			||||||
        :label="$options.i18n.issuesLabel"
 | 
					        :label="$options.i18n.issuesLabel"
 | 
				
			||||||
 | 
					        label-for="issues_access_level"
 | 
				
			||||||
        :help-text="
 | 
					        :help-text="
 | 
				
			||||||
          s__(
 | 
					          s__(
 | 
				
			||||||
            'ProjectSettings|Flexible tool to collaboratively develop ideas and plan work in this project.',
 | 
					            'ProjectSettings|Flexible tool to collaboratively develop ideas and plan work in this project.',
 | 
				
			||||||
| 
						 | 
					@ -661,6 +664,7 @@ export default {
 | 
				
			||||||
        "
 | 
					        "
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <project-feature-setting
 | 
					        <project-feature-setting
 | 
				
			||||||
 | 
					          id="issues_access_level"
 | 
				
			||||||
          v-model="issuesAccessLevel"
 | 
					          v-model="issuesAccessLevel"
 | 
				
			||||||
          :label="$options.i18n.issuesLabel"
 | 
					          :label="$options.i18n.issuesLabel"
 | 
				
			||||||
          :options="featureAccessLevelOptions"
 | 
					          :options="featureAccessLevelOptions"
 | 
				
			||||||
| 
						 | 
					@ -687,9 +691,11 @@ export default {
 | 
				
			||||||
      <project-setting-row
 | 
					      <project-setting-row
 | 
				
			||||||
        ref="repository-settings"
 | 
					        ref="repository-settings"
 | 
				
			||||||
        :label="$options.i18n.repositoryLabel"
 | 
					        :label="$options.i18n.repositoryLabel"
 | 
				
			||||||
 | 
					        label-for="repository_access_level"
 | 
				
			||||||
        :help-text="repositoryHelpText"
 | 
					        :help-text="repositoryHelpText"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <project-feature-setting
 | 
					        <project-feature-setting
 | 
				
			||||||
 | 
					          id="repository_access_level"
 | 
				
			||||||
          v-model="repositoryAccessLevel"
 | 
					          v-model="repositoryAccessLevel"
 | 
				
			||||||
          :label="$options.i18n.repositoryLabel"
 | 
					          :label="$options.i18n.repositoryLabel"
 | 
				
			||||||
          :options="featureAccessLevelOptions"
 | 
					          :options="featureAccessLevelOptions"
 | 
				
			||||||
| 
						 | 
					@ -701,9 +707,11 @@ export default {
 | 
				
			||||||
        <project-setting-row
 | 
					        <project-setting-row
 | 
				
			||||||
          ref="merge-request-settings"
 | 
					          ref="merge-request-settings"
 | 
				
			||||||
          :label="$options.i18n.mergeRequestsLabel"
 | 
					          :label="$options.i18n.mergeRequestsLabel"
 | 
				
			||||||
 | 
					          label-for="merge_requests_access_level"
 | 
				
			||||||
          :help-text="s__('ProjectSettings|Submit changes to be merged upstream.')"
 | 
					          :help-text="s__('ProjectSettings|Submit changes to be merged upstream.')"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <project-feature-setting
 | 
					          <project-feature-setting
 | 
				
			||||||
 | 
					            id="merge_requests_access_level"
 | 
				
			||||||
            v-model="mergeRequestsAccessLevel"
 | 
					            v-model="mergeRequestsAccessLevel"
 | 
				
			||||||
            :label="$options.i18n.mergeRequestsLabel"
 | 
					            :label="$options.i18n.mergeRequestsLabel"
 | 
				
			||||||
            :options="repoFeatureAccessLevelOptions"
 | 
					            :options="repoFeatureAccessLevelOptions"
 | 
				
			||||||
| 
						 | 
					@ -715,9 +723,11 @@ export default {
 | 
				
			||||||
        <project-setting-row
 | 
					        <project-setting-row
 | 
				
			||||||
          ref="fork-settings"
 | 
					          ref="fork-settings"
 | 
				
			||||||
          :label="$options.i18n.forksLabel"
 | 
					          :label="$options.i18n.forksLabel"
 | 
				
			||||||
 | 
					          label-for="forking_access_level"
 | 
				
			||||||
          :help-text="s__('ProjectSettings|Users can copy the repository to a new project.')"
 | 
					          :help-text="s__('ProjectSettings|Users can copy the repository to a new project.')"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <project-feature-setting
 | 
					          <project-feature-setting
 | 
				
			||||||
 | 
					            id="forking_access_level"
 | 
				
			||||||
            v-model="forkingAccessLevel"
 | 
					            v-model="forkingAccessLevel"
 | 
				
			||||||
            :label="$options.i18n.forksLabel"
 | 
					            :label="$options.i18n.forksLabel"
 | 
				
			||||||
            :options="featureAccessLevelOptions"
 | 
					            :options="featureAccessLevelOptions"
 | 
				
			||||||
| 
						 | 
					@ -764,6 +774,7 @@ export default {
 | 
				
			||||||
        <project-setting-row
 | 
					        <project-setting-row
 | 
				
			||||||
          ref="pipeline-settings"
 | 
					          ref="pipeline-settings"
 | 
				
			||||||
          :label="$options.i18n.ciCdLabel"
 | 
					          :label="$options.i18n.ciCdLabel"
 | 
				
			||||||
 | 
					          label-for="builds_access_level"
 | 
				
			||||||
          :help-text="
 | 
					          :help-text="
 | 
				
			||||||
            s__(
 | 
					            s__(
 | 
				
			||||||
              'ProjectSettings|Build, test, and deploy your changes. Does not apply to project integrations.',
 | 
					              'ProjectSettings|Build, test, and deploy your changes. Does not apply to project integrations.',
 | 
				
			||||||
| 
						 | 
					@ -771,6 +782,7 @@ export default {
 | 
				
			||||||
          "
 | 
					          "
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <project-feature-setting
 | 
					          <project-feature-setting
 | 
				
			||||||
 | 
					            id="builds_access_level"
 | 
				
			||||||
            v-model="buildsAccessLevel"
 | 
					            v-model="buildsAccessLevel"
 | 
				
			||||||
            :label="$options.i18n.ciCdLabel"
 | 
					            :label="$options.i18n.ciCdLabel"
 | 
				
			||||||
            :options="repoFeatureAccessLevelOptions"
 | 
					            :options="repoFeatureAccessLevelOptions"
 | 
				
			||||||
| 
						 | 
					@ -785,6 +797,7 @@ export default {
 | 
				
			||||||
        ref="container-registry-settings"
 | 
					        ref="container-registry-settings"
 | 
				
			||||||
        :help-path="registryHelpPath"
 | 
					        :help-path="registryHelpPath"
 | 
				
			||||||
        :label="$options.i18n.containerRegistryLabel"
 | 
					        :label="$options.i18n.containerRegistryLabel"
 | 
				
			||||||
 | 
					        label-for="container_registry_access_level"
 | 
				
			||||||
        :help-text="
 | 
					        :help-text="
 | 
				
			||||||
          s__('ProjectSettings|Every project can have its own space to store its Docker images')
 | 
					          s__('ProjectSettings|Every project can have its own space to store its Docker images')
 | 
				
			||||||
        "
 | 
					        "
 | 
				
			||||||
| 
						 | 
					@ -803,6 +816,7 @@ export default {
 | 
				
			||||||
          </gl-sprintf>
 | 
					          </gl-sprintf>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <project-feature-setting
 | 
					        <project-feature-setting
 | 
				
			||||||
 | 
					          id="container_registry_access_level"
 | 
				
			||||||
          v-model="containerRegistryAccessLevel"
 | 
					          v-model="containerRegistryAccessLevel"
 | 
				
			||||||
          :options="featureAccessLevelOptions"
 | 
					          :options="featureAccessLevelOptions"
 | 
				
			||||||
          :disabled-select-input="isProjectPrivate"
 | 
					          :disabled-select-input="isProjectPrivate"
 | 
				
			||||||
| 
						 | 
					@ -813,9 +827,11 @@ export default {
 | 
				
			||||||
      <project-setting-row
 | 
					      <project-setting-row
 | 
				
			||||||
        ref="analytics-settings"
 | 
					        ref="analytics-settings"
 | 
				
			||||||
        :label="$options.i18n.analyticsLabel"
 | 
					        :label="$options.i18n.analyticsLabel"
 | 
				
			||||||
 | 
					        label-for="analytics_access_level"
 | 
				
			||||||
        :help-text="s__('ProjectSettings|View project analytics.')"
 | 
					        :help-text="s__('ProjectSettings|View project analytics.')"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <project-feature-setting
 | 
					        <project-feature-setting
 | 
				
			||||||
 | 
					          id="analytics_access_level"
 | 
				
			||||||
          v-model="analyticsAccessLevel"
 | 
					          v-model="analyticsAccessLevel"
 | 
				
			||||||
          :label="$options.i18n.analyticsLabel"
 | 
					          :label="$options.i18n.analyticsLabel"
 | 
				
			||||||
          :options="featureAccessLevelOptions"
 | 
					          :options="featureAccessLevelOptions"
 | 
				
			||||||
| 
						 | 
					@ -827,9 +843,11 @@ export default {
 | 
				
			||||||
        v-if="requirementsAvailable"
 | 
					        v-if="requirementsAvailable"
 | 
				
			||||||
        ref="requirements-settings"
 | 
					        ref="requirements-settings"
 | 
				
			||||||
        :label="$options.i18n.requirementsLabel"
 | 
					        :label="$options.i18n.requirementsLabel"
 | 
				
			||||||
 | 
					        label-for="requirements_access_level"
 | 
				
			||||||
        :help-text="s__('ProjectSettings|Requirements management system.')"
 | 
					        :help-text="s__('ProjectSettings|Requirements management system.')"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <project-feature-setting
 | 
					        <project-feature-setting
 | 
				
			||||||
 | 
					          id="requirements_access_level"
 | 
				
			||||||
          v-model="requirementsAccessLevel"
 | 
					          v-model="requirementsAccessLevel"
 | 
				
			||||||
          :label="$options.i18n.requirementsLabel"
 | 
					          :label="$options.i18n.requirementsLabel"
 | 
				
			||||||
          :options="featureAccessLevelOptions"
 | 
					          :options="featureAccessLevelOptions"
 | 
				
			||||||
| 
						 | 
					@ -839,9 +857,11 @@ export default {
 | 
				
			||||||
      </project-setting-row>
 | 
					      </project-setting-row>
 | 
				
			||||||
      <project-setting-row
 | 
					      <project-setting-row
 | 
				
			||||||
        :label="$options.i18n.securityAndComplianceLabel"
 | 
					        :label="$options.i18n.securityAndComplianceLabel"
 | 
				
			||||||
 | 
					        label-for="security_and_compliance_access_level"
 | 
				
			||||||
        :help-text="s__('ProjectSettings|Security and compliance for this project.')"
 | 
					        :help-text="s__('ProjectSettings|Security and compliance for this project.')"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <project-feature-setting
 | 
					        <project-feature-setting
 | 
				
			||||||
 | 
					          id="security_and_compliance_access_level"
 | 
				
			||||||
          v-model="securityAndComplianceAccessLevel"
 | 
					          v-model="securityAndComplianceAccessLevel"
 | 
				
			||||||
          :label="$options.i18n.securityAndComplianceLabel"
 | 
					          :label="$options.i18n.securityAndComplianceLabel"
 | 
				
			||||||
          :options="featureAccessLevelOptions"
 | 
					          :options="featureAccessLevelOptions"
 | 
				
			||||||
| 
						 | 
					@ -852,9 +872,11 @@ export default {
 | 
				
			||||||
      <project-setting-row
 | 
					      <project-setting-row
 | 
				
			||||||
        ref="wiki-settings"
 | 
					        ref="wiki-settings"
 | 
				
			||||||
        :label="$options.i18n.wikiLabel"
 | 
					        :label="$options.i18n.wikiLabel"
 | 
				
			||||||
 | 
					        label-for="wiki_access_level"
 | 
				
			||||||
        :help-text="s__('ProjectSettings|Pages for project documentation.')"
 | 
					        :help-text="s__('ProjectSettings|Pages for project documentation.')"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <project-feature-setting
 | 
					        <project-feature-setting
 | 
				
			||||||
 | 
					          id="wiki_access_level"
 | 
				
			||||||
          v-model="wikiAccessLevel"
 | 
					          v-model="wikiAccessLevel"
 | 
				
			||||||
          :label="$options.i18n.wikiLabel"
 | 
					          :label="$options.i18n.wikiLabel"
 | 
				
			||||||
          :options="featureAccessLevelOptions"
 | 
					          :options="featureAccessLevelOptions"
 | 
				
			||||||
| 
						 | 
					@ -865,9 +887,11 @@ export default {
 | 
				
			||||||
      <project-setting-row
 | 
					      <project-setting-row
 | 
				
			||||||
        ref="snippet-settings"
 | 
					        ref="snippet-settings"
 | 
				
			||||||
        :label="$options.i18n.snippetsLabel"
 | 
					        :label="$options.i18n.snippetsLabel"
 | 
				
			||||||
 | 
					        label-for="snippets_access_level"
 | 
				
			||||||
        :help-text="s__('ProjectSettings|Share code with others outside the project.')"
 | 
					        :help-text="s__('ProjectSettings|Share code with others outside the project.')"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <project-feature-setting
 | 
					        <project-feature-setting
 | 
				
			||||||
 | 
					          id="snippets_access_level"
 | 
				
			||||||
          v-model="snippetsAccessLevel"
 | 
					          v-model="snippetsAccessLevel"
 | 
				
			||||||
          :label="$options.i18n.snippetsLabel"
 | 
					          :label="$options.i18n.snippetsLabel"
 | 
				
			||||||
          :options="featureAccessLevelOptions"
 | 
					          :options="featureAccessLevelOptions"
 | 
				
			||||||
| 
						 | 
					@ -918,10 +942,12 @@ export default {
 | 
				
			||||||
      <project-setting-row
 | 
					      <project-setting-row
 | 
				
			||||||
        ref="model-experiments-settings"
 | 
					        ref="model-experiments-settings"
 | 
				
			||||||
        :label="$options.i18n.modelExperimentsLabel"
 | 
					        :label="$options.i18n.modelExperimentsLabel"
 | 
				
			||||||
 | 
					        label-for="model_experiments_access_level"
 | 
				
			||||||
        :help-text="$options.i18n.modelExperimentsHelpText"
 | 
					        :help-text="$options.i18n.modelExperimentsHelpText"
 | 
				
			||||||
        :help-path="$options.modelExperimentsHelpPath"
 | 
					        :help-path="$options.modelExperimentsHelpPath"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <project-feature-setting
 | 
					        <project-feature-setting
 | 
				
			||||||
 | 
					          id="model_experiments_access_level"
 | 
				
			||||||
          v-model="modelExperimentsAccessLevel"
 | 
					          v-model="modelExperimentsAccessLevel"
 | 
				
			||||||
          :label="$options.i18n.modelExperimentsLabel"
 | 
					          :label="$options.i18n.modelExperimentsLabel"
 | 
				
			||||||
          :options="featureAccessLevelOptions"
 | 
					          :options="featureAccessLevelOptions"
 | 
				
			||||||
| 
						 | 
					@ -932,10 +958,12 @@ export default {
 | 
				
			||||||
      <project-setting-row
 | 
					      <project-setting-row
 | 
				
			||||||
        ref="model-registry-settings"
 | 
					        ref="model-registry-settings"
 | 
				
			||||||
        :label="$options.i18n.modelRegistryLabel"
 | 
					        :label="$options.i18n.modelRegistryLabel"
 | 
				
			||||||
 | 
					        label-for="model_registry_access_level"
 | 
				
			||||||
        :help-text="$options.i18n.modelRegistryHelpText"
 | 
					        :help-text="$options.i18n.modelRegistryHelpText"
 | 
				
			||||||
        :help-path="$options.modelRegistryHelpPath"
 | 
					        :help-path="$options.modelRegistryHelpPath"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <project-feature-setting
 | 
					        <project-feature-setting
 | 
				
			||||||
 | 
					          id="model_registry_access_level"
 | 
				
			||||||
          v-model="modelRegistryAccessLevel"
 | 
					          v-model="modelRegistryAccessLevel"
 | 
				
			||||||
          :label="$options.i18n.modelRegistryLabel"
 | 
					          :label="$options.i18n.modelRegistryLabel"
 | 
				
			||||||
          :options="featureAccessLevelOptions"
 | 
					          :options="featureAccessLevelOptions"
 | 
				
			||||||
| 
						 | 
					@ -948,6 +976,7 @@ export default {
 | 
				
			||||||
        ref="pages-settings"
 | 
					        ref="pages-settings"
 | 
				
			||||||
        :help-path="pagesHelpPath"
 | 
					        :help-path="pagesHelpPath"
 | 
				
			||||||
        :label="$options.i18n.pagesLabel"
 | 
					        :label="$options.i18n.pagesLabel"
 | 
				
			||||||
 | 
					        label-for="pages_access_level"
 | 
				
			||||||
        :help-text="
 | 
					        :help-text="
 | 
				
			||||||
          s__(
 | 
					          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.',
 | 
					            '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
 | 
					        <project-feature-setting
 | 
				
			||||||
 | 
					          id="pages_access_level"
 | 
				
			||||||
          v-model="pagesAccessLevel"
 | 
					          v-model="pagesAccessLevel"
 | 
				
			||||||
          :label="$options.i18n.pagesLabel"
 | 
					          :label="$options.i18n.pagesLabel"
 | 
				
			||||||
          :access-control-forced="pagesAccessControlForced"
 | 
					          :access-control-forced="pagesAccessControlForced"
 | 
				
			||||||
| 
						 | 
					@ -965,11 +995,13 @@ export default {
 | 
				
			||||||
      <project-setting-row
 | 
					      <project-setting-row
 | 
				
			||||||
        ref="monitor-settings"
 | 
					        ref="monitor-settings"
 | 
				
			||||||
        :label="$options.i18n.monitorLabel"
 | 
					        :label="$options.i18n.monitorLabel"
 | 
				
			||||||
 | 
					        label-for="monitor_access_level"
 | 
				
			||||||
        :help-text="
 | 
					        :help-text="
 | 
				
			||||||
          s__('ProjectSettings|Monitor the health of your project and respond to incidents.')
 | 
					          s__('ProjectSettings|Monitor the health of your project and respond to incidents.')
 | 
				
			||||||
        "
 | 
					        "
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <project-feature-setting
 | 
					        <project-feature-setting
 | 
				
			||||||
 | 
					          id="monitor_access_level"
 | 
				
			||||||
          v-model="monitorAccessLevel"
 | 
					          v-model="monitorAccessLevel"
 | 
				
			||||||
          :label="$options.i18n.monitorLabel"
 | 
					          :label="$options.i18n.monitorLabel"
 | 
				
			||||||
          :options="featureAccessLevelOptions"
 | 
					          :options="featureAccessLevelOptions"
 | 
				
			||||||
| 
						 | 
					@ -980,10 +1012,12 @@ export default {
 | 
				
			||||||
      <project-setting-row
 | 
					      <project-setting-row
 | 
				
			||||||
        ref="environments-settings"
 | 
					        ref="environments-settings"
 | 
				
			||||||
        :label="$options.i18n.environmentsLabel"
 | 
					        :label="$options.i18n.environmentsLabel"
 | 
				
			||||||
 | 
					        label-for="environments_access_level"
 | 
				
			||||||
        :help-text="$options.i18n.environmentsHelpText"
 | 
					        :help-text="$options.i18n.environmentsHelpText"
 | 
				
			||||||
        :help-path="environmentsHelpPath"
 | 
					        :help-path="environmentsHelpPath"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <project-feature-setting
 | 
					        <project-feature-setting
 | 
				
			||||||
 | 
					          id="environments_access_level"
 | 
				
			||||||
          v-model="environmentsAccessLevel"
 | 
					          v-model="environmentsAccessLevel"
 | 
				
			||||||
          :label="$options.i18n.environmentsLabel"
 | 
					          :label="$options.i18n.environmentsLabel"
 | 
				
			||||||
          :options="featureAccessLevelOptions"
 | 
					          :options="featureAccessLevelOptions"
 | 
				
			||||||
| 
						 | 
					@ -994,10 +1028,12 @@ export default {
 | 
				
			||||||
      <project-setting-row
 | 
					      <project-setting-row
 | 
				
			||||||
        ref="feature-flags-settings"
 | 
					        ref="feature-flags-settings"
 | 
				
			||||||
        :label="$options.i18n.featureFlagsLabel"
 | 
					        :label="$options.i18n.featureFlagsLabel"
 | 
				
			||||||
 | 
					        label-for="feature_flags_access_level"
 | 
				
			||||||
        :help-text="$options.i18n.featureFlagsHelpText"
 | 
					        :help-text="$options.i18n.featureFlagsHelpText"
 | 
				
			||||||
        :help-path="featureFlagsHelpPath"
 | 
					        :help-path="featureFlagsHelpPath"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <project-feature-setting
 | 
					        <project-feature-setting
 | 
				
			||||||
 | 
					          id="feature_flags_access_level"
 | 
				
			||||||
          v-model="featureFlagsAccessLevel"
 | 
					          v-model="featureFlagsAccessLevel"
 | 
				
			||||||
          :label="$options.i18n.featureFlagsLabel"
 | 
					          :label="$options.i18n.featureFlagsLabel"
 | 
				
			||||||
          :options="featureAccessLevelOptions"
 | 
					          :options="featureAccessLevelOptions"
 | 
				
			||||||
| 
						 | 
					@ -1008,10 +1044,12 @@ export default {
 | 
				
			||||||
      <project-setting-row
 | 
					      <project-setting-row
 | 
				
			||||||
        ref="infrastructure-settings"
 | 
					        ref="infrastructure-settings"
 | 
				
			||||||
        :label="$options.i18n.infrastructureLabel"
 | 
					        :label="$options.i18n.infrastructureLabel"
 | 
				
			||||||
 | 
					        label-for="infrastructure_access_level"
 | 
				
			||||||
        :help-text="$options.i18n.infrastructureHelpText"
 | 
					        :help-text="$options.i18n.infrastructureHelpText"
 | 
				
			||||||
        :help-path="infrastructureHelpPath"
 | 
					        :help-path="infrastructureHelpPath"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <project-feature-setting
 | 
					        <project-feature-setting
 | 
				
			||||||
 | 
					          id="infrastructure_access_level"
 | 
				
			||||||
          v-model="infrastructureAccessLevel"
 | 
					          v-model="infrastructureAccessLevel"
 | 
				
			||||||
          :label="$options.i18n.infrastructureLabel"
 | 
					          :label="$options.i18n.infrastructureLabel"
 | 
				
			||||||
          :options="featureAccessLevelOptions"
 | 
					          :options="featureAccessLevelOptions"
 | 
				
			||||||
| 
						 | 
					@ -1022,10 +1060,12 @@ export default {
 | 
				
			||||||
      <project-setting-row
 | 
					      <project-setting-row
 | 
				
			||||||
        ref="releases-settings"
 | 
					        ref="releases-settings"
 | 
				
			||||||
        :label="$options.i18n.releasesLabel"
 | 
					        :label="$options.i18n.releasesLabel"
 | 
				
			||||||
 | 
					        label-for="releases_access_level"
 | 
				
			||||||
        :help-text="$options.i18n.releasesHelpText"
 | 
					        :help-text="$options.i18n.releasesHelpText"
 | 
				
			||||||
        :help-path="releasesHelpPath"
 | 
					        :help-path="releasesHelpPath"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <project-feature-setting
 | 
					        <project-feature-setting
 | 
				
			||||||
 | 
					          id="releases_access_level"
 | 
				
			||||||
          v-model="releasesAccessLevel"
 | 
					          v-model="releasesAccessLevel"
 | 
				
			||||||
          :label="$options.i18n.releasesLabel"
 | 
					          :label="$options.i18n.releasesLabel"
 | 
				
			||||||
          :options="featureAccessLevelOptions"
 | 
					          :options="featureAccessLevelOptions"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@ module Projects
 | 
				
			||||||
    class TagsController < ::Projects::Registry::ApplicationController
 | 
					    class TagsController < ::Projects::Registry::ApplicationController
 | 
				
			||||||
      include PackagesHelper
 | 
					      include PackagesHelper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      before_action :authorize_destroy_container_image!, only: [:destroy]
 | 
					      before_action :authorize_destroy_container_image_tag!, only: [:destroy]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      LIMIT = 15
 | 
					      LIMIT = 15
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,7 @@ module Mutations
 | 
				
			||||||
      LIMIT = 20
 | 
					      LIMIT = 20
 | 
				
			||||||
      TOO_MANY_TAGS_ERROR_MESSAGE = "Number of tags is greater than #{LIMIT}"
 | 
					      TOO_MANY_TAGS_ERROR_MESSAGE = "Number of tags is greater than #{LIMIT}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      authorize :destroy_container_image
 | 
					      authorize :destroy_container_image_tag
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      argument :id,
 | 
					      argument :id,
 | 
				
			||||||
        ::Types::GlobalIDType[::ContainerRepository],
 | 
					        ::Types::GlobalIDType[::ContainerRepository],
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,9 +5,9 @@ module Types
 | 
				
			||||||
    class ContainerRepositoryTag < BasePermissionType
 | 
					    class ContainerRepositoryTag < BasePermissionType
 | 
				
			||||||
      graphql_name 'ContainerRepositoryTagPermissions'
 | 
					      graphql_name 'ContainerRepositoryTagPermissions'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      ability_field :destroy_container_image,
 | 
					      ability_field :destroy_container_image_tag,
 | 
				
			||||||
        name: 'destroy_container_repository_tag',
 | 
					        name: 'destroy_container_repository_tag',
 | 
				
			||||||
        resolver_method: :destroy_container_image
 | 
					        resolver_method: :destroy_container_image_tag
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,10 @@ class LfsFileLock < ApplicationRecord
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  validates :project_id, :user_id, :path, presence: true
 | 
					  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)
 | 
					  def can_be_unlocked_by?(current_user, forced = false)
 | 
				
			||||||
    return true if current_user.id == user_id
 | 
					    return true if current_user.id == user_id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,7 @@ module ContainerRegistry
 | 
				
			||||||
    condition(:protected_for_delete) { @subject.protected_for_delete?(@user) }
 | 
					    condition(:protected_for_delete) { @subject.protected_for_delete?(@user) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    rule { protected_for_delete }.policy do
 | 
					    rule { protected_for_delete }.policy do
 | 
				
			||||||
      prevent :destroy_container_image
 | 
					      prevent :destroy_container_image_tag
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -565,6 +565,7 @@ class ProjectPolicy < BasePolicy
 | 
				
			||||||
    enable :create_container_image
 | 
					    enable :create_container_image
 | 
				
			||||||
    enable :update_container_image
 | 
					    enable :update_container_image
 | 
				
			||||||
    enable :destroy_container_image
 | 
					    enable :destroy_container_image
 | 
				
			||||||
 | 
					    enable :destroy_container_image_tag
 | 
				
			||||||
    enable :create_environment
 | 
					    enable :create_environment
 | 
				
			||||||
    enable :update_environment
 | 
					    enable :update_environment
 | 
				
			||||||
    enable :destroy_environment
 | 
					    enable :destroy_environment
 | 
				
			||||||
| 
						 | 
					@ -784,6 +785,7 @@ class ProjectPolicy < BasePolicy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  rule { container_registry_disabled }.policy do
 | 
					  rule { container_registry_disabled }.policy do
 | 
				
			||||||
    prevent(*create_read_update_admin_destroy(:container_image))
 | 
					    prevent(*create_read_update_admin_destroy(:container_image))
 | 
				
			||||||
 | 
					    prevent :destroy_container_image_tag
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  rule { anonymous & ~public_project }.prevent_all
 | 
					  rule { anonymous & ~public_project }.prevent_all
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,6 +7,7 @@ module Auth
 | 
				
			||||||
      :read_container_image,
 | 
					      :read_container_image,
 | 
				
			||||||
      :create_container_image,
 | 
					      :create_container_image,
 | 
				
			||||||
      :destroy_container_image,
 | 
					      :destroy_container_image,
 | 
				
			||||||
 | 
					      :destroy_container_image_tag,
 | 
				
			||||||
      :update_container_image,
 | 
					      :update_container_image,
 | 
				
			||||||
      :admin_container_image,
 | 
					      :admin_container_image,
 | 
				
			||||||
      :build_read_container_image,
 | 
					      :build_read_container_image,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,17 +34,15 @@ module Lfs
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # rubocop: disable CodeReuse/ActiveRecord
 | 
					 | 
				
			||||||
    def lock
 | 
					    def lock
 | 
				
			||||||
      return @lock if defined?(@lock)
 | 
					      return @lock if defined?(@lock)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @lock = if params[:id].present?
 | 
					      @lock = if params[:id].present?
 | 
				
			||||||
                project.lfs_file_locks.find(params[:id])
 | 
					                project.lfs_file_locks.find(params[:id])
 | 
				
			||||||
              elsif params[:path].present?
 | 
					              elsif params[:path].present?
 | 
				
			||||||
                project.lfs_file_locks.find_by!(path: params[:path])
 | 
					                project.lfs_file_locks.for_path!(params[:path])
 | 
				
			||||||
              end
 | 
					              end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
    # rubocop: enable CodeReuse/ActiveRecord
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,7 +36,7 @@ module Projects
 | 
				
			||||||
      def can_destroy?
 | 
					      def can_destroy?
 | 
				
			||||||
        return true if container_expiration_policy
 | 
					        return true if container_expiration_policy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        can?(current_user, :destroy_container_image, project)
 | 
					        can?(current_user, :destroy_container_image_tag, project)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      def valid_regex?
 | 
					      def valid_regex?
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,7 @@ module Projects
 | 
				
			||||||
        @container_repository = container_repository
 | 
					        @container_repository = container_repository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        unless container_expiration_policy?
 | 
					        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
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        @tag_names = params[:tags]
 | 
					        @tag_names = params[:tags]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,7 @@ module AntiAbuse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def validate(record)
 | 
					    def validate(record)
 | 
				
			||||||
      return if record.errors.include?(:email)
 | 
					      return if record.errors.include?(:email)
 | 
				
			||||||
 | 
					      return unless ::Gitlab::CurrentSettings.enforce_email_subaddress_restrictions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      email = record.email
 | 
					      email = record.email
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,14 +25,10 @@ module AntiAbuse
 | 
				
			||||||
    private
 | 
					    private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def prevent_banned_user_email_reuse?(email)
 | 
					    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?
 | 
					      ::Users::BannedUser.by_detumbled_email(email).exists?
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def limit_normalized_email_reuse?(email)
 | 
					    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
 | 
					      Email.users_by_detumbled_email_count(email) >= NORMALIZED_EMAIL_ACCOUNT_LIMIT
 | 
				
			||||||
    end
 | 
					    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.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'
 | 
					      = f.number_field :pages_extra_deployments_default_expiry_seconds, class: 'form-control gl-form-input'
 | 
				
			||||||
      .form-text.gl-text-subtle
 | 
					      .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))
 | 
					        = 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
 | 
					    %h5
 | 
				
			||||||
      = s_("AdminSettings|Configure Let's Encrypt")
 | 
					      = s_("AdminSettings|Configure Let's Encrypt")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,7 +22,7 @@ module AntiAbuse
 | 
				
			||||||
    attr_reader :banned_user
 | 
					    attr_reader :banned_user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def ban_users_with_the_same_detumbled_email!
 | 
					    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"
 | 
					      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
 | 
					feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/443486
 | 
				
			||||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161804
 | 
					introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161804
 | 
				
			||||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/516169
 | 
					rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/516169
 | 
				
			||||||
milestone: '17.9'
 | 
					milestone: '17.10'
 | 
				
			||||||
group: group::environments
 | 
					group: group::environments
 | 
				
			||||||
type: wip
 | 
					type: wip
 | 
				
			||||||
default_enabled: false
 | 
					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
 | 
					  - 1
 | 
				
			||||||
- - cluster_agent
 | 
					- - cluster_agent
 | 
				
			||||||
  - 1
 | 
					  - 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
 | 
					- - clusters_agents_auto_flow_work_items_closed_event
 | 
				
			||||||
  - 1
 | 
					  - 1
 | 
				
			||||||
- - clusters_agents_auto_flow_work_items_created_event
 | 
					- - clusters_agents_auto_flow_work_items_created_event
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23182,7 +23182,7 @@ A tag from a container repository.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| Name | Type | Description |
 | 
					| 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`
 | 
					### `ContainerTagsExpirationPolicy`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -250,7 +250,7 @@ If you have Kubernetes clusters connected with GitLab, [upgrade your GitLab agen
 | 
				
			||||||
### Elasticsearch
 | 
					### Elasticsearch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Before updating GitLab, confirm advanced search migrations are complete by
 | 
					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
 | 
					After updating GitLab, you may have to upgrade
 | 
				
			||||||
[Elasticsearch if the new version breaks compatibility](../integration/advanced_search/elasticsearch.md#version-requirements).
 | 
					[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 >}}
 | 
					{{< /tabs >}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Check for pending advanced search migrations
 | 
					## Advanced search migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Check for pending migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{{< details >}}
 | 
					{{< details >}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -511,3 +513,15 @@ sudo -u git -H bundle exec rake gitlab:elastic:list_pending_migrations
 | 
				
			||||||
{{< /tab >}}
 | 
					{{< /tab >}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{{< /tabs >}}
 | 
					{{< /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
 | 
					### Other limits
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| Setting                                                        | Default for GitLab.com |
 | 
					| 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 ) |
 | 
					| 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                  |
 | 
					| Maximum payload size                                                | 25 MB                  |
 | 
				
			||||||
| Timeout                                                        | 10 seconds             |
 | 
					| Timeout                                                             | 10 seconds             |
 | 
				
			||||||
| [Multiple Pages deployments](../project/pages/_index.md#limits) | 100 extra deployments (Premium tier), 500 extra deployments (Ultimate tier) |
 | 
					| [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:
 | 
					For self-managed instance limits, see:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -104,7 +104,7 @@ To improve your security, try these features:
 | 
				
			||||||
| Feature | Tier | Add-on | Offering | Status |
 | 
					| 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 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 |
 | 
					| [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 |
 | 
					| [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 |
 | 
					| [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
 | 
					## Expiring deployments
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{{< details >}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- Tier: Premium, Ultimate
 | 
					 | 
				
			||||||
- Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{{< /details >}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{{< history >}}
 | 
					{{< history >}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/162826) in GitLab 17.4.
 | 
					- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/162826) in GitLab 17.4.
 | 
				
			||||||
| 
						 | 
					@ -233,10 +226,6 @@ deploy-pages:
 | 
				
			||||||
      - public
 | 
					      - 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.
 | 
					Expired deployments are stopped by a cron job that runs every 10 minutes.
 | 
				
			||||||
Stopped deployments are subsequently deleted by another cron job that also
 | 
					Stopped deployments are subsequently deleted by another cron job that also
 | 
				
			||||||
runs every 10 minutes. To recover it, follow the steps described in
 | 
					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.
 | 
					   list.
 | 
				
			||||||
1. Expand the deployment you want to recover and select **Restore**.
 | 
					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
 | 
					### Delete a Deployment
 | 
				
			||||||
 | 
					
 | 
				
			||||||
To 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
 | 
					To restore a stopped deployment that has not been deleted yet, see
 | 
				
			||||||
[Recover a stopped deployment](#recover-a-stopped-deployment).
 | 
					[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
 | 
					## User-defined job names
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{{< history >}}
 | 
					{{< history >}}
 | 
				
			||||||
| 
						 | 
					@ -493,3 +314,8 @@ deployment is triggered:
 | 
				
			||||||
pages:
 | 
					pages:
 | 
				
			||||||
  pages: false
 | 
					  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'
 | 
					        requires :tag_name, type: String, desc: 'The name of the tag'
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
      delete ':id/registry/repositories/:repository_id/tags/:tag_name', requirements: REPOSITORY_ENDPOINT_REQUIREMENTS do
 | 
					      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
 | 
					        result = ::Projects::ContainerRepository::DeleteTagsService
 | 
				
			||||||
          .new(repository.project, current_user, tags: [declared_params[:tag_name]])
 | 
					          .new(repository.project, current_user, tags: [declared_params[:tag_name]])
 | 
				
			||||||
| 
						 | 
					@ -205,8 +205,8 @@ module API
 | 
				
			||||||
        authorize! :read_container_image, repository
 | 
					        authorize! :read_container_image, repository
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      def authorize_destroy_container_image!
 | 
					      def authorize_destroy_container_image_tag!
 | 
				
			||||||
        authorize! :destroy_container_image, repository
 | 
					        authorize! :destroy_container_image_tag, tag
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      def authorize_admin_container_image!
 | 
					      def authorize_admin_container_image!
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -82,6 +82,10 @@ RSpec.describe Projects::Registry::TagsController do
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe 'POST destroy' do
 | 
					  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
 | 
					    context 'when user has access to registry' do
 | 
				
			||||||
      before do
 | 
					      before do
 | 
				
			||||||
        project.add_developer(user)
 | 
					        project.add_developer(user)
 | 
				
			||||||
| 
						 | 
					@ -96,12 +100,16 @@ RSpec.describe Projects::Registry::TagsController do
 | 
				
			||||||
          expect_delete_tags(%w[rc1])
 | 
					          expect_delete_tags(%w[rc1])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          destroy_tag('rc1')
 | 
					          destroy_tag('rc1')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          expect(controller).to have_received(:authorize_destroy_container_image_tag!)
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        it 'makes it possible to delete a tag that ends with a dot' do
 | 
					        it 'makes it possible to delete a tag that ends with a dot' do
 | 
				
			||||||
          expect_delete_tags(%w[test.])
 | 
					          expect_delete_tags(%w[test.])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          destroy_tag('test.')
 | 
					          destroy_tag('test.')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          expect(controller).to have_received(:authorize_destroy_container_image_tag!)
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        it 'tracks the event', :snowplow do
 | 
					        it 'tracks the event', :snowplow do
 | 
				
			||||||
| 
						 | 
					@ -114,6 +122,21 @@ RSpec.describe Projects::Registry::TagsController do
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    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
 | 
					    private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def destroy_tag(name)
 | 
					    def destroy_tag(name)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -164,6 +164,7 @@ describe('Pipeline New Form', () => {
 | 
				
			||||||
        expect(mockCiConfigVariables).toHaveBeenCalled();
 | 
					        expect(mockCiConfigVariables).toHaveBeenCalled();
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    describe('when the ciInputsForPipelines flag is enabled', () => {
 | 
					    describe('when the ciInputsForPipelines flag is enabled', () => {
 | 
				
			||||||
      beforeEach(async () => {
 | 
					      beforeEach(async () => {
 | 
				
			||||||
        mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
 | 
					        mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,17 +1,24 @@
 | 
				
			||||||
import { GlFormGroup, GlLoadingIcon } from '@gitlab/ui';
 | 
					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 VueApollo from 'vue-apollo';
 | 
				
			||||||
import Vue from 'vue';
 | 
					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 createMockApollo from 'helpers/mock_apollo_helper';
 | 
				
			||||||
import waitForPromises from 'helpers/wait_for_promises';
 | 
					import waitForPromises from 'helpers/wait_for_promises';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { fetchPolicies } from '~/lib/graphql';
 | 
				
			||||||
import { reportToSentry } from '~/ci/utils';
 | 
					import { reportToSentry } from '~/ci/utils';
 | 
				
			||||||
import ciConfigVariablesQuery from '~/ci/pipeline_new/graphql/queries/ci_config_variables.graphql';
 | 
					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';
 | 
					import PipelineVariablesForm from '~/ci/pipeline_new/components/pipeline_variables_form.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Vue.use(VueApollo);
 | 
					Vue.use(VueApollo);
 | 
				
			||||||
jest.mock('~/ci/utils');
 | 
					jest.mock('~/ci/utils');
 | 
				
			||||||
 | 
					jest.mock('@gitlab/ui/dist/utils', () => ({
 | 
				
			||||||
 | 
					  GlBreakpointInstance: {
 | 
				
			||||||
 | 
					    getBreakpointSize: jest.fn(),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe('PipelineVariablesForm', () => {
 | 
					describe('PipelineVariablesForm', () => {
 | 
				
			||||||
  let wrapper;
 | 
					  let wrapper;
 | 
				
			||||||
| 
						 | 
					@ -19,16 +26,41 @@ describe('PipelineVariablesForm', () => {
 | 
				
			||||||
  let mockCiConfigVariables;
 | 
					  let mockCiConfigVariables;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const defaultProps = {
 | 
					  const defaultProps = {
 | 
				
			||||||
    projectPath: 'group/project',
 | 
					 | 
				
			||||||
    defaultBranch: 'main',
 | 
					    defaultBranch: 'main',
 | 
				
			||||||
 | 
					    isMaintainer: true,
 | 
				
			||||||
 | 
					    projectPath: 'group/project',
 | 
				
			||||||
    refParam: 'feature',
 | 
					    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]];
 | 
					    const handlers = [[ciConfigVariablesQuery, mockCiConfigVariables]];
 | 
				
			||||||
    mockApollo = createMockApollo(handlers);
 | 
					    mockApollo = createMockApollo(handlers);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    wrapper = shallowMount(PipelineVariablesForm, {
 | 
					    wrapper = shallowMountExtended(PipelineVariablesForm, {
 | 
				
			||||||
      apolloProvider: mockApollo,
 | 
					      apolloProvider: mockApollo,
 | 
				
			||||||
      propsData: { ...defaultProps, ...props },
 | 
					      propsData: { ...defaultProps, ...props },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
| 
						 | 
					@ -36,8 +68,11 @@ describe('PipelineVariablesForm', () => {
 | 
				
			||||||
    await waitForPromises();
 | 
					    await waitForPromises();
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
 | 
					 | 
				
			||||||
  const findForm = () => wrapper.findComponent(GlFormGroup);
 | 
					  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(() => {
 | 
					  beforeEach(() => {
 | 
				
			||||||
    mockCiConfigVariables = jest.fn().mockResolvedValue({
 | 
					    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', () => {
 | 
					  describe('query configuration', () => {
 | 
				
			||||||
    it('has correct apollo query configuration', async () => {
 | 
					    it('has correct apollo query configuration', async () => {
 | 
				
			||||||
      await createComponent();
 | 
					      await createComponent();
 | 
				
			||||||
      const { apollo } = wrapper.vm.$options;
 | 
					      const { apollo } = wrapper.vm.$options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(apollo.ciConfigVariables).toMatchObject({
 | 
					      expect(apollo.ciConfigVariables.fetchPolicy).toBe(fetchPolicies.NO_CACHE);
 | 
				
			||||||
        fetchPolicy: fetchPolicies.NO_CACHE,
 | 
					      expect(apollo.ciConfigVariables.query).toBe(ciConfigVariablesQuery);
 | 
				
			||||||
        query: ciConfigVariablesQuery,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('makes query with correct variables', async () => {
 | 
					    it('makes query with correct variables', async () => {
 | 
				
			||||||
| 
						 | 
					@ -86,10 +153,73 @@ describe('PipelineVariablesForm', () => {
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('reports to sentry when query fails', async () => {
 | 
					    it('reports to sentry when query fails', async () => {
 | 
				
			||||||
      mockCiConfigVariables = jest.fn().mockRejectedValue(new Error('GraphQL error'));
 | 
					      const error = new Error('GraphQL error');
 | 
				
			||||||
 | 
					 | 
				
			||||||
      await createComponent();
 | 
					      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 { 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';
 | 
					import projectSettingRow from '~/pages/projects/shared/permissions/components/project_setting_row.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe('Project Setting Row', () => {
 | 
					describe('Project Setting Row', () => {
 | 
				
			||||||
  let wrapper;
 | 
					  let wrapper;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const findLabel = () => wrapper.findByTestId('project-settings-row-label');
 | 
				
			||||||
 | 
					  const findHelpText = () => wrapper.findByTestId('project-settings-row-help-text');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const createComponent = (customProps = {}) => {
 | 
					  const createComponent = (customProps = {}) => {
 | 
				
			||||||
    const propsData = { ...customProps };
 | 
					    return shallowMountExtended(projectSettingRow, {
 | 
				
			||||||
    return shallowMount(projectSettingRow, { propsData });
 | 
					      propsData: {
 | 
				
			||||||
 | 
					        ...customProps,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      stubs: { GlFormGroup },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeEach(() => {
 | 
					  beforeEach(() => {
 | 
				
			||||||
| 
						 | 
					@ -19,25 +26,26 @@ describe('Project Setting Row', () => {
 | 
				
			||||||
    wrapper = createComponent({ label: 'Test label' });
 | 
					    wrapper = createComponent({ label: 'Test label' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await nextTick();
 | 
					    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', () => {
 | 
					  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 () => {
 | 
					  it('should apply gl-text-disabled class to label when locked', async () => {
 | 
				
			||||||
    wrapper = createComponent({ label: 'Test label', locked: true });
 | 
					    wrapper = createComponent({ label: 'Test label', locked: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await nextTick();
 | 
					    await nextTick();
 | 
				
			||||||
    expect(wrapper.find('label').classes()).toContain('gl-text-disabled');
 | 
					    expect(findLabel().classes()).toContain('gl-text-disabled');
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should render default slot content', () => {
 | 
					  it('should render default slot content', () => {
 | 
				
			||||||
    wrapper = shallowMount(projectSettingRow, {
 | 
					    wrapper = shallowMountExtended(projectSettingRow, {
 | 
				
			||||||
      slots: {
 | 
					      slots: {
 | 
				
			||||||
        'label-icon': GlIcon,
 | 
					        'label-icon': GlIcon,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      stubs: { GlFormGroup },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    expect(wrapper.findComponent(GlIcon).exists()).toBe(true);
 | 
					    expect(wrapper.findComponent(GlIcon).exists()).toBe(true);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
| 
						 | 
					@ -63,10 +71,10 @@ describe('Project Setting Row', () => {
 | 
				
			||||||
    wrapper = createComponent({ helpText: 'Test text' });
 | 
					    wrapper = createComponent({ helpText: 'Test text' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await nextTick();
 | 
					    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', () => {
 | 
					  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',
 | 
					        helpPath: '/help/user/ai_features',
 | 
				
			||||||
        helpText: 'Use AI-powered features in this project.',
 | 
					        helpText: 'Use AI-powered features in this project.',
 | 
				
			||||||
        label: 'GitLab Duo',
 | 
					        label: 'GitLab Duo',
 | 
				
			||||||
 | 
					        labelFor: null,
 | 
				
			||||||
        locked: false,
 | 
					        locked: false,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
| 
						 | 
					@ -985,6 +986,7 @@ describe('Settings Panel', () => {
 | 
				
			||||||
        helpPath: '/help/user/duo_amazon_q/_index.md',
 | 
					        helpPath: '/help/user/duo_amazon_q/_index.md',
 | 
				
			||||||
        helpText: 'This project can use Amazon Q.',
 | 
					        helpText: 'This project can use Amazon Q.',
 | 
				
			||||||
        label: 'Amazon Q',
 | 
					        label: 'Amazon Q',
 | 
				
			||||||
 | 
					        labelFor: null,
 | 
				
			||||||
        locked: false,
 | 
					        locked: false,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,7 @@ RSpec.describe Mutations::ContainerRepositories::DestroyTags, feature_category:
 | 
				
			||||||
  let(:id) { repository.to_global_id }
 | 
					  let(:id) { repository.to_global_id }
 | 
				
			||||||
  let(:current_user) { create(:user) }
 | 
					  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
 | 
					  describe '#resolve' do
 | 
				
			||||||
    let(:tags) { %w[A C D E] }
 | 
					    let(:tags) { %w[A C D E] }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require 'spec_helper'
 | 
					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) }
 | 
					  let_it_be(:lfs_file_lock, reload: true) { create(:lfs_file_lock) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  subject { lfs_file_lock }
 | 
					  subject { lfs_file_lock }
 | 
				
			||||||
| 
						 | 
					@ -57,4 +57,18 @@ RSpec.describe LfsFileLock do
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    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
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9036,6 +9036,10 @@ RSpec.describe User, feature_category: :user_profile do
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    subject(:validate) { new_user.validate }
 | 
					    subject(:validate) { new_user.validate }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    before do
 | 
				
			||||||
 | 
					      stub_application_setting(enforce_email_subaddress_restrictions: true)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    shared_examples 'adds a validation error' do |reason|
 | 
					    shared_examples 'adds a validation error' do |reason|
 | 
				
			||||||
      specify do
 | 
					      specify do
 | 
				
			||||||
        expect(::Gitlab::AppLogger).to receive(:info).with(
 | 
					        expect(::Gitlab::AppLogger).to receive(:info).with(
 | 
				
			||||||
| 
						 | 
					@ -9090,9 +9094,9 @@ RSpec.describe User, feature_category: :user_profile do
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      context 'when the feature flag is disabled' do
 | 
					      context 'when the enforce_email_subaddress_restrictions application setting is disabled' do
 | 
				
			||||||
        before do
 | 
					        before do
 | 
				
			||||||
          stub_feature_flags(limit_normalized_email_reuse: false)
 | 
					          stub_application_setting(enforce_email_subaddress_restrictions: false)
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        it 'does not perform the check' do
 | 
					        it 'does not perform the check' do
 | 
				
			||||||
| 
						 | 
					@ -9158,16 +9162,14 @@ RSpec.describe User, feature_category: :user_profile do
 | 
				
			||||||
          end
 | 
					          end
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        context 'when feature flag is disabled' do
 | 
					        context 'when enforce_email_subaddress_restrictions application setting is disabled' do
 | 
				
			||||||
          before do
 | 
					          before do
 | 
				
			||||||
            stub_feature_flags(block_banned_user_normalized_email_reuse: false)
 | 
					            stub_application_setting(enforce_email_subaddress_restrictions: false)
 | 
				
			||||||
          end
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          it 'does not perform the check' do
 | 
					          it 'does not perform the check' do
 | 
				
			||||||
            expect(::Users::BannedUser).not_to receive(:by_detumbled_email)
 | 
					            expect(::Users::BannedUser).not_to receive(:by_detumbled_email)
 | 
				
			||||||
          end
 | 
					          end
 | 
				
			||||||
 | 
					 | 
				
			||||||
          it_behaves_like 'checking normalized email reuse limit'
 | 
					 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,7 @@ RSpec.describe ContainerRegistry::TagPolicy, feature_category: :container_regist
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  subject { described_class.new(user, tag) }
 | 
					  subject { described_class.new(user, tag) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe 'destroy_container_image' do
 | 
					  describe 'destroy_container_image_tag' do
 | 
				
			||||||
    using RSpec::Parameterized::TableSyntax
 | 
					    using RSpec::Parameterized::TableSyntax
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    shared_examples 'matching expected result with protection rules' do
 | 
					    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)
 | 
					        allow(tag).to receive(:protection_rule).and_return(protection_rule)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      it { is_expected.to send(expected_result, :destroy_container_image) }
 | 
					      it { is_expected.to send(expected_result, :destroy_container_image_tag) }
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    context 'for admin', :enable_admin_mode do
 | 
					    context 'for admin', :enable_admin_mode do
 | 
				
			||||||
      let(:user) { build_stubbed(:admin) }
 | 
					      let(:user) { build_stubbed(:admin) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      it { expect_allowed(:destroy_container_image) }
 | 
					      it { expect_allowed(:destroy_container_image_tag) }
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    context 'for owner' do
 | 
					    context 'for owner' do
 | 
				
			||||||
| 
						 | 
					@ -38,7 +38,7 @@ RSpec.describe ContainerRegistry::TagPolicy, feature_category: :container_regist
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      context 'when tag has no protection rule' do
 | 
					      context 'when tag has no protection rule' do
 | 
				
			||||||
        it { expect_allowed(:destroy_container_image) }
 | 
					        it { expect_allowed(:destroy_container_image_tag) }
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      context 'when tag has protection rule' do
 | 
					      context 'when tag has protection rule' do
 | 
				
			||||||
| 
						 | 
					@ -60,7 +60,7 @@ RSpec.describe ContainerRegistry::TagPolicy, feature_category: :container_regist
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      context 'when tag has no protection rule' do
 | 
					      context 'when tag has no protection rule' do
 | 
				
			||||||
        it { expect_allowed(:destroy_container_image) }
 | 
					        it { expect_allowed(:destroy_container_image_tag) }
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      context 'when tag has protection rule' do
 | 
					      context 'when tag has protection rule' do
 | 
				
			||||||
| 
						 | 
					@ -82,7 +82,7 @@ RSpec.describe ContainerRegistry::TagPolicy, feature_category: :container_regist
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      context 'when tag has no protection rule' do
 | 
					      context 'when tag has no protection rule' do
 | 
				
			||||||
        it { expect_allowed(:destroy_container_image) }
 | 
					        it { expect_allowed(:destroy_container_image_tag) }
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      context 'when tag has protection rule' do
 | 
					      context 'when tag has protection rule' do
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2977,7 +2977,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let(:developer_operations_permissions) do
 | 
					    let(:developer_operations_permissions) do
 | 
				
			||||||
      guest_operations_permissions + [
 | 
					      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
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,6 +41,10 @@ RSpec.describe RegistrationsController, :with_current_organization, type: :reque
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let!(:banned_user) { create(:user, :banned, email: normalized_email) }
 | 
					        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
 | 
					        it 'renders new action with correct error message', :aggregate_failures do
 | 
				
			||||||
          request
 | 
					          request
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -48,9 +52,9 @@ RSpec.describe RegistrationsController, :with_current_organization, type: :reque
 | 
				
			||||||
          expect(response).to render_template(:new)
 | 
					          expect(response).to render_template(:new)
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        context 'when feature flag is disabled' do
 | 
					        context 'when enforce_email_subaddress_restrictions application setting is disabled' do
 | 
				
			||||||
          before do
 | 
					          before do
 | 
				
			||||||
            stub_feature_flags(block_banned_user_normalized_email_reuse: false)
 | 
					            stub_application_setting(enforce_email_subaddress_restrictions: false)
 | 
				
			||||||
          end
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          it 'does not re-render the form' do
 | 
					          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_commit_status create_container_image create_deployment
 | 
				
			||||||
      create_environment create_merge_request_from
 | 
					      create_environment create_merge_request_from
 | 
				
			||||||
      create_pipeline create_release
 | 
					      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
 | 
					      read_terraform_state resolve_note update_build cancel_build update_commit_status
 | 
				
			||||||
      update_container_image update_deployment update_environment
 | 
					      update_container_image update_deployment update_environment
 | 
				
			||||||
      update_merge_request update_pipeline update_release destroy_release
 | 
					      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
 | 
					  # The banned user cannot be instantiated as banned because validators prevent users from
 | 
				
			||||||
  # being created that have similar characteristics of previously banned users.
 | 
					  # being created that have similar characteristics of previously banned users.
 | 
				
			||||||
  before do
 | 
					  before do
 | 
				
			||||||
 | 
					    stub_application_setting(enforce_email_subaddress_restrictions: true)
 | 
				
			||||||
    banned_user.ban!
 | 
					    banned_user.ban!
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,9 +45,9 @@ RSpec.describe AntiAbuse::BanDuplicateUsersWorker, feature_category: :instance_r
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      it_behaves_like 'executing the ban duplicate users worker'
 | 
					      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
 | 
					        before do
 | 
				
			||||||
          stub_feature_flags(auto_ban_via_detumbled_email: false)
 | 
					          stub_application_setting(enforce_email_subaddress_restrictions: false)
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        it_behaves_like 'does not ban the duplicate user'
 | 
					        it_behaves_like 'does not ban the duplicate user'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue