Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									307115302b
								
							
						
					
					
						commit
						54a2245581
					
				|  | @ -3,8 +3,8 @@ import { s__ } from '~/locale'; | |||
| import { createAlert } from '~/alert'; | ||||
| import { reportToSentry } from '~/ci/utils'; | ||||
| import CrudComponent from '~/vue_shared/components/crud_component.vue'; | ||||
| import InputsTableSkeletonLoader from './inputs_table_skeleton_loader.vue'; | ||||
| import PipelineInputsTable from './pipeline_inputs_table.vue'; | ||||
| import InputsTableSkeletonLoader from './pipeline_inputs_table/inputs_table_skeleton_loader.vue'; | ||||
| import PipelineInputsTable from './pipeline_inputs_table/pipeline_inputs_table.vue'; | ||||
| import getPipelineInputsQuery from './graphql/queries/pipeline_creation_inputs.query.graphql'; | ||||
| 
 | ||||
| export default { | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <script> | ||||
| import { GlCollapsibleListbox, GlFormInput, GlTooltipDirective } from '@gitlab/ui'; | ||||
| import { GlCollapsibleListbox, GlFormInput, GlFormTextarea } from '@gitlab/ui'; | ||||
| import { __ } from '~/locale'; | ||||
| import validation, { initForm } from '~/vue_shared/directives/validation'; | ||||
| 
 | ||||
|  | @ -9,7 +9,7 @@ import validation, { initForm } from '~/vue_shared/directives/validation'; | |||
|  * This component dynamically renders the appropriate input field based on the data type. | ||||
|  * It supports multiple input types: | ||||
|  * - Boolean: Rendered as a dropdown with true/false options | ||||
|  * - Array: Rendered as a text input field unless options are included | ||||
|  * - Array: Rendered as a text area input field unless options are included | ||||
|  * - Number: Rendered as a number input field unless options are included | ||||
|  * - String/Others: Rendered as a text input field unless options are included | ||||
|  * | ||||
|  | @ -25,9 +25,18 @@ const INPUT_TYPES = { | |||
| }; | ||||
| 
 | ||||
| const feedbackMap = { | ||||
|   valueMissing: { | ||||
|     isInvalid: (el) => el.validity?.valueMissing, | ||||
|     message: __('This is mandatory and must be defined.'), | ||||
|   arrayFormatMismatch: { | ||||
|     isInvalid: (el) => { | ||||
|       if (el.dataset.jsonArray !== 'true' || !el.value) return false; | ||||
| 
 | ||||
|       try { | ||||
|         const parsed = JSON.parse(el.value); | ||||
|         return !Array.isArray(parsed); | ||||
|       } catch { | ||||
|         return true; | ||||
|       } | ||||
|     }, | ||||
|     message: __('The value must be a valid JSON array format: [1,2,3] or [{"key": "value"}]'), | ||||
|   }, | ||||
|   numberTypeMismatch: { | ||||
|     isInvalid: (el) => { | ||||
|  | @ -43,6 +52,10 @@ const feedbackMap = { | |||
|     isInvalid: (el) => el.validity?.patternMismatch, | ||||
|     message: __('The value must match the defined regular expression.'), | ||||
|   }, | ||||
|   valueMissing: { | ||||
|     isInvalid: (el) => el.validity?.valueMissing, | ||||
|     message: __('This is required and must be defined.'), | ||||
|   }, | ||||
| }; | ||||
| 
 | ||||
| export default { | ||||
|  | @ -50,9 +63,9 @@ export default { | |||
|   components: { | ||||
|     GlCollapsibleListbox, | ||||
|     GlFormInput, | ||||
|     GlFormTextarea, | ||||
|   }, | ||||
|   directives: { | ||||
|     GlTooltip: GlTooltipDirective, | ||||
|     validation: validation(feedbackMap), | ||||
|   }, | ||||
|   props: { | ||||
|  | @ -99,27 +112,39 @@ export default { | |||
|       } | ||||
|       return this.item.options?.map((option) => ({ value: option, text: option })) || []; | ||||
|     }, | ||||
|     hasArrayFormatError() { | ||||
|       const field = this.form.fields[this.item.name]; | ||||
|       return this.isArrayType && field?.feedback === feedbackMap.arrayFormatMismatch.message; | ||||
|     }, | ||||
|     hasValidationFeedback() { | ||||
|       return Boolean(this.validationFeedback); | ||||
|     }, | ||||
|     headerText() { | ||||
|       return this.item.type === INPUT_TYPES.BOOLEAN ? __('Value') : __('Options'); | ||||
|     }, | ||||
|     invalidTooltipTitle() { | ||||
|       const field = this.form.fields[this.item.name]; | ||||
| 
 | ||||
|       if (this.item.regex && field?.feedback === feedbackMap.regexMismatch.message) { | ||||
|         return `${__('Pattern')}: ${this.item.regex}`; | ||||
|       } | ||||
| 
 | ||||
|       return ''; | ||||
|     isArrayType() { | ||||
|       return this.item.type === INPUT_TYPES.ARRAY && Boolean(!this.item.options?.length); | ||||
|     }, | ||||
|     isDropdown() { | ||||
|       return this.item.type === INPUT_TYPES.BOOLEAN || Boolean(this.item.options?.length); | ||||
|     }, | ||||
|     validationFeedback() { | ||||
|       const field = this.form.fields[this.item.name]; | ||||
|       return field?.feedback || ''; | ||||
|       const feedback = field?.feedback || ''; | ||||
| 
 | ||||
|       return feedback === feedbackMap.regexMismatch.message && this.item.regex | ||||
|         ? `${feedback} ${__('Pattern')}: ${this.item.regex}` | ||||
|         : feedback; | ||||
|     }, | ||||
|     validationState() { | ||||
|       // Override validation state for array format errors | ||||
|       // This handles cases where checkValidity() returns true but our custom array validation fails | ||||
|       if (this.hasArrayFormatError) { | ||||
|         return false; | ||||
|       } | ||||
| 
 | ||||
|       const field = this.form.fields[this.item.name]; | ||||
|       return field?.state; | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|  | @ -153,6 +178,7 @@ export default { | |||
| 
 | ||||
| <template> | ||||
|   <div> | ||||
|     <!-- Dropdown for booleans or any type with options --> | ||||
|     <gl-collapsible-listbox | ||||
|       v-if="isDropdown" | ||||
|       v-model="inputValue" | ||||
|  | @ -161,8 +187,23 @@ export default { | |||
|       :header-text="headerText" | ||||
|       :items="dropdownOptions" | ||||
|     /> | ||||
|     <template v-else> | ||||
| 
 | ||||
|     <!-- Textarea for arrays without options --> | ||||
|     <gl-form-textarea | ||||
|       v-else-if="isArrayType" | ||||
|       v-model="inputValue" | ||||
|       v-validation:[form.showValidation] | ||||
|       :aria-label="item.name" | ||||
|       :data-json-array="'true'" | ||||
|       :name="item.name" | ||||
|       :required="item.required" | ||||
|       :state="validationState" | ||||
|       rows="3" | ||||
|     /> | ||||
| 
 | ||||
|     <!-- Regular input for strings and numbers without options --> | ||||
|     <gl-form-input | ||||
|       v-else | ||||
|       v-model="inputValue" | ||||
|       v-validation:[form.showValidation] | ||||
|       :aria-label="item.name" | ||||
|  | @ -170,17 +211,16 @@ export default { | |||
|       :name="item.name" | ||||
|       :pattern="item.regex" | ||||
|       :required="item.required" | ||||
|         :state="form.fields[item.name].state" | ||||
|         data-testid="value-input" | ||||
|       :state="validationState" | ||||
|     /> | ||||
| 
 | ||||
|     <!-- Validation feedback --> | ||||
|     <div | ||||
|       v-if="hasValidationFeedback" | ||||
|         v-gl-tooltip="invalidTooltipTitle" | ||||
|       class="gl-mt-4 gl-text-danger" | ||||
|       data-testid="validation-feedback" | ||||
|     > | ||||
|       {{ validationFeedback }} | ||||
|     </div> | ||||
|     </template> | ||||
|   </div> | ||||
| </template> | ||||
|  | @ -1,6 +1,6 @@ | |||
| <script> | ||||
| import { GlSkeletonLoader } from '@gitlab/ui'; | ||||
| import { getSkeletonRectProps } from './utils'; | ||||
| import { getSkeletonRectProps } from '../utils'; | ||||
| 
 | ||||
| export default { | ||||
|   name: 'InputsTableSkeletonLoader', | ||||
|  | @ -72,6 +72,7 @@ function parseDatasetToProps(data) { | |||
|     shouldUpgradeSlack, | ||||
|     customizeJiraIssueEnabled, | ||||
|     amazonQReady, | ||||
|     amazonQAutoReviewEnabled, | ||||
|   } = parseBooleanInData(booleanAttributes); | ||||
| 
 | ||||
|   return { | ||||
|  | @ -119,6 +120,7 @@ function parseDatasetToProps(data) { | |||
|       amazonQInstanceUid, | ||||
|       amazonQAwsProviderUrl, | ||||
|       amazonQAwsAudience, | ||||
|       amazonQAutoReviewEnabled, | ||||
|     }, | ||||
|     learnMorePath, | ||||
|     aboutPricingUrl, | ||||
|  |  | |||
|  | @ -63,8 +63,8 @@ export default { | |||
|   }, | ||||
|   computed: { | ||||
|     wrapperClasses() { | ||||
|       if (this.status === STATUS_MERGED) return 'gl-bg-blue-50'; | ||||
|       if (this.status === STATUS_CLOSED) return 'gl-bg-red-50'; | ||||
|       if (this.status === STATUS_MERGED) return 'gl-bg-feedback-info'; | ||||
|       if (this.status === STATUS_CLOSED) return 'gl-bg-feedback-danger'; | ||||
|       return null; | ||||
|     }, | ||||
|     hasActionsSlot() { | ||||
|  |  | |||
|  | @ -0,0 +1,112 @@ | |||
| <script> | ||||
| import { GlIcon, GlLabel } from '@gitlab/ui'; | ||||
| import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; | ||||
| import IssueMilestone from '~/issuable/components/issue_milestone.vue'; | ||||
| import { isScopedLabel } from '~/lib/utils/common_utils'; | ||||
| 
 | ||||
| export default { | ||||
|   components: { | ||||
|     GlIcon, | ||||
|     GlLabel, | ||||
|     WorkItemTypeIcon, | ||||
|     IssueMilestone, | ||||
|     IssueWeight: () => import('ee_component/issues/components/issue_weight.vue'), | ||||
|     IssueHealthStatus: () => | ||||
|       import('ee_component/related_items_tree/components/issue_health_status.vue'), | ||||
|   }, | ||||
|   props: { | ||||
|     item: { | ||||
|       type: Object, | ||||
|       required: true, | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     itemReferencePath() { | ||||
|       const { reference } = this.item; | ||||
|       return reference.split('#')[0]; | ||||
|     }, | ||||
|     directNamespaceReference() { | ||||
|       return this.itemReferencePath.split('/').slice(-1)[0]; | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     scopedLabel(label) { | ||||
|       return isScopedLabel(label); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| <template> | ||||
|   <li | ||||
|     class="board-card gl-border gl-relative gl-mb-3 gl-rounded-base gl-border-section gl-bg-section gl-leading-normal hover:gl-bg-subtle dark:hover:gl-bg-gray-200" | ||||
|   > | ||||
|     <button | ||||
|       class="board-card-button gl-block gl-h-full gl-w-full gl-rounded-base gl-border-0 gl-bg-transparent gl-p-4 gl-text-left gl-outline-none focus:gl-focus" | ||||
|     > | ||||
|       <div class="gl-flex" dir="auto"> | ||||
|         <h4 | ||||
|           class="board-card-title gl-mb-0 gl-mt-0 gl-min-w-0 gl-hyphens-auto gl-break-words gl-text-base" | ||||
|         > | ||||
|           <gl-icon | ||||
|             v-if="item.confidential" | ||||
|             name="eye-slash" | ||||
|             data-testid="confidential-icon" | ||||
|             :title="__('Confidential')" | ||||
|             class="gl-mr-2 gl-cursor-help" | ||||
|             :aria-label="__('Confidential')" | ||||
|             variant="warning" | ||||
|           /> | ||||
|           <a | ||||
|             :href="item.path || item.webUrl || ''" | ||||
|             :title="item.title" | ||||
|             class="gl-text-default hover:gl-text-default" | ||||
|             data-testid="board-card-title-link" | ||||
|             @mousemove.stop | ||||
|             >{{ item.title }}</a | ||||
|           > | ||||
|         </h4> | ||||
|       </div> | ||||
|       <div v-if="item.labels.length > 0" class="board-card-labels gl-mt-2 gl-flex gl-flex-wrap"> | ||||
|         <gl-label | ||||
|           v-for="label in item.labels" | ||||
|           :key="label.id" | ||||
|           class="js-no-trigger gl-mr-2 gl-mt-2" | ||||
|           :background-color="label.color" | ||||
|           :title="label.title" | ||||
|           :description="label.description" | ||||
|           target="#" | ||||
|           :scoped="scopedLabel(label)" | ||||
|         /> | ||||
|       </div> | ||||
|       <div class="board-card-footer gl-mt-3 gl-flex gl-items-end gl-justify-between"> | ||||
|         <div | ||||
|           class="align-items-start board-card-number-container gl-flex gl-flex-wrap-reverse gl-overflow-hidden" | ||||
|         > | ||||
|           <span class="board-info-items gl-inline-block gl-leading-20"> | ||||
|             <span | ||||
|               class="board-card-number gl-mr-3 gl-mt-3 gl-gap-2 gl-overflow-hidden gl-text-sm gl-text-subtle" | ||||
|             > | ||||
|               <work-item-type-icon :work-item-type="item.type.name" show-tooltip-on-hover /> | ||||
|               <span | ||||
|                 :title="itemReferencePath" | ||||
|                 data-placement="bottom" | ||||
|                 class="board-item-path gl-cursor-help gl-truncate gl-font-bold" | ||||
|               > | ||||
|                 {{ directNamespaceReference }} | ||||
|               </span> | ||||
|               #{{ item.iid }} | ||||
|             </span> | ||||
|             <issue-weight v-if="item.weight !== undefined" :weight="item.weight.value" /> | ||||
|             <issue-milestone | ||||
|               v-if="item.milestone" | ||||
|               data-testid="issue-milestone" | ||||
|               :milestone="item.milestone" | ||||
|               class="gl-mr-3 gl-inline-flex gl-max-w-15 gl-cursor-help gl-items-center gl-align-bottom gl-text-sm gl-text-subtle" | ||||
|             /> | ||||
|             <issue-health-status v-if="item.healthStatus" :health-status="item.healthStatus" /> | ||||
|           </span> | ||||
|         </div> | ||||
|       </div> | ||||
|     </button> | ||||
|   </li> | ||||
| </template> | ||||
|  | @ -6,13 +6,15 @@ import { | |||
|   getGroupOptions, | ||||
|   getPoolNameForGrouping, | ||||
|   groupBy, | ||||
| } from 'ee_else_ce/work_items/pages/object_pools'; | ||||
| } from 'ee_else_ce/work_items/pages/local_board/object_pools'; | ||||
| import BoardCard from './board_card.vue'; | ||||
| 
 | ||||
| export default { | ||||
|   name: 'LocalBoard', | ||||
|   components: { | ||||
|     GlButton, | ||||
|     GlFormSelect, | ||||
|     BoardCard, | ||||
|   }, | ||||
|   props: { | ||||
|     workItemListData: { | ||||
|  | @ -77,21 +79,44 @@ export default { | |||
|         {{ s__('WorkItem|Back') }} | ||||
|       </gl-button> | ||||
|     </div> | ||||
|     <div class="gl-mt-6 gl-flex gl-w-full gl-flex-nowrap gl-overflow-x-scroll"> | ||||
|       <div v-for="(group, index) in groups" :key="index" class="gl-mx-4 gl-w-1/5"> | ||||
|         <p>{{ group.title }}</p> | ||||
|         <ul class="gl-list-none gl-p-0"> | ||||
|     <div class="gl-flex gl-h-[calc(100vh-60px-48px)] gl-min-h-0 gl-grow gl-flex-col"> | ||||
|       <div | ||||
|         class="gl-min-h-0 gl-w-full gl-grow gl-overflow-x-auto gl-whitespace-nowrap gl-py-5 gl-pl-0 gl-pr-5 xl:gl-pl-3 xl:gl-pr-6" | ||||
|       > | ||||
|         <div | ||||
|           v-for="(group, index) in groups" | ||||
|           :key="index" | ||||
|           class="gl-inline-flex gl-h-full gl-align-top" | ||||
|         > | ||||
|           <div | ||||
|             class="gl-relative gl-inline-block gl-h-full gl-w-[400px] gl-whitespace-normal gl-px-3 gl-align-top" | ||||
|           > | ||||
|             <div | ||||
|               class="gl-relative gl-flex gl-h-full gl-flex-col gl-rounded-base gl-bg-strong dark:gl-bg-subtle" | ||||
|             > | ||||
|               <header class="gl-relative"> | ||||
|                 <h3 class="gl-m-0 gl-flex gl-h-9 gl-items-center gl-px-3 gl-text-base"> | ||||
|                   <span class="gl-grow gl-truncate gl-p-1"> | ||||
|                     {{ group.title }} | ||||
|                   </span> | ||||
|                 </h3> | ||||
|               </header> | ||||
|               <div class="gl-relative gl-flex gl-h-full gl-min-h-0 gl-flex-col"> | ||||
|                 <ul | ||||
|                   class="gl-mb-0 gl-h-full gl-w-full gl-list-none gl-overflow-x-hidden gl-p-3 gl-pt-2" | ||||
|                 > | ||||
|                   <li v-if="group.items.length === 0" class="p-2 gl-rounded gl-bg-strong gl-p-0"> | ||||
|                     <p> | ||||
|                       {{ s__('WorkItem|No items') }} | ||||
|                     </p> | ||||
|                   </li> | ||||
|           <li v-for="id in group.items" :key="id" class="p-2 gl-rounded my-2 gl-bg-strong gl-p-0"> | ||||
|             <a :href="items[id].webUrl">{{ items[id].reference }}</a> | ||||
|             <p>{{ items[id].title }}</p> | ||||
|           </li> | ||||
|                   <board-card v-for="id in group.items" :key="id" :item="items[id]" /> | ||||
|                 </ul> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | @ -35,6 +35,7 @@ export const getReactions = (widgets) => { | |||
| const transformItem = (input) => { | ||||
|   return { | ||||
|     id: input.id, | ||||
|     iid: input.iid, | ||||
|     title: input.title, | ||||
|     state: input.state, | ||||
|     type: input.workItemType, | ||||
|  | @ -98,7 +98,7 @@ const LabelToken = () => | |||
| const MilestoneToken = () => | ||||
|   import('~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'); | ||||
| const UserToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/user_token.vue'); | ||||
| const LocalBoard = () => import('./local_board.vue'); | ||||
| const LocalBoard = () => import('./local_board/local_board.vue'); | ||||
| 
 | ||||
| const statusMap = { | ||||
|   [STATUS_OPEN]: STATE_OPEN, | ||||
|  |  | |||
|  | @ -12,10 +12,6 @@ module Mutations | |||
|         required: true, | ||||
|         description: 'Global IDs of the to-do items to process (a maximum of 100 is supported at once).' | ||||
| 
 | ||||
|       field :todos, [::Types::TodoType], | ||||
|         null: false, | ||||
|         description: 'Updated to-do items.' | ||||
| 
 | ||||
|       def resolve(ids:, **kwargs) | ||||
|         check_update_limit!(amount: ids.size) | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,38 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Mutations | ||||
|   module Todos | ||||
|     class DeleteMany < BaseMany | ||||
|       graphql_name 'TodoDeleteMany' | ||||
| 
 | ||||
|       def resolve(ids:) | ||||
|         verify_rate_limit! | ||||
|         check_update_limit!(amount: ids.size) | ||||
| 
 | ||||
|         todos = authorized_find_all_pending_by_current_user(model_ids_of(ids)) | ||||
|         deleted_ids = process_todos(todos) | ||||
| 
 | ||||
|         { | ||||
|           deleted_ids: deleted_ids, | ||||
|           errors: errors_on_objects(todos) | ||||
|         } | ||||
|       end | ||||
| 
 | ||||
|       private | ||||
| 
 | ||||
|       def process_todos(todos) | ||||
|         ::Todos::Delete::DoneTodosService.new.execute(todos) | ||||
|       end | ||||
| 
 | ||||
|       def todo_state_to_find | ||||
|         :done | ||||
|       end | ||||
| 
 | ||||
|       def verify_rate_limit! | ||||
|         return unless Gitlab::ApplicationRateLimiter.throttled?(:bulk_delete_todos, scope: [current_user]) | ||||
| 
 | ||||
|         raise_resource_not_available_error!('This endpoint has been requested too many times. Try again later.') | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -5,6 +5,10 @@ module Mutations | |||
|     class ResolveMany < BaseMany | ||||
|       graphql_name 'TodoResolveMany' | ||||
| 
 | ||||
|       field :todos, [::Types::TodoType], | ||||
|         null: false, | ||||
|         description: 'Resolved to-do items.' | ||||
| 
 | ||||
|       private | ||||
| 
 | ||||
|       def process_todos(todos) | ||||
|  |  | |||
|  | @ -5,6 +5,10 @@ module Mutations | |||
|     class RestoreMany < BaseMany | ||||
|       graphql_name 'TodoRestoreMany' | ||||
| 
 | ||||
|       field :todos, [::Types::TodoType], | ||||
|         null: false, | ||||
|         description: 'Restored to-do items.' | ||||
| 
 | ||||
|       private | ||||
| 
 | ||||
|       def process_todos(todos) | ||||
|  |  | |||
|  | @ -10,6 +10,10 @@ module Mutations | |||
|         required: true, | ||||
|         description: 'Time until which the todos should be snoozed.' | ||||
| 
 | ||||
|       field :todos, [::Types::TodoType], | ||||
|         null: false, | ||||
|         description: 'Snoozed to-do items.' | ||||
| 
 | ||||
|       private | ||||
| 
 | ||||
|       def process_todos(todos, snooze_until:) | ||||
|  |  | |||
|  | @ -5,6 +5,10 @@ module Mutations | |||
|     class UnsnoozeMany < BaseMany | ||||
|       graphql_name 'TodoUnsnoozeMany' | ||||
| 
 | ||||
|       field :todos, [::Types::TodoType], | ||||
|         null: false, | ||||
|         description: 'Unsnoozed to-do items.' | ||||
| 
 | ||||
|       private | ||||
| 
 | ||||
|       def process_todos(todos) | ||||
|  |  | |||
|  | @ -152,6 +152,7 @@ module Types | |||
|     mount_mutation Mutations::Todos::UnSnooze, experiment: { milestone: '17.4' } | ||||
|     mount_mutation Mutations::Todos::SnoozeMany, experiment: { milestone: '17.9' } | ||||
|     mount_mutation Mutations::Todos::UnsnoozeMany, experiment: { milestone: '17.9' } | ||||
|     mount_mutation Mutations::Todos::DeleteMany, experiment: { milestone: '17.11' } | ||||
|     mount_mutation Mutations::Snippets::Destroy | ||||
|     mount_mutation Mutations::Snippets::Update | ||||
|     mount_mutation Mutations::Snippets::Create | ||||
|  |  | |||
|  | @ -15,6 +15,8 @@ module Packages | |||
| 
 | ||||
|       attribute :fields, default: -> { {} } | ||||
| 
 | ||||
|       ignore_column :semver_patch_convert_to_bigint, remove_with: '18.1', remove_after: '2025-05-20' | ||||
| 
 | ||||
|       validates :package, :project, presence: true | ||||
|       validates :fields, json_schema: { filename: 'terraform_module_metadata', detail_errors: true }, | ||||
|         if: -> { fields.present? } | ||||
|  |  | |||
|  | @ -71,6 +71,7 @@ class IssuePolicy < IssuablePolicy | |||
| 
 | ||||
|   rule { group_issue & can?(:read_group) }.policy do | ||||
|     enable :create_note | ||||
|     enable :award_emoji | ||||
|   end | ||||
| 
 | ||||
|   rule { ~notes_widget_enabled }.policy do | ||||
|  |  | |||
|  | @ -317,6 +317,7 @@ module Projects | |||
| 
 | ||||
|     def remove_registry_tags | ||||
|       return true unless Gitlab.config.registry.enabled | ||||
|       return false if protected_by_tag_protection_rules? | ||||
|       return false unless remove_legacy_registry_tags | ||||
| 
 | ||||
|       results = [] | ||||
|  | @ -327,6 +328,20 @@ module Projects | |||
|       results.all? | ||||
|     end | ||||
| 
 | ||||
|     def protected_by_tag_protection_rules? | ||||
|       return false if Feature.disabled?(:container_registry_protected_tags, project) | ||||
|       return false if current_user.can_admin_all_resources? | ||||
| 
 | ||||
|       return false unless project.has_container_registry_protected_tag_rules?( | ||||
|         action: 'delete', | ||||
|         access_level: current_user.max_member_access_for_project(project.id) | ||||
|       ) | ||||
| 
 | ||||
|       return false unless project.has_container_registry_tags? | ||||
| 
 | ||||
|       true | ||||
|     end | ||||
| 
 | ||||
|     ## | ||||
|     # This method makes sure that we correctly remove registry tags | ||||
|     # for legacy image repository (when repository path equals project path). | ||||
|  |  | |||
|  | @ -0,0 +1,20 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Todos | ||||
|   module Delete | ||||
|     class DoneTodosService < ::Todos::Destroy::BaseService | ||||
|       extend ::Gitlab::Utils::Override | ||||
| 
 | ||||
|       MAX_UPDATE_AMOUNT = 100 | ||||
| 
 | ||||
|       def execute(todos) | ||||
|         todos_to_delete = todos.done.limit(MAX_UPDATE_AMOUNT) # we limit that in the service layer as well | ||||
|         ids = todos_to_delete.ids # rubocop: disable CodeReuse/ActiveRecord -- we need ids to return from mutation | ||||
| 
 | ||||
|         todos_to_delete.delete_all | ||||
| 
 | ||||
|         ids | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -1228,17 +1228,6 @@ | |||
|   :tags: | ||||
|   - :cronjob_child | ||||
|   :queue_namespace: :dependency_proxy_blob | ||||
| - :name: dependency_proxy_blob:virtual_registries_packages_destroy_orphan_cached_responses | ||||
|   :worker_name: VirtualRegistries::Packages::DestroyOrphanCachedResponsesWorker | ||||
|   :feature_category: :virtual_registry | ||||
|   :has_external_dependencies: false | ||||
|   :urgency: :low | ||||
|   :resource_boundary: :unknown | ||||
|   :weight: 1 | ||||
|   :idempotent: true | ||||
|   :tags: | ||||
|   - :cronjob_child | ||||
|   :queue_namespace: :dependency_proxy_blob | ||||
| - :name: dependency_proxy_manifest:dependency_proxy_cleanup_manifest | ||||
|   :worker_name: DependencyProxy::CleanupManifestWorker | ||||
|   :feature_category: :virtual_registry | ||||
|  |  | |||
|  | @ -1,14 +0,0 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module VirtualRegistries | ||||
|   module Packages | ||||
|     class DestroyOrphanCachedResponsesWorker < ::VirtualRegistries::Packages::Cache::DestroyOrphanEntriesWorker | ||||
|       data_consistency :sticky | ||||
|       urgency :low | ||||
|       idempotent! | ||||
| 
 | ||||
|       queue_namespace :dependency_proxy_blob | ||||
|       feature_category :virtual_registry | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -1,9 +0,0 @@ | |||
| --- | ||||
| name: virtual_registry_maven_cleanup_new_worker_class | ||||
| feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/512436 | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/177996 | ||||
| rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/513796 | ||||
| milestone: '17.9' | ||||
| group: group::package registry | ||||
| type: beta | ||||
| default_enabled: true | ||||
|  | @ -8,7 +8,9 @@ milestone: "17.0" | |||
| introduced_by_url: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/150885' | ||||
| time_frame: none | ||||
| data_source: system | ||||
| instrumentation_class: RunnerRegistrationTokensAllowedMetric | ||||
| instrumentation_class: GitlabSettingsMetric | ||||
| options: | ||||
|   setting_method: allow_runner_registration_token? | ||||
| data_category: optional | ||||
| performance_indicator_type: [] | ||||
| tiers: | ||||
|  |  | |||
|  | @ -6,4 +6,4 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/167613 | |||
| milestone: '17.5' | ||||
| queued_migration_version: 20240930123056 | ||||
| finalize_after: '2024-10-22' | ||||
| finalized_by: # version of the migration that finalized this BBM | ||||
| finalized_by: '20250320231609' | ||||
|  |  | |||
|  | @ -0,0 +1,17 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class InitConversionOfSemverPatchColumnInPkgsTerraformModuleMetadataToBigint < Gitlab::Database::Migration[2.2] | ||||
|   disable_ddl_transaction! | ||||
|   milestone '17.11' | ||||
| 
 | ||||
|   TABLE = :packages_terraform_module_metadata | ||||
|   COLUMN = :semver_patch | ||||
| 
 | ||||
|   def up | ||||
|     initialize_conversion_of_integer_to_bigint(TABLE, COLUMN) | ||||
|   end | ||||
| 
 | ||||
|   def down | ||||
|     revert_initialize_conversion_of_integer_to_bigint(TABLE, COLUMN) | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,17 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class BackfillPkgsTerraformModulesMetadataForSemverBigintConversion < Gitlab::Database::Migration[2.2] | ||||
|   restrict_gitlab_migration gitlab_schema: :gitlab_main | ||||
|   milestone '17.11' | ||||
| 
 | ||||
|   TABLE = :packages_terraform_module_metadata | ||||
|   COLUMN = :semver_patch | ||||
| 
 | ||||
|   def up | ||||
|     backfill_conversion_of_integer_to_bigint(TABLE, COLUMN, primary_key: :package_id) | ||||
|   end | ||||
| 
 | ||||
|   def down | ||||
|     revert_backfill_conversion_of_integer_to_bigint(TABLE, COLUMN, primary_key: :package_id) | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,21 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class FinalizeHkBackfillPagesDeploymentStatesProjectId < Gitlab::Database::Migration[2.2] | ||||
|   milestone '17.11' | ||||
| 
 | ||||
|   disable_ddl_transaction! | ||||
| 
 | ||||
|   restrict_gitlab_migration gitlab_schema: :gitlab_main_cell | ||||
| 
 | ||||
|   def up | ||||
|     ensure_batched_background_migration_is_finished( | ||||
|       job_class_name: 'BackfillPagesDeploymentStatesProjectId', | ||||
|       table_name: :pages_deployment_states, | ||||
|       column_name: :pages_deployment_id, | ||||
|       job_arguments: [:project_id, :pages_deployments, :project_id, :pages_deployment_id], | ||||
|       finalize: true | ||||
|     ) | ||||
|   end | ||||
| 
 | ||||
|   def down; end | ||||
| end | ||||
|  | @ -0,0 +1 @@ | |||
| ec6bfbed328bb354b47710c9a3e88b1550cf80378360f99640663a253f35d474 | ||||
|  | @ -0,0 +1 @@ | |||
| c90c2e7d28eb5a3514b71519f45fa4221a90557dbd77724652c7ade09e4de60c | ||||
|  | @ -0,0 +1 @@ | |||
| 8a1662c05b7fc79ad7769c024eba2e1996b4581b75b5f80dc490f05e6e1f01b0 | ||||
|  | @ -3934,6 +3934,20 @@ RETURN NEW; | |||
| END | ||||
| $$; | ||||
| 
 | ||||
| CREATE FUNCTION trigger_dd7cb7bd6c9e() RETURNS trigger | ||||
|     LANGUAGE plpgsql | ||||
|     AS $$ | ||||
| DECLARE | ||||
|   row_data JSONB; | ||||
| BEGIN | ||||
|   row_data := to_jsonb(NEW); | ||||
|   IF row_data ? 'semver_patch_convert_to_bigint' THEN | ||||
|     NEW."semver_patch_convert_to_bigint" := NEW."semver_patch"; | ||||
|   END IF; | ||||
|   RETURN NEW; | ||||
| END; | ||||
| $$; | ||||
| 
 | ||||
| CREATE FUNCTION trigger_de59b81d3044() RETURNS trigger | ||||
|     LANGUAGE plpgsql | ||||
|     AS $$ | ||||
|  | @ -19467,6 +19481,7 @@ CREATE TABLE packages_terraform_module_metadata ( | |||
|     semver_minor integer, | ||||
|     semver_patch integer, | ||||
|     semver_prerelease text, | ||||
|     semver_patch_convert_to_bigint bigint, | ||||
|     CONSTRAINT check_46aa6c883a CHECK ((char_length(semver_prerelease) <= 255)), | ||||
|     CONSTRAINT chk_rails_49f7b485ae CHECK ((char_length((fields)::text) <= 10485760)) | ||||
| ); | ||||
|  | @ -41147,6 +41162,8 @@ CREATE TRIGGER trigger_dbe374a57cbb BEFORE INSERT OR UPDATE ON status_page_publi | |||
| 
 | ||||
| CREATE TRIGGER trigger_dc13168b8025 BEFORE INSERT OR UPDATE ON vulnerability_flags FOR EACH ROW EXECUTE FUNCTION trigger_dc13168b8025(); | ||||
| 
 | ||||
| CREATE TRIGGER trigger_dd7cb7bd6c9e BEFORE INSERT OR UPDATE ON packages_terraform_module_metadata FOR EACH ROW EXECUTE FUNCTION trigger_dd7cb7bd6c9e(); | ||||
| 
 | ||||
| CREATE TRIGGER trigger_de59b81d3044 BEFORE INSERT OR UPDATE ON bulk_import_export_batches FOR EACH ROW EXECUTE FUNCTION trigger_de59b81d3044(); | ||||
| 
 | ||||
| CREATE TRIGGER trigger_delete_project_namespace_on_project_delete AFTER DELETE ON projects FOR EACH ROW WHEN ((old.project_namespace_id IS NOT NULL)) EXECUTE FUNCTION delete_associated_project_namespace(); | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ title: GitLab Duo add-on seat management with LDAP | |||
| {{< details >}} | ||||
| 
 | ||||
| - Tier: Premium, Ultimate | ||||
| - Offering: GitLab Self-Managed, GitLab Dedicated | ||||
| - Offering: GitLab Self-Managed | ||||
| 
 | ||||
| {{< /details >}} | ||||
| 
 | ||||
|  |  | |||
|  | @ -10796,6 +10796,29 @@ Input type: `TodoCreateInput` | |||
| | <a id="mutationtodocreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | ||||
| | <a id="mutationtodocreatetodo"></a>`todo` | [`Todo`](#todo) | To-do item created. | | ||||
| 
 | ||||
| ### `Mutation.todoDeleteMany` | ||||
| 
 | ||||
| {{< details >}} | ||||
| **Introduced** in GitLab 17.11. | ||||
| **Status**: Experiment. | ||||
| {{< /details >}} | ||||
| 
 | ||||
| Input type: `TodoDeleteManyInput` | ||||
| 
 | ||||
| #### Arguments | ||||
| 
 | ||||
| | Name | Type | Description | | ||||
| | ---- | ---- | ----------- | | ||||
| | <a id="mutationtododeletemanyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | ||||
| | <a id="mutationtododeletemanyids"></a>`ids` | [`[TodoID!]!`](#todoid) | Global IDs of the to-do items to process (a maximum of 100 is supported at once). | | ||||
| 
 | ||||
| #### Fields | ||||
| 
 | ||||
| | Name | Type | Description | | ||||
| | ---- | ---- | ----------- | | ||||
| | <a id="mutationtododeletemanyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | ||||
| | <a id="mutationtododeletemanyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | ||||
| 
 | ||||
| ### `Mutation.todoMarkDone` | ||||
| 
 | ||||
| Input type: `TodoMarkDoneInput` | ||||
|  | @ -10837,7 +10860,7 @@ Input type: `TodoResolveManyInput` | |||
| | ---- | ---- | ----------- | | ||||
| | <a id="mutationtodoresolvemanyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | ||||
| | <a id="mutationtodoresolvemanyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | ||||
| | <a id="mutationtodoresolvemanytodos"></a>`todos` | [`[Todo!]!`](#todo) | Updated to-do items. | | ||||
| | <a id="mutationtodoresolvemanytodos"></a>`todos` | [`[Todo!]!`](#todo) | Resolved to-do items. | | ||||
| 
 | ||||
| ### `Mutation.todoRestore` | ||||
| 
 | ||||
|  | @ -10875,7 +10898,7 @@ Input type: `TodoRestoreManyInput` | |||
| | ---- | ---- | ----------- | | ||||
| | <a id="mutationtodorestoremanyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | ||||
| | <a id="mutationtodorestoremanyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | ||||
| | <a id="mutationtodorestoremanytodos"></a>`todos` | [`[Todo!]!`](#todo) | Updated to-do items. | | ||||
| | <a id="mutationtodorestoremanytodos"></a>`todos` | [`[Todo!]!`](#todo) | Restored to-do items. | | ||||
| 
 | ||||
| ### `Mutation.todoSnooze` | ||||
| 
 | ||||
|  | @ -10925,7 +10948,7 @@ Input type: `TodoSnoozeManyInput` | |||
| | ---- | ---- | ----------- | | ||||
| | <a id="mutationtodosnoozemanyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | ||||
| | <a id="mutationtodosnoozemanyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | ||||
| | <a id="mutationtodosnoozemanytodos"></a>`todos` | [`[Todo!]!`](#todo) | Updated to-do items. | | ||||
| | <a id="mutationtodosnoozemanytodos"></a>`todos` | [`[Todo!]!`](#todo) | Snoozed to-do items. | | ||||
| 
 | ||||
| ### `Mutation.todoUnSnooze` | ||||
| 
 | ||||
|  | @ -10973,7 +10996,7 @@ Input type: `TodoUnsnoozeManyInput` | |||
| | ---- | ---- | ----------- | | ||||
| | <a id="mutationtodounsnoozemanyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | ||||
| | <a id="mutationtodounsnoozemanyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | ||||
| | <a id="mutationtodounsnoozemanytodos"></a>`todos` | [`[Todo!]!`](#todo) | Updated to-do items. | | ||||
| | <a id="mutationtodounsnoozemanytodos"></a>`todos` | [`[Todo!]!`](#todo) | Unsnoozed to-do items. | | ||||
| 
 | ||||
| ### `Mutation.todosMarkAllDone` | ||||
| 
 | ||||
|  |  | |||
|  | @ -39,7 +39,7 @@ Prerequisites: | |||
| | `projects_to_exclude`        | array of integers | no        | The IDs of projects to exclude from the feature.  | | ||||
| 
 | ||||
| ```shell | ||||
| curl --header PUT "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/7/security_settings?secret_push_protection_enabled=true&projects_to_exclude[]=1&projects_to_exclude[]=2" | ||||
| curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/7/security_settings?secret_push_protection_enabled=true&projects_to_exclude[]=1&projects_to_exclude[]=2" | ||||
| ``` | ||||
| 
 | ||||
| Example response: | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ title: Protected packages API | |||
| - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/151741) in GitLab 17.1 [with a flag](../administration/feature_flags.md) named `packages_protected_packages`. Disabled by default. | ||||
| - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/472655) in GitLab 17.5. | ||||
| - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/472655) in GitLab 17.6. Feature flag `packages_protected_packages` removed. | ||||
| - [Added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/180063) `minimum_access_level_for_delete` attribute in GitLab 17.11 [with a flag](../administration/feature_flags.md) named `packages_protected_packages_delete`. Disabled by default. | ||||
| 
 | ||||
| {{< /history >}} | ||||
| 
 | ||||
|  | @ -62,6 +63,7 @@ Example response: | |||
|   "project_id": 7, | ||||
|   "package_name_pattern": "@flightjs/flight-package-0", | ||||
|   "package_type": "npm", | ||||
|   "minimum_access_level_for_delete": "owner", | ||||
|   "minimum_access_level_for_push": "maintainer" | ||||
|  }, | ||||
|  { | ||||
|  | @ -69,6 +71,7 @@ Example response: | |||
|   "project_id": 7, | ||||
|   "package_name_pattern": "@flightjs/flight-package-1", | ||||
|   "package_type": "npm", | ||||
|   "minimum_access_level_for_delete": "owner", | ||||
|   "minimum_access_level_for_push": "maintainer" | ||||
|  } | ||||
| ] | ||||
|  | @ -89,7 +92,8 @@ Supported attributes: | |||
| | `id`                                  | integer/string  | Yes      | ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths). | | ||||
| | `package_name_pattern`                | string          | Yes      | Package name protected by the protection rule. For example `@my-scope/my-package-*`. Wildcard character `*` allowed. | | ||||
| | `package_type`                        | string          | Yes      | Package type protected by the protection rule. For example `npm`. | | ||||
| | `minimum_access_level_for_push`       | string          | Yes      | Minimum GitLab access level able to push a package. Must be at least `maintainer`. For example `maintainer`, `owner` or `admin`. | | ||||
| | `minimum_access_level_for_delete`     | string          | Yes      | Minimum GitLab access level required to delete a package. Valid values include `null`, `owner` or `admin`. If the value is `null`, the default minimum access level is `maintainer`. Must be provided when `minimum_access_level_for_push` is not set. Behind a feature flag named `packages_protected_packages_delete`. Disabled by default. | | ||||
| | `minimum_access_level_for_push`       | string          | Yes      | Minimum GitLab access level required to push a package. Valid values include `null`, `maintainer`, `owner` or `admin`. If the value is `null`, the default minimum access level is `developer`. Must be provided when `minimum_access_level_for_delete` is not set. | | ||||
| 
 | ||||
| If successful, returns [`201`](rest/troubleshooting.md#status-codes) and the created package protection rule. | ||||
| 
 | ||||
|  | @ -112,6 +116,7 @@ curl --request POST \ | |||
|   --data '{ | ||||
|        "package_name_pattern": "package-name-pattern-*", | ||||
|        "package_type": "npm", | ||||
|        "minimum_access_level_for_delete": "owner", | ||||
|        "minimum_access_level_for_push": "maintainer" | ||||
|     }' | ||||
| ``` | ||||
|  | @ -132,7 +137,8 @@ Supported attributes: | |||
| | `package_protection_rule_id`          | integer         | Yes      | ID of the package protection rule to be updated. | | ||||
| | `package_name_pattern`                | string          | No       | Package name protected by the protection rule. For example `@my-scope/my-package-*`. Wildcard character `*` allowed. | | ||||
| | `package_type`                        | string          | No       | Package type protected by the protection rule. For example `npm`. | | ||||
| | `minimum_access_level_for_push`       | string          | No       | Minimum GitLab access level able to push a package. Must be at least `maintainer`. For example `maintainer`, `owner` or `admin`. | | ||||
| | `minimum_access_level_for_delete`     | string          | No       | Minimum GitLab access level required to delete a package. Valid values include `null`, `owner` or `admin`. If the value is `null`, the default minimum access level is `maintainer`. Must be provided when `minimum_access_level_for_push` is not set. Behind a feature flag named `packages_protected_packages_delete`. Disabled by default. | | ||||
| | `minimum_access_level_for_push`       | string          | No       | Minimum GitLab access level required to push a package. Valid values include `null`, `maintainer`, `owner` or `admin`. If the value is `null`, the default minimum access level is `developer`. Must be provided when `minimum_access_level_for_delete` is not set. | | ||||
| 
 | ||||
| If successful, returns [`200`](rest/troubleshooting.md#status-codes) and the updated package protection rule. | ||||
| 
 | ||||
|  |  | |||
|  | @ -1693,7 +1693,7 @@ Supported general project attributes: | |||
| | `ci_id_token_sub_claim_components`                 | array             | No       | Fields included in the `sub` claim of the [ID Token](../ci/secrets/id_token_authentication.md). Accepts an array starting with `project_path`. The array might also include `ref_type` and `ref`. Defaults to `["project_path", "ref_type", "ref"]`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/477260) in GitLab 17.10. | | ||||
| | `ci_separated_caches`                              | boolean           | No       | Set whether or not caches should be [separated](../ci/caching/_index.md#cache-key-names) by branch protection status. | | ||||
| | `ci_restrict_pipeline_cancellation_role`           | string            | No       | Set the [role required to cancel a pipeline or job](../ci/pipelines/settings.md#restrict-roles-that-can-cancel-pipelines-or-jobs). One of `developer`, `maintainer`, or `no_one`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/429921) in GitLab 16.8. Premium and Ultimate only. | | ||||
| | `ci_pipeline_variables_minimum_override_role`      | string            | No       | When `restrict_user_defined_variables` is enabled, you can specify which role can override variables. One of `owner`, `maintainer`, `developer` or `no_one_allowed`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/440338) in GitLab 17.1. | | ||||
| | `ci_pipeline_variables_minimum_override_role`      | string            | No       | You can specify which role can override variables. One of `owner`, `maintainer`, `developer` or `no_one_allowed`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/440338) in GitLab 17.1. In GitLab 17.1 to 17.7, `restrict_user_defined_variables` must be enabled. | | ||||
| | `ci_push_repository_for_job_token_allowed`         | boolean           | No       | Enable or disable the ability to push to the project repository using job token. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/389060) in GitLab 17.2. | | ||||
| | `container_expiration_policy_attributes`           | hash              | No       | Update the image cleanup policy for this project. Accepts: `cadence` (string), `keep_n` (integer), `older_than` (string), `name_regex` (string), `name_regex_delete` (string), `name_regex_keep` (string), `enabled` (boolean). | | ||||
| | `container_registry_enabled`                       | boolean           | No       | _(Deprecated)_ Enable container registry for this project. Use `container_registry_access_level` instead. | | ||||
|  | @ -1737,7 +1737,7 @@ Supported general project attributes: | |||
| | `repository_storage`                               | string            | No       | Which storage shard the repository is on. _(administrators only)_ | | ||||
| | `request_access_enabled`                           | boolean           | No       | Allow users to request member access. | | ||||
| | `resolve_outdated_diff_discussions`                | boolean           | No       | Automatically resolve merge request diffs discussions on lines changed with a push. | | ||||
| | `restrict_user_defined_variables`                  | boolean           | No       | Allow only users with the Maintainer role to pass user-defined variables when triggering a pipeline. For example when the pipeline is triggered in the UI, with the API, or by a trigger token. | | ||||
| | `restrict_user_defined_variables`                  | boolean           | No       | _([Deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154510) in GitLab 17.7 in favour of `ci_pipeline_variables_minimum_override_role`)_ Allow only users with the Maintainer role to pass user-defined variables when triggering a pipeline. For example when the pipeline is triggered in the UI, with the API, or by a trigger token. | | ||||
| | `service_desk_enabled`                             | boolean           | No       | Enable or disable Service Desk feature. | | ||||
| | `shared_runners_enabled`                           | boolean           | No       | Enable instance runners for this project. | | ||||
| | `show_default_award_emojis`                        | boolean           | No       | Show default emoji reactions. | | ||||
|  |  | |||
|  | @ -138,7 +138,7 @@ instead. If you supply values to both fields, the `require_reauthentication_to_a | |||
| 
 | ||||
| The `require_password_to_approve` field will be removed in v5 of the GitLab REST API. | ||||
| 
 | ||||
| ## Pull mirroring configuration with Project API | ||||
| ## Pull mirroring configuration with the projects API endpoint | ||||
| 
 | ||||
| Breaking change. [Related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/494294). | ||||
| 
 | ||||
|  | @ -146,3 +146,10 @@ In GitLab 17.6, the [pull mirroring configuration with the Projects API](../proj | |||
| It is replaced by a new configuration and endpoint, [`projects/:id/mirror/pull`](../project_pull_mirroring.md#configure-pull-mirroring-for-a-project). | ||||
| 
 | ||||
| The previous configuration using the Projects API will be removed in v5 of the GitLab REST API. | ||||
| 
 | ||||
| ## `restrict_user_defined_variables` parameter with the projects API endpoint | ||||
| 
 | ||||
| In GitLab 17.7, the [`restrict_user_defined_variables` parameter in Projects API](../projects.md#edit-a-project) | ||||
| is deprecated in favour of using only `ci_pipeline_variables_minimum_override_role`. | ||||
| 
 | ||||
| To match the same behavior of `restrict_user_defined_variables: false` set `ci_pipeline_variables_minimum_override_role` as `developer`. | ||||
|  |  | |||
|  | @ -5267,7 +5267,7 @@ job: | |||
| 
 | ||||
| {{< /history >}} | ||||
| 
 | ||||
| Use `secrets:token` to explicitly select a token to use when authenticating with Vault by referencing the token's CI/CD variable. | ||||
| Use `secrets:token` to explicitly select a token to use when authenticating with the external secrets provider by referencing the token's CI/CD variable. | ||||
| 
 | ||||
| **Keyword type**: Job keyword. You can use it only as part of a job. | ||||
| 
 | ||||
|  | @ -5292,7 +5292,9 @@ job: | |||
| 
 | ||||
| **Additional details**: | ||||
| 
 | ||||
| - When the `token` keyword is not set, the first ID token is used to authenticate. | ||||
| - When the `token` keyword is not set and there is only one token defined, the defined token will automatically be used. | ||||
| - If there is more than one token defined, you should specify which token to use by setting the `token` keyword. | ||||
|   If you do not specify which token to use, it is not possible to predict which token is used each time the job runs. | ||||
| 
 | ||||
| ### `services` | ||||
| 
 | ||||
|  |  | |||
|  | @ -194,6 +194,12 @@ This is well-documentation in the [Rails guides](https://guides.rubyonrails.org/ | |||
| This sets the Time To Live (TTL) for the cache entry, and is the single most useful | ||||
| (and most commonly used) cache option. This is supported in most Rails caching helpers. | ||||
| 
 | ||||
| The TTL, if not set with `expires_in`, | ||||
| [defaults to 8 hours](https://gitlab.com/gitlab-org/gitlab/-/blob/a3e435da6e9f7c98dc05eccb1caa03c1aed5a2a8/lib/gitlab/redis/cache.rb#L26). | ||||
| Consider using an 8 hour TTL for general caching, as this matches a workday and would mean that a user would generally only have one cache-miss per day for the same content. | ||||
| 
 | ||||
| When writing large amounts of data, consider using a shorter expiry to decrease its impact on the memory usage. | ||||
| 
 | ||||
| ##### `race_condition_ttl` | ||||
| 
 | ||||
| This option prevents multiple uncached hits for a key at the same time. | ||||
|  | @ -203,6 +209,12 @@ then sets the new cache value. | |||
| Used when a cache key is under very heavy load to prevent multiple simultaneous | ||||
| writes, but should be set to a low value, such as 10 seconds. | ||||
| 
 | ||||
| #### Rails cache behavior on GitLab | ||||
| 
 | ||||
| `Rails.cache` uses Redis as the store. | ||||
| GitLab instances, like GitLab.com, can configure Redis for [key eviction](https://redis.io/docs/latest/develop/reference/eviction/). | ||||
| See the [Redis development guide](../development/redis.md#caching). | ||||
| 
 | ||||
| ### When to use HTTP caching | ||||
| 
 | ||||
| Use conditional GET caching when the entire response is cacheable: | ||||
|  |  | |||
|  | @ -793,6 +793,14 @@ There are two processes that automatically lock tables: | |||
| - The `Database::MonitorLockedTablesWorker` locks tables if needed. | ||||
|   This can be disabled by the `lock_tables_in_monitoring` feature flag. | ||||
| 
 | ||||
| `Gitlab::Database::MigrationHelpers::AutomaticLockWritesOnTables` compares the list of tables before and after each database migration runs. Then | ||||
| it locks the newly added tables on the relevant database. This does not | ||||
| cover all cases. Because some migrations need | ||||
| to re-create the tables within the same transactional migration. For example: To convert standard unpartitioned tables into partitioned tables. | ||||
| See [this example](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/184636). | ||||
| Such migrations leave the re-created tables unlocked. But the daily cron job of `Database::MonitorLockedTablesWorker` takes care of | ||||
| alerting about this on Slack, and then automatically locks these tables for writes. | ||||
| 
 | ||||
| ### Manually lock tables | ||||
| 
 | ||||
| If you need to manually lock a table, use a database migration. | ||||
|  |  | |||
|  | @ -262,6 +262,8 @@ To add a new visualization render type: | |||
| 
 | ||||
| 1. Create a new Vue component that accepts `data` and `options` properties. | ||||
|    See [`line_chart.vue`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/line_chart.vue) as an example. | ||||
| 1. Add relevant storybook stories for the different states of the visualization | ||||
|    See [`line_chart.stories.js`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/line_chart.stories.js) as an example. | ||||
| 1. Add your component to the list of conditional components imports in [`analytics_dashboard_panel.vue`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/assets/javascripts/analytics/analytics_dashboards/components/analytics_dashboard_panel.vue). | ||||
| 1. Add your component to the schema's list of `AnalyticsVisualization` enum type in [`analytics_visualization.json`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/validators/json_schemas/analytics_visualization.json). | ||||
| 
 | ||||
|  |  | |||
|  | @ -9,7 +9,7 @@ title: Redis development guidelines | |||
| 
 | ||||
| GitLab uses [Redis](https://redis.io) for the following distinct purposes: | ||||
| 
 | ||||
| - Caching (mostly via `Rails.cache`). | ||||
| - [Caching](#caching) (mostly via `Rails.cache`). | ||||
| - As a job processing queue with [Sidekiq](sidekiq/_index.md). | ||||
| - To manage the shared application state. | ||||
| - To store CI trace chunks. | ||||
|  | @ -231,7 +231,24 @@ and make sure that the fix for them works as expected. | |||
| 
 | ||||
| - [Database query recorder](database/query_recorder.md) | ||||
| 
 | ||||
| ## Utility classes | ||||
| ## Caching | ||||
| 
 | ||||
| The Redis instance used by `Rails.cache` can be | ||||
| [configured with a key eviction policy](https://docs.gitlab.com/omnibus/settings/redis/#setting-the-redis-cache-instance-as-an-lru), | ||||
| generally LRU, where the "least recently used" cache items are evicted (deleted) when a memory limit is reached. | ||||
| 
 | ||||
| See GitLab.com's | ||||
| [key eviction configuration](https://gitlab.com/gitlab-com/gl-infra/chef-repo/-/blob/f7c1494f5546b12fe12352590590b8a883aa6388/roles/gprd-base-db-redis-cluster-cache.json#L54) | ||||
| for its Redis instance used by `Rails.cache`, `redis-cluster-cache`. This Redis instance should not reach its | ||||
| [max memory limit](https://gitlab.com/gitlab-com/gl-infra/chef-repo/-/blob/f7c1494f5546b12fe12352590590b8a883aa6388/roles/gprd-base-db-redis-cluster-cache.json#L54) | ||||
| because key eviction at maxmemory comes at the cost of latency while eviction is taking place, see [this issue](https://gitlab.com/gitlab-com/gl-infra/observability/team/-/issues/1601) for details. See [current memory usage for `redis-cluster-cache`](https://dashboards.gitlab.net/goto/1BcjmqhNR?orgId=1). | ||||
| 
 | ||||
| As data in this cache can disappear earlier than its set expiration time, | ||||
| use `Rails.cache` for data that is truly cache-like and ephemeral. | ||||
| 
 | ||||
| For data that should be reliably persisted in Redis rather than cached, you can use [`Gitlab::Redis::SharedState`](#gitlabrediscachesharedstatequeues). | ||||
| 
 | ||||
| ### Utility classes | ||||
| 
 | ||||
| We have some extra classes to help with specific use cases. These are | ||||
| mostly for fine-grained control of Redis usage, so they wouldn't be used | ||||
|  | @ -250,7 +267,7 @@ following is true: | |||
| 1. We want to manipulate data on a non-cache Redis instance. | ||||
| 1. `Rails.cache` does not support the operations we want to perform. | ||||
| 
 | ||||
| ### `Gitlab::Redis::{Cache,SharedState,Queues}` | ||||
| #### `Gitlab::Redis::{Cache,SharedState,Queues}` | ||||
| 
 | ||||
| These classes wrap the Redis instances (using | ||||
| [`Gitlab::Redis::Wrapper`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/redis/wrapper.rb)) | ||||
|  | @ -266,13 +283,27 @@ Gitlab::Redis::SharedState.with { |redis| redis.get(key) } | |||
| Gitlab::Redis::Cache.with { |redis| redis.sismember(key, value) } | ||||
| ``` | ||||
| 
 | ||||
| ### `Gitlab::Redis::Boolean` | ||||
| `Gitlab::Redis::Cache` [shares the same Redis instance](https://gitlab.com/gitlab-org/gitlab/-/blob/fbcb99a6edec13a4d26010ce927b705f13db1ef8/config/initializers/7_redis.rb#L18) | ||||
| as `Rails.cache`, and so has a key eviction policy if one [is configured](#caching). Use this class for data | ||||
| that is truly cache-like and could be regenerated if absent. | ||||
| 
 | ||||
| Ensure you **always** set a TTL for keys when using this class | ||||
| as it does not set a default TTL, unlike `Rails.cache` whose default TTL | ||||
| [is 8 hours](https://gitlab.com/gitlab-org/gitlab/-/blob/a3e435da6e9f7c98dc05eccb1caa03c1aed5a2a8/lib/gitlab/redis/cache.rb#L26). Consider using an 8 hour TTL for general caching, this matches a workday and would mean that a user would generally only have one cache-miss per day for the same content. | ||||
| 
 | ||||
| When you anticipate adding a large workload to the cache or are in doubt about its production impact, please reach out to [`#g_durability`](https://gitlab.enterprise.slack.com/archives/C07U8G0LHEH). | ||||
| 
 | ||||
| `Gitlab::Redis::SharedState` [will not be configured with a key eviction policy](https://docs.gitlab.com/omnibus/settings/redis/#setting-the-redis-cache-instance-as-an-lru). | ||||
| Use this class for data that cannot be regenerated and is expected to be persisted until its set expiration time. | ||||
| It also does not set a default TTL for keys, so a TTL should nearly always be set for keys when using this class. | ||||
| 
 | ||||
| #### `Gitlab::Redis::Boolean` | ||||
| 
 | ||||
| In Redis, every value is a string. | ||||
| [`Gitlab::Redis::Boolean`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/redis/boolean.rb) | ||||
| makes sure that booleans are encoded and decoded consistently. | ||||
| 
 | ||||
| ### `Gitlab::Redis::HLL` | ||||
| #### `Gitlab::Redis::HLL` | ||||
| 
 | ||||
| The Redis [`PFCOUNT`](https://redis.io/docs/latest/commands/pfcount/), | ||||
| [`PFADD`](https://redis.io/docs/latest/commands/pfadd/), and | ||||
|  | @ -284,7 +315,7 @@ see [HyperLogLogs in Redis](https://thoughtbot.com/blog/hyperloglogs-in-redis). | |||
| [`Gitlab::Redis::HLL`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/redis/hll.rb) | ||||
| provides a convenient interface for adding and counting values in HyperLogLogs. | ||||
| 
 | ||||
| ### `Gitlab::SetCache` | ||||
| #### `Gitlab::SetCache` | ||||
| 
 | ||||
| For cases where we need to efficiently check the whether an item is in a group | ||||
| of items, we can use a Redis set. | ||||
|  |  | |||
|  | @ -1199,7 +1199,8 @@ In most specs, the Rails cache is actually an in-memory store. This is replaced | |||
| between specs, so calls to `Rails.cache.read` and `Rails.cache.write` are safe. | ||||
| However, if a spec makes direct Redis calls, it should mark itself with the | ||||
| `:clean_gitlab_redis_cache`, `:clean_gitlab_redis_shared_state` or | ||||
| `:clean_gitlab_redis_queues` traits as appropriate. | ||||
| `:clean_gitlab_redis_queues` traits depending on | ||||
| [which Redis instance](../../development/redis.md#gitlabrediscachesharedstatequeues) is being used. | ||||
| 
 | ||||
| #### Background jobs / Sidekiq | ||||
| 
 | ||||
|  |  | |||
|  | @ -63,7 +63,7 @@ Prerequisites: | |||
| 
 | ||||
| To install the GitLab for Jira Cloud app: | ||||
| 
 | ||||
| 1. In Jira, in the sidebar, select **Apps > Explore more apps** and search for `GitLab for Jira Cloud`. | ||||
| 1. In Jira, on the top bar, select **Apps > Explore more apps** and search for `GitLab for Jira Cloud`. | ||||
| 1. Select **GitLab for Jira Cloud**, then select **Get it now**. | ||||
| 
 | ||||
| Alternatively, [get the app directly from the Atlassian Marketplace](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?tab=overview&hosting=cloud). | ||||
|  |  | |||
|  | @ -10,7 +10,14 @@ module API | |||
|             expose :project_id, documentation: { type: 'integer', example: 1 } | ||||
|             expose :package_name_pattern, documentation: { type: 'string', example: 'flightjs/flight' } | ||||
|             expose :package_type, documentation: { type: 'string', example: 'npm' } | ||||
|             expose :minimum_access_level_for_delete, documentation: { type: 'string', example: 'owner' } | ||||
|             expose :minimum_access_level_for_push, documentation: { type: 'string', example: 'maintainer' } | ||||
| 
 | ||||
|             def minimum_access_level_for_delete | ||||
|               return if ::Feature.disabled?(:packages_protected_packages_delete, object.project) | ||||
| 
 | ||||
|               object.minimum_access_level_for_delete | ||||
|             end | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|  |  | |||
|  | @ -46,14 +46,30 @@ module API | |||
|             Wildcard character * allowed.' | ||||
|           requires :package_type, type: String, values: Packages::Protection::Rule.package_types.keys, | ||||
|             desc: 'Package type protected by the rule. For example npm.' | ||||
|           requires :minimum_access_level_for_push, type: String, | ||||
|           optional :minimum_access_level_for_delete, type: String, | ||||
|             values: Packages::Protection::Rule.minimum_access_level_for_deletes.keys, | ||||
|             desc: 'Minimum GitLab access level required to delete a package. ' \ | ||||
|               'Valid values include `null`, `owner` or `admin`. ' \ | ||||
|               'If the value is `null`, the default minimum access level is `maintainer`. ' \ | ||||
|               'Must be provided when `minimum_access_level_for_push` is not set. ' \ | ||||
|               'Behind a feature flag named `packages_protected_packages_delete`. Disabled by default.' | ||||
|           optional :minimum_access_level_for_push, type: String, | ||||
|             values: Packages::Protection::Rule.minimum_access_level_for_pushes.keys, | ||||
|             desc: 'Minimum GitLab access level able to push a package. Must be at least `maintainer`. | ||||
|             For example `maintainer`, `owner` or `admin`.' | ||||
|             desc: 'Minimum GitLab access level required to push a package. ' \ | ||||
|               'Valid values include `null`, `maintainer`, `owner` or `admin`. ' \ | ||||
|               'If the value is `null`, the default minimum access level is `developer`. ' \ | ||||
|               'Must be provided when `minimum_access_level_for_delete` is not set.' | ||||
|           at_least_one_of :minimum_access_level_for_push, :minimum_access_level_for_delete | ||||
|         end | ||||
|         post do | ||||
|           response = ::Packages::Protection::CreateRuleService.new(project: user_project, current_user: current_user, | ||||
|             params: declared_params).execute | ||||
|           params = declared_params | ||||
|           params.except!(:minimum_access_level_for_delete) if Feature.disabled?(:packages_protected_packages_delete, | ||||
|             user_project) | ||||
| 
 | ||||
|           response = | ||||
|             ::Packages::Protection::CreateRuleService | ||||
|               .new(project: user_project, current_user: current_user, params: params) | ||||
|               .execute | ||||
| 
 | ||||
|           render_api_error!({ error: response.message }, :unprocessable_entity) if response.error? | ||||
| 
 | ||||
|  | @ -81,16 +97,31 @@ module API | |||
|               Wildcard character * allowed.' | ||||
|             optional :package_type, type: String, values: Packages::Protection::Rule.package_types.keys, | ||||
|               desc: 'Package type protected by the rule. For example npm.' | ||||
|             optional :minimum_access_level_for_delete, type: String, | ||||
|               values: Packages::Protection::Rule.minimum_access_level_for_deletes.keys, | ||||
|               desc: 'Minimum GitLab access level required to delete a package. ' \ | ||||
|                 'Valid values include `null`, `owner` or `admin`. ' \ | ||||
|                 'If the value is `null`, the default minimum access level is `maintainer`. ' \ | ||||
|                 'Must be provided when `minimum_access_level_for_push` is not set. ' \ | ||||
|                 'Behind a feature flag named `packages_protected_packages_delete`. Disabled by default.' | ||||
|             optional :minimum_access_level_for_push, type: String, | ||||
|               values: Packages::Protection::Rule.minimum_access_level_for_pushes.keys, | ||||
|               desc: 'Minimum GitLab access level able to push a package. Must be at least `maintainer`. | ||||
|               For example `maintainer`, `owner` or `admin`.' | ||||
|               desc: 'Minimum GitLab access level required to push a package. ' \ | ||||
|                 'Valid values include `null`, `maintainer`, `owner` or `admin`. ' \ | ||||
|                 'If the value is `null`, the default minimum access level is `developer`. ' \ | ||||
|                 'Must be provided when `minimum_access_level_for_delete` is not set.' | ||||
|           end | ||||
|           patch do | ||||
|             package_protection_rule = user_project.package_protection_rules.find(params[:package_protection_rule_id]) | ||||
| 
 | ||||
|             response = ::Packages::Protection::UpdateRuleService.new(package_protection_rule, | ||||
|               current_user: current_user, params: declared_params(include_missing: false)).execute | ||||
|             params = declared_params(include_missing: false) | ||||
|             params.except!(:minimum_access_level_for_delete) if Feature.disabled?(:packages_protected_packages_delete, | ||||
|               user_project) | ||||
| 
 | ||||
|             response = | ||||
|               ::Packages::Protection::UpdateRuleService | ||||
|                 .new(package_protection_rule, current_user: current_user, params: params) | ||||
|                 .execute | ||||
| 
 | ||||
|             render_api_error!({ error: response.message }, :unprocessable_entity) if response.error? | ||||
| 
 | ||||
|  |  | |||
|  | @ -51,9 +51,7 @@ module API | |||
| 
 | ||||
|           token = find_token(resource, params[:token_id]) | ||||
| 
 | ||||
|           if token.nil? | ||||
|             next not_found!("Could not find #{source_type} access token with token_id: #{params[:token_id]}") | ||||
|           end | ||||
|           next not_found!("#{source_type} Access Token") if token.nil? | ||||
| 
 | ||||
|           resource.members.load | ||||
|           present token, with: Entities::ResourceAccessToken, resource: resource | ||||
|  | @ -76,9 +74,7 @@ module API | |||
|           resource = find_source(source_type, params[:id]) | ||||
|           token = find_token(resource, params[:token_id]) | ||||
| 
 | ||||
|           if token.nil? | ||||
|             next not_found!("Could not find #{source_type} access token with token_id: #{params[:token_id]}") | ||||
|           end | ||||
|           next not_found!("#{source_type} Access Token") if token.nil? | ||||
| 
 | ||||
|           service = ::ResourceAccessTokens::RevokeService.new( | ||||
|             current_user, | ||||
|  |  | |||
|  | @ -99,7 +99,8 @@ module Gitlab | |||
|             threshold: -> { application_settings.downstream_pipeline_trigger_limit_per_project_user_sha }, interval: 1.minute | ||||
|           }, | ||||
|           expanded_diff_files: { threshold: 6, interval: 1.minute }, | ||||
|           glql: { threshold: 1, interval: 15.minutes } | ||||
|           glql: { threshold: 1, interval: 15.minutes }, | ||||
|           bulk_delete_todos: { threshold: 6, interval: 1.minute } | ||||
|         }.freeze | ||||
|       end | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,6 +6,8 @@ module Gitlab | |||
|       CACHE_NAMESPACE = 'cache:gitlab' | ||||
| 
 | ||||
|       class << self | ||||
|         # Configuration options for Rails.cache, not Gitlab::Redis::Cache | ||||
|         # | ||||
|         # Full list of options: | ||||
|         # https://api.rubyonrails.org/classes/ActiveSupport/Cache/RedisCacheStore.html#method-c-new | ||||
|         # pool argument event not documented in the link above is handled by RedisCacheStore see: | ||||
|  |  | |||
|  | @ -1,15 +0,0 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Gitlab | ||||
|   module Usage | ||||
|     module Metrics | ||||
|       module Instrumentations | ||||
|         class RunnerRegistrationTokensAllowedMetric < GenericMetric | ||||
|           def value | ||||
|             Gitlab::CurrentSettings.allow_runner_registration_token? | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -6290,6 +6290,9 @@ msgstr "" | |||
| msgid "AmazonQ|Audience" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "AmazonQ|Automatic code reviews can only be enabled when Amazon Q is set to \"On by default\"" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "AmazonQ|Availability" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -6332,6 +6335,9 @@ msgstr "" | |||
| msgid "AmazonQ|Delete the IAM role." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "AmazonQ|Enable automatic code reviews" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "AmazonQ|Enter the IAM role's ARN." | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -59499,6 +59505,9 @@ msgstr "" | |||
| msgid "The user you are trying to deactivate has been active in the past %{minimum_inactive_days} days and cannot be deactivated" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "The value must be a valid JSON array format: [1,2,3] or [{\"key\": \"value\"}]" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "The value must contain only numbers." | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -60255,10 +60264,10 @@ msgstr "" | |||
| msgid "This is an experimental feature developed by GitLab Incubation Engineering." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "This is mandatory and must be defined." | ||||
| msgid "This is really just a warning" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "This is really just a warning" | ||||
| msgid "This is required and must be defined." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "This is some information. %{linkStart}Learn more%{linkEnd}." | ||||
|  |  | |||
|  | @ -6,9 +6,9 @@ import waitForPromises from 'helpers/wait_for_promises'; | |||
| import { createAlert } from '~/alert'; | ||||
| 
 | ||||
| import CrudComponent from '~/vue_shared/components/crud_component.vue'; | ||||
| import InputsTableSkeletonLoader from '~/ci/common/pipeline_inputs/inputs_table_skeleton_loader.vue'; | ||||
| import InputsTableSkeletonLoader from '~/ci/common/pipeline_inputs/pipeline_inputs_table/inputs_table_skeleton_loader.vue'; | ||||
| import PipelineInputsForm from '~/ci/common/pipeline_inputs/pipeline_inputs_form.vue'; | ||||
| import PipelineInputsTable from '~/ci/common/pipeline_inputs/pipeline_inputs_table.vue'; | ||||
| import PipelineInputsTable from '~/ci/common/pipeline_inputs/pipeline_inputs_table/pipeline_inputs_table.vue'; | ||||
| import getPipelineInputsQuery from '~/ci/common/pipeline_inputs/graphql/queries/pipeline_creation_inputs.query.graphql'; | ||||
| /** mock data to be replaced with fixtures - https://gitlab.com/gitlab-org/gitlab/-/issues/525243 */ | ||||
| import { | ||||
|  |  | |||
|  | @ -1,14 +1,13 @@ | |||
| import { nextTick } from 'vue'; | ||||
| import { GlCollapsibleListbox, GlFormInput } from '@gitlab/ui'; | ||||
| import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; | ||||
| import { GlCollapsibleListbox, GlFormInput, GlFormTextarea } from '@gitlab/ui'; | ||||
| import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; | ||||
| import DynamicValueRenderer from '~/ci/common/pipeline_inputs/dynamic_value_renderer.vue'; | ||||
| import DynamicValueRenderer from '~/ci/common/pipeline_inputs/pipeline_inputs_table/dynamic_value_renderer.vue'; | ||||
| 
 | ||||
| describe('DynamicValueRenderer', () => { | ||||
|   let wrapper; | ||||
| 
 | ||||
|   const defaultProps = { | ||||
|     item: { name: 'input1', description: '', type: 'STRING', default: 'value1', regex: '[a-z]+' }, | ||||
|     item: { name: 'input1', description: '', type: 'STRING', default: 'value1' }, | ||||
|   }; | ||||
| 
 | ||||
|   const createComponent = ({ props = {} } = {}) => { | ||||
|  | @ -19,20 +18,24 @@ describe('DynamicValueRenderer', () => { | |||
|       }, | ||||
|       stubs: { | ||||
|         GlFormInput, | ||||
|       }, | ||||
|       directives: { | ||||
|         GlTooltip: createMockDirective('gl-tooltip'), | ||||
|         GlFormTextarea, | ||||
|       }, | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   const findInput = () => wrapper.findByTestId('value-input'); | ||||
|   const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox); | ||||
|   const findInput = () => wrapper.findComponent(GlFormInput); | ||||
|   const findTextarea = () => wrapper.findComponent(GlFormTextarea); | ||||
|   const findValidationFeedback = () => wrapper.findByTestId('validation-feedback'); | ||||
| 
 | ||||
|   const getInputType = (type, hasOptions) => { | ||||
|     if (hasOptions || type === 'BOOLEAN') return 'dropdown'; | ||||
|     if (type === 'ARRAY') return 'textarea'; | ||||
|     return 'input'; | ||||
|   }; | ||||
| 
 | ||||
|   const setInputValue = async (value) => { | ||||
|     await findInput().setValue(value); | ||||
|     await findInput().trigger('blur'); | ||||
| 
 | ||||
|     await nextTick(); | ||||
|   }; | ||||
|  | @ -72,29 +75,18 @@ describe('DynamicValueRenderer', () => { | |||
|     }); | ||||
| 
 | ||||
|     describe('array type handling', () => { | ||||
|       it('renders text input for array type and displays array as JSON string', () => { | ||||
|         const arrayValue = ['item1', 'item2', 'item3']; | ||||
|       it('renders a textarea for array inputs', () => { | ||||
|         createComponent({ | ||||
|           props: { | ||||
|             item: { ...defaultProps.item, type: 'ARRAY', default: arrayValue }, | ||||
|             item: { | ||||
|               ...defaultProps.item, | ||||
|               type: 'ARRAY', | ||||
|             }, | ||||
|           }, | ||||
|         }); | ||||
| 
 | ||||
|         expect(findInput().exists()).toBe(true); | ||||
|         expect(findInput().attributes('type')).toBe('text'); | ||||
|         expect(findInput().props('value')).toBe(JSON.stringify(arrayValue)); | ||||
|       }); | ||||
| 
 | ||||
|       it('renders text input for array type with complex objects and displays as JSON string', () => { | ||||
|         const complexArrayValue = [{ hello: '2' }, '4', '6']; | ||||
|         createComponent({ | ||||
|           props: { | ||||
|             item: { ...defaultProps.item, type: 'ARRAY', default: complexArrayValue }, | ||||
|           }, | ||||
|         }); | ||||
| 
 | ||||
|         expect(findInput().exists()).toBe(true); | ||||
|         expect(findInput().props('value')).toBe(JSON.stringify(complexArrayValue)); | ||||
|         expect(findTextarea().exists()).toBe(true); | ||||
|         expect(findInput().exists()).toBe(false); | ||||
|       }); | ||||
| 
 | ||||
|       it('renders dropdown for array type when options are provided', () => { | ||||
|  | @ -165,9 +157,16 @@ describe('DynamicValueRenderer', () => { | |||
|             props: { item: { ...defaultProps.item, type, default: value } }, | ||||
|           }); | ||||
| 
 | ||||
|           if (usesDropdown) { | ||||
|           const inputType = getInputType(type, usesDropdown); | ||||
| 
 | ||||
|           switch (inputType) { | ||||
|             case 'dropdown': | ||||
|               expect(findDropdown().props('selected')).toBe(expectedDisplayValue); | ||||
|           } else { | ||||
|               break; | ||||
|             case 'textarea': | ||||
|               expect(findTextarea().props('value')).toBe(expectedDisplayValue); | ||||
|               break; | ||||
|             default: | ||||
|               expect(findInput().props('value')).toBe(expectedDisplayValue); | ||||
|           } | ||||
|         }, | ||||
|  | @ -189,9 +188,16 @@ describe('DynamicValueRenderer', () => { | |||
|             props: { item: { ...defaultProps.item, type } }, | ||||
|           }); | ||||
| 
 | ||||
|           if (usesDropdown) { | ||||
|           const inputType = getInputType(type, usesDropdown); | ||||
| 
 | ||||
|           switch (inputType) { | ||||
|             case 'dropdown': | ||||
|               await findDropdown().vm.$emit('select', inputValue); | ||||
|           } else { | ||||
|               break; | ||||
|             case 'textarea': | ||||
|               await findTextarea().vm.$emit('input', inputValue); | ||||
|               break; | ||||
|             default: | ||||
|               await findInput().vm.$emit('input', inputValue); | ||||
|           } | ||||
| 
 | ||||
|  | @ -202,10 +208,12 @@ describe('DynamicValueRenderer', () => { | |||
|   }); | ||||
| 
 | ||||
|   describe('validation', () => { | ||||
|     describe('attributes', () => { | ||||
|       it.each` | ||||
|         description                     | itemProps              | attribute            | expectedValue | ||||
|         ${'regex pattern attribute'}    | ${{ regex: '[a-z]+' }} | ${'pattern'}         | ${'[a-z]+'} | ||||
|         ${'number type data attribute'} | ${{ type: 'NUMBER' }}  | ${'data-field-type'} | ${'NUMBER'} | ||||
|         ${'json array data attribute'}  | ${{ type: 'ARRAY' }}   | ${'data-json-array'} | ${'true'} | ||||
|       `('applies $description to input', ({ itemProps, attribute, expectedValue }) => {
 | ||||
|         createComponent({ | ||||
|           props: { | ||||
|  | @ -216,7 +224,8 @@ describe('DynamicValueRenderer', () => { | |||
|           }, | ||||
|         }); | ||||
| 
 | ||||
|       expect(findInput().attributes(attribute)).toBe(expectedValue); | ||||
|         const inputComponent = itemProps.type === 'ARRAY' ? findTextarea() : findInput(); | ||||
|         expect(inputComponent.attributes(attribute)).toBe(expectedValue); | ||||
|       }); | ||||
| 
 | ||||
|       // Separate test for required attribute because vue 2 and vue 3 handle required differently
 | ||||
|  | @ -234,37 +243,57 @@ describe('DynamicValueRenderer', () => { | |||
|     }); | ||||
| 
 | ||||
|     describe('invalid text feedback', () => { | ||||
|     it.each` | ||||
|       name                                   | itemProps              | value             | expectedMessage | ||||
|       ${'required field is empty'}           | ${{ required: true }}  | ${''}             | ${'This is mandatory and must be defined.'} | ||||
|       ${'non-numeric value in number field'} | ${{ type: 'NUMBER' }}  | ${'not-a-number'} | ${'The value must contain only numbers.'} | ||||
|       ${'pattern does not match'}            | ${{ regex: '[a-z]+' }} | ${'123'}          | ${'The value must match the defined regular expression.'} | ||||
|     `('displays validation feedback when $name', async ({ itemProps, value, expectedMessage }) => {
 | ||||
|       it('displays validation feedback when required field is empty', async () => { | ||||
|         createComponent({ | ||||
|           props: { | ||||
|             item: { | ||||
|               ...defaultProps.item, | ||||
|             ...itemProps, | ||||
|               required: true, | ||||
|             }, | ||||
|           }, | ||||
|         }); | ||||
| 
 | ||||
|       await setInputValue(value); | ||||
|         await setInputValue(''); | ||||
| 
 | ||||
|         expect(findValidationFeedback().exists()).toBe(true); | ||||
|       expect(findValidationFeedback().text()).toContain(expectedMessage); | ||||
|         expect(findValidationFeedback().text()).toContain('This is required and must be defined.'); | ||||
|       }); | ||||
| 
 | ||||
|     it('shows tooltip with pattern information for regex validation errors', async () => { | ||||
|       createComponent(); | ||||
|       it('displays validation feedback for non-numeric value in number field', async () => { | ||||
|         createComponent({ | ||||
|           props: { | ||||
|             item: { | ||||
|               ...defaultProps.item, | ||||
|               type: 'NUMBER', | ||||
|             }, | ||||
|           }, | ||||
|         }); | ||||
| 
 | ||||
|         await setInputValue('not-a-number'); | ||||
| 
 | ||||
|         expect(findValidationFeedback().exists()).toBe(true); | ||||
|         expect(findValidationFeedback().text()).toContain('The value must contain only numbers.'); | ||||
|       }); | ||||
| 
 | ||||
|       it('includes pattern information in validation feedback for regex errors', async () => { | ||||
|         const regex = '[a-z]+'; | ||||
|         createComponent({ | ||||
|           props: { | ||||
|             item: { | ||||
|               ...defaultProps.item, | ||||
|               regex, | ||||
|             }, | ||||
|           }, | ||||
|         }); | ||||
| 
 | ||||
|         await setInputValue('123'); | ||||
| 
 | ||||
|       const validationFeedbackTooltipDirective = getBinding( | ||||
|         findValidationFeedback().element, | ||||
|         'gl-tooltip', | ||||
|         expect(findValidationFeedback().exists()).toBe(true); | ||||
|         expect(findValidationFeedback().text()).toContain( | ||||
|           'The value must match the defined regular expression.', | ||||
|         ); | ||||
|       expect(validationFeedbackTooltipDirective.value).toBe(`Pattern: ${defaultProps.item.regex}`); | ||||
|         expect(findValidationFeedback().text()).toContain(`Pattern: ${regex}`); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | @ -1,6 +1,6 @@ | |||
| import { GlSkeletonLoader } from '@gitlab/ui'; | ||||
| import { shallowMount } from '@vue/test-utils'; | ||||
| import InputsTableSkeletonLoader from '~/ci/common/pipeline_inputs/inputs_table_skeleton_loader.vue'; | ||||
| import InputsTableSkeletonLoader from '~/ci/common/pipeline_inputs/pipeline_inputs_table/inputs_table_skeleton_loader.vue'; | ||||
| 
 | ||||
| describe('InputsTableSkeletonLoader', () => { | ||||
|   let wrapper; | ||||
|  | @ -1,7 +1,7 @@ | |||
| import { GlIcon, GlTableLite } from '@gitlab/ui'; | ||||
| import { mountExtended } from 'helpers/vue_test_utils_helper'; | ||||
| import PipelineInputsTable from '~/ci/common/pipeline_inputs/pipeline_inputs_table.vue'; | ||||
| import DynamicValueRenderer from '~/ci/common/pipeline_inputs/dynamic_value_renderer.vue'; | ||||
| import PipelineInputsTable from '~/ci/common/pipeline_inputs/pipeline_inputs_table/pipeline_inputs_table.vue'; | ||||
| import DynamicValueRenderer from '~/ci/common/pipeline_inputs/pipeline_inputs_table/dynamic_value_renderer.vue'; | ||||
| import Markdown from '~/vue_shared/components/markdown/non_gfm_markdown.vue'; | ||||
| 
 | ||||
| describe('PipelineInputsTable', () => { | ||||
|  | @ -0,0 +1,92 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Mutations::Todos::DeleteMany, feature_category: :notifications do | ||||
|   include GraphqlHelpers | ||||
| 
 | ||||
|   let_it_be(:current_user) { create(:user) } | ||||
|   let_it_be(:author) { create(:user) } | ||||
|   let_it_be(:other_user) { create(:user) } | ||||
| 
 | ||||
|   let_it_be(:todo1) { create(:todo, user: current_user, author: author, state: :done) } | ||||
|   let_it_be(:todo2) { create(:todo, user: current_user, author: author, state: :pending) } | ||||
| 
 | ||||
|   let_it_be(:other_user_todo) { create(:todo, user: other_user, author: author, state: :pending) } | ||||
| 
 | ||||
|   let(:mutation) { described_class.new(object: nil, context: query_context, field: nil) } | ||||
| 
 | ||||
|   describe '#process_todos' do | ||||
|     context 'when the action is not called too many times' do | ||||
|       before do | ||||
|         allow(Gitlab::ApplicationRateLimiter).to( | ||||
|           receive(:throttled?).with(:bulk_delete_todos, scope: [current_user]).and_return(false) | ||||
|         ) | ||||
|       end | ||||
| 
 | ||||
|       it 'deletes a single todo' do | ||||
|         expect { resolve_mutation([todo1]) }.to change { current_user.todos.count }.by(-1) | ||||
| 
 | ||||
|         expect(todo2.reload).not_to be_nil | ||||
|       end | ||||
| 
 | ||||
|       it 'does not delete a pending todo' do | ||||
|         result = resolve_mutation([todo2]) | ||||
| 
 | ||||
|         expect(todo2.reload).not_to be_nil | ||||
| 
 | ||||
|         expect(result).to match( | ||||
|           errors: be_empty, | ||||
|           deleted_ids: [] | ||||
|         ) | ||||
|       end | ||||
| 
 | ||||
|       it 'ignores requests for todos which do not belong to the current user' do | ||||
|         resolve_mutation([other_user_todo]) | ||||
| 
 | ||||
|         expect(other_user_todo.reload).not_to be_nil | ||||
|       end | ||||
| 
 | ||||
|       it 'deletes multiple todos' do | ||||
|         todo4 = create(:todo, user: current_user, author: author, state: :done) | ||||
| 
 | ||||
|         expect { resolve_mutation([todo1, todo4, todo2]) }.to change { current_user.todos.count }.by(-2) | ||||
|       end | ||||
| 
 | ||||
|       it 'returns ids of the deleted todos' do | ||||
|         result = resolve_mutation([todo1, todo2]) | ||||
| 
 | ||||
|         expect(result).to match( | ||||
|           errors: be_empty, | ||||
|           deleted_ids: [todo1.id] | ||||
|         ) | ||||
|       end | ||||
| 
 | ||||
|       it 'skips todo that does not belong to the current user' do | ||||
|         expect { resolve_mutation([todo1, todo2, other_user_todo]) }.to change { current_user.todos.count }.by(-1) | ||||
| 
 | ||||
|         expect(other_user_todo.reload).not_to be_nil | ||||
|       end | ||||
| 
 | ||||
|       it 'fails if too many todos are requested for update' do | ||||
|         expect { resolve_mutation([todo1] * 101) }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when the action is called too many times' do | ||||
|       it 'raises error' do | ||||
|         expect(Gitlab::ApplicationRateLimiter).to( | ||||
|           receive(:throttled?).with(:bulk_delete_todos, scope: [current_user]).and_return(true) | ||||
|         ) | ||||
| 
 | ||||
|         expect do | ||||
|           resolve_mutation([todo1]) | ||||
|         end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, /too many times/) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def resolve_mutation(todos) | ||||
|     mutation.resolve(ids: todos.map { |todo| global_id_of(todo) }) | ||||
|   end | ||||
| end | ||||
|  | @ -13,6 +13,7 @@ RSpec.describe API::Entities::Projects::Packages::Protection::Rule, feature_cate | |||
|       :project_id, | ||||
|       :package_name_pattern, | ||||
|       :package_type, | ||||
|       :minimum_access_level_for_delete, | ||||
|       :minimum_access_level_for_push | ||||
|     ] | ||||
|   end | ||||
|  |  | |||
|  | @ -1,12 +1,22 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| require 'rails/generators/testing/behaviour' | ||||
| require 'generators/gitlab/click_house/migration_generator' | ||||
| require 'fileutils' | ||||
| 
 | ||||
| if ::Gitlab.next_rails? | ||||
|   require 'rails/generators/testing/behavior' | ||||
| else | ||||
|   require 'rails/generators/testing/behaviour' | ||||
| end | ||||
| 
 | ||||
| RSpec.describe Gitlab::ClickHouse::MigrationGenerator, feature_category: :database do | ||||
|   if ::Gitlab.next_rails? | ||||
|     include Rails::Generators::Testing::Behavior | ||||
|   else | ||||
|     include Rails::Generators::Testing::Behaviour | ||||
|   end | ||||
| 
 | ||||
|   include FileUtils | ||||
| 
 | ||||
|   let(:migration_name) { "CreateProjects" } | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ RSpec.describe 'cross-database foreign keys' do | |||
|   # The issue corresponding to the loose foreign key conversion | ||||
|   # should be added as a comment along with the name of the column. | ||||
|   let!(:allowed_cross_database_foreign_keys) do | ||||
|     [ | ||||
|     keys = [ | ||||
|       'zoekt_indices.zoekt_enabled_namespace_id', | ||||
|       'zoekt_repositories.project_id', | ||||
|       'zoekt_replicas.zoekt_enabled_namespace_id', | ||||
|  | @ -19,7 +19,6 @@ RSpec.describe 'cross-database foreign keys' do | |||
|       'ci_cost_settings.runner_id',                              # The fk_rails_6a70651f75 FK needs to be dropped | ||||
|       'ci_runner_taggings.tag_id',                               # https://gitlab.com/gitlab-org/gitlab/-/issues/467664 | ||||
|       'ci_runner_taggings_instance_type.tag_id',                 # https://gitlab.com/gitlab-org/gitlab/-/issues/467664 | ||||
|       'ci_job_artifact_states.partition_id', | ||||
|       'ci_secure_file_states.ci_secure_file_id', | ||||
|       'dependency_proxy_blob_states.dependency_proxy_blob_id', | ||||
|       'dependency_proxy_blob_states.group_id', | ||||
|  | @ -36,6 +35,14 @@ RSpec.describe 'cross-database foreign keys' do | |||
|       'snippet_repositories.snippet_project_id', | ||||
|       'upload_states.upload_id' | ||||
|     ] | ||||
| 
 | ||||
|     keys << if ::Gitlab.next_rails? | ||||
|               'ci_job_artifact_states.partition_id.job_artifact_id' | ||||
|             else | ||||
|               'ci_job_artifact_states.partition_id' | ||||
|             end | ||||
| 
 | ||||
|     keys | ||||
|   end | ||||
| 
 | ||||
|   def foreign_keys_for(table_name) | ||||
|  | @ -58,7 +65,7 @@ RSpec.describe 'cross-database foreign keys' do | |||
|       foreign_keys_for(table).each do |fk| | ||||
|         next unless is_cross_db?(fk) | ||||
| 
 | ||||
|         column = "#{fk.from_table}.#{fk.column}" | ||||
|         column = "#{fk.from_table}.#{Array.wrap(fk.column).join('.')}" | ||||
|         allowlist.delete(column) | ||||
| 
 | ||||
|         expect(allowed_cross_database_foreign_keys).to include(column), "Found extra cross-database foreign key #{column} referencing #{fk.to_table} with constraint name #{fk.name}. When a foreign key references another database you must use a Loose Foreign Key instead https://docs.gitlab.com/ee/development/database/loose_foreign_keys.html ." | ||||
|  | @ -76,7 +83,9 @@ RSpec.describe 'cross-database foreign keys' do | |||
|       table, _ = entry.split('.') | ||||
| 
 | ||||
|       all_foreign_keys_for_table = foreign_keys_for(table) | ||||
|       fk_entry = all_foreign_keys_for_table.find { |fk| "#{fk.from_table}.#{fk.column}" == entry } | ||||
|       fk_entry = all_foreign_keys_for_table.find do |fk| | ||||
|         "#{fk.from_table}.#{Array.wrap(fk.column).join('.')}" == entry | ||||
|       end | ||||
| 
 | ||||
|       expect(fk_entry).to be_present, | ||||
|         "`#{entry}` is no longer a foreign key. " \ | ||||
|  |  | |||
|  | @ -1,22 +0,0 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RunnerRegistrationTokensAllowedMetric, feature_category: :runner do | ||||
|   using RSpec::Parameterized::TableSyntax | ||||
| 
 | ||||
|   context 'for runner registration tokens enabled option' do | ||||
|     where(:allow_runner_registration_token, :expected_value) do | ||||
|       true  | true | ||||
|       false | false | ||||
|     end | ||||
| 
 | ||||
|     with_them do | ||||
|       before do | ||||
|         stub_application_setting(allow_runner_registration_token: allow_runner_registration_token) | ||||
|       end | ||||
| 
 | ||||
|       it_behaves_like 'a correct instrumented metric value', { time_frame: 'none' } | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -95,6 +95,19 @@ RSpec.describe Packages::TerraformModule::Metadatum, type: :model, feature_categ | |||
|           end | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'for semver_patch_convert_to_bigint' do | ||||
|         let(:metadatum) { create(:terraform_module_metadatum, semver: '1.2.3') } | ||||
| 
 | ||||
|         subject do | ||||
|           metadatum.connection.execute(<<~SQL) | ||||
|             SELECT semver_patch_convert_to_bigint FROM packages_terraform_module_metadata WHERE package_id = #{metadatum.package_id} | ||||
|           SQL | ||||
|           .first['semver_patch_convert_to_bigint'] | ||||
|         end | ||||
| 
 | ||||
|         it { is_expected.to eq(metadatum.semver_patch) } | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when the parent project is destroyed' do | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ require 'spec_helper' | |||
| RSpec.describe API::ProjectPackagesProtectionRules, :aggregate_failures, feature_category: :package_registry do | ||||
|   let_it_be(:project) { create(:project, :private) } | ||||
|   let_it_be(:other_project) { create(:project, :private) } | ||||
|   let_it_be(:protection_rule) { create(:package_protection_rule, project: project) } | ||||
|   let_it_be_with_reload(:protection_rule) { create(:package_protection_rule, project: project) } | ||||
|   let_it_be(:protection_rule_id) { protection_rule.id } | ||||
| 
 | ||||
|   let_it_be(:maintainer) { create(:user, maintainer_of: [project, other_project]) } | ||||
|  | @ -18,9 +18,12 @@ RSpec.describe API::ProjectPackagesProtectionRules, :aggregate_failures, feature | |||
|   let(:url) { "/projects/#{project.id}/#{path}" } | ||||
| 
 | ||||
|   let(:params) do | ||||
|     { package_name_pattern: '@my-new-scope/my-package-*', | ||||
|     { | ||||
|       package_name_pattern: '@my-new-scope/my-package-*', | ||||
|       package_type: protection_rule.package_type, | ||||
|       minimum_access_level_for_push: protection_rule.minimum_access_level_for_push } | ||||
|       minimum_access_level_for_delete: protection_rule.minimum_access_level_for_delete, | ||||
|       minimum_access_level_for_push: protection_rule.minimum_access_level_for_push | ||||
|     } | ||||
|   end | ||||
| 
 | ||||
|   shared_examples 'rejecting project packages protection rules request when enough permissions' do | ||||
|  | @ -46,6 +49,27 @@ RSpec.describe API::ProjectPackagesProtectionRules, :aggregate_failures, feature | |||
| 
 | ||||
|         expect(response).to have_gitlab_http_status(:ok) | ||||
|         expect(json_response.count).to eq(2) | ||||
|         expect(json_response).to include({ | ||||
|           'id' => protection_rule.id, | ||||
|           'project_id' => protection_rule.project_id, | ||||
|           'package_name_pattern' => protection_rule.package_name_pattern, | ||||
|           'package_type' => protection_rule.package_type, | ||||
|           'minimum_access_level_for_delete' => protection_rule.minimum_access_level_for_delete, | ||||
|           'minimum_access_level_for_push' => protection_rule.minimum_access_level_for_push | ||||
|         }) | ||||
|       end | ||||
| 
 | ||||
|       context 'when feature flag :packages_protected_packages_delete is disabled' do | ||||
|         before do | ||||
|           stub_feature_flags(packages_protected_packages_delete: false) | ||||
|         end | ||||
| 
 | ||||
|         it 'gets the package protection rules without minimum_access_level_for_delete' do | ||||
|           get_package_rules | ||||
| 
 | ||||
|           expect(response).to have_gitlab_http_status(:ok) | ||||
|           expect(json_response).to all include('minimum_access_level_for_delete' => nil) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       it_behaves_like 'rejecting project packages protection rules request when enough permissions' | ||||
|  | @ -71,23 +95,79 @@ RSpec.describe API::ProjectPackagesProtectionRules, :aggregate_failures, feature | |||
|       it 'creates a package protection rule' do | ||||
|         expect { post_package_rule }.to change { Packages::Protection::Rule.count }.by(1) | ||||
|         expect(response).to have_gitlab_http_status(:created) | ||||
| 
 | ||||
|         expect(json_response).to include( | ||||
|           'id' => Integer, | ||||
|           'project_id' => project.id, | ||||
|           'package_name_pattern' => params[:package_name_pattern], | ||||
|           'package_type' => params[:package_type], | ||||
|           'minimum_access_level_for_delete' => params[:minimum_access_level_for_delete], | ||||
|           'minimum_access_level_for_push' => params[:minimum_access_level_for_push] | ||||
|         ) | ||||
|       end | ||||
| 
 | ||||
|       context 'with invalid package_type' do | ||||
|       context 'when feature flag :packages_protected_packages_delete is disabled' do | ||||
|         before do | ||||
|           params[:package_type] = "not in enum" | ||||
|           stub_feature_flags(packages_protected_packages_delete: false) | ||||
|         end | ||||
| 
 | ||||
|         it 'creates a package protection rule with blank minimum_access_level_for_delete' do | ||||
|           expect { post_package_rule }.to change { Packages::Protection::Rule.count }.by(1) | ||||
|           expect(response).to have_gitlab_http_status(:created) | ||||
| 
 | ||||
|           expect(json_response).to include( | ||||
|             'id' => Integer, | ||||
|             'project_id' => project.id, | ||||
|             'package_name_pattern' => params[:package_name_pattern], | ||||
|             'package_type' => params[:package_type], | ||||
|             'minimum_access_level_for_delete' => nil, | ||||
|             'minimum_access_level_for_push' => params[:minimum_access_level_for_push] | ||||
|           ) | ||||
| 
 | ||||
|           expect(Packages::Protection::Rule.find(json_response['id']).minimum_access_level_for_delete).to be_nil | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'without minimum_access_level_for_delete' do | ||||
|         let(:params) { super().except(:minimum_access_level_for_delete) } | ||||
| 
 | ||||
|         it 'creates a package protection rule' do | ||||
|           expect { post_package_rule }.to change { Packages::Protection::Rule.count }.by(1) | ||||
|           expect(response).to have_gitlab_http_status(:created) | ||||
| 
 | ||||
|           expect(json_response).to include( | ||||
|             'id' => Integer, | ||||
|             'project_id' => project.id, | ||||
|             'package_name_pattern' => params[:package_name_pattern], | ||||
|             'package_type' => params[:package_type], | ||||
|             'minimum_access_level_for_delete' => nil, | ||||
|             'minimum_access_level_for_push' => params[:minimum_access_level_for_push] | ||||
|           ) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'with blank minimum_access_level_for_delete' do | ||||
|         let(:params) { super().merge(minimum_access_level_for_delete: '') } | ||||
| 
 | ||||
|         it 'does not create a package protection rule' do | ||||
|           expect { post_package_rule }.to not_change(Packages::Protection::Rule, :count) | ||||
|           expect(response).to have_gitlab_http_status(:bad_request) | ||||
|           expect(json_response).to include 'error' => 'minimum_access_level_for_delete does not have a valid value' | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'with invalid package_type' do | ||||
|         let(:params) { super().merge(package_type: 'not in enum') } | ||||
| 
 | ||||
|         it 'does not create a package protection rule' do | ||||
|           expect { post_package_rule }.to not_change(Packages::Protection::Rule, :count) | ||||
|           expect(response).to have_gitlab_http_status(:bad_request) | ||||
|           expect(json_response).to include 'error' => 'package_type does not have a valid value' | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'with invalid minimum_access_level_for_push' do | ||||
|         before do | ||||
|           params[:minimum_access_level_for_push] = "not in enum" | ||||
|         end | ||||
|         let(:params) { super().merge(minimum_access_level_for_push: 'not in enum') } | ||||
| 
 | ||||
|         it 'does not create a package protection rule' do | ||||
|           expect { post_package_rule }.to not_change(Packages::Protection::Rule, :count) | ||||
|  | @ -95,10 +175,30 @@ RSpec.describe API::ProjectPackagesProtectionRules, :aggregate_failures, feature | |||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'with already existing package_name_pattern' do | ||||
|         before do | ||||
|           params[:package_name_pattern] = protection_rule.package_name_pattern | ||||
|       context 'without minimum_access_levels' do | ||||
|         let(:params) { super().except(:minimum_access_level_for_push, :minimum_access_level_for_delete) } | ||||
| 
 | ||||
|         it 'does not create a package protection rule' do | ||||
|           expect { post_package_rule }.to not_change(Packages::Protection::Rule, :count) | ||||
|           expect(response).to have_gitlab_http_status(:bad_request) | ||||
|           expect(json_response).to include( | ||||
|             'error' => 'minimum_access_level_for_push, minimum_access_level_for_delete are missing, ' \ | ||||
|               'at least one parameter must be provided' | ||||
|           ) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'with invalid minimum_access_levels' do | ||||
|         let(:params) { super().merge(minimum_access_level_for_push: nil, minimum_access_level_for_delete: nil) } | ||||
| 
 | ||||
|         it 'does not create a package protection rule' do | ||||
|           expect { post_package_rule }.to not_change(Packages::Protection::Rule, :count) | ||||
|           expect(response).to have_gitlab_http_status(:unprocessable_entity) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'with already existing package_name_pattern' do | ||||
|         let(:params) { super().merge(package_name_pattern: protection_rule.package_name_pattern) } | ||||
| 
 | ||||
|         it 'does not create a package protection rule' do | ||||
|           expect { post_package_rule }.to not_change(Packages::Protection::Rule, :count) | ||||
|  | @ -128,9 +228,7 @@ RSpec.describe API::ProjectPackagesProtectionRules, :aggregate_failures, feature | |||
|       let_it_be(:changed_scope) { '@my-changed-scope/my-package-*' } | ||||
| 
 | ||||
|       context 'with full changeset' do | ||||
|         before do | ||||
|           params[:package_name_pattern] = changed_scope | ||||
|         end | ||||
|         let(:params) { super().merge(package_name_pattern: changed_scope) } | ||||
| 
 | ||||
|         it 'updates a package protection rule' do | ||||
|           patch_package_rule | ||||
|  | @ -152,28 +250,77 @@ RSpec.describe API::ProjectPackagesProtectionRules, :aggregate_failures, feature | |||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'with invalid package_type' do | ||||
|         before do | ||||
|           params[:package_type] = "not in enum" | ||||
|       context 'with changing minimum_access_level_for_delete' do | ||||
|         let(:params) { super().merge(minimum_access_level_for_delete: 'admin') } | ||||
| 
 | ||||
|         it 'updates a package protection rule' do | ||||
|           patch_package_rule | ||||
| 
 | ||||
|           expect(response).to have_gitlab_http_status(:ok) | ||||
|           expect(json_response["minimum_access_level_for_delete"]).to eq('admin') | ||||
|         end | ||||
| 
 | ||||
|         context 'when feature flag :packages_protected_packages_delete is disabled' do | ||||
|           before do | ||||
|             stub_feature_flags(packages_protected_packages_delete: false) | ||||
|           end | ||||
| 
 | ||||
|           it 'keeps old value for minimum_access_level_for_delete' do | ||||
|             expect { patch_package_rule }.to not_change { protection_rule.reload.minimum_access_level_for_delete } | ||||
| 
 | ||||
|             expect(response).to have_gitlab_http_status(:ok) | ||||
| 
 | ||||
|             expect(json_response["minimum_access_level_for_delete"]).to be_nil | ||||
|           end | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'with nil value for minimum_access_level_for_delete' do | ||||
|         let(:params) { super().merge(minimum_access_level_for_delete: nil) } | ||||
| 
 | ||||
|         it 'updates a package protection rule' do | ||||
|           patch_package_rule | ||||
| 
 | ||||
|           expect(response).to have_gitlab_http_status(:ok) | ||||
|           expect(json_response["minimum_access_level_for_delete"]).to be_nil | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'with invalid package_type' do | ||||
|         let(:params) { super().merge(package_type: 'not in enum') } | ||||
| 
 | ||||
|         it_behaves_like 'returning response status', :bad_request | ||||
|       end | ||||
| 
 | ||||
|       context 'with invalid minimum_access_level_for_push' do | ||||
|         before do | ||||
|           params[:minimum_access_level_for_push] = "not in enum" | ||||
|         end | ||||
|         let(:params) { super().merge(minimum_access_level_for_push: 'not in enum') } | ||||
| 
 | ||||
|         it_behaves_like 'returning response status', :bad_request | ||||
|       end | ||||
| 
 | ||||
|       context 'with already existing package_name_pattern' do | ||||
|         before do | ||||
|           other_package_protection_rule = create(:package_protection_rule, project: project, | ||||
|             package_name_pattern: "@my-scope/my-package-*") | ||||
|           params[:package_name_pattern] = other_package_protection_rule.package_name_pattern | ||||
|       context 'with blank minimum_access_level_for_delete' do | ||||
|         let(:params) { super().merge(minimum_access_level_for_delete: '') } | ||||
| 
 | ||||
|         it_behaves_like 'returning response status', :bad_request | ||||
| 
 | ||||
|         it 'returns error message' do | ||||
|           expect { patch_package_rule }.to not_change(Packages::Protection::Rule, :count) | ||||
|           expect(json_response).to include 'error' => 'minimum_access_level_for_delete does not have a valid value' | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'with invalid minimum_access_levels' do | ||||
|         let(:params) { super().merge(minimum_access_level_for_push: nil, minimum_access_level_for_delete: nil) } | ||||
| 
 | ||||
|         it_behaves_like 'returning response status', :unprocessable_entity | ||||
|       end | ||||
| 
 | ||||
|       context 'with already existing package_name_pattern' do | ||||
|         let_it_be(:existing_package_protection_rule) do | ||||
|           create(:package_protection_rule, project: project, package_name_pattern: '@my-scope/my-package-*') | ||||
|         end | ||||
| 
 | ||||
|         let(:params) { super().merge(package_name_pattern: existing_package_protection_rule.package_name_pattern) } | ||||
| 
 | ||||
|         it_behaves_like 'returning response status', :unprocessable_entity | ||||
|       end | ||||
|  | @ -201,9 +348,7 @@ RSpec.describe API::ProjectPackagesProtectionRules, :aggregate_failures, feature | |||
| 
 | ||||
|       it 'deletes the package protection rule' do | ||||
|         destroy_package_rule | ||||
|         expect do | ||||
|           Packages::Protection::Rule.find(protection_rule.id) | ||||
|         end.to raise_error(ActiveRecord::RecordNotFound) | ||||
|         expect { Packages::Protection::Rule.find(protection_rule.id) }.to raise_error(ActiveRecord::RecordNotFound) | ||||
|         expect(response).to have_gitlab_http_status(:no_content) | ||||
|       end | ||||
| 
 | ||||
|  |  | |||
|  | @ -342,7 +342,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do | |||
|             get_token | ||||
| 
 | ||||
|             expect(response).to have_gitlab_http_status(:not_found) | ||||
|             expect(response.body).to include("Could not find #{source_type} access token with token_id: #{token_id}") | ||||
|             expect(json_response["message"]).to eq("404 #{source_type} Access Token Not Found") | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|  | @ -353,7 +353,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do | |||
|             get_token | ||||
| 
 | ||||
|             expect(response).to have_gitlab_http_status(:not_found) | ||||
|             expect(response.body).to include("Could not find #{source_type} access token with token_id: #{token_id}") | ||||
|             expect(json_response["message"]).to eq("404 #{source_type} Access Token Not Found") | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|  | @ -416,7 +416,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do | |||
|             delete_token | ||||
| 
 | ||||
|             expect(response).to have_gitlab_http_status(:not_found) | ||||
|             expect(response.body).to include("Could not find #{source_type} access token with token_id: #{token_id}") | ||||
|             expect(json_response["message"]).to eq("404 #{source_type} Access Token Not Found") | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|  | @ -427,7 +427,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do | |||
|             delete_token | ||||
| 
 | ||||
|             expect(response).to have_gitlab_http_status(:not_found) | ||||
|             expect(response.body).to include("Could not find #{source_type} access token with token_id: #{token_id}") | ||||
|             expect(json_response["message"]).to eq("404 #{source_type} Access Token Not Found") | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|  |  | |||
|  | @ -102,7 +102,7 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi | |||
|         context 'with different pipeline sources' do | ||||
|           before do | ||||
|             # We're creating many pipelines | ||||
|             allow(Gitlab::QueryLimiting).to receive(:threshold).and_return(477) | ||||
|             allow(Gitlab::QueryLimiting).to receive(:threshold).and_return(479) | ||||
| 
 | ||||
|             external_pull_request = create(:external_pull_request, project: project) | ||||
|             create(:ci_pipeline, project: project, source: :external_pull_request_event, external_pull_request: external_pull_request) | ||||
|  | @ -513,6 +513,69 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi | |||
|         destroy_project(project, user) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     describe 'tag protection rules handling' do | ||||
|       let_it_be_with_refind(:project) { create(:project, :repository, namespace: user.namespace) } | ||||
| 
 | ||||
|       subject { destroy_project(project, user) } | ||||
| 
 | ||||
|       context 'when there are tag protection rules' do | ||||
|         before_all do | ||||
|           create(:container_registry_protection_tag_rule, | ||||
|             project: project, | ||||
|             tag_name_pattern: 'xyz', | ||||
|             minimum_access_level_for_delete: Gitlab::Access::ADMIN | ||||
|           ) | ||||
| 
 | ||||
|           project.add_owner(user) | ||||
|           project.container_repositories << create(:container_repository) | ||||
|         end | ||||
| 
 | ||||
|         context 'when there are registry tags' do | ||||
|           before do | ||||
|             stub_container_registry_tags(repository: project.full_path, tags: ['tag']) | ||||
|             allow_any_instance_of(described_class) | ||||
|               .to receive(:remove_legacy_registry_tags).and_return(true) | ||||
|           end | ||||
| 
 | ||||
|           context 'when the feature container_registry_protected_tags is disabled' do | ||||
|             before do | ||||
|               stub_feature_flags(container_registry_protected_tags: false) | ||||
|             end | ||||
| 
 | ||||
|             it { is_expected.to be true } | ||||
|           end | ||||
| 
 | ||||
|           context 'when the current user is an admin', :enable_admin_mode do | ||||
|             let(:user) { build_stubbed(:admin) } | ||||
| 
 | ||||
|             it { is_expected.to be true } | ||||
|           end | ||||
| 
 | ||||
|           context 'when the current user role meets the minimum access level for delete' do | ||||
|             before_all do | ||||
|               project.container_registry_protection_tag_rules.first.update!( | ||||
|                 minimum_access_level_for_delete: Gitlab::Access::OWNER | ||||
|               ) | ||||
|             end | ||||
| 
 | ||||
|             it { is_expected.to be true } | ||||
|           end | ||||
| 
 | ||||
|           context 'when the current user role does not meet the minimum access level for delete' do | ||||
|             it { is_expected.to be false } | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         context 'when there are no registry tags' do | ||||
|           it { is_expected.to be true } | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when there are no tag protection rules' do | ||||
|         it { is_expected.to be true } | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'for a forked project with LFS objects' do | ||||
|  |  | |||
|  | @ -10,11 +10,11 @@ RSpec.shared_examples 'abilities without group level work items license' do | |||
|       # disallowed | ||||
|       expect(permissions(non_member_user, work_item)).to be_disallowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :delete_work_item, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji | ||||
|       ) | ||||
|       expect(permissions(non_member_user, confidential_work_item)).to be_disallowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :delete_work_item, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji, | ||||
|         :move_work_item, :clone_work_item | ||||
|       ) | ||||
|     end | ||||
|  | @ -23,17 +23,17 @@ RSpec.shared_examples 'abilities without group level work items license' do | |||
|       # disallowed | ||||
|       expect(permissions(guest, not_persisted_work_item)).to be_disallowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :delete_work_item, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji, | ||||
|         :move_work_item, :clone_work_item | ||||
|       ) | ||||
|       expect(permissions(guest, work_item)).to be_disallowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :delete_work_item, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji, | ||||
|         :move_work_item, :clone_work_item | ||||
|       ) | ||||
|       expect(permissions(guest, confidential_work_item)).to be_disallowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :delete_work_item, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji, | ||||
|         :move_work_item, :clone_work_item | ||||
|       ) | ||||
|     end | ||||
|  | @ -42,11 +42,11 @@ RSpec.shared_examples 'abilities without group level work items license' do | |||
|       # disallowed | ||||
|       expect(permissions(planner, work_item)).to be_disallowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :delete_work_item, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji | ||||
|       ) | ||||
|       expect(permissions(planner, confidential_work_item)).to be_disallowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :delete_work_item, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji, | ||||
|         :move_work_item, :clone_work_item | ||||
|       ) | ||||
|     end | ||||
|  | @ -55,12 +55,12 @@ RSpec.shared_examples 'abilities without group level work items license' do | |||
|       # disallowed | ||||
|       expect(permissions(reporter, work_item)).to be_disallowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :delete_work_item, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji, | ||||
|         :move_work_item, :clone_work_item | ||||
|       ) | ||||
|       expect(permissions(reporter, confidential_work_item)).to be_disallowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :delete_work_item, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji, | ||||
|         :move_work_item, :clone_work_item | ||||
|       ) | ||||
|     end | ||||
|  | @ -69,22 +69,22 @@ RSpec.shared_examples 'abilities without group level work items license' do | |||
|       # disallowed | ||||
|       expect(permissions(group_guest, work_item)).to be_disallowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :delete_work_item, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji, | ||||
|         :move_work_item, :clone_work_item | ||||
|       ) | ||||
|       expect(permissions(group_guest, confidential_work_item)).to be_disallowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :delete_work_item, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji, | ||||
|         :move_work_item, :clone_work_item | ||||
|       ) | ||||
|       expect(permissions(group_guest_author, authored_work_item)).to be_disallowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :delete_work_item, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji, | ||||
|         :move_work_item, :clone_work_item | ||||
|       ) | ||||
|       expect(permissions(group_guest_author, authored_confidential_work_item)).to be_disallowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :delete_work_item, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji, | ||||
|         :move_work_item, :clone_work_item | ||||
|       ) | ||||
|     end | ||||
|  | @ -93,12 +93,12 @@ RSpec.shared_examples 'abilities without group level work items license' do | |||
|       # disallowed | ||||
|       expect(permissions(group_planner, work_item)).to be_disallowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :delete_work_item, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji, | ||||
|         :move_work_item, :clone_work_item | ||||
|       ) | ||||
|       expect(permissions(group_planner, confidential_work_item)).to be_disallowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :delete_work_item, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji, | ||||
|         :move_work_item, :clone_work_item | ||||
|       ) | ||||
|     end | ||||
|  | @ -107,12 +107,12 @@ RSpec.shared_examples 'abilities without group level work items license' do | |||
|       # disallowed | ||||
|       expect(permissions(group_reporter, work_item)).to be_disallowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :delete_work_item, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji, | ||||
|         :move_work_item, :clone_work_item | ||||
|       ) | ||||
|       expect(permissions(group_reporter, confidential_work_item)).to be_disallowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :delete_work_item, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, | ||||
|         :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji, | ||||
|         :move_work_item, :clone_work_item | ||||
|       ) | ||||
|     end | ||||
|  | @ -128,9 +128,9 @@ RSpec.shared_examples 'abilities with group level work items license' do | |||
|     it 'checks project guest abilities' do | ||||
|       # allowed | ||||
|       expect(permissions(guest, not_persisted_work_item)).to be_allowed( | ||||
|         :read_work_item, :read_issue, :read_note, :create_note) | ||||
|         :read_work_item, :read_issue, :read_note, :create_note, :award_emoji) | ||||
|       expect(permissions(guest, work_item)).to be_allowed( | ||||
|         :read_work_item, :read_issue, :read_note, :create_note) | ||||
|         :read_work_item, :read_issue, :read_note, :create_note, :award_emoji) | ||||
| 
 | ||||
|       # disallowed | ||||
|       expect(permissions(guest, not_persisted_work_item)).to be_disallowed( | ||||
|  | @ -143,14 +143,14 @@ RSpec.shared_examples 'abilities with group level work items license' do | |||
|       ) | ||||
|       expect(permissions(guest, confidential_work_item)).to be_disallowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :admin_parent_link, | ||||
|         :set_work_item_metadata, :admin_work_item_link, :create_note, :move_work_item, :clone_work_item | ||||
|         :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji, :move_work_item, :clone_work_item | ||||
|       ) | ||||
|     end | ||||
| 
 | ||||
|     it 'checks project planner abilities' do | ||||
|       # allowed | ||||
|       expect(permissions(planner, work_item)).to be_allowed( | ||||
|         :read_work_item, :read_issue, :read_note, :create_note | ||||
|         :read_work_item, :read_issue, :read_note, :create_note, :award_emoji | ||||
|       ) | ||||
| 
 | ||||
|       # disallowed | ||||
|  | @ -160,14 +160,14 @@ RSpec.shared_examples 'abilities with group level work items license' do | |||
|       ) | ||||
|       expect(permissions(planner, confidential_work_item)).to be_disallowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :admin_parent_link, | ||||
|         :set_work_item_metadata, :admin_work_item_link, :create_note, :move_work_item, :clone_work_item | ||||
|         :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji, :move_work_item, :clone_work_item | ||||
|       ) | ||||
|     end | ||||
| 
 | ||||
|     it 'checks project reporter abilities' do | ||||
|       # allowed | ||||
|       expect(permissions(reporter, work_item)).to be_allowed( | ||||
|         :read_work_item, :read_issue, :read_note, :create_note | ||||
|         :read_work_item, :read_issue, :read_note, :create_note, :award_emoji | ||||
|       ) | ||||
| 
 | ||||
|       # disallowed | ||||
|  | @ -177,7 +177,7 @@ RSpec.shared_examples 'abilities with group level work items license' do | |||
|       ) | ||||
|       expect(permissions(reporter, confidential_work_item)).to be_disallowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :admin_parent_link, | ||||
|         :set_work_item_metadata, :admin_work_item_link, :create_note, :move_work_item, :clone_work_item | ||||
|         :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji, :move_work_item, :clone_work_item | ||||
|       ) | ||||
|     end | ||||
| 
 | ||||
|  | @ -185,18 +185,18 @@ RSpec.shared_examples 'abilities with group level work items license' do | |||
|       # allowed | ||||
|       expect(permissions(group_guest, not_persisted_work_item)).to be_allowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_parent_link, :set_work_item_metadata, :admin_work_item_link, | ||||
|         :create_note | ||||
|         :create_note, :award_emoji | ||||
|       ) | ||||
|       expect(permissions(group_guest, work_item)).to be_allowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_parent_link, :admin_work_item_link, :create_note | ||||
|         :read_work_item, :read_issue, :read_note, :admin_parent_link, :admin_work_item_link, :create_note, :award_emoji | ||||
|       ) | ||||
|       expect(permissions(group_guest_author, authored_work_item)).to be_allowed( | ||||
|         :read_work_item, :read_issue, :read_note, :update_work_item, :delete_work_item, :admin_parent_link, | ||||
|         :admin_work_item_link, :create_note | ||||
|         :admin_work_item_link, :create_note, :award_emoji | ||||
|       ) | ||||
|       expect(permissions(group_guest_author, authored_confidential_work_item)).to be_allowed( | ||||
|         :read_work_item, :read_issue, :read_note, :update_work_item, :delete_work_item, :admin_parent_link, | ||||
|         :admin_work_item_link, :create_note | ||||
|         :admin_work_item_link, :create_note, :award_emoji | ||||
|       ) | ||||
| 
 | ||||
|       # disallowed | ||||
|  | @ -206,7 +206,7 @@ RSpec.shared_examples 'abilities with group level work items license' do | |||
|       ) | ||||
|       expect(permissions(group_guest, confidential_work_item)).to be_disallowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :delete_work_item, | ||||
|         :set_work_item_metadata, :create_note, :move_work_item, :clone_work_item | ||||
|         :set_work_item_metadata, :create_note, :award_emoji, :move_work_item, :clone_work_item | ||||
|       ) | ||||
|       expect(permissions(group_guest_author, authored_work_item)).to be_disallowed( | ||||
|         :admin_work_item, :set_work_item_metadata, :move_work_item, :clone_work_item | ||||
|  | @ -220,11 +220,11 @@ RSpec.shared_examples 'abilities with group level work items license' do | |||
|       # allowed | ||||
|       expect(permissions(group_planner, work_item)).to be_allowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :admin_parent_link, | ||||
|         :set_work_item_metadata, :admin_work_item_link, :create_note, :move_work_item, :clone_work_item | ||||
|         :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji, :move_work_item, :clone_work_item | ||||
|       ) | ||||
|       expect(permissions(group_planner, confidential_work_item)).to be_allowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :admin_parent_link, | ||||
|         :set_work_item_metadata, :admin_work_item_link, :create_note, :move_work_item, :clone_work_item | ||||
|         :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji, :move_work_item, :clone_work_item | ||||
|       ) | ||||
| 
 | ||||
|       # disallowed | ||||
|  | @ -236,11 +236,11 @@ RSpec.shared_examples 'abilities with group level work items license' do | |||
|       # allowed | ||||
|       expect(permissions(group_reporter, work_item)).to be_allowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :admin_parent_link, | ||||
|         :set_work_item_metadata, :admin_work_item_link, :create_note, :move_work_item, :clone_work_item | ||||
|         :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji, :move_work_item, :clone_work_item | ||||
|       ) | ||||
|       expect(permissions(group_reporter, confidential_work_item)).to be_allowed( | ||||
|         :read_work_item, :read_issue, :read_note, :admin_work_item, :update_work_item, :admin_parent_link, | ||||
|         :set_work_item_metadata, :admin_work_item_link, :create_note, :move_work_item, :clone_work_item | ||||
|         :set_work_item_metadata, :admin_work_item_link, :create_note, :award_emoji, :move_work_item, :clone_work_item | ||||
|       ) | ||||
| 
 | ||||
|       # disallowed | ||||
|  |  | |||
|  | @ -496,7 +496,6 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do | |||
|         'Vulnerabilities::Statistics::AdjustmentWorker' => 3, | ||||
|         'VulnerabilityExports::ExportDeletionWorker' => 3, | ||||
|         'VulnerabilityExports::ExportWorker' => 3, | ||||
|         'VirtualRegistries::Packages::DestroyOrphanCachedResponsesWorker' => 0, | ||||
|         'VirtualRegistries::Packages::Cache::DestroyOrphanEntriesWorker' => 0, | ||||
|         'WaitForClusterCreationWorker' => 3, | ||||
|         'WebHookWorker' => 4, | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue