Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-05-15 15:14:53 +00:00
parent 03a752ebe4
commit 7151b07ad4
68 changed files with 1381 additions and 467 deletions

View File

@ -0,0 +1,105 @@
# The methods listed here have been identified as "unused" by the linter
# scripts/lint/unused_helper_methods.rb, and are potential targets for future
# removal.
#
# If it turns out that a method you are attempting to remove is in fact in use,
# remove it from this file and add it to `excluded_methods.yml`.
#
tag_pair_for_link:
file: ee/app/helpers/admin/application_settings_helper.rb
start_free_trial_data:
file: ee/app/helpers/billing_plans_helper.rb
compliance_frameworks_list_data:
file: ee/app/helpers/compliance_management/compliance_framework/group_settings_helper.rb
project_vulnerability_path:
file: ee/app/helpers/ee/gitlab_routing_helper.rb
user_group_saml_omniauth_metadata_path:
file: ee/app/helpers/ee/gitlab_routing_helper.rb
saas_user_caps_i18n_string:
file: ee/app/helpers/ee/groups/settings_helper.rb
project_compliance_framework_app_data:
file: ee/app/helpers/ee/projects_helper.rb
discover_page_hand_raise_lead_data:
file: ee/app/helpers/gitlab_subscriptions/hand_raise_leads_helper.rb
discover_duo_pro_hand_raise_lead_data:
file: ee/app/helpers/gitlab_subscriptions/hand_raise_leads_helper.rb
buy_addon_data:
file: ee/app/helpers/subscriptions_helper.rb
group_icon:
file: app/helpers/avatars_helper.rb
topic_icon:
file: app/helpers/avatars_helper.rb
project_branches:
file: app/helpers/branches_helper.rb
can_view_namespace_catalog?:
file: app/helpers/ci/catalog/resources_helper.rb
js_ci_catalog_data:
file: app/helpers/ci/catalog/resources_helper.rb
environments_list_data:
file: app/helpers/environments_helper.rb
event_feed_title:
file: app/helpers/events_helper.rb
event_feed_summary:
file: app/helpers/events_helper.rb
group_title_link:
file: app/helpers/groups_helper.rb
has_dismissed_ide_environments_callout?:
file: app/helpers/ide_helper.rb
integration_issue_type:
file: app/helpers/integrations_helper.rb
integration_todo_target_type:
file: app/helpers/integrations_helper.rb
user_dropdown_label:
file: app/helpers/issuables_helper.rb
project_dropdown_label:
file: app/helpers/issuables_helper.rb
group_dropdown_label:
file: app/helpers/issuables_helper.rb
text_color_for_bg:
file: app/helpers/labels_helper.rb
manage_labels_title:
file: app/helpers/labels_helper.rb
show_projects?:
file: app/helpers/projects_helper.rb
request_access_group_members_path:
file: app/helpers/routing/groups/members_helper.rb
approve_access_request_group_member_path:
file: app/helpers/routing/groups/members_helper.rb
resend_invite_group_member_path:
file: app/helpers/routing/groups/members_helper.rb
edit_pipeline_schedule_path:
file: app/helpers/routing/pipeline_schedules_helper.rb
play_pipeline_schedule_path:
file: app/helpers/routing/pipeline_schedules_helper.rb
take_ownership_pipeline_schedule_path:
file: app/helpers/routing/pipeline_schedules_helper.rb
request_access_project_members_path:
file: app/helpers/routing/projects/members_helper.rb
leave_project_members_path:
file: app/helpers/routing/projects/members_helper.rb
approve_access_request_project_member_path:
file: app/helpers/routing/projects/members_helper.rb
resend_invite_project_member_path:
file: app/helpers/routing/projects/members_helper.rb
search_filter_link:
file: app/helpers/search_helper.rb
sidebar_tracking_attributes_by_object:
file: app/helpers/sidebars_helper.rb
scope_avatar_classes:
file: app/helpers/sidebars_helper.rb
sort_value_stars_asc:
file: app/helpers/sorting_titles_values_helper.rb
display_subscription_banner!;:
file: app/helpers/subscribable_banner_helper.rb
group_or_project_milestone_path:
file: app/helpers/timeboxes_helper.rb
can_admin_project_milestones?:
file: app/helpers/timeboxes_helper.rb
render_two_factor_auth_recovery_settings_check;:
file: app/helpers/users/callouts_helper.rb
dismiss_two_factor_auth_recovery_settings_check;:
file: app/helpers/users/callouts_helper.rb
all_visibility_levels_restricted?:
file: app/helpers/visibility_level_helper.rb
link_to_wiki_page:
file: app/helpers/wiki_helper.rb

View File

@ -1 +1 @@
da07ea6b72e37e8d07020e7be6cd4a378ddc442a
bad4daa394053a0f74090abdda47f9035f4e3f9a

View File

@ -18,7 +18,10 @@ export default {
class="gl-mx-3"
icon="close"
category="tertiary"
:to="$options.DESIGNS_ROUTE_NAME"
:to="{
name: $options.DESIGNS_ROUTE_NAME,
query: $route.query,
}"
/>
</div>
</template>

View File

