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,26 +187,40 @@ export default {
|
|||
:header-text="headerText"
|
||||
:items="dropdownOptions"
|
||||
/>
|
||||
<template v-else>
|
||||
<gl-form-input
|
||||
v-model="inputValue"
|
||||
v-validation:[form.showValidation]
|
||||
:aria-label="item.name"
|
||||
:data-field-type="item.type"
|
||||
:name="item.name"
|
||||
:pattern="item.regex"
|
||||
:required="item.required"
|
||||
:state="form.fields[item.name].state"
|
||||
data-testid="value-input"
|
||||
/>
|
||||
<div
|
||||
v-if="hasValidationFeedback"
|
||||
v-gl-tooltip="invalidTooltipTitle"
|
||||
class="gl-mt-4 gl-text-danger"
|
||||
data-testid="validation-feedback"
|
||||
>
|
||||
{{ validationFeedback }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 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"
|
||||
:data-field-type="item.type"
|
||||
:name="item.name"
|
||||
:pattern="item.regex"
|
||||
:required="item.required"
|
||||
:state="validationState"
|
||||
/>
|
||||
|
||||
<!-- Validation feedback -->
|
||||
<div
|
||||
v-if="hasValidationFeedback"
|
||||
class="gl-mt-4 gl-text-danger"
|
||||
data-testid="validation-feedback"
|
||||
>
|
||||
{{ validationFeedback }}
|
||||
</div>
|
||||
</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,20 +79,43 @@ 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">
|
||||
<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>
|
||||
</ul>
|
||||
<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>
|
||||
<board-card v-for="id in group.items" :key="id" :item="items[id]" />
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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,10 +157,17 @@ describe('DynamicValueRenderer', () => {
|
|||
props: { item: { ...defaultProps.item, type, default: value } },
|
||||
});
|
||||
|
||||
if (usesDropdown) {
|
||||
expect(findDropdown().props('selected')).toBe(expectedDisplayValue);
|
||||
} else {
|
||||
expect(findInput().props('value')).toBe(expectedDisplayValue);
|
||||
const inputType = getInputType(type, usesDropdown);
|
||||
|
||||
switch (inputType) {
|
||||
case 'dropdown':
|
||||
expect(findDropdown().props('selected')).toBe(expectedDisplayValue);
|
||||
break;
|
||||
case 'textarea':
|
||||
expect(findTextarea().props('value')).toBe(expectedDisplayValue);
|
||||
break;
|
||||
default:
|
||||
expect(findInput().props('value')).toBe(expectedDisplayValue);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -189,10 +188,17 @@ describe('DynamicValueRenderer', () => {
|
|||
props: { item: { ...defaultProps.item, type } },
|
||||
});
|
||||
|
||||
if (usesDropdown) {
|
||||
await findDropdown().vm.$emit('select', inputValue);
|
||||
} else {
|
||||
await findInput().vm.$emit('input', inputValue);
|
||||
const inputType = getInputType(type, usesDropdown);
|
||||
|
||||
switch (inputType) {
|
||||
case 'dropdown':
|
||||
await findDropdown().vm.$emit('select', inputValue);
|
||||
break;
|
||||
case 'textarea':
|
||||
await findTextarea().vm.$emit('input', inputValue);
|
||||
break;
|
||||
default:
|
||||
await findInput().vm.$emit('input', inputValue);
|
||||
}
|
||||
|
||||
expect(wrapper.emitted('update')[0][0].value).toEqual(expectedTypedValue);
|
||||
|
|
@ -202,69 +208,92 @@ describe('DynamicValueRenderer', () => {
|
|||
});
|
||||
|
||||
describe('validation', () => {
|
||||
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'}
|
||||
`('applies $description to input', ({ itemProps, attribute, expectedValue }) => {
|
||||
createComponent({
|
||||
props: {
|
||||
item: {
|
||||
...defaultProps.item,
|
||||
...itemProps,
|
||||
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: {
|
||||
item: {
|
||||
...defaultProps.item,
|
||||
...itemProps,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const inputComponent = itemProps.type === 'ARRAY' ? findTextarea() : findInput();
|
||||
expect(inputComponent.attributes(attribute)).toBe(expectedValue);
|
||||
});
|
||||
|
||||
expect(findInput().attributes(attribute)).toBe(expectedValue);
|
||||
});
|
||||
|
||||
// Separate test for required attribute because vue 2 and vue 3 handle required differently
|
||||
it('applies required attribute to input', () => {
|
||||
createComponent({
|
||||
props: {
|
||||
item: {
|
||||
...defaultProps.item,
|
||||
required: true,
|
||||
// Separate test for required attribute because vue 2 and vue 3 handle required differently
|
||||
it('applies required attribute to input', () => {
|
||||
createComponent({
|
||||
props: {
|
||||
item: {
|
||||
...defaultProps.item,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(findInput().element.hasAttribute('required')).toBe(true);
|
||||
});
|
||||
expect(findInput().element.hasAttribute('required')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
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 }) => {
|
||||
createComponent({
|
||||
props: {
|
||||
item: {
|
||||
...defaultProps.item,
|
||||
...itemProps,
|
||||
describe('invalid text feedback', () => {
|
||||
it('displays validation feedback when required field is empty', async () => {
|
||||
createComponent({
|
||||
props: {
|
||||
item: {
|
||||
...defaultProps.item,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await setInputValue('');
|
||||
|
||||
expect(findValidationFeedback().exists()).toBe(true);
|
||||
expect(findValidationFeedback().text()).toContain('This is required and must be defined.');
|
||||
});
|
||||
|
||||
await setInputValue(value);
|
||||
it('displays validation feedback for non-numeric value in number field', async () => {
|
||||
createComponent({
|
||||
props: {
|
||||
item: {
|
||||
...defaultProps.item,
|
||||
type: 'NUMBER',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(findValidationFeedback().exists()).toBe(true);
|
||||
expect(findValidationFeedback().text()).toContain(expectedMessage);
|
||||
});
|
||||
await setInputValue('not-a-number');
|
||||
|
||||
it('shows tooltip with pattern information for regex validation errors', async () => {
|
||||
createComponent();
|
||||
expect(findValidationFeedback().exists()).toBe(true);
|
||||
expect(findValidationFeedback().text()).toContain('The value must contain only numbers.');
|
||||
});
|
||||
|
||||
await setInputValue('123');
|
||||
it('includes pattern information in validation feedback for regex errors', async () => {
|
||||
const regex = '[a-z]+';
|
||||
createComponent({
|
||||
props: {
|
||||
item: {
|
||||
...defaultProps.item,
|
||||
regex,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const validationFeedbackTooltipDirective = getBinding(
|
||||
findValidationFeedback().element,
|
||||
'gl-tooltip',
|
||||
);
|
||||
expect(validationFeedbackTooltipDirective.value).toBe(`Pattern: ${defaultProps.item.regex}`);
|
||||
await setInputValue('123');
|
||||
|
||||
expect(findValidationFeedback().exists()).toBe(true);
|
||||
expect(findValidationFeedback().text()).toContain(
|
||||
'The value must match the defined regular expression.',
|
||||
);
|
||||
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
|
||||
include Rails::Generators::Testing::Behaviour
|
||||
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
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ RSpec.shared_examples 'rejecting protection rules request when handling rule ids
|
|||
let(:url) { "/projects/#{project_id}/#{path}" }
|
||||
|
||||
where(:project_id, :protection_rule_id, :status) do
|
||||
ref(:valid_project_id) | 'invalid' | :bad_request
|
||||
ref(:valid_project_id) | non_existing_record_id | :not_found
|
||||
ref(:valid_project_id) | 'invalid' | :bad_request
|
||||
ref(:valid_project_id) | non_existing_record_id | :not_found
|
||||
ref(:other_project_id) | ref(:valid_protection_rule_id) | :not_found
|
||||
end
|
||||
|
||||
|
|
@ -44,8 +44,8 @@ RSpec.shared_examples 'rejecting protection rules request when invalid project'
|
|||
let(:url) { "/projects/#{project_id}/#{path}" }
|
||||
|
||||
where(:project_id, :status) do
|
||||
'invalid' | :not_found
|
||||
non_existing_record_id | :not_found
|
||||
'invalid' | :not_found
|
||||
non_existing_record_id | :not_found
|
||||
end
|
||||
|
||||
with_them do
|
||||
|
|
|
|||
|
|
@ -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