Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-03-25 15:08:02 +00:00
parent 307115302b
commit 54a2245581
69 changed files with 1113 additions and 339 deletions

View File

@ -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 {

View File

@ -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>

View File

@ -1,6 +1,6 @@
<script>
import { GlSkeletonLoader } from '@gitlab/ui';
import { getSkeletonRectProps } from './utils';
import { getSkeletonRectProps } from '../utils';
export default {
name: 'InputsTableSkeletonLoader',

View File

@ -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,

View File

@ -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() {

View File

@ -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>

View File

@ -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>

View File

@ -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,

View File

@ -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,

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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:)

View File

@ -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)

View File

@ -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

View File

@ -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? }

View File

@ -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

View File

@ -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).

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
ec6bfbed328bb354b47710c9a3e88b1550cf80378360f99640663a253f35d474

View File

@ -0,0 +1 @@
c90c2e7d28eb5a3514b71519f45fa4221a90557dbd77724652c7ade09e4de60c

View File

@ -0,0 +1 @@
8a1662c05b7fc79ad7769c024eba2e1996b4581b75b5f80dc490f05e6e1f01b0

View File

@ -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();

View File

@ -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 >}}

View File

@ -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`

View File

@ -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:

View File

@ -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.

View File

@ -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. |

View File

@ -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`.

View File

@ -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`

View File

@ -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:

View File

@ -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.

View File

@ -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).

View File

@ -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.

View File

@ -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

View File

@ -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).

View File

@ -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

View File

@ -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?

View File

@ -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,

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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}."

View File

@ -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 {

View File

@ -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}`);
});
});
});
});

View File

@ -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;

View File

@ -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', () => {

View File

@ -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

View File

@ -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

View File

@ -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" }

View File

@ -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. " \

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,