@ -1,7 +1,8 @@
<script>
import { GlIcon, GlButton, GlBadge } from '@gitlab/ui';
import { GlIcon, GlButton, GlBadge, GlTooltipDirective } from '@gitlab/ui';
import NumberToHumanSize from '~/vue_shared/components/number_to_human_size/number_to_human_size.vue';
import UserDate from '~/vue_shared/components/user_date.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { SHORT_DATE_FORMAT_WITH_TIME } from '~/vue_shared/constants';
import { s__ } from '~/locale';
import { joinPaths } from '~/lib/utils/url_utility';
@ -16,6 +17,10 @@ export default {
GlIcon,
GlButton,
GlBadge,
TimeAgo,
},
directives: {
GlTooltip: GlTooltipDirective,
},
i18n: {
deleteError: s__(
@ -27,8 +32,6 @@ export default {
error: s__('Pages|Has error'),
activeState: s__('Pages|Active'),
stoppedState: s__('Pages|Stopped'),
primaryDeploymentTitle: s__('Pages|Primary deployment'),
pathPrefixLabel: s__('Pages|Path prefix'),
createdLabel: s__('Pages|Created'),
deployJobLabel: s__('Pages|Deploy job'),
rootDirLabel: s__('Pages|Root directory'),
@ -39,6 +42,7 @@ export default {
deleteBtnLabel: s__('Pages|Delete'),
restoreBtnLabel: s__('Pages|Restore'),
expiresAtLabel: s__('Pages|Expires at'),
neverExpires: s__('Pages|Never expires'),
},
static: {
SHORT_DATE_FORMAT_WITH_TIME,
@ -53,25 +57,12 @@ export default {
data() {
return {
hasError: false,
showDetail: false,
deleteInProgress: false,
restoreInProgress: false,
detailContainerHeight: 0,
};
},
computed: {
isPrimary() {
return !this.deployment.pathPrefix;
},
detailHeight() {
return this.showDetail ? this.detailContainerHeight : 0;
},
detailStyle() {
return {
height: `${this.detailHeight}px`,
visibility: this.showDetail ? 'visible' : 'hidden',
};
},
ciBuildUrl() {
return joinPaths(
gon.relative_url_root || '/',
@ -80,14 +71,14 @@ export default {
`${this.deployment.ciBuildId}`,
);
},
formattedRootDirectory() {
return `/${this.deployment.rootDirectory || 'public'}`;
},
},
mounted() {
this.calculateDetailHeight();
},
methods: {
toggleDetail() {
this.showDetail = !this.showDetail;
},
calculateDetailHeight() {
this.detailContainerHeight = this.$refs.details?.scrollHeight;
},
@ -137,203 +128,150 @@ export default {
</script>
<template>
<li class="gl-flex gl-flex-col gl-gap-2" @click="toggleDetail">
<div class="gl-flex gl-items-center gl-justify-start gl-gap-3">
<div class="gl-flex gl-justify-center gl-gap-3">
<div data-testid="deployment-state">
<gl-badge
v-if="hasError"
variant="danger"
size="sm"
icon="error"
icon-size="sm"
data-testid="error-badge"
>
{{ $options.i18n.error }}
</gl-badge>
<gl-badge
v-if="deployment.active"
variant="success"
size="sm"
icon="check-circle-filled"
icon-size="sm"
>
{{ $options.i18n.activeState }}
</gl-badge>
<gl-badge v-else variant="neutral" size="sm" icon="status-stopped" icon-size="sm">
{{ $options.i18n.stoppedState }}
</gl-badge>
</div>
</div>
</div>
<li
class="!gl-grid gl-grid-cols-[1fr,1fr] gl-gap-2 gl-py-4 md:gl-grid-cols-[1fr,3fr,2fr] md:gl-gap-0"
>
<div
class="gl-flex gl-flex-col gl-gap-4 gl-overflow-hidden md:gl-flex-row md:gl-items-center md:gl-justify-between md:gl-gap-7"
class="gl-flex gl-flex-col gl-items-start gl-justify-center gl-gap-2 md:gl-justify-start"
data-testid="deployment-state"
>
<div class="gl-flex gl-items-center gl-gap-4">
<div>
<gl-icon
name="chevron-lg-right"
:class="{ 'gl-rotate-90': showDetail }"
class="reduce-motion:gl-transition-none gl-transition-transform"
variant="subtle"
/>
</div>
<div data-testid="deployment-type" class="gl-flex gl-flex-col gl-gap-2 gl-text-nowrap">
<template v-if="isPrimary">
<gl-icon name="home" class="mr-1" variant="subtle" />
<span class="sr-only">
{{ $options.i18n.primaryDeploymentTitle }}
</span>
</template>
<template v-else>
<div class="gl-sr-only">{{ $options.i18n.pathPrefixLabel }}</div>
<div>
<gl-icon name="environment" class="mr-1" variant="subtle" />
{{ deployment.pathPrefix }}
</div>
</template>
</div>
<div class="gl-flex gl-flex-col gl-gap-2 gl-truncate gl-text-nowrap">
<div class="gl-flex gl-items-center gl-gap-2" data-testid="deployment-url">
<a
v-if="deployment.active"
:href="deployment.url"
target="_blank"
class="gl-w-full gl-truncate"
@click.stop
>
{{ deployment.url }}
</a>
<span v-else class="gl-w-full gl-truncate gl-text-subtle">
{{ deployment.url }}
</span>
</div>
</div>
</div>
<div class="gl-flex gl-flex-col gl-items-stretch gl-gap-5 md:gl-items-end">
<div class="gl-flex gl-items-end gl-justify-between gl-gap-6 md:gl-justify-end">
<div
class="gl-flex gl-flex-col gl-gap-2 gl-text-nowrap"
data-testid="deployment-created-at"
>
<div class="gl-text-sm gl-text-subtle">{{ $options.i18n.createdLabel }}</div>
<div>
<gl-icon name="play" class="mr-1" variant="subtle" />
<user-date
:date="deployment.createdAt"
:date-format="$options.static.SHORT_DATE_FORMAT_WITH_TIME"
/>
</div>
</div>
</div>
</div>
</div>
<div
ref="details"
:style="detailStyle"
data-testid="deployment-details"
class="gl-flex gl-flex-col gl-gap-4 gl-overflow-hidden gl-transition-all motion-reduce:gl-transition-none md:gl-flex-row md:gl-gap-7"
>
<div class="gl-flex gl-flex-col gl-gap-2 gl-text-nowrap" data-testid="deployment-ci-build-id">
<div class="gl-text-sm gl-text-subtle">{{ $options.i18n.deployJobLabel }}</div>
<div>
<gl-icon name="deployments" class="mr-1" variant="subtle" />
<a :href="ciBuildUrl" @click.stop>
{{ deployment.ciBuildId }}
</a>
</div>
</div>
<div
class="gl-flex gl-flex-col gl-gap-2 gl-text-nowrap"
data-testid="deployment-root-directory"
<gl-badge
v-if="hasError"
variant="danger"
size="sm"
icon="error"
icon-size="sm"
data-testid="error-badge"
>
<div class="gl-text-sm gl-text-subtle">{{ $options.i18n.rootDirLabel }}</div>
<div>
<gl-icon name="folder" class="mr-1" variant="subtle" />
/{{ deployment.rootDirectory || 'public' }}
</div>
{{ $options.i18n.error }}
</gl-badge>
<gl-badge
v-if="deployment.active"
variant="success"
size="sm"
icon="check-circle-filled"
icon-size="sm"
>
{{ $options.i18n.activeState }}
</gl-badge>
<gl-badge v-else variant="neutral" size="sm" icon="status-stopped" icon-size="sm">
{{ $options.i18n.stoppedState }}
</gl-badge>
</div>
<div
class="gl-col-start-1 gl-row-start-2 gl-flex gl-flex-col gl-gap-2 md:gl-col-start-2 md:gl-row-start-1"
>
<div data-testid="deployment-url">
<a
v-if="deployment.active"
:href="deployment.url"
target="_blank"
class="gl-w-full gl-truncate !gl-text-link"
@click.stop
>
{{ deployment.url }}
</a>
<span v-else class="gl-w-full gl-truncate gl-text-subtle">
{{ deployment.url }}
</span>
</div>
<div class="gl-flex gl-flex-col gl-gap-2 gl-text-nowrap" data-testid="deployment-file-count">
<div class="gl-text-sm gl-text-subtle">{{ $options.i18n.filesLabel }}</div>
<div>
<gl-icon name="documents" class="mr-1" variant="subtle" />
{{ deployment.fileCount }}
</div>
</div>
<div class="gl-flex gl-flex-col gl-gap-2 gl-text-nowrap" data-testid="deployment-size">
<div class="gl-text-sm gl-text-subtle">{{ $options.i18n.sizeLabel }}</div>
<div>
<gl-icon name="disk" class="mr-1" variant="subtle" />
<p class="gl-mb-0" data-testid="deployment-ci-build-id">
<gl-icon name="deployments" />
<span class="gl-text-subtle">{{ $options.i18n.deployJobLabel }}:</span>
<a :href="ciBuildUrl" class="!gl-text-link" @click.stop>
{{ deployment.ciBuildId }}
</a>
</p>
<p class="gl-mb-0 gl-flex gl-items-center gl-gap-2 gl-text-subtle">
<gl-icon name="folder" />
<span
v-gl-tooltip
:title="$options.i18n.rootDirLabel"
data-testid="deployment-root-directory"
>{{ formattedRootDirectory }}</span
>
<span aria-hidden="true">·</span>
<span data-testid="deployment-file-count"
>{{ deployment.fileCount }} {{ $options.i18n.filesLabel }}</span
>
<span aria-hidden="true">·</span>
<span data-testid="deployment-size">
{{ deployment.sizeLabel }}
<number-to-human-size :value="deployment.size" />
</div>
</div>
<div class="gl-flex gl-flex-col gl-gap-2 gl-text-nowrap" data-testid="deployment-updated-at">
<div class="gl-text-sm gl-text-subtle">{{ $options.i18n.lastUpdatedLabel }}</div>
<div>
<gl-icon name="clear-all" class="mr-1" variant="subtle" />
<user-date
:date="deployment.updatedAt"
:date-format="$options.static.SHORT_DATE_FORMAT_WITH_TIME"
/>
</div>
</div>
<div
v-if="deployment.active && deployment.expiresAt"
class="gl-flex gl-flex-col gl-gap-2 gl-text-nowrap"
data-testid="deployment-expires-at"
>
<div class="gl-text-sm gl-text-subtle">
{{ $options.i18n.expiresAtLabel }}
</div>
<div>
<gl-icon name="remove" class="gl-mr-2" variant="subtle" />
<user-date
:date="deployment.expiresAt"
:date-format="$options.static.SHORT_DATE_FORMAT_WITH_TIME"
/>
</div>
</div>
<div v-if="!deployment.active" class="gl-flex gl-flex-col gl-gap-2 gl-text-nowrap">
<div class="gl-text-sm gl-text-subtle">
</span>
</p>
</div>
<div
class="gl-col-start-1 gl-row-start-3 gl-mt-3 gl-flex gl-flex-col gl-gap-2 md:gl-col-start-2 md:gl-flex-row md:gl-items-center"
>
<p class="gl-mb-0 gl-text-sm gl-text-subtle" data-testid="deployment-created-at">
{{ $options.i18n.createdLabel }}
<time-ago :time="deployment.createdAt" />
</p>
<template v-if="deployment.updatedAt">
<span class="gl-hidden md:gl-inline" aria-hidden="true">·</span>
<p class="gl-mb-0 gl-text-sm gl-text-subtle" data-testid="deployment-updated-at">
{{ $options.i18n.lastUpdatedLabel }}
<time-ago :time="deployment.updatedAt" />
</p>
</template>
</div>
<div
class="gl-col-start-2 gl-row-start-1 gl-flex gl-flex-col gl-items-end gl-justify-between gl-gap-2 md:gl-col-start-3"
data-testid="deployment-details"
>
<gl-button
v-if="deployment.active"
v-gl-tooltip
icon="remove"
category="tertiary"
:title="$options.i18n.deleteBtnLabel"
:loading="deleteInProgress"
data-testid="deployment-delete"
@click.stop="deleteDeployment"
/>
<gl-button
v-else
v-gl-tooltip
icon="redo"
category="tertiary"
:title="$options.i18n.restoreBtnLabel"
:loading="restoreInProgress"
data-testid="deployment-restore"
@click.stop="restoreDeployment"
/>
</div>
<div
class="gl-col-start-1 gl-row-start-4 gl-flex gl-flex-col gl-justify-between gl-gap-2 md:gl-col-start-3 md:gl-row-start-3 md:gl-mt-3 md:gl-items-end"
>
<template v-if="!deployment.active">
<p class="gl-mb-0 gl-text-sm gl-text-danger">
{{ $options.i18n.deleteScheduledAtLabel }}
</div>
<div>
<gl-icon name="remove" class="mr-1" variant="subtle" />
<user-date
:date="deployment.deletedAt"
:date-format="$options.static.SHORT_DATE_FORMAT_WITH_TIME"
/>
</div>
</div>
<div class="gl-hidden gl-flex-grow md:gl-block"></div>
<div class="gl-flex gl-items-end md:gl-h-full">
<gl-button
v-if="deployment.active"
icon="remove"
category="secondary"
variant="danger"
size="small"
:loading="deleteInProgress"
data-testid="deployment-delete"
@click.stop="deleteDeployment"
>
{{ $options.i18n.deleteBtnLabel }}
</gl-button>
<gl-button
v-else
icon="redo"
category="secondary"
variant="confirm"
size="small"
:loading="restoreInProgress"
data-testid="deployment-restore"
@click.stop="restoreDeployment"
>
{{ $options.i18n.restoreBtnLabel }}
</gl-button>
</div>
</p>
</template>
<p v-else class="gl-mb-0 gl-text-sm gl-text-subtle" data-testid="deployment-expires-at">
<template v-if="deployment.expiresAt">
{{ $options.i18n.expiresAtLabel }}
<user-date
:date="deployment.expiresAt"
:date-format="$options.static.SHORT_DATE_FORMAT_WITH_TIME"
/>
</template>
<template v-else>{{ $options.i18n.neverExpires }}</template>
</p>
</div>
</li>
</template>
<style scoped></style>

View File

@ -4,7 +4,8 @@ import { debounce, isEmpty } from 'lodash';
import { __ } from '~/locale';
import { getUsers } from '~/rest_api';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { memberName } from '../utils/member_utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { memberName, searchUsers } from '../utils/member_utils';
import {
SEARCH_DELAY,
USERS_FILTER_ALL,
@ -22,6 +23,8 @@ export default {
GlIcon,
GlSprintf,
},
mixins: [glFeatureFlagsMixin()],
inject: ['searchUrl'],
props: {
placeholder: {
type: String,
@ -133,6 +136,9 @@ export default {
}));
},
retrieveUsersRequest() {
if (this.glFeatures.newImplementationOfInviteMembersSearch) {
return searchUsers(this.searchUrl, this.query);
}
return getUsers(this.query, this.queryOptions);
},
retrieveUsers: debounce(async function debouncedRetrieveUsers() {

View File

@ -29,6 +29,7 @@ export default (function initInviteMembersModal() {
hasGitlabSubscription: parseBoolean(el.dataset.hasGitlabSubscription),
addSeatsHref: el.dataset.addSeatsHref,
hasBsoEnabled: parseBoolean(el.dataset.hasBsoFeatureEnabled),
searchUrl: el.dataset.searchUrl,
},
render: (createElement) =>
createElement(InviteMembersModal, {

View File

@ -1,8 +1,20 @@
import { DEFAULT_PER_PAGE } from '~/api';
import axios from '~/lib/utils/axios_utils';
export function memberName(member) {
// user defined tokens(invites by email) will have email in `name` and will not contain `username`
return member.username || member.name;
}
export function searchUsers(url, search) {
return axios.get(url, {
params: {
search,
per_page: DEFAULT_PER_PAGE,
},
});
}
export function triggerExternalAlert() {
return false;
}

View File

@ -23,11 +23,11 @@ export const TRACKING_HANDLE_LABEL_MAP = {
export const COMMON_HANDLES = [COMMAND_HANDLE, USER_HANDLE, PROJECT_HANDLE, PATH_HANDLE];
export const SEARCH_SCOPE_PLACEHOLDER = {
[COMMAND_HANDLE]: s__('CommandPalette|command'),
[USER_HANDLE]: s__('CommandPalette|user (enter at least 3 chars)'),
[PROJECT_HANDLE]: s__('CommandPalette|project (enter at least 3 chars)'),
[COMMAND_HANDLE]: s__('CommandPalette|Search for a page or action'),
[USER_HANDLE]: s__('CommandPalette|Search by username (minimum 3 characters)'),
[PROJECT_HANDLE]: s__('CommandPalette|Search by project (minimum 3 characters)'),
[ISSUE_HANDLE]: s__('CommandPalette|issue (enter at least 3 chars)'),
[PATH_HANDLE]: s__('CommandPalette|go to project file'),
[PATH_HANDLE]: s__('CommandPalette|Search by filename'),
};
export const SEARCH_SCOPE = {

View File

@ -18,7 +18,10 @@ export default {
class="gl-mx-3"
icon="close"
category="tertiary"
:to="$options.ROUTES.workItem"
:to="{
name: $options.ROUTES.workItem,
query: $route.query,
}"
/>
</div>
</template>

View File

@ -301,6 +301,10 @@ export default {
}
if (this.isEditing && this.createFlow) {
this.startEditing();
// Reset edit state as the description
// can also be populated from localStorage
// when creating a new work item.
this.wasEdited = false;
}
},
error() {
@ -328,6 +332,18 @@ export default {
if (this.descriptionTemplate === this.descriptionText) {
return;
}
if (this.createFlow && !this.wasEdited && hasContent && this.appliedTemplate === '') {
// If the template was fetched on component mount
// while in create flow, we may also have populated
// the description from localStorage. In this case,
// we need avoid showing the warning on first load.
// while also setting appliedTemplate to the current
// template such that reset is possible.
this.appliedTemplate = this.descriptionTemplate;
this.wasEdited = true;
return;
}
if (!isUnchangedTemplate && (isDirty || hasContent)) {
this.showTemplateApplyWarning = true;
} else {

View File

@ -83,7 +83,7 @@
position: sticky;
top: calc(#{$calc-application-header-height} + #{$settings-sticky-header-height - $gl-spacing-scale-3});
left: 0;
margin-top: -1px;
margin-top: -2px; // Fix retina issue with half pixel rendering
width: 100%;
height: 1px;
box-shadow: 0 #{$gl-spacing-scale-3} 0 var(--gl-border-color-default);

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Members
module InviteModalActions
extend ActiveSupport::Concern
def invite_search
users = Members::InviteUsersFinder.new(current_user, source, search: invite_search_params[:search]).execute
.page(1)
.per(invite_search_per_page)
render json: UserSerializer.new.represent(users)
end
private
def invite_search_per_page
(pagination_params[:per_page] || 20).to_i
end
def invite_search_params
params.permit(:search)
end
end
end

View File

@ -2,6 +2,7 @@
class Groups::GroupMembersController < Groups::ApplicationController
include MembershipActions
include Members::InviteModalActions
include MembersPresentation
include SortingHelper
include Gitlab::Utils::StrongMemoize

View File

@ -2,6 +2,7 @@
class Projects::ProjectMembersController < Projects::ApplicationController
include MembershipActions
include Members::InviteModalActions
include MembersPresentation
include SortingHelper

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
# Used for searching users that can be added to group/project members
#
# Arguments:
# current_user - which user use
# resource - group or project
# search: string
module Members
class InviteUsersFinder < UsersFinder
attr_reader :resource
def initialize(current_user, resource, search: nil)
@current_user = current_user
@resource = resource
@params = { search: search }
end
def base_scope
users = User.active.without_project_bot
users = scope_for_resource(users)
users.order_id_desc
end
def scope_for_resource(users)
users
end
end
end
Members::InviteUsersFinder.prepend_mod

View File

@ -309,16 +309,24 @@ class ContainerRepository < ApplicationRecord
self.find_by(project: path.repository_project, name: path.repository_name)
end
def has_protected_tag_rules_for_delete?(user)
return true if user.nil?
def protected_from_delete_by_tag_rules?(user)
return true unless user
# Check for immutable tag protection rules
if Feature.enabled?(:container_registry_immutable_tags, project) && project.has_container_registry_immutable_tag_rules? && has_tags?
return true
end
# Admins are not restricted by mutable tag protection rules
return false if user.can_admin_all_resources?
# Check for mutable tag protection rules
return false unless project.has_container_registry_protected_tag_rules?(
action: 'delete',
access_level: project.team.max_member_access(user.id)
access_level: project.team.max_member_access(user.id),
include_immutable: false
)
# This is an API call so we put it last
return false unless has_tags?
true

View File

@ -3543,9 +3543,15 @@ class Project < ApplicationRecord
end
end
def has_container_registry_protected_tag_rules?(action:, access_level:)
strong_memoize_with(:has_container_registry_protected_tag_rules, action, access_level) do
container_registry_protection_tag_rules.for_actions_and_access([action], access_level).exists?
def has_container_registry_protected_tag_rules?(action:, access_level:, include_immutable: true)
strong_memoize_with(:has_container_registry_protected_tag_rules, action, access_level, include_immutable) do
container_registry_protection_tag_rules.for_actions_and_access([action], access_level, include_immutable:).exists?
end
end
def has_container_registry_immutable_tag_rules?
strong_memoize_with(:has_container_registry_immutable_tag_rules) do
container_registry_protection_tag_rules.immutable.exists?
end
end

View File

@ -3,7 +3,7 @@
class ContainerRepositoryPolicy < BasePolicy
delegate { @subject.project }
condition(:protected_for_delete) { @subject.has_protected_tag_rules_for_delete?(@user) }
condition(:protected_for_delete) { @subject.protected_from_delete_by_tag_rules?(@user) }
rule { protected_for_delete }.policy do
prevent :destroy_container_image

View File

@ -3,4 +3,5 @@
.js-invite-members-modal{ data: { is_project: 'false',
access_levels: access_level_roles_user_can_assign(group, group.access_level_roles).to_json,
reload_page_on_submit: content_for(:reload_on_member_invite_success).present?.to_s,
search_url: invite_search_group_group_members_url(group, format: :json),
help_link: help_page_url('user/permissions.md') }.merge(common_invite_modal_dataset(group)).merge(users_filter_data(group)) }

View File

@ -3,4 +3,5 @@
.js-invite-members-modal{ data: { is_project: 'true',
access_levels: ProjectMember.permissible_access_level_roles(current_user, project).to_json,
reload_page_on_submit: content_for(:reload_on_member_invite_success).present?.to_s,
search_url: invite_search_namespace_project_project_members_url(namespace_id: project.namespace, project_id: project, format: :json),
help_link: help_page_url('user/permissions.md') }.merge(common_invite_modal_dataset(project)).merge(users_filter_data(project.group)) }

View File

@ -0,0 +1,10 @@
---
name: new_implementation_of_invite_members_search
description: New implementation of "Invite Members" search
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/460261
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/190070
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/540306
milestone: '18.1'
group: group::authentication
type: gitlab_com_derisk
default_enabled: false

View File

@ -128,6 +128,8 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
end
delete :leave
get :invite_search, format: :json
end
end

View File

@ -189,6 +189,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :project_members, except: [:show, :new, :create, :edit], constraints: { id: %r{[a-zA-Z./0-9_\-#%+:]+} }, concerns: :access_requestable do
collection do
delete :leave
get :invite_search, format: :json
end
member do

View File

@ -1,8 +1,9 @@
---
migration_job_name: BackfillProtectedBranchMergeAccessLevelsProtectedBranchNamespaceId
description: Backfills sharding key `protected_branch_merge_access_levels.protected_branch_namespace_id` from `protected_branches`.
description: Backfills sharding key `protected_branch_merge_access_levels.protected_branch_namespace_id`
from `protected_branches`.
feature_category: source_code_management
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/174564
milestone: '17.7'
queued_migration_version: 20241204130230
finalized_by: # version of the migration that finalized this BBM
finalized_by: '20250511231556'

View File

@ -0,0 +1,11 @@
---
table_name: compromised_password_detections
classes:
- Users::CompromisedPasswordDetection
feature_categories:
- system_access
description: Stores detections of user passwords being compromised
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/191112
milestone: '18.1'
gitlab_schema: gitlab_main_clusterwide
table_size: small

View File

@ -8,14 +8,6 @@ description: PyPI package metadata
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27632
milestone: '13.0'
gitlab_schema: gitlab_main_cell
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: package_id
table: packages_packages
sharding_key: project_id
belongs_to: package
desired_sharding_key_migration_job_name: BackfillPackagesPypiMetadataProjectId
sharding_key:
project_id: projects
table_size: small

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
class CreateCompromisedPasswordDetections < Gitlab::Database::Migration[2.3]
disable_ddl_transaction!
milestone '18.1'
def up
create_table :compromised_password_detections do |t|
t.timestamps_with_timezone null: false
t.datetime_with_timezone :resolved_at, null: true, index: false
t.references :user,
null: false,
foreign_key: { on_delete: :cascade },
index: true
t.index :user_id,
name: "index_unresolved_compromised_password_detection_on_user_id",
unique: true,
where: "resolved_at IS NULL"
end
end
def down
drop_table :compromised_password_detections
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class FinalizeHkBackfillProtectedBranchMergeAccessLevelsProtectedBranch61982 < Gitlab::Database::Migration[2.3]
milestone '18.1'
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_main_cell
def up
ensure_batched_background_migration_is_finished(
job_class_name: 'BackfillProtectedBranchMergeAccessLevelsProtectedBranchNamespaceId',
table_name: :protected_branch_merge_access_levels,
column_name: :id,
job_arguments: [:protected_branch_namespace_id, :protected_branches, :namespace_id, :protected_branch_id],
finalize: true
)
end
def down; end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class AddPackagesPypiMetadataProjectIdNotNull < Gitlab::Database::Migration[2.3]
milestone '18.1'
disable_ddl_transaction!
def up
add_not_null_constraint :packages_pypi_metadata, :project_id
end
def down
remove_not_null_constraint :packages_pypi_metadata, :project_id
end
end

View File

@ -0,0 +1 @@
ccfc2c9f52ef631b077ff6dedcd50966fe0390476ed52198c21e380e6a3968a0

View File

@ -0,0 +1 @@
f1c7700c9cc75028eb9cc9fb1e03be37b28b603ff70b4b7bfaae965112efb041

View File

@ -0,0 +1 @@
dd9a26d16d78fe6d6fdc3dd20137eb83b53952f76bc4e2b6de5d4b732dd238dc

View File

@ -12507,6 +12507,23 @@ CREATE SEQUENCE compliance_requirements_id_seq
ALTER SEQUENCE compliance_requirements_id_seq OWNED BY compliance_requirements.id;
CREATE TABLE compromised_password_detections (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
resolved_at timestamp with time zone,
user_id bigint NOT NULL
);
CREATE SEQUENCE compromised_password_detections_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE compromised_password_detections_id_seq OWNED BY compromised_password_detections.id;
CREATE TABLE container_expiration_policies (
project_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
@ -19453,6 +19470,7 @@ CREATE TABLE packages_pypi_metadata (
CONSTRAINT check_379019d5da CHECK ((char_length(required_python) <= 255)),
CONSTRAINT check_65d8dbbd9f CHECK ((char_length(author_email) <= 2048)),
CONSTRAINT check_76afb6d4f3 CHECK ((char_length(summary) <= 255)),
CONSTRAINT check_77e2d63abb CHECK ((project_id IS NOT NULL)),
CONSTRAINT check_80308aa9bd CHECK ((char_length(description) <= 4000)),
CONSTRAINT check_b1f32be96c CHECK ((char_length(description_content_type) <= 128))
);
@ -27004,6 +27022,8 @@ ALTER TABLE ONLY compliance_requirements ALTER COLUMN id SET DEFAULT nextval('co
ALTER TABLE ONLY compliance_requirements_controls ALTER COLUMN id SET DEFAULT nextval('compliance_requirements_controls_id_seq'::regclass);
ALTER TABLE ONLY compromised_password_detections ALTER COLUMN id SET DEFAULT nextval('compromised_password_detections_id_seq'::regclass);
ALTER TABLE ONLY container_registry_protection_rules ALTER COLUMN id SET DEFAULT nextval('container_registry_protection_rules_id_seq'::regclass);
ALTER TABLE ONLY container_registry_protection_tag_rules ALTER COLUMN id SET DEFAULT nextval('container_registry_protection_tag_rules_id_seq'::regclass);
@ -29315,6 +29335,9 @@ ALTER TABLE ONLY compliance_requirements_controls
ALTER TABLE ONLY compliance_requirements
ADD CONSTRAINT compliance_requirements_pkey PRIMARY KEY (id);
ALTER TABLE ONLY compromised_password_detections
ADD CONSTRAINT compromised_password_detections_pkey PRIMARY KEY (id);
ALTER TABLE ONLY virtual_registries_packages_maven_registry_upstreams
ADD CONSTRAINT constraint_vreg_pkgs_mvn_reg_upst_on_unique_regid_pos UNIQUE (registry_id, "position") DEFERRABLE INITIALLY DEFERRED;
@ -34534,6 +34557,8 @@ CREATE INDEX index_compliance_management_frameworks_on_name_trigram ON complianc
CREATE INDEX index_compliance_requirements_on_namespace_id ON compliance_requirements USING btree (namespace_id);
CREATE INDEX index_compromised_password_detections_on_user_id ON compromised_password_detections USING btree (user_id);
CREATE INDEX index_container_expiration_policies_on_next_run_at_and_enabled ON container_expiration_policies USING btree (next_run_at, enabled);
CREATE INDEX index_container_registry_data_repair_details_on_status ON container_registry_data_repair_details USING btree (status);
@ -37482,6 +37507,8 @@ CREATE UNIQUE INDEX index_unit_test_failures_unique_columns ON ci_unit_test_fail
CREATE UNIQUE INDEX index_unresolved_alerts_on_project_id_and_fingerprint ON alert_management_alerts USING btree (project_id, fingerprint) WHERE ((fingerprint IS NOT NULL) AND (status <> 2));
CREATE UNIQUE INDEX index_unresolved_compromised_password_detection_on_user_id ON compromised_password_detections USING btree (user_id) WHERE (resolved_at IS NULL);
CREATE UNIQUE INDEX index_upcoming_reconciliations_on_namespace_id ON upcoming_reconciliations USING btree (namespace_id);
CREATE INDEX index_upcoming_reconciliations_on_organization_id ON upcoming_reconciliations USING btree (organization_id);
@ -45416,6 +45443,9 @@ ALTER TABLE ONLY resource_state_events
ALTER TABLE ONLY resource_milestone_events
ADD CONSTRAINT fk_rails_c940fb9fc5 FOREIGN KEY (milestone_id) REFERENCES milestones(id) ON DELETE CASCADE;
ALTER TABLE ONLY compromised_password_detections
ADD CONSTRAINT fk_rails_c95dee3ea4 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY gpg_signatures
ADD CONSTRAINT fk_rails_c97176f5f7 FOREIGN KEY (gpg_key_id) REFERENCES gpg_keys(id) ON DELETE SET NULL;

View File

@ -501,6 +501,31 @@ You can change the maximum time a job can run before it times out:
- At the [runner level](../ci/runners/configure_runners.md#set-the-maximum-job-timeout).
This limit must be 10 minutes or longer.
### Maximum number of jobs in a pipeline
You can limit the maximum number of jobs in a pipeline. The number
of jobs in a pipeline is checked at pipeline creation and when new commit statuses are created.
Pipelines that have too many jobs fail with a `size_limit_exceeded` error.
- On GitLab.com, a limit is
[defined for each subscription tier](../user/gitlab_com/_index.md#cicd),
and this limit affects all projects with that tier.
- On GitLab Self-Managed, [Premium or Ultimate](https://about.gitlab.com/pricing/) subscriptions,
this limit is defined under a `default` plan that affects all
projects. This limit is disabled (`0`) by default.
To change the limit for a GitLab Self-Managed instance, change the `default` plan's limit with the following
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session) command:
```ruby
# If limits don't exist for the default plan, you can create one with:
# Plan.default.create_limits!
Plan.default.actual_limits.update!(ci_pipeline_size: 500)
```
Set the limit to `0` to disable it.
### Maximum number of deployment jobs in a pipeline
You can limit the maximum number of deployment jobs in a pipeline. A deployment is
@ -531,7 +556,7 @@ When this limit is exceeded, pipeline creation fails with the error `downstream
Increasing this limit is not recommended. The default limit protects your GitLab instance from excessive resource consumption, potential pipeline recursion, and database overload.
Instead of increasing the limit, restructure your CI/CD configuration by splitting large pipeline hierarchies into smaller pipelines or using parallel jobs.
Instead of increasing the limit, restructure your CI/CD configuration by splitting large pipeline hierarchies into smaller pipelines. Consider using `needs` between jobs or dependent stages within a single pipeline.
{{< /alert >}}

View File

@ -36,7 +36,7 @@ Event data does not include source code or other customer-created content stored
For more information, see also:
- [Metrics dictionary](https://metrics.gitlab.com/?status=active) for a list of events and metrics
- [Customer product usage information](https://handbook.gitlab.com/handbook/legal/privacy/customer-product-usage-information/) for data privacy policy
- [Customer product usage information](https://handbook.gitlab.com/handbook/legal/privacy/customer-product-usage-information/)
### Benefits of event data
@ -81,7 +81,3 @@ The log file is located at:
- `/home/git/gitlab/log/product_usage_data.log` on self-compiled installations
While these logs provide thorough visibility into data transmission, they're designed specifically for inspection by security teams rather than feature usage analysis. For more detailed information about logging system, see the [Log system documentation](../logs/_index.md#product-usage-data-log).
### Frequently asked questions on event data
You can access frequently asked questions on event data [here](https://handbook.gitlab.com/handbook/legal/privacy/product-usage-events-faq/).

View File

@ -16,7 +16,6 @@ GitLab Inc. periodically collects information about your instance in order
to perform various actions.
For free GitLab Self-Managed instances, all usage statistics are [opt-out](#enable-or-disable-service-ping).
For information about other tiers, see [Customer Product Usage Information](https://handbook.gitlab.com/handbook/legal/privacy/customer-product-usage-information/#service-ping-formerly-known-as-usage-ping).
## Service Ping
@ -34,7 +33,7 @@ There are several other benefits to enabling Service Ping:
- Analyze the users' activities over time of your GitLab installation.
- A [DevOps Score](../analytics/dev_ops_reports.md) to give you an overview of your entire instance's adoption of concurrent DevOps from planning to monitoring.
- More proactive support (assuming that our [Customer Success Managers (CSMs)](https://handbook.gitlab.com/job-families/sales/customer-success-management/) and support organization used the data to deliver more value).
- More proactive support through Customer Success Managers (CSMs) who can use the collected data.
- Insight and advice into how to get the most value out of your investment in GitLab.
- Reports that show how you compare against other similar organizations (anonymized), with specific advice and recommendations on how to improve your DevOps processes.
- Participation in our [Registration Features Program](#registration-features-program) to receive free paid features.
@ -44,7 +43,7 @@ There are several other benefits to enabling Service Ping:
In GitLab versions 14.1 and later, GitLab Free customers with a GitLab Self-Managed instance running
GitLab Enterprise Edition can receive paid features by [enabling registration features](#enable-registration-features) and sending us
activity data through Service Ping. Features introduced here do not remove the feature from its paid
tier. Instances on a paid tier are subject to our [Product Usage Data policy](https://handbook.gitlab.com/handbook/legal/privacy/customer-product-usage-information/) managed by [Cloud Licensing](https://about.gitlab.com/pricing/licensing-faq/cloud-licensing/).
tier. Instances on a paid tier are subject to the [Product Usage Data policy](https://handbook.gitlab.com/handbook/legal/privacy/customer-product-usage-information/) managed by [Cloud Licensing](https://about.gitlab.com/pricing/licensing-faq/cloud-licensing/).
### Available features
@ -137,7 +136,6 @@ If your GitLab instance is behind a proxy, set the appropriate
{{< alert type="note" >}}
Whether you can disable Service Ping completely depends on the instance's tier and the specific license.
For more information, see [Customer Product Usage Information](https://handbook.gitlab.com/handbook/legal/privacy/customer-product-usage-information/#service-ping-formerly-known-as-usage-ping).
Service Ping settings only control whether the data is being shared with GitLab, or limited to only internal use by the instance.
Even if you disable Service Ping, the `gitlab_service_ping_worker` background job still periodically generates a Service Ping payload for your instance.
The payload is available in the [Metrics and profiling](#manually-upload-service-ping-payload) admin section.
@ -202,7 +200,6 @@ the **Admin** area.
## Enable or disable optional data in Service Ping
GitLab differentiates between operational and optional collected data.
For more information, see [Customer product usage information](https://handbook.gitlab.com/handbook/legal/privacy/customer-product-usage-information/#service-ping-formerly-known-as-usage-ping).
### Through the UI

View File

@ -820,6 +820,11 @@ Example response:
Add or update the pipeline status of a commit. If the commit is associated with a merge request,
the API call must target the commit in the merge request's source branch.
If a pipeline already exists and it exceeds the [maximum number of jobs in a single pipeline limit](../administration/instance_limits.md#maximum-number-of-jobs-in-a-pipeline):
- If `pipeline_id` is specified, a `422` error is returned: `The number of jobs has exceeded the limit`.
- Otherwise, a new pipeline is created.
```plaintext
POST /projects/:id/statuses/:sha
```

View File

@ -78,7 +78,7 @@ Advanced search works with the following versions of Elasticsearch.
| GitLab version | Elasticsearch version |
|-----------------------|-----------------------------|
| GitLab 15.0 and later | Elasticsearch 7.x and later |
| GitLab 15.0 and later | Elasticsearch 7.x and 8.x |
| GitLab 14.0 to 14.10 | Elasticsearch 6.8 to 7.x |
Advanced search follows the [Elasticsearch end-of-life policy](https://www.elastic.co/support/eol).

View File

@ -75,6 +75,7 @@ the related documentation:
| Artifacts maximum size (compressed) | 1 GB | See [Maximum artifacts size](../../administration/settings/continuous_integration.md#set-maximum-artifacts-size). |
| Artifacts [expiry time](../../ci/yaml/_index.md#artifactsexpire_in) | 30 days unless otherwise specified | See [Default artifacts expiration](../../administration/settings/continuous_integration.md#set-default-artifacts-expiration). Artifacts created before June 22, 2020 have no expiry. |
| Scheduled Pipeline Cron | `*/5 * * * *` | See [Pipeline schedules advanced configuration](../../administration/cicd/_index.md#change-maximum-scheduled-pipeline-frequency). |
| Maximum jobs in a single pipeline | `500` for Free tier, `1000` for all trial tiers, `1500` for Premium, and `2000` for Ultimate. | See [Maximum number of jobs in a pipeline](../../administration/instance_limits.md#maximum-number-of-jobs-in-a-pipeline). |
| Maximum jobs in active pipelines | `500` for Free tier, `1000` for all trial tiers, `20000` for Premium, and `100000` for Ultimate. | See [Number of jobs in active pipelines](../../administration/instance_limits.md#number-of-jobs-in-active-pipelines). |
| Maximum CI/CD subscriptions to a project | `2` | See [Number of CI/CD subscriptions to a project](../../administration/instance_limits.md#number-of-cicd-subscriptions-to-a-project). |
| Maximum number of pipeline triggers in a project | `25000` | See [Limit the number of pipeline triggers](../../administration/instance_limits.md#limit-the-number-of-pipeline-triggers). |

View File

@ -181,3 +181,21 @@ This error occurs when a Sidekiq worker processing the import
restarts due to high CPU or memory usage during import.
To configure workers for imports, see
[Sidekiq configuration](../../project/import/_index.md#sidekiq-configuration).
## Error: `BulkImports::FileDownloadService::ServiceError Invalid content type`
When using direct transfer between GitLab instances, you might encounter the following error:
```plaintext
BulkImports::FileDownloadService::ServiceError Invalid content type
```
This error is related to how network traffic is routed between instances.
If a content type other than `application/gzip` is returned,
your network requests might be bypassing GitLab Workhorse.
To resolve this issue:
- Check that your Ingress is configured to route traffic through
GitLab Workhorse on port `8181` rather than directly to Puma.
- Consider enabling [proxy downloads](../../../administration/object_storage.md#proxy-download) for object storage.

View File

@ -2,7 +2,7 @@
stage: Package
group: Package Registry
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
title: Virtual Registry
title: Virtual registry
---
{{< details >}}
@ -36,8 +36,7 @@ With this approach, you can configure your applications to use one virtual regis
To configure the virtual registry:
- You need a top-level group with a GitLab license with at least the premium level.
- You must be at least Maintainer of the top-level group.
- You need a top-level group with at least the Maintainer role.
- Make sure you enable the dependency proxy setting. It's enabled by default, but [administrators can turn it off](../../../administration/packages/dependency_proxy.md).
- You must configure authentication for your supported [package format](#supported-package-formats).
@ -60,7 +59,12 @@ When a virtual registry receives a request for a package:
### Caching system
All upstream registries have a caching system. A caching system stores request paths in a cache entry, and serves the responses for identical requests from the GitLab virtual registry. This way, the virtual registry does not have to contact the upstream again when the same package is requested.
All upstream registries have a caching system that:
- Stores requests in a cache entry
- Serves the responses for identical requests from the GitLab virtual registry
This way, the virtual registry does not have to contact the upstream again when the same package is requested.
If a requested path has not been cached in any of the available upstreams:
@ -71,9 +75,9 @@ If the requested path has been cached in any of the available upstreams:
1. The virtual registry checks the [cache validity period](#cache-validity-period) to see if the cache entry needs to be refreshed before forwarding the response.
1. If the cache is valid, the cache entry of the upstream fulfills the request.
- At this point, notice the virtual registry does not walk through the ordered list of upstreams again. If a lower priority upstream has cached the request, and a higher priority upstream has the requested file but not in its cache, the request is fulfilled with the lower priority upstream cache entry. This is by design.
- If a lower priority upstream has the request in its cache, and a higher priority contains the file but has not cached the request, the lower priority upstream fulfills the request. The virtual registry does not walk the ordered list of upstreams again.
If an upstream can't be found to fulfill the request, the virtual registry returns a `404 Not Found` error.
The virtual registry returns a `404 Not Found` error if it cannot find an upstream to fulfill the request.
#### Cache validity period
@ -83,26 +87,32 @@ that a cache entry is considered valid to fulfill a request.
Before the virtual registry pulls from an existing cache entry,
it checks the cache validity period to determine if the entry must be refreshed or not.
If the entry is outside the validity period, the virtual registry checks if the upstream response is identical to the one in the cache. If:
If the entry is outside the validity period, the virtual registry checks
if the upstream response is identical to the one in the cache. If:
- The response is identical, the entry is used to fulfill the request.
- The response is not identical, the response is downloaded again from the upstream to overwrite the upstream cache entry.
If network conditions prevent the virtual registry from connecting to the upstream, the caching system uses the available cache entry to serve the request. This way, as long as the virtual registry has the response related to a request in the cache, that request is fulfilled, even when outside the validity period.
If the virtual registry cannot connect to an upstream due to network conditions,
the upstream serves the request with the available cache entry.
As long as the virtual registry has the response related to
a request in the cache, that request is fulfilled,
even when outside the validity period.
##### Set the cache validity period
The cache validity period is important in the overall performance of the virtual registry to fulfill requests. Contacting external registries is a costly operation. Smaller validity periods increase the amount of checks, and longer periods decrease them.
You can turn off cache validity checks by setting it to 0.
You can turn off cache validity checks by setting it to `0`.
The default value of the cache validity period is 24 hours.
The default value of the cache validity period is `24` hours.
You should set the cache validity period to 0 when the external registry targeted by the upstream is known to have immutable responses. This is often the case when using official public registries. For more information, check your [supported package format](#supported-package-formats).
You should set the cache validity period to `0` when the external registry targeted by the upstream is known to have immutable responses. This is often the case with official public registries. For more information, check your [supported package format](#supported-package-formats).
### Object storage usage
Cache entries save their files in object storage in [the `dependency_proxy` bucket](../../../administration/object_storage.md#configure-the-parameters-of-each-object).
Cache entries save their files in object storage in the [`dependency_proxy` bucket](../../../administration/object_storage.md#configure-the-parameters-of-each-object).
Object storage usage counts towards the top-level group [object storage usage limit](../../storage_usage_quotas.md#view-storage).
@ -116,9 +126,12 @@ Virtual registry performance might vary based on factors like:
### Tradeoffs
Virtual registries are more advanced than public registries. When you pull dependencies with a virtual registry, it might take longer than other registries, such as public, official registries.
Virtual registries are more advanced than public registries.
When you pull dependencies with a virtual registry,
it might take longer than other registries, such as public, official registries.
Compared with public registries, virtual registries also support multiple upstream registries and authentication, but these advantages are not free.
Compared with public registries, virtual registries
also support multiple upstream registries and authentication.
### Upstream prioritization
@ -132,7 +145,7 @@ When you manage a list of private upstream registries:
- You should prioritize registries with the most packages at the top of the list. This approach:
- Increases the chances that a high-priority registry can fulfill the request
- Prevents walking the entire ordered list to find a valid upstream registry
- You should put registries that host the least amount of packages at the bottom of the list.
- You should put registries with the least amount of packages at the bottom of the list.
### Performance improvements with usage
@ -142,8 +155,6 @@ When an upstream registry caches a request, the time to fulfill an identical req
### Use the CI/CD cache
GitLab CI/CD jobs can further increase their performance by [using caching in GitLab CI/CD](../../../ci/caching/_index.md#common-use-cases-for-caches) for dependencies.
You can use [caching in GitLab CI/CD](../../../ci/caching/_index.md#common-use-cases-for-caches) so that jobs do not have to download dependencies from the virtual registry.
By using caching in the CI/CD cache, jobs can avoid downloading dependencies from the virtual registry, which improves the execution time.
However, this enhancement comes with the cost of duplicating the storage for each dependency. Each dependency is stored in the virtual registry and the CI/CD cache.
This method improves execution time, but also duplicates storage for each dependency (dependencies are stored in the CI/CD cache and virtual registry).

View File

@ -56,18 +56,18 @@ When using the Maven virtual registry, remember the following restrictions:
## Manage the virtual registry
Manage the virtual registry with the dedicated virtual registry API.
Manage the virtual registry with the [Maven virtual registry API](../../../../api/maven_virtual_registries.md#manage-virtual-registries).
You cannot configure the virtual registry in the UI, but this [epic 15090](https://gitlab.com/groups/gitlab-org/-/epics/15090) proposes the implementation of a virtual registry UI.
You cannot configure the virtual registry in the UI, but [epic 15090](https://gitlab.com/groups/gitlab-org/-/epics/15090) proposes the implementation of a virtual registry UI.
### Authenticate to the virtual registry API
The virtual registry API uses [REST API authentication](../../../../api/rest/authentication.md) methods. You must authenticate to the API to manage virtual registry objects.
Read operations are open to users that can [use the virtual registry](#use-the-virtual-registry).
Read operations are available to users that can [use the virtual registry](#use-the-virtual-registry).
Write operations, such as [creating a new registry](#create-and-manage-a-virtual-registry) or [adding upstreams](#manage-upstream-registries) are restricted to
direct maintainers of the top-level group that hosts the virtual registry.
Write operations, such as [creating a new registry](#create-and-manage-a-virtual-registry) or [adding upstreams](#manage-upstream-registries), are restricted to
direct maintainers of the top-level group of the virtual registry.
### Create and manage a virtual registry
@ -81,11 +81,11 @@ curl --fail-with-body \
--url "https://gitlab.example.com/api/v4/groups/<group_id>/-/virtual_registries/packages/maven/registries"
```
- `<header>` is the [authentication header](../../../../api/rest/authentication.md).
- `<group_id>` is the top-level group ID.
- `<registry_name>` is the registry name. Required.
- `<header>`: The [authentication header](../../../../api/rest/authentication.md).
- `<group_id>`: The top-level group ID.
- `<registry_name>`: The registry name.
To see other endpoints and examples related to managing a virtual registry, see the [API documentation](../../../../api/maven_virtual_registries.md#manage-virtual-registries).
For more information about other endpoints and examples related to Maven virtual registries, see the [API](../../../../api/maven_virtual_registries.md#manage-virtual-registries).
### Manage upstream registries
@ -105,24 +105,20 @@ curl --fail-with-body \
--url "https://gitlab.example.com/api/v4/virtual_registries/packages/maven/registries/<registry_id>/upstreams"
```
- `<header>` is the [authentication header](../../../../api/rest/authentication.md).
- `<registry_id>` is the Maven virtual registry ID.
- `<upstream_name>` is the upstream registry name. Required.
- `<upstream_url>` is the Maven upstream URL. Required.
- `<upstream_username>` is the username to use with the Maven upstream. Required if an `<upstream_password>` is set.
- `<upstream_password>` is the password to use with the Maven upstream. Required if an `<upstream_username>` is set.
- `<upstream_cache_validity_hours>` is the [cache validity period](../_index.md#cache-validity-period) in hours. Optional. The default value is `24`. `0` disables the cache entry checks.
- if the `<upstream_url>` is set to [Maven central](#use-maven-central-as-an-upstream), the validity period is set to `0`.
- `<header>`: The [authentication header](../../../../api/rest/authentication.md).
- `<registry_id>`: The Maven virtual registry ID.
- `<upstream_name>`: The upstream registry name.
- `<upstream_url>`: The Maven upstream URL.
- `<upstream_username>`: The username to use with the Maven upstream. Required if an `<upstream_password>` is set.
- `<upstream_password>`: The password to use with the Maven upstream. Required if an `<upstream_username>` is set.
- `<upstream_cache_validity_hours>`: (optional) The [cache validity period](../_index.md#cache-validity-period) in hours. The default value is `24`. To turn off cache entry checks, set to `0`.
- if the `<upstream_url>` is set to Maven central:
- You must use the following URL: `https://repo1.maven.org/maven2`
- The validity period is set to `0` by default. All files on Maven central are immutable.
`<upstream_username>` and `<upstream_password>` are optional. If not set, a public (anonymous) request is used to access the upstream.
To see other endpoints and examples related to managing an upstream registry, including updating the upstream registry position in the list, see the [API documentation](../../../../api/maven_virtual_registries.md#manage-upstream-registries).
#### Use Maven central as an upstream
To configure an upstream to Maven central, use the following URL: `https://repo1.maven.org/maven2`.
On Maven central, all files are immutable. You should set the [cache validity period](../_index.md#cache-validity-period) to `0` to disable cache checks with Maven central.
For more information about other endpoints and examples, like updating the upstream registry position in the list, see the [API](../../../../api/maven_virtual_registries.md#manage-upstream-registries).
### Manage cache entries
@ -130,7 +126,7 @@ If necessary, cache entries can be inspected or destroyed.
The next time the virtual registry receives a request for the file that was referenced by the destroyed cache entry, the list of upstreams is [walked again](../_index.md#caching-system) to find an upstream that can fulfill this request.
To see the endpoints and examples related to managing cache entries, see the [API documentation](../../../../api/maven_virtual_registries.md#manage-cache-entries).
To learn more about managing cache entries, see the [API](../../../../api/maven_virtual_registries.md#manage-cache-entries).
## Use the virtual registry
@ -166,7 +162,8 @@ The Maven virtual registry supports the following Maven clients:
You must declare virtual registries in the Maven client configuration.
All clients must be authenticated. For the client authentication, you can use a custom HTTP header or Basic Auth. You should use one of the configurations below for each client.
All clients must be authenticated. For the client authentication, you can use a custom HTTP header or Basic Auth.
You should use one of the configurations below for each client.
{{< tabs >}}
@ -216,10 +213,8 @@ To configure a Maven virtual registry as an additional registry, in the `pom.xml
</repositories>
```
Where:
- `<registry_id>` is the ID of the Maven virtual registry.
- `<id>` contains the same ID of the `<server>` used in the `settings.xml`.
- `<id>`: The same ID of the `<server>` used in the `settings.xml`.
- `<registry_id>`: The ID of the Maven virtual registry.
To configure a Maven virtual registry as a replacement of the default registry, in the `settings.xml`, add a `mirrors` element:
@ -239,9 +234,7 @@ To configure a Maven virtual registry as a replacement of the default registry,
</settings>
```
Where:
- `<registry_id>` is the ID of the Maven virtual registry.
- `<registry_id>`: The ID of the Maven virtual registry.
{{< /tab >}}
@ -300,9 +293,7 @@ Add a `repositories` section to your
}
```
Where:
- `<registry_id>` is the ID of the Maven virtual registry.
- `<registry_id>`: The ID of the Maven virtual registry.
{{< /tab >}}
@ -317,7 +308,7 @@ Where:
Authentication for [SBT](https://www.scala-sbt.org/index.html) is based on
[basic HTTP Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication).
You must to provide a name and a password.
You must provide a name and a password.
In your [`build.sbt`](https://www.scala-sbt.org/1.x/docs/Directories.html#sbt+build+definition+files), add the following lines:
@ -327,14 +318,13 @@ resolvers += ("gitlab" at "<endpoint_url>")
credentials += Credentials("GitLab Virtual Registry", "<host>", "<username>", "<token>")
```
Where:
- `<endpoint_url>`: The Maven virtual registry URL.
For example, `https://gitlab.example.com/api/v4/virtual_registries/packages/maven/<registry_id>`, where `<registry_id>` is the ID of the Maven virtual registry.
- `<host>`: The host present in the `<endpoint_url>` without the protocol scheme or the port. For example, `gitlab.example.com`.
- `<username>`: The username.
- `<token>`: The configured token.
- `<endpoint_url>` is the Maven virtual registry URL.
For example, `https://gitlab.example.com/api/v4/virtual_registries/packages/maven/<registry_id>`. `<registry_id>` is the ID of the Maven virtual registry.
- `<host>` is the host present in the `<endpoint_url>` without the protocol scheme or the port. For example, `gitlab.example.com`.
- `<username>` is the username.
- `<token>` is the configured token.
- Make sure that the first argument of `Credentials` is `"GitLab Virtual Registry"`. This realm name must _exactly match_ the [basic auth realm](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Authentication#www-authenticate_and_proxy-authenticate_headers) sent by the Maven virtual registry.
Make sure that the first argument of `Credentials` is `"GitLab Virtual Registry"`. This realm name must _exactly match_ the [Basic Auth realm](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Authentication#www-authenticate_and_proxy-authenticate_headers) sent by the Maven virtual registry.
{{< /tab >}}

View File

@ -311,6 +311,12 @@ For more information, see the history.
{{< /alert >}}
{{< alert type="note" >}}
Deleting a branch rule is not available for rules targeting `all branches`.
{{< /alert >}}
Prerequisites:
- You must have at least the Maintainer role for the project.

View File

@ -7,6 +7,8 @@ Create a file in `ActiveContext::Config.migrations_path`.
ActiveContext supports several field types for defining collection schemas:
- `bigint`: For large numeric values (accepts `index: true/false`, defaults to `false`)
- `integer`: For standard numeric values (accepts `index: true/false`, defaults to `false`)
- `smallint`: For small numeric values (accepts `index: true/false`, defaults to `false`)
- `boolean`: For boolean values (accepts `index: true/false`, defaults to `true`)
- `keyword`: For exact-match searchable string fields (always indexed, no `index` option)
- `text`: For full-text searchable content (accepts `index: true/false`, defaults to `false`)
@ -24,6 +26,8 @@ class CreateMergeRequests < ActiveContext::Migration[1.0]
create_collection :merge_requests, number_of_partitions: 3 do |c|
c.bigint :issue_id, index: true
c.bigint :namespace_id, index: true
c.integer :iid, index: true
c.smallint :priority, index: true
c.boolean :is_draft
c.keyword :traversal_ids
c.text :description

View File

@ -13,6 +13,14 @@ module ActiveContext
fields << Field::Bigint.new(name, index: index)
end
def integer(name, index: false)
fields << Field::Integer.new(name, index: index)
end
def smallint(name, index: false)
fields << Field::Smallint.new(name, index: index)
end
def boolean(name, index: true)
fields << Field::Boolean.new(name, index: index)
end
@ -39,6 +47,8 @@ module ActiveContext
end
class Bigint < Field; end
class Integer < Field; end
class Smallint < Field; end
class Boolean < Field; end
class Keyword < Field; end
class Text < Field; end

View File

@ -70,6 +70,10 @@ module ActiveContext
mappings[field.name] = case field
when Field::Bigint
{ type: 'long' }
when Field::Integer
{ type: 'integer' }
when Field::Smallint
{ type: 'short' }
when Field::Boolean
{ type: 'boolean' }
when Field::Keyword

View File

@ -76,6 +76,12 @@ module ActiveContext
when Field::Bigint
# Bigint is 8 bytes
fixed_columns << [field, 8]
when Field::Integer
# Integer is 4 bytes
fixed_columns << [field, 4]
when Field::Smallint
# Smallint is 2 bytes
fixed_columns << [field, 2]
when Field::Boolean
# Boolean is 1 byte
fixed_columns << [field, 1]
@ -100,6 +106,10 @@ module ActiveContext
case field
when Field::Bigint
table.bigint(field.name, **field.options.except(:index))
when Field::Integer
table.integer(field.name, **field.options.except(:index))
when Field::Smallint
table.integer(field.name, limit: 2, **field.options.except(:index))
when Field::Boolean
table.boolean(field.name, **field.options.except(:index))
when Field::Keyword, Field::Text

View File

@ -93,6 +93,7 @@ module Gitlab
push_frontend_feature_flag(:new_project_creation_form, current_user, type: :wip)
push_frontend_feature_flag(:work_items_client_side_boards, current_user)
push_frontend_feature_flag(:glql_work_items, current_user, type: :wip)
push_frontend_feature_flag(:new_implementation_of_invite_members_search)
end
# Exposes the state of a feature flag to the frontend code.

View File

@ -9,6 +9,7 @@ module Gitlab
DEFAULT_PREFIX = 'rf-'
DEFAULT_RUNNER_COUNT = 40
DEFAULT_JOB_COUNT = DEFAULT_RUNNER_COUNT * 10
NO_ORGANIZATION_ERROR = "No organization found. Ensure user has an organization or pass an organization_id"
TAG_LIST = %w[gitlab-org docker ruby 2gb mysql linux shared shell deploy hhvm windows build postgres ios stage
android stz front back review-apps pc java scraper test kubernetes staging no-priority osx php nodejs
@ -56,9 +57,11 @@ module Gitlab
@user = User.find_by_username(username)
@registration_prefix = options[:registration_prefix] || DEFAULT_PREFIX
@runner_count = options[:runner_count] || DEFAULT_RUNNER_COUNT
@organization_id = nil
@organization_id = options[:organization_id] || @user.organizations.first&.id
@groups = {}
@projects = {}
raise NO_ORGANIZATION_ERROR unless @organization_id
end
# seed returns an array of hashes of projects to its assigned runners
@ -72,7 +75,6 @@ module Gitlab
runner_count: @runner_count
)
@organization_id = ensure_organization_id
groups_and_projects = create_groups_and_projects
runner_ids = create_runners(groups_and_projects)
@ -113,25 +115,6 @@ module Gitlab
true
end
def ensure_organization_id
args = {
name: 'GitLab',
path: 'gitlab'
}
organization_id = ::Organizations::Organization.find_by_path(args[:path])&.id
return organization_id if organization_id
if Feature.enabled?(:allow_organization_creation, @user)
logger.info(message: 'Creating organization', **args)
service = ::Organizations::CreateService.new(current_user: @user, params: args)
return execute_service!(service, :organization)&.id
end
::Organizations::Organization::DEFAULT_ORGANIZATION_ID
end
def create_groups_and_projects
root_group_1 = ensure_group(name: 'top-level group 1', organization_id: @organization_id)
root_group_2 = ensure_group(name: 'top-level group 2', organization_id: @organization_id)

View File

@ -3354,6 +3354,9 @@ msgstr ""
msgid "Account: %{account}"
msgstr ""
msgid "AccountTokens|Service Account/Tokens"
msgstr ""
msgid "Achievements"
msgstr ""
@ -14942,24 +14945,24 @@ msgstr ""
msgid "CommandPalette|Project files"
msgstr ""
msgid "CommandPalette|Search by filename"
msgstr ""
msgid "CommandPalette|Search by project (minimum 3 characters)"
msgstr ""
msgid "CommandPalette|Search by username (minimum 3 characters)"
msgstr ""
msgid "CommandPalette|Search for a page or action"
msgstr ""
msgid "CommandPalette|Settings"
msgstr ""
msgid "CommandPalette|command"
msgstr ""
msgid "CommandPalette|go to project file"
msgstr ""
msgid "CommandPalette|issue (enter at least 3 chars)"
msgstr ""
msgid "CommandPalette|project (enter at least 3 chars)"
msgstr ""
msgid "CommandPalette|user (enter at least 3 chars)"
msgstr ""
msgid "Commands applied"
msgstr ""
@ -35540,6 +35543,12 @@ msgstr ""
msgid "LDAP|No LDAP synchronizations"
msgstr ""
msgid "LDAP|Select server"
msgstr ""
msgid "LDAP|Server"
msgstr ""
msgid "LDAP|Sync method"
msgstr ""
@ -43968,6 +43977,9 @@ msgstr ""
msgid "Pages|Last updated"
msgstr ""
msgid "Pages|Never expires"
msgstr ""
msgid "Pages|No deployments yet"
msgstr ""
@ -43980,9 +43992,6 @@ msgstr ""
msgid "Pages|Path prefix"
msgstr ""
msgid "Pages|Primary deployment"
msgstr ""
msgid "Pages|Restore"
msgstr ""
@ -51743,6 +51752,9 @@ msgstr ""
msgid "Roles and permissions"
msgstr ""
msgid "Roles|Groups"
msgstr ""
msgid "Roll up totals may reflect child items you dont have access to."
msgstr ""
@ -53386,12 +53398,21 @@ msgstr ""
msgid "ScanResultPolicy|%{listType} according to the %{buttonType}"
msgstr ""
msgid "ScanResultPolicy|Add exception"
msgstr ""
msgid "ScanResultPolicy|Add exception(s)"
msgstr ""
msgid "ScanResultPolicy|Add new criteria"
msgstr ""
msgid "ScanResultPolicy|Add new license"
msgstr ""
msgid "ScanResultPolicy|Add policy exception"
msgstr ""
msgid "ScanResultPolicy|Age criteria can only be added for pre-existing vulnerabilities"
msgstr ""
@ -53500,6 +53521,9 @@ msgstr ""
msgid "ScanResultPolicy|For scanners that require builds, when a project does not have a build pipeline."
msgstr ""
msgid "ScanResultPolicy|Groups"
msgstr ""
msgid "ScanResultPolicy|If selected, the following choices will overwrite %{linkStart}project settings%{linkEnd} but only affect the branches selected in the policy."
msgstr ""
@ -53566,6 +53590,9 @@ msgstr ""
msgid "ScanResultPolicy|Override project approval settings"
msgstr ""
msgid "ScanResultPolicy|Policy Exception settings"
msgstr ""
msgid "ScanResultPolicy|Pre-existing"
msgstr ""
@ -53608,6 +53635,9 @@ msgstr ""
msgid "ScanResultPolicy|Required scanners defined in the condition did not run or produce any artifacts."
msgstr ""
msgid "ScanResultPolicy|Roles"
msgstr ""
msgid "ScanResultPolicy|Save allowlist"
msgstr ""
@ -53629,9 +53659,15 @@ msgstr ""
msgid "ScanResultPolicy|Select list type"
msgstr ""
msgid "ScanResultPolicy|Service accounts/tokens"
msgstr ""
msgid "ScanResultPolicy|Severity is:"
msgstr ""
msgid "ScanResultPolicy|Source branch pattern"
msgstr ""
msgid "ScanResultPolicy|Specify the packages where this license requires approval before use"
msgstr ""
@ -58822,6 +58858,9 @@ msgstr ""
msgid "Source-Branch"
msgstr ""
msgid "SourceBranchPattern|Source branch patterns"
msgstr ""
msgid "SourceEditor|\"el\" parameter is required for createInstance()"
msgstr ""

View File

@ -8,8 +8,8 @@ require 'parallel'
require 'rainbow'
require 'yaml'
UNUSED_METHODS = 49
EXCLUDED_METHODS_PATH = '.gitlab/lint/unused_helper_methods/excluded_methods.yml'
POTENTIAL_METHODS_PATH = '.gitlab/lint/unused_helper_methods/potential_methods_to_remove.yml'
print_output = %w[true 1].include? ENV["REPORT_ALL_UNUSED_METHODS"]
@ -72,18 +72,21 @@ if print_output
exit 0
end
if unused.size > UNUSED_METHODS
added = unused.size - UNUSED_METHODS
potential_methods_count = YAML.load_file(POTENTIAL_METHODS_PATH, symbolize_names: true).size
if unused.size > potential_methods_count
added = unused.size - potential_methods_count
puts Rainbow("ERROR: #{added} unused methods were added. Please remove them.").red.bright
exit 1
elsif unused.size < UNUSED_METHODS
elsif unused.size < potential_methods_count
warning = <<~UPDATE_UNUSED
WARNING: It appears you have removed unused methods. Thank you!
🏆 It appears you have removed unused methods. Thank you!
Please update scripts/lint/unused_helper_methods.rb to reflect the new number:
UNUSED_METHODS = #{unused.size}
Please update potential_methods_to_remove.yml with the current list of unused methods.
UPDATE_UNUSED
puts Rainbow(warning).yellow.bright
print Rainbow(warning).yellow.bright
exit 1
end

View File

@ -193,6 +193,7 @@ RSpec.describe '.gitlab/ci/rules.gitlab-ci.yml', :unlimited_max_formatted_output
'.gitlab/changelog_config.yml',
'.gitlab/CODEOWNERS',
'.gitlab/lint/unused_helper_methods/excluded_methods.yml',
'.gitlab/lint/unused_helper_methods/potential_methods_to_remove.yml',
'.gitleaksignore',
'.gitpod.yml',
'.graphqlrc',

View File

@ -0,0 +1,97 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Members::InviteUsersFinder, feature_category: :groups_and_projects do
let_it_be(:current_user) { create(:user, :with_namespace) }
let_it_be(:root_group) { create(:group) }
let_it_be(:regular_user) { create(:user) }
let_it_be(:admin_user) { create(:user, :admin) }
let_it_be(:banned_user) { create(:user, :banned) }
let_it_be(:blocked_user) { create(:user, :blocked) }
let_it_be(:ldap_blocked_user) { create(:user, :ldap_blocked) }
let_it_be(:external_user) { create(:user, :external) }
let_it_be(:unconfirmed_user) { create(:user, confirmed_at: nil) }
let_it_be(:omniauth_user) { create(:omniauth_user) }
let_it_be(:internal_user) { Users::Internal.alert_bot }
let_it_be(:project_bot_user) { create(:user, :project_bot) }
let_it_be(:service_account_user) { create(:user, :service_account) }
before_all do
root_group.add_owner(current_user)
end
subject(:finder) do
described_class.new(current_user, resource)
end
describe '#execute' do
shared_examples 'searchable' do
let(:searchable_users_ordered_by_id_desc) do
[
current_user,
regular_user,
admin_user,
external_user,
unconfirmed_user,
omniauth_user,
service_account_user
].sort_by(&:id).reverse
end
it 'returns searchable users ordered by id descending' do
expect(finder.execute).to eq(searchable_users_ordered_by_id_desc)
end
context 'for search param' do
subject(:finder) do
described_class.new(current_user, resource, search: search)
end
context 'with empty string' do
let(:search) { '' }
it 'returns searchable users ordered by id descending' do
expect(finder.execute).to eq(searchable_users_ordered_by_id_desc)
end
end
context "with a user's name" do
let(:search) { regular_user.name }
it 'returns users that match the name' do
expect(finder.execute).to eq([regular_user])
end
end
end
end
context 'for root_group' do
let_it_be(:resource) { root_group }
include_examples 'searchable'
end
context 'for subgroup' do
let_it_be(:subgroup) { create(:group, parent: root_group) }
let_it_be(:resource) { subgroup }
include_examples 'searchable'
end
context 'for project within group namespace' do
let_it_be(:project) { create(:project, namespace: root_group, creator: current_user) }
let_it_be(:resource) { project }
include_examples 'searchable'
end
context 'for project within user namespace' do
let_it_be(:project) { create(:project, namespace: current_user.namespace) }
let_it_be(:resource) { project }
include_examples 'searchable'
end
end
end

View File

@ -1,25 +1,32 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
import { GlButton } from '@gitlab/ui';
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import { mount } from '@vue/test-utils';
import CloseButton from '~/design_management/components/toolbar/close_button.vue';
import { DESIGNS_ROUTE_NAME } from '~/design_management/router/constants';
describe('Design management toolbar close button', () => {
Vue.use(VueRouter);
const router = new VueRouter({
routes: [
{ path: '/', name: 'workItemList', component: { template: '<div>Designs</div>' } },
{ path: '/designs', name: 'designs', component: { template: '<div>Design detail</div>' } },
],
mode: 'history',
});
let wrapper;
function createComponent() {
wrapper = shallowMount(CloseButton, {
stubs: {
RouterLink: RouterLinkStub,
},
wrapper = mount(CloseButton, {
router,
});
}
it('links back to designs list', async () => {
it('links back to designs list', () => {
createComponent();
await waitForPromises();
expect(wrapper.findComponent(GlButton).attributes('to')).toBe(DESIGNS_ROUTE_NAME);
expect(wrapper.findComponent(GlButton).attributes().href).toEqual('/designs');
});
});

View File

@ -1,6 +1,5 @@
import VueApollo from 'vue-apollo';
import Vue from 'vue';
import { GlIcon } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import PagesDeployment from '~/gitlab_pages/components/deployment.vue';
import deletePagesDeploymentMutation from '~/gitlab_pages/queries/delete_pages_deployment.mutation.graphql';
@ -8,6 +7,7 @@ import restorePagesDeploymentMutation from '~/gitlab_pages/queries/restore_pages
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import UserDate from '~/vue_shared/components/user_date.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import {
primaryDeployment,
environmentDeployment,
@ -51,10 +51,10 @@ describe('PagesDeployment', () => {
const findErrorBadge = () => wrapper.findByTestId('error-badge');
describe.each`
description | deployment | isPrimary
${'Primary deployment'} | ${primaryDeployment} | ${true}
${'Environment deployment'} | ${environmentDeployment} | ${false}
`('$description', ({ deployment, isPrimary }) => {
description | deployment
${'Primary deployment'} | ${primaryDeployment}
${'Environment deployment'} | ${environmentDeployment}
`('$description', ({ deployment }) => {
beforeEach(() => {
createComponent({ deployment });
});
@ -63,7 +63,7 @@ describe('PagesDeployment', () => {
it('renders deployment details', () => {
expect(wrapper.findByTestId('deployment-url').text()).toBe(deployment.url);
expect(
wrapper.findByTestId('deployment-created-at').findComponent(UserDate).props('date'),
wrapper.findByTestId('deployment-created-at').findComponent(TimeAgo).props('time'),
).toBe(deployment.createdAt);
expect(wrapper.findByTestId('deployment-ci-build-id').text()).toContain(
deployment.ciBuildId.toString(),
@ -76,7 +76,7 @@ describe('PagesDeployment', () => {
);
expect(wrapper.findByTestId('deployment-size').text()).toContain('1.0 KiB');
expect(
wrapper.findByTestId('deployment-updated-at').findComponent(UserDate).props('date'),
wrapper.findByTestId('deployment-updated-at').findComponent(TimeAgo).props('time'),
).toBe(deployment.updatedAt);
if (deployment.expiresAt) {
@ -84,35 +84,9 @@ describe('PagesDeployment', () => {
wrapper.findByTestId('deployment-expires-at').findComponent(UserDate).props('date'),
).toBe(deployment.expiresAt);
} else {
expect(wrapper.findByTestId('deployment-expires-at').exists()).toBe(false);
expect(wrapper.findByTestId('deployment-expires-at').text()).toBe('Never expires');
}
});
it('toggles deployment details on click', async () => {
expect(wrapper.findByTestId('deployment-details').isVisible()).toBe(false);
await wrapper.trigger('click');
expect(wrapper.findByTestId('deployment-details').isVisible()).toBe(true);
});
if (isPrimary) {
it('shows "Primary deployment" as deployment type label for screen readers', () => {
expect(wrapper.findByTestId('deployment-type').text()).toContain('Primary deployment');
});
it('shows the "home" icon', () => {
expect(wrapper.findByTestId('deployment-type').findComponent(GlIcon).props('name')).toBe(
'home',
);
});
} else {
it('shows the pathPrefix', () => {
expect(wrapper.findByTestId('deployment-type').text()).toContain(
environmentDeployment.pathPrefix,
);
});
}
});
describe('deployment is active', () => {
@ -156,7 +130,7 @@ describe('PagesDeployment', () => {
expect(wrapper.findByTestId('deployment-delete').exists()).toBe(false);
});
it('restores deployment when delete button is clicked', async () => {
it('restores deployment when restore button is clicked', async () => {
await restoreDeployment();
expect(restorePagesDeploymentMutationHandler).toHaveBeenCalledWith({

View File

@ -5,6 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import * as UserApi from '~/api/user_api';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
import { VALID_TOKEN_BACKGROUND, INVALID_TOKEN_BACKGROUND } from '~/invite_members/constants';
import * as MembersUtils from '~/invite_members/utils/member_utils';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
const label = 'testgroup';
@ -16,8 +17,9 @@ const handleEnterSpy = jest.fn();
/** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */
let wrapper;
const searchUrl = 'https://example.com/gitlab/groups/mygroup/-/group_members/invite_search.json';
const createComponent = ({ props = {} } = {}) => {
const createComponent = ({ props = {}, glFeatures = {} } = {}) => {
wrapper = mountExtended(MembersTokenSelect, {
propsData: {
ariaLabelledby: label,
@ -25,6 +27,7 @@ const createComponent = ({ props = {} } = {}) => {
placeholder,
...props,
},
provide: { glFeatures, searchUrl },
});
};
@ -89,6 +92,25 @@ describe('MembersTokenSelect', () => {
});
describe('users', () => {
describe('when `newImplementationOfInviteMembersSearch` is enabled', () => {
let tokenSelector;
beforeEach(() => {
jest.spyOn(MembersUtils, 'searchUsers').mockResolvedValue({ data: allUsers });
createComponent({ glFeatures: { newImplementationOfInviteMembersSearch: true } });
tokenSelector = findTokenSelector();
});
it('calls the API with search parameter with whitespaces and is trimmed', async () => {
tokenSelector.vm.$emit('text-input', ' foo@bar.com ');
await waitForPromises();
expect(MembersUtils.searchUsers).toHaveBeenCalledWith(searchUrl, 'foo@bar.com');
expect(tokenSelector.props('hideDropdownWithNoItems')).toBe(false);
});
});
beforeEach(() => {
jest.spyOn(UserApi, 'getUsers').mockResolvedValue({ data: allUsers });
createComponent();

View File

@ -1,4 +1,7 @@
import { memberName, triggerExternalAlert } from '~/invite_members/utils/member_utils';
import MockAdapter from 'axios-mock-adapter';
import { memberName, searchUsers, triggerExternalAlert } from '~/invite_members/utils/member_utils';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/lib/utils/url_utility');
@ -13,6 +16,25 @@ describe('Member Name', () => {
});
});
describe('searchUsers', () => {
let mockAxios;
beforeEach(() => {
mockAxios = new MockAdapter(axios);
});
it('should call axios.get with correct URL and params', async () => {
const url = 'https://example.com/gitlab/groups/mygroup/-/group_members/invite_search.json';
const search = 'my user';
mockAxios.onGet().replyOnce(HTTP_STATUS_OK);
await searchUsers(url, search);
expect(mockAxios.history.get[0]).toEqual(
expect.objectContaining({ url, params: { search, per_page: 20 } }),
);
});
});
describe('Trigger External Alert', () => {
it('returns false', () => {
expect(triggerExternalAlert()).toBe(false);

View File

@ -1,25 +1,34 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import { mount } from '@vue/test-utils';
import Vue from 'vue';
import VueRouter from 'vue-router';
import CloseButton from '~/work_items/components/design_management/design_preview/close_button.vue';
import { ROUTES } from '~/work_items/constants';
describe('Design management toolbar close button', () => {
let wrapper;
function createComponent() {
wrapper = shallowMount(CloseButton, {
stubs: {
RouterLink: RouterLinkStub,
Vue.use(VueRouter);
const router = new VueRouter({
routes: [
{ path: '/', name: 'workItemList', component: { template: '<div>Work items list</div>' } },
{
path: '/workItem',
name: 'workItem',
component: { template: '<div>Work items detail</div>' },
},
});
}
],
mode: 'history',
});
it('links back to designs list', async () => {
const createComponent = () => {
wrapper = mount(CloseButton, {
router,
});
};
it('links back to designs list', () => {
createComponent();
await waitForPromises();
expect(wrapper.findComponent(GlButton).attributes('to')).toEqual(ROUTES.workItem);
expect(wrapper.findComponent(GlButton).attributes().href).toEqual('/workItem');
});
});

View File

@ -402,6 +402,25 @@ describe('WorkItemDescription', () => {
expect(findApplyTemplate().exists()).toBe(true);
});
it('does not display a warning when a description is pre-populated in create mode', async () => {
// Mimic component mount with a pre-populated description
await createComponent({
editMode: true,
workItemId: newWorkItemId(workItemQueryResponse.data.workItem.workItemType.name),
});
findDescriptionTemplateListbox().vm.$emit('selectTemplate', {
name: 'default',
projectId: 1,
catagory: 'catagory',
});
await nextTick();
await waitForPromises();
expect(findDescriptionTemplateWarning().exists()).toBe(false);
expect(findCancelApplyTemplate().exists()).toBe(false);
expect(findApplyTemplate().exists()).toBe(false);
});
it('hides the warning when the cancel button is clicked', async () => {
expect(findDescriptionTemplateWarning().exists()).toBe(true);
findCancelApplyTemplate().vm.$emit('click');

View File

@ -5,7 +5,8 @@ require 'spec_helper'
NULL_LOGGER = Gitlab::JsonLogger.new('/dev/null')
RSpec.describe ::Gitlab::Seeders::Ci::Runner::RunnerFleetSeeder, feature_category: :fleet_visibility do
let_it_be(:user) { create(:user, :admin, username: 'test-admin') }
let_it_be(:user_organization) { create(:organization) }
let_it_be(:user) { create(:user, :admin, username: 'test-admin', organizations: [user_organization]) }
subject(:seeder) do
described_class.new(NULL_LOGGER,
@ -92,29 +93,46 @@ RSpec.describe ::Gitlab::Seeders::Ci::Runner::RunnerFleetSeeder, feature_categor
end
end
context 'when organization cannot be created' do
before do
allow_next_instance_of(::Organizations::CreateService, current_user: user, params: anything) do |service|
allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'test error'))
end
context 'when organization is passed to the initializer' do
let(:other_organization) { create(:organization) }
subject(:seed_with_organization) do
described_class.new(NULL_LOGGER,
username: user.username,
registration_prefix: registration_prefix,
runner_count: runner_count,
organization_id: other_organization.id
).seed
end
it 'raises RuntimeError' do
expect { seed }.to raise_error(RuntimeError)
it 'assigns organization_id to created entities' do
expect { seed_with_organization }.not_to raise_error
expect(Group.search(registration_prefix).pluck(:organization_id)).to all(eq(other_organization.id))
end
end
context 'when feature flag allow_organization_creation is disabled' do
let_it_be(:default_organization) { create(:organization, :default) }
context 'when organization is not passed to the initializer' do
it 'assigns organization_id of the user to created entities' do
expect { seed }.not_to raise_error
expect(Group.search(registration_prefix).pluck(:organization_id)).to all(eq(user.organizations.first.id))
end
end
before do
stub_feature_flags(allow_organization_creation: false)
context 'when no organization can be used' do
let(:user_without_org) { create(:user, organizations: []) }
subject(:seed_without_organization) do
described_class.new(NULL_LOGGER,
username: user_without_org.username,
registration_prefix: registration_prefix,
runner_count: runner_count
).seed
end
it 'uses the default organization ID' do
expect(::Organizations::Organization).not_to receive(:default_organization)
expect { seed }.not_to raise_error
expect(Group.search(registration_prefix).pluck(:organization_id)).to all(eq(default_organization.id))
it 'fails with error' do
expect { seed_without_organization }.to raise_error(
"No organization found. Ensure user has an organization or pass an organization_id"
)
end
end
end

View File

@ -1257,37 +1257,18 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
end
end
describe '#has_protected_tag_rules_for_delete?' do
describe '#protected_from_delete_by_tag_rules?' do
let_it_be_with_refind(:project) { create(:project, path: 'test') }
let_it_be(:user) { create(:user) }
let(:has_tags) { true }
subject { repository.has_protected_tag_rules_for_delete?(user) }
subject { repository.protected_from_delete_by_tag_rules?(user) }
before do
allow(repository).to receive(:has_tags?).and_return(has_tags)
end
context 'when the project does not have tag protection rules' do
it { is_expected.to be_falsey }
end
context 'when the user is nil' do
let(:user) { nil }
it { is_expected.to be_truthy }
end
context 'when the project has tag protection rules' do
let_it_be(:project) { create(:project, path: 'test') }
before_all do
create(
:container_registry_protection_tag_rule,
project: project,
minimum_access_level_for_delete: Gitlab::Access::OWNER
)
end
shared_examples 'checking for mutable tag protection rules' do
context 'for admin' do
before do
allow(user).to receive(:can_admin_all_resources?).and_return(true)
@ -1296,7 +1277,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
it { is_expected.to be_falsey }
end
context 'when user has lower access level' do
context 'when the user has a lower access level' do
before_all do
project.add_maintainer(user)
end
@ -1310,7 +1291,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
end
end
context 'when user has the same or higher access level' do
context 'when the user meets the minimum access level' do
before_all do
project.add_owner(user)
end
@ -1318,5 +1299,79 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
it { is_expected.to be_falsey }
end
end
context 'when the project does not have tag protection rules' do
it { is_expected.to be_falsey }
end
context 'when the user is nil' do
let(:user) { nil }
it { is_expected.to be_truthy }
end
context 'when the project only has mutable tag protection rules' do
before_all do
create(
:container_registry_protection_tag_rule,
project: project,
minimum_access_level_for_delete: Gitlab::Access::OWNER
)
end
it_behaves_like 'checking for mutable tag protection rules'
end
context 'when the project has immutable tag protection rule only' do
before_all do
create(
:container_registry_protection_tag_rule,
:immutable,
project: project
)
end
it { is_expected.to be_truthy }
context 'when the container repository does not have tags' do
let(:has_tags) { false }
it { is_expected.to be_falsey }
end
context 'when the feature container_registry_immutable_tags is disabled' do
before do
stub_feature_flags(container_registry_immutable_tags: false)
end
it { is_expected.to be_falsey }
end
end
context 'when the project has both immutable and mutable tags' do
before_all do
create(
:container_registry_protection_tag_rule,
:immutable,
project: project
)
create(
:container_registry_protection_tag_rule,
project: project,
tag_name_pattern: 'mutable',
minimum_access_level_for_delete: Gitlab::Access::OWNER
)
end
it { is_expected.to be_truthy }
context 'when the feature container_registry_immutable_tags is disabled' do
before do
stub_feature_flags(container_registry_immutable_tags: false)
end
it_behaves_like 'checking for mutable tag protection rules'
end
end
end
end

View File

@ -9944,8 +9944,9 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
describe '#has_container_registry_protected_tag_rules?' do
let_it_be_with_refind(:project) { create(:project) }
let(:include_immutable) { true }
subject { project.has_container_registry_protected_tag_rules?(action: 'delete', access_level: Gitlab::Access::OWNER) }
subject { project.has_container_registry_protected_tag_rules?(action: 'delete', access_level: Gitlab::Access::OWNER, include_immutable: include_immutable) }
it 'returns false when there is no matching tag protection rule' do
create(:container_registry_protection_tag_rule,
@ -9957,25 +9958,100 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
expect(subject).to eq(false)
end
it 'returns true when there exists a matching tag protection rule' do
it 'returns true when there is a matching tag protection rule' do
create(
:container_registry_protection_tag_rule,
project: project,
minimum_access_level_for_push: :maintainer,
minimum_access_level_for_delete: :admin
minimum_access_level_for_push: Gitlab::Access::MAINTAINER,
minimum_access_level_for_delete: Gitlab::Access::ADMIN
)
expect(subject).to eq(true)
end
it 'memoizes the call' do
context 'with immutable tag rules only' do
before_all do
create(:container_registry_protection_tag_rule, :immutable, project: project)
end
context 'when include_immutable is true' do
let(:include_immutable) { true }
it { is_expected.to be true }
end
context 'when include_immutable is false' do
let(:include_immutable) { false }
it { is_expected.to be false }
end
end
context 'with both mutable and immutable tag rules' do
before_all do
create(:container_registry_protection_tag_rule, :immutable, project: project)
create(
:container_registry_protection_tag_rule,
project: project,
tag_name_pattern: 'mutable',
minimum_access_level_for_push: Gitlab::Access::MAINTAINER,
minimum_access_level_for_delete: Gitlab::Access::ADMIN
)
end
context 'when include_immutable is true' do
let(:include_immutable) { true }
it { is_expected.to be true }
end
context 'when include_immutable is false' do
let(:include_immutable) { false }
it { is_expected.to be true }
end
end
it 'memoizes calls with the same parameters' do
allow(project.container_registry_protection_tag_rules).to receive(:for_actions_and_access).and_call_original
2.times do
project.has_container_registry_protected_tag_rules?(action: 'push', access_level: :maintainer)
project.has_container_registry_protected_tag_rules?(action: 'push', access_level: :maintainer, include_immutable: true)
end
expect(project.container_registry_protection_tag_rules).to have_received(:for_actions_and_access).with(%w[push], :maintainer).once
expect(project.container_registry_protection_tag_rules).to have_received(:for_actions_and_access).with(%w[push], :maintainer, include_immutable: true).once
end
end
describe '#has_container_registry_immutable_tag_rules?' do
let_it_be_with_refind(:project) { create(:project) }
subject { project.has_container_registry_immutable_tag_rules? }
before_all do
create(:container_registry_protection_tag_rule, project: project)
end
context 'when there is no immutable tag rule' do
it { is_expected.to be false }
end
context 'when there is an immutable tag rule' do
before_all do
create(:container_registry_protection_tag_rule, :immutable, tag_name_pattern: 'immutable', project: project)
end
it { is_expected.to be true }
end
it 'memoizes calls with the same parameters' do
allow(project.container_registry_protection_tag_rules).to receive(:immutable).and_call_original
2.times do
project.has_container_registry_immutable_tag_rules?
end
expect(project.container_registry_protection_tag_rules).to have_received(:immutable).once
end
end

View File

@ -24,7 +24,57 @@ RSpec.describe ContainerRepositoryPolicy, feature_category: :container_registry
allow(container_repository).to receive(:has_tags?).and_return(has_tags)
end
context 'when the project has tag protection rules' do
context 'when the project has an immutable tag protection rule' do
before_all do
create(
:container_registry_protection_tag_rule,
:immutable,
project: project
)
end
context 'when the container repository has tags' do
let(:has_tags) { true }
[:owner, :maintainer, :developer].each do |user_role|
context "when the user is #{user_role}" do
before do
project.send(:"add_#{user_role}", user)
end
it { expect_disallowed(:destroy_container_image) }
end
end
context 'when the current user is an admin', :enable_admin_mode do
let(:user) { build_stubbed(:admin) }
it { expect_disallowed(:destroy_container_image) }
end
end
context 'when the container repository does not have tags' do
let(:has_tags) { false }
[:owner, :maintainer, :developer].each do |user_role|
context "when the user is #{user_role}" do
before do
project.send(:"add_#{user_role}", user)
end
it { expect_allowed(:destroy_container_image) }
end
end
context 'when the current user is an admin', :enable_admin_mode do
let(:user) { build_stubbed(:admin) }
it { expect_allowed(:destroy_container_image) }
end
end
end
context 'when the project has a mutable tag protection rule' do
before_all do
create(
:container_registry_protection_tag_rule,

View File

@ -29,4 +29,91 @@ RSpec.describe Groups::GroupMembersController, feature_category: :groups_and_pro
it_behaves_like 'request_accessable'
end
describe 'GET /groups/*group_id/-/group_members/invite_search.json' do
subject(:request) do
get invite_search_group_group_members_path(membershipable, params: params, format: :json)
end
let(:params) { {} }
let_it_be(:regular_user) { create(:user) }
let_it_be(:admin_user) { create(:user, :admin) }
let_it_be(:banned_user) { create(:user, :banned) }
let_it_be(:blocked_user) { create(:user, :blocked) }
let_it_be(:ldap_blocked_user) { create(:user, :ldap_blocked) }
let_it_be(:external_user) { create(:user, :external) }
let_it_be(:unconfirmed_user) { create(:user, confirmed_at: nil) }
let_it_be(:omniauth_user) { create(:omniauth_user) }
let_it_be(:internal_user) { Users::Internal.alert_bot }
let_it_be(:project_bot_user) { create(:user, :project_bot) }
let_it_be(:service_account_user) { create(:user, :service_account) }
let(:searchable_users) do
[
user,
regular_user,
admin_user,
external_user,
unconfirmed_user,
omniauth_user,
service_account_user
]
end
before do
sign_in(user)
end
context 'when user has permission to manage group members' do
before_all do
membershipable.add_owner(user)
end
it 'returns searchable users' do
request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.pluck('id')).to match_array(searchable_users.map(&:id))
end
context 'for search param' do
let(:params) { { search: search } }
context 'with empty string' do
let(:search) { '' }
it 'returns searchable users' do
request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.pluck('id')).to match_array(searchable_users.map(&:id))
end
end
context "with a user's name" do
let(:search) { regular_user.name }
it 'returns users that match the name' do
request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.pluck('id')).to contain_exactly(regular_user.id)
end
end
end
end
context 'when user does not have permission to manage group members' do
before_all do
membershipable.add_maintainer(user)
end
it 'returns 403 forbidden' do
request
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
end

View File

@ -6,7 +6,7 @@ require_relative '../concerns/membership_actions_shared_examples'
RSpec.describe Projects::ProjectMembersController, feature_category: :groups_and_projects do
let_it_be(:user) { create(:user) }
let_it_be(:membershipable) { create(:project, :public, namespace: create(:group, :public)) }
let_it_be(:membershipable) { create(:project, :public, namespace: create(:group, :public), creator: user) }
let(:membershipable_path) { project_path(membershipable) }
@ -20,4 +20,96 @@ RSpec.describe Projects::ProjectMembersController, feature_category: :groups_and
it_behaves_like 'request_accessable'
end
describe 'GET /*namespace_id/:project_id/-/project_members/invite_search.json' do
subject(:request) do
get invite_search_namespace_project_project_members_path(
namespace_id: membershipable.namespace,
project_id: membershipable,
params: params,
format: :json
)
end
let(:params) { {} }
let_it_be(:regular_user) { create(:user) }
let_it_be(:admin_user) { create(:user, :admin) }
let_it_be(:banned_user) { create(:user, :banned) }
let_it_be(:blocked_user) { create(:user, :blocked) }
let_it_be(:ldap_blocked_user) { create(:user, :ldap_blocked) }
let_it_be(:external_user) { create(:user, :external) }
let_it_be(:unconfirmed_user) { create(:user, confirmed_at: nil) }
let_it_be(:omniauth_user) { create(:omniauth_user) }
let_it_be(:internal_user) { Users::Internal.alert_bot }
let_it_be(:project_bot_user) { create(:user, :project_bot) }
let_it_be(:service_account_user) { create(:user, :service_account) }
let(:searchable_users) do
[
user,
regular_user,
admin_user,
external_user,
unconfirmed_user,
omniauth_user,
service_account_user
]
end
before do
sign_in(user)
end
context 'when user has permission to manage project members' do
before_all do
membershipable.add_maintainer(user)
end
it 'returns searchable users' do
request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.pluck('id')).to match_array(searchable_users.map(&:id))
end
context 'for search param' do
let(:params) { { search: search } }
context 'with empty string' do
let(:search) { '' }
it 'returns searchable users' do
request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.pluck('id')).to match_array(searchable_users.map(&:id))
end
end
context "with a user's name" do
let(:search) { regular_user.name }
it 'returns users that match the name' do
request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.pluck('id')).to contain_exactly(regular_user.id)
end
end
end
end
context 'when user does not have permission to manage project members' do
before_all do
membershipable.add_developer(user)
end
it 'returns 404 not_found' do
request
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end

View File

@ -185,4 +185,14 @@ EOS
)
end
end
def simulate_post_receive(project, branch_name, identifier)
oldrev = project.repository.commit(branch_name).sha
yield
newrev = project.repository.commit(branch_name).sha
changes = Base64.encode64("#{oldrev} #{newrev} refs/heads/#{branch_name}")
Repositories::PostReceiveWorker.new.perform("project-#{project.id}", identifier, changes)
end
end

View File

@ -25,7 +25,7 @@ RSpec.describe 'gitlab:seed:runner_fleet rake task', :silence_stdout, feature_ca
context 'with admin username', :enable_admin_mode do
let(:username) { 'runner_fleet_seed' }
let!(:admin) { create(:user, :admin, username: username) }
let!(:admin) { create(:user, :admin, :with_organization, username: username) }
it 'performs runner fleet seed successfully' do
expect { rake_task }

View File

@ -22,7 +22,7 @@ module Tooling
class DownloadJobTrace
DEFAULT_TRACE_MARKER = 'failure-analyzer'
DEFAULT_MAX_ATTEMPTS = 5
DEFAULT_RETRY_DELAY_SECONDS = 10
DEFAULT_RETRY_DELAY_SECONDS = 20
def initialize(
api_url: ENV['CI_API_V4_URL'],