Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-06-12 21:10:00 +00:00
parent 4c184f0a70
commit 645c20e091
64 changed files with 604 additions and 496 deletions

View File

@ -61,6 +61,7 @@ workflow:
- bundle exec orchestrator metrics start --interval 1
after_script:
- !reference [.gitlab-qa-report, after_script]
- source $CI_PROJECT_DIR/scripts/utils.sh
- |
section_start "logs_section" "Saving environment logs"
bundle exec orchestrator log events --save || true

View File

@ -3556,6 +3556,7 @@ Gitlab/BoundedContexts:
- 'ee/lib/ee/sidebars/projects/menus/settings_menu.rb'
- 'ee/lib/ee/sidebars/projects/menus/work_items_menu.rb'
- 'ee/lib/ee/sidebars/projects/panel.rb'
- 'ee/lib/ee/sidebars/projects/super_sidebar_panel.rb'
- 'ee/lib/ee/sidebars/user_settings/menus/access_tokens_menu.rb'
- 'ee/lib/ee/sidebars/user_settings/panel.rb'
- 'ee/lib/ee/sidebars/your_work/panel.rb'
@ -3643,6 +3644,7 @@ Gitlab/BoundedContexts:
- 'ee/lib/sidebars/groups/menus/wiki_menu.rb'
- 'ee/lib/sidebars/groups/menus/work_item_epics_menu.rb'
- 'ee/lib/sidebars/projects/menus/learn_gitlab_menu.rb'
- 'ee/lib/sidebars/projects/super_sidebar_menus/duo_agents_menu.rb'
- 'ee/lib/sidebars/user_settings/menus/profile_billing_menu.rb'
- 'ee/lib/sidebars/your_work/menus/environments_dashboard_menu.rb'
- 'ee/lib/sidebars/your_work/menus/operations_dashboard_menu.rb'

View File

@ -1 +1 @@
7562adb6b7cc44f2997fe37692bbd9ff911cfd6d
55f4fb94e39f60e1d4c76deac5d6f20c6202c68f

View File

@ -218,7 +218,7 @@
{"name":"gitaly","version":"18.1.0.pre.rc1","platform":"ruby","checksum":"8f65a0c5bb3694c91c9fa4bfa7ceabfc131846b78feed8ee32a744aaacf6e70a"},
{"name":"gitlab","version":"4.19.0","platform":"ruby","checksum":"3f645e3e195dbc24f0834fbf83e8ccfb2056d8e9712b01a640aad418a6949679"},
{"name":"gitlab-chronic","version":"0.10.6","platform":"ruby","checksum":"a244d11a1396d2aac6ae9b2f326adf1605ec1ad20c29f06e8b672047d415a9ac"},
{"name":"gitlab-cloud-connector","version":"1.15.0","platform":"ruby","checksum":"19c45cd38e0d8721c61809bb05a4d593a365854bb60bb7e78ad765613d668193"},
{"name":"gitlab-cloud-connector","version":"1.17.0","platform":"ruby","checksum":"b9eaf5544cebb66667be560cc032fd6e26ccb6c35c0912b3cd1fadb7cbcfbf34"},
{"name":"gitlab-crystalball","version":"1.1.0","platform":"ruby","checksum":"bd314742a89cad8cb858fec41fc5282ff64ccf262cffa1d5b118f053c5c382a8"},
{"name":"gitlab-dangerfiles","version":"4.9.2","platform":"ruby","checksum":"d5c050f685d8720f6e70191a7d1216854d860dbdea5b455f87abe7542e005798"},
{"name":"gitlab-experiment","version":"0.9.1","platform":"ruby","checksum":"f230ee742154805a755d5f2539dc44d93cdff08c5bbbb7656018d61f93d01f48"},

View File

@ -744,7 +744,7 @@ GEM
terminal-table (>= 1.5.1)
gitlab-chronic (0.10.6)
numerizer (~> 0.2)
gitlab-cloud-connector (1.15.0)
gitlab-cloud-connector (1.17.0)
activesupport (~> 7.0)
jwt (~> 2.9.3)
gitlab-crystalball (1.1.0)

View File

@ -218,7 +218,7 @@
{"name":"gitaly","version":"18.1.0.pre.rc1","platform":"ruby","checksum":"8f65a0c5bb3694c91c9fa4bfa7ceabfc131846b78feed8ee32a744aaacf6e70a"},
{"name":"gitlab","version":"4.19.0","platform":"ruby","checksum":"3f645e3e195dbc24f0834fbf83e8ccfb2056d8e9712b01a640aad418a6949679"},
{"name":"gitlab-chronic","version":"0.10.6","platform":"ruby","checksum":"a244d11a1396d2aac6ae9b2f326adf1605ec1ad20c29f06e8b672047d415a9ac"},
{"name":"gitlab-cloud-connector","version":"1.15.0","platform":"ruby","checksum":"19c45cd38e0d8721c61809bb05a4d593a365854bb60bb7e78ad765613d668193"},
{"name":"gitlab-cloud-connector","version":"1.17.0","platform":"ruby","checksum":"b9eaf5544cebb66667be560cc032fd6e26ccb6c35c0912b3cd1fadb7cbcfbf34"},
{"name":"gitlab-crystalball","version":"1.1.0","platform":"ruby","checksum":"bd314742a89cad8cb858fec41fc5282ff64ccf262cffa1d5b118f053c5c382a8"},
{"name":"gitlab-dangerfiles","version":"4.9.2","platform":"ruby","checksum":"d5c050f685d8720f6e70191a7d1216854d860dbdea5b455f87abe7542e005798"},
{"name":"gitlab-experiment","version":"0.9.1","platform":"ruby","checksum":"f230ee742154805a755d5f2539dc44d93cdff08c5bbbb7656018d61f93d01f48"},

View File

@ -738,7 +738,7 @@ GEM
terminal-table (>= 1.5.1)
gitlab-chronic (0.10.6)
numerizer (~> 0.2)
gitlab-cloud-connector (1.15.0)
gitlab-cloud-connector (1.17.0)
activesupport (~> 7.0)
jwt (~> 2.9.3)
gitlab-crystalball (1.1.0)

View File

@ -317,7 +317,7 @@ export default {
<div
class="align-items-start board-card-number-container gl-flex gl-flex-wrap-reverse gl-overflow-hidden"
>
<span class="board-info-items gl-flex gl-items-center gl-leading-20">
<span class="board-info-items gl-inline-block gl-leading-20">
<gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-5" />
<span
v-if="showBoardCardNumber"

View File

@ -46,6 +46,7 @@ export default {
:filtered-search-term-key="$options.FILTERED_SEARCH_TERM_KEY"
:filtered-search-namespace="$options.FILTERED_SEARCH_NAMESPACE"
:filtered-search-recent-searches-storage-key="$options.RECENT_SEARCHES_STORAGE_KEY_GROUPS"
:filtered-search-input-placeholder="__('Search')"
:sort-options="$options.SORT_OPTIONS"
:default-sort-option="$options.SORT_OPTION_UPDATED"
:timestamp-type-map="$options.timestampTypeMap"

View File

@ -52,12 +52,12 @@ export const SORT_OPTION_NAME = {
};
export const SORT_OPTION_CREATED = {
value: 'created',
value: 'created_at',
text: SORT_LABEL_CREATED,
};
export const SORT_OPTION_UPDATED = {
value: 'latest_activity',
value: 'updated_at',
text: SORT_LABEL_UPDATED,
};

View File

@ -20,7 +20,7 @@ export const formatGroupForGraphQLResolver = (group) => ({
viewEditPage: group.can_edit,
},
webUrl: group.web_url,
groupMembersCount: group.group_members_count,
groupMembersCount: group.group_members_count ?? null,
isLinkedToSubscription: group.is_linked_to_subscription,
permanentDeletionDate: group.permanent_deletion_date,
maxAccessLevel: {
@ -31,8 +31,8 @@ export const formatGroupForGraphQLResolver = (group) => ({
parent: {
id: group.parent_id,
},
descendantGroupsCount: group.subgroup_count,
projectsCount: group.project_count,
descendantGroupsCount: group.subgroup_count ?? null,
projectsCount: group.project_count ?? null,
children: group.children?.length ? group.children.map(formatGroupForGraphQLResolver) : [],
childrenCount: group.subgroup_count,
childrenCount: group.subgroup_count ?? 0,
});

View File

@ -64,6 +64,11 @@ export default {
type: String,
required: true,
},
filteredSearchInputPlaceholder: {
type: String,
required: false,
default: __('Filter or search (3 character minimum)'),
},
sortOptions: {
type: Array,
required: true,
@ -197,7 +202,7 @@ export default {
return this.initialSort;
}
return `${this.defaultSortOption.value}_${SORT_DIRECTION_ASC}`;
return `${this.defaultSortOption.value}_${SORT_DIRECTION_DESC}`;
},
activeSortOption() {
return this.sortOptions.find((sortItem) => this.sort.includes(sortItem.value));
@ -532,6 +537,7 @@ export default {
:filtered-search-term-key="filteredSearchTermKey"
:filtered-search-recent-searches-storage-key="filteredSearchRecentSearchesStorageKey"
:filtered-search-query="$route.query"
:search-input-placeholder="filteredSearchInputPlaceholder"
:is-ascending="isAscending"
:sort-options="sortOptions"
:active-sort-option="activeSortOption"

View File

@ -64,7 +64,7 @@ export default {
</script>
<template>
<import-projects-table v-bind="$attrs">
<template #filter="{ importAllButtonText, showModalHandler }">
<template #filter="{ importAllButtonText, showImportAllModal }">
<gl-tabs v-model="selectedRelationTypeTabIdx" content-class="!gl-py-0 gl-mb-3">
<gl-tab v-for="tab in $options.relationTypes" :key="tab.title" :title="tab.title" lazy>
<div
@ -92,7 +92,7 @@ export default {
:loading="isImportingAnyRepo"
:disabled="!hasImportableRepos"
type="button"
@click="showModalHandler"
@click="showImportAllModal"
>
{{ importAllButtonText }}
</gl-button>

View File

@ -29,10 +29,6 @@ export default {
type: String,
required: true,
},
provider: {
type: String,
required: true,
},
filterable: {
type: Boolean,
required: false,
@ -65,8 +61,6 @@ export default {
optionalStagesSelection: Object.fromEntries(
this.optionalStages.map(({ name, selected }) => [name, selected]),
),
showImportAllModal: false,
showNamespaceRequiredModal: false,
};
},
@ -78,7 +72,6 @@ export default {
'hasImportableRepos',
'hasIncompatibleRepos',
'importAllCount',
'getImportTarget',
]),
pagePaginationStateKey() {
@ -108,14 +101,6 @@ export default {
fromHeaderText() {
return sprintf(__('From %{providerTitle}'), { providerTitle: this.providerTitle });
},
missingNamespaceCount() {
return this.reposMissingTargetNamespace().length;
},
isManifestImport() {
return this.provider === 'manifest';
},
},
mounted() {
@ -138,28 +123,10 @@ export default {
'importAll',
]),
showModalHandler() {
if (this.missingNamespaceCount > 0) {
this.showNamespaceRequiredModal = true;
} else {
this.showImportAllModal = true;
}
},
reposMissingTargetNamespace() {
if (this.isManifestImport) {
return [];
}
return this.repositories.filter((repo) => {
if (repo.importSource.target) return false;
const target = this.getImportTarget(repo.importSource.id);
return !target?.targetNamespace;
});
showImportAllModal() {
this.$refs.importAllModal.show();
},
},
actionPrimary: { text: __('Okay') },
};
</script>
@ -171,14 +138,14 @@ export default {
<template v-if="hasIncompatibleRepos">
<slot name="incompatible-repos-warning"></slot>
</template>
<slot name="filter" v-bind="{ showModalHandler, importAllButtonText }">
<slot name="filter" v-bind="{ showImportAllModal, importAllButtonText }">
<div class="gl-mb-5 gl-flex gl-flex-wrap gl-justify-between">
<gl-button
variant="confirm"
:loading="isImportingAnyRepo"
:disabled="!hasImportableRepos"
type="button"
@click="showModalHandler"
@click="showImportAllModal"
>{{ importAllButtonText }}</gl-button
>
@ -203,7 +170,6 @@ export default {
/>
<gl-modal
ref="importAllModal"
v-model="showImportAllModal"
modal-id="import-all-modal"
:title="s__('ImportProjects|Import repositories')"
:ok-title="__('Import')"
@ -217,19 +183,6 @@ export default {
)
}}
</gl-modal>
<gl-modal
ref="namespaceRequiredModal"
v-model="showNamespaceRequiredModal"
modal-id="namespace-required-modal"
:title="s__('ImportProjects|Namespace required')"
:action-primary="$options.actionPrimary"
>
{{
s__(
'ImportProjects|Select a destination namespace for each repository before importing all.',
)
}}
</gl-modal>
<div v-if="repositories.length" class="gl-w-full">
<table class="table gl-table">
<thead>

View File

@ -60,8 +60,6 @@ export default {
return {
isSelectedForReimport: false,
showMembershipsModal: false,
userHasSelectedNamespace: false,
showNamespaceRequiredError: false,
};
},
@ -74,13 +72,8 @@ export default {
},
showMembershipsWarning() {
const usersNamespaceIsSelected = this.importTarget.targetNamespace === this.userNamespace;
return this.isNotImporting && usersNamespaceIsSelected;
},
isNotImporting() {
return this.isImportNotStarted || this.isSelectedForReimport;
const userNamespaceSelected = this.importTarget.targetNamespace === this.userNamespace;
return (this.isImportNotStarted || this.isSelectedForReimport) && userNamespaceSelected;
},
isFinished() {
@ -135,16 +128,6 @@ export default {
this.updateImportTarget({ newName: value });
},
},
shouldBlockImportForNamespace() {
// destination repo pre-selected eg. manifest imports
if (this.importTarget.targetNamespace) {
return false;
}
return (
!this.repo.importSource.target && this.isNotImporting && !this.userHasSelectedNamespace
);
},
},
methods: {
@ -173,9 +156,7 @@ export default {
},
onImportClick() {
if (this.shouldBlockImportForNamespace) {
this.showNamespaceRequiredError = true;
} else if (this.showMembershipsWarning) {
if (this.showMembershipsWarning) {
this.showMembershipsModal = true;
} else {
this.handleImportRepo();
@ -183,8 +164,6 @@ export default {
},
onSelect(value) {
this.userHasSelectedNamespace = true;
this.showNamespaceRequiredError = false;
this.updateImportTarget({ targetNamespace: value });
},
},
@ -230,7 +209,6 @@ export default {
<div class="gl-flex gl-w-full gl-items-stretch">
<import-target-dropdown
:selected="importTarget.targetNamespace"
:toggle-text="s__('ImportProjects|Select namespace')"
:user-namespace="userNamespace"
@select="onSelect"
/>
@ -246,14 +224,6 @@ export default {
data-testid="project-path-field"
/>
</div>
<p
v-if="showNamespaceRequiredError"
class="gl-m-0 gl-mt-2 gl-text-danger"
role="alert"
data-testid="namespace-required-warning"
>
{{ s__('ImportProjects|Select a destination namespace.') }}
</p>
</template>
<template v-else-if="repo.importedProject">{{ displayFullPath }}</template>
</div>
@ -294,11 +264,9 @@ export default {
<gl-modal
v-if="showMembershipsWarning"
v-model="showMembershipsModal"
modal-id="show-memberships-modal"
:title="
s__('ImportProjects|Are you sure you want to import the project to a personal namespace?')
"
data-testid="memberships-warning-modal"
:action-primary="$options.actionPrimary"
:action-cancel="$options.actionCancel"
@primary="handleImportRepo"

View File

@ -42,7 +42,6 @@ export function initStoreFromElement(element) {
export function initPropsFromElement(element) {
return {
providerTitle: element.dataset.providerTitle,
provider: element.dataset.provider,
filterable: parseBoolean(element.dataset.filterable),
paginatable: parseBoolean(element.dataset.paginatable),
optionalStages: JSON.parse(element.dataset.optionalStages),

View File

@ -19,6 +19,6 @@ export const getImportTarget = (state) => (repoId) => {
return {
newName: repo.importSource.sanitizedName,
targetNamespace: null,
targetNamespace: state.defaultTargetNamespace,
};
};

View File

@ -19,12 +19,13 @@ export default {
integrationsGrouped() {
return this.integrations.reduce(
(integrations, integration) => {
if (integration.active) {
if (integration.name === 'amazon_q') {
// Amazon Q will appear in the General Section, not here.
} else if (integration.active) {
integrations.active.push(integration);
} else {
integrations.inactive.push(integration);
}
return integrations;
},
{ active: [], inactive: [] },

View File

@ -8,7 +8,7 @@ import axios from '~/lib/utils/axios_utils';
import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '~/visibility_level/constants';
import { ACCESS_LEVEL_LABELS, ACCESS_LEVEL_NO_ACCESS_INTEGER } from '~/access_level/constants';
import { __ } from '~/locale';
import { numberToMetricPrefix } from '~/lib/utils/number_utils';
import { numberToMetricPrefix, isNumeric } from '~/lib/utils/number_utils';
import { ACTION_DELETE, ACTION_LEAVE } from '~/vue_shared/components/list_actions/constants';
import {
TIMESTAMP_TYPES,
@ -110,6 +110,15 @@ export default {
groupMembersCount() {
return numberToMetricPrefix(this.group.groupMembersCount);
},
showDescendantGroupsCount() {
return isNumeric(this.group.descendantGroupsCount);
},
showProjectsCount() {
return isNumeric(this.group.projectsCount);
},
showGroupMembersCount() {
return isNumeric(this.group.groupMembersCount);
},
hasActionDelete() {
return this.group.availableActions?.includes(ACTION_DELETE);
},
@ -181,18 +190,21 @@ export default {
<template #stats>
<group-list-item-inactive-badge :group="group" />
<list-item-stat
v-if="showDescendantGroupsCount"
:tooltip-text="$options.i18n.subgroups"
icon-name="subgroup"
:stat="descendantGroupsCount"
data-testid="subgroups-count"
/>
<list-item-stat
v-if="showProjectsCount"
:tooltip-text="$options.i18n.projects"
icon-name="project"
:stat="projectsCount"
data-testid="projects-count"
/>
<list-item-stat
v-if="showGroupMembersCount"
:tooltip-text="$options.i18n.directMembers"
icon-name="users"
:stat="groupMembersCount"

View File

@ -626,7 +626,6 @@ module Ci
end
def ensure_organization_id
return if Feature.disabled?(:populate_organization_id_in_runner_tables, owner)
return unless instance_type? || owner.present?
self.organization_id = instance_type? ? nil : owner.organization_id

View File

@ -198,8 +198,6 @@ module Ci
end
def ensure_organization_id
return if Feature.disabled?(:populate_organization_id_in_runner_tables, runner.owner)
self.organization_id = runner.organization_id
end

View File

@ -41,6 +41,7 @@ class Packages::PackageFile < ApplicationRecord
validates :file_name, uniqueness: { scope: :package }, if: -> { !pending_destruction? && package&.pypi? }
validates :file_sha256, format: { with: Gitlab::Regex.sha256_regex }, if: -> { package&.pypi? }, allow_nil: true
validate :ensure_unique_conan_file_name, if: -> { !pending_destruction? && package&.conan? }, on: :create
scope :recent, -> { order(id: :desc) }
scope :limit_recent, ->(limit) { recent.limit(limit) }
@ -187,6 +188,22 @@ class Packages::PackageFile < ApplicationRecord
carrierwave_file.copy_to(new_file_path)
carrierwave_file.delete
end
def ensure_unique_conan_file_name
return unless conan_file_metadatum && conan_file_metadatum.recipe_revision_id.present?
return unless self.class.installable.where(
package_id: package_id,
file_name: file_name,
packages_conan_file_metadata: {
recipe_revision_id: conan_file_metadatum.recipe_revision_id,
package_reference_id: conan_file_metadatum.package_reference_id,
package_revision_id: conan_file_metadatum.package_revision_id
}
).joins(:conan_file_metadatum).exists?
errors.add(:file_name, _('already exists for the given recipe revision, package reference, and package revision'))
end
end
Packages::PackageFile.prepend_mod_with('Packages::PackageFile')

View File

@ -29,7 +29,7 @@ class WikiPage
scope :with_canonical_slug, ->(slug) do
slug_table_name = klass.reflect_on_association(:slugs).table_name
joins(:slugs).where(slug_table_name => { canonical: true, slug: slug }).order(created_at: :asc)
joins(:slugs).where(slug_table_name => { canonical: true, slug: slug })
end
scope :for_project, ->(project) do
where(project: project)
@ -77,6 +77,13 @@ class WikiPage
.limit(2)
if conflict.present?
# Ensure the conflict record will be the orphaned record when doing a page update
if canonical_slug.size > 1
old_slug, _new_slug = canonical_slug
meta, conflict = conflict, meta if conflict.canonical_slug == old_slug
end
transaction(requires_new: false) do
conflict.todos.each_batch do |batch|
batch.update_all(target_id: meta.id)

View File

@ -10,10 +10,7 @@ module Ci
# @param [Int] namespace_id: the ID of the parent namespace of the deleted project
def initialize(project_id, namespace_id)
@project_id = project_id
@organization_id =
if Feature.enabled?(:populate_organization_id_in_runner_tables, Project.actor_from_id(project_id))
Namespace.find(namespace_id).organization_id
end
@organization_id = Namespace.find(namespace_id).organization_id
end
def execute
@ -62,16 +59,10 @@ module Ci
# rubocop: disable CodeReuse/ActiveRecord -- this query is too specific to generalize on the models
runner_projects = Ci::RunnerProject.where(Ci::RunnerProject.arel_table[:runner_id].eq(runner_id_column))
if @organization_id.nil?
<<~SQL
sharding_key_id = (#{runner_projects.order(id: :asc).limit(1).select(:project_id).to_sql})
SQL
else
<<~SQL
sharding_key_id = (#{runner_projects.order(id: :asc).limit(1).select(:project_id).to_sql}),
organization_id = #{@organization_id}
SQL
end
<<~SQL
sharding_key_id = (#{runner_projects.order(id: :asc).limit(1).select(:project_id).to_sql}),
organization_id = #{@organization_id}
SQL
# rubocop: enable CodeReuse/ActiveRecord
end
end

View File

@ -3,7 +3,7 @@
- extra_data = local_assigns.fetch(:extra_data, {})
- filterable = local_assigns.fetch(:filterable, true)
- paginatable = local_assigns.fetch(:paginatable, false)
- default_namespace_path = local_assigns[:default_namespace]&.full_path
- default_namespace_path = (local_assigns[:default_namespace] || current_user.namespace).full_path
- cancel_path = local_assigns.fetch(:cancel_path, nil)
- details_path = local_assigns.fetch(:details_path, nil)
- provider_title = Gitlab::ImportSources.title(local_assigns.fetch(:provider))

View File

@ -1,10 +0,0 @@
---
name: populate_organization_id_in_runner_tables
description: Enable app code to populate new organization_id column in runner tables
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/523694
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/193202
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/547421
milestone: '18.1'
group: group::runner
type: gitlab_com_derisk
default_enabled: false

View File

@ -387,8 +387,8 @@ Settings.gitlab_docs['host'] = nil unless Settings.gitlab_docs.enabled
#
Gitlab.ee do
Settings['geo'] ||= {}
# For backwards compatibility, default to gitlab_url and if so, ensure it ends with "/"
Settings.geo['node_name'] = Settings.geo['node_name'].presence || Settings.gitlab['url'].chomp('/').concat('/')
# For backwards compatibility, default to gitlab_url
Settings.geo['node_name'] = Settings.geo['node_name'].presence || Settings.gitlab['url']
#
# Registry replication

View File

@ -71,6 +71,8 @@
- 1
- - auth_saml_group_sync
- 1
- - authn_cleanup_scim_group_memberships
- 1
- - authn_sync_group_scim_identity_record
- 1
- - authn_sync_group_scim_token_record

View File

@ -28242,7 +28242,7 @@ GPG signature for a signed commit.
| <a id="groupmarkdownpaths"></a>`markdownPaths` {{< icon name="warning-solid" >}} | [`MarkdownPaths`](#markdownpaths) | **Introduced** in GitLab 18.1. **Status**: Experiment. Namespace relevant paths to create markdown links on the UI. |
| <a id="groupmarkedfordeletionon"></a>`markedForDeletionOn` {{< icon name="warning-solid" >}} | [`Time`](#time) | **Introduced** in GitLab 16.11. **Status**: Experiment. Date when group was scheduled to be deleted. |
| <a id="groupmathrenderinglimitsenabled"></a>`mathRenderingLimitsEnabled` | [`Boolean`](#boolean) | Indicates if math rendering limits are used for the group. |
| <a id="groupmavenvirtualregistries"></a>`mavenVirtualRegistries` {{< icon name="warning-solid" >}} | [`MavenVirtualRegistryConnection`](#mavenvirtualregistryconnection) | **Introduced** in GitLab 18.1. **Status**: Experiment. Maven virtual registries registered to the group. |
| <a id="groupmavenvirtualregistries"></a>`mavenVirtualRegistries` {{< icon name="warning-solid" >}} | [`MavenVirtualRegistryConnection`](#mavenvirtualregistryconnection) | **Introduced** in GitLab 18.1. **Status**: Experiment. Maven virtual registries registered to the group. Returns null if the `maven_virtual_registry` feature flag is disabled. |
| <a id="groupmaxaccesslevel"></a>`maxAccessLevel` | [`AccessLevel!`](#accesslevel) | Maximum access level of the current user in the group. |
| <a id="groupmentionsdisabled"></a>`mentionsDisabled` | [`Boolean`](#boolean) | Indicates if a group is disabled from getting mentioned. |
| <a id="groupname"></a>`name` | [`String`](#string) | Name of the group. |
@ -31444,6 +31444,22 @@ Maven metadata.
| <a id="mavenmetadatapath"></a>`path` | [`String!`](#string) | Path of the Maven package. |
| <a id="mavenmetadataupdatedat"></a>`updatedAt` | [`Time!`](#time) | Date of most recent update. |
### `MavenUpstream`
Represents the upstream registries of a Maven virtual registry.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mavenupstreamcachevalidityhours"></a>`cacheValidityHours` {{< icon name="warning-solid" >}} | [`Int!`](#int) | **Introduced** in GitLab 18.1. **Status**: Experiment. Time before the cache expires for the upstream registry. |
| <a id="mavenupstreamdescription"></a>`description` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.1. **Status**: Experiment. Description of the upstream registry. |
| <a id="mavenupstreamid"></a>`id` {{< icon name="warning-solid" >}} | [`ID!`](#id) | **Introduced** in GitLab 18.1. **Status**: Experiment. ID of the upstream registry. |
| <a id="mavenupstreamname"></a>`name` {{< icon name="warning-solid" >}} | [`String!`](#string) | **Introduced** in GitLab 18.1. **Status**: Experiment. Name of the upstream registry. |
| <a id="mavenupstreampassword"></a>`password` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.1. **Status**: Experiment. Password to sign in to the upstream registry. |
| <a id="mavenupstreamurl"></a>`url` {{< icon name="warning-solid" >}} | [`String!`](#string) | **Introduced** in GitLab 18.1. **Status**: Experiment. URL of the upstream registry. |
| <a id="mavenupstreamusername"></a>`username` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.1. **Status**: Experiment. Username to sign in to the upstream registry. |
### `MavenVirtualRegistry`
Represents a Maven virtual registry.
@ -31455,6 +31471,7 @@ Represents a Maven virtual registry.
| <a id="mavenvirtualregistrydescription"></a>`description` | [`String`](#string) | Description of the virtual registry. |
| <a id="mavenvirtualregistryid"></a>`id` | [`ID!`](#id) | ID of the virtual registry. |
| <a id="mavenvirtualregistryname"></a>`name` | [`String!`](#string) | Name of the virtual registry. |
| <a id="mavenvirtualregistryupstreams"></a>`upstreams` {{< icon name="warning-solid" >}} | [`[MavenUpstream!]`](#mavenupstream) | **Introduced** in GitLab 18.1. **Status**: Experiment. List of upstream registries for the Maven virtual registry. |
### `MemberApproval`

View File

@ -2119,6 +2119,7 @@ Example response:
]
```
<!--
### Credentials inventory management
{{< details >}}
@ -2137,7 +2138,7 @@ Example response:
The Credentials Inventory API allows top-level-group owners to view, revoke, and rotate the credentials of their enterprise users on GitLab.com.
Prerequisites:
- You must have the Owner role for the group.
#### List all personal access tokens for a group
@ -2325,9 +2326,7 @@ Other possible responses:
- `401: Unauthorized` if the access token is invalid.
- `403: Forbidden` if the access token does not have the required permissions.
## Delete an SSH key for a user
## Delete an SSH key for an enterprise user
#### Delete an SSH key for an enterprise user
Deletes a specified SSH public key for an enterprise user associated with the top-level group.
@ -2349,7 +2348,7 @@ Other possible responses:
- `401: Unauthorized` if the SSH Key is invalid.
- `403: Forbidden` if the user does not have the required permissions.
## Rotate a personal access token for an enterprise user
#### Rotate a personal access token for an enterprise user
Rotates a specified personal access token for an enterprise user associated with the top-level group. This revokes the previous token and creates a new token
that expires after one week.
@ -2401,7 +2400,7 @@ Other possible responses:
- `404: Not Found` if the user is an group owner but the token does not exist.
- `405: Method Not Allowed` if the token is not a personal access token.
## Rotate a group or project access token for an enterprise user
#### Rotate a group or project access token for an enterprise user
Rotates a specified group or project access token for an enterprise user associated with the top-level group. This revokes the previous token and creates a new token
that expires after one week.
@ -2451,3 +2450,4 @@ Other possible responses:
- You do not have access to the specified token.
- `403: Forbidden` if the token is not allowed to rotate itself or token is not a bot user token.
- `404: Not Found` if the user is a group owner but the token does not exist.
-->

View File

@ -58,6 +58,12 @@ Use [GitLab Duo Chat](../user/gitlab_duo_chat/_index.md) to interact with an AI
- Ask about GitLab: Get answers about how GitLab works, concepts, and step-by-step instructions.
- Code-related queries: Ask for explanations of code snippets, generate tests, or refactor selected code in your IDE.
## Editor Extensions team runbook
Use the [Editor Extensions team runbook](https://gitlab.com/gitlab-com/runbooks/-/tree/master/docs/editor-extensions)
to learn more about debugging all supported editor extensions. For internal users, this runbook contains instructions
for requesting internal help.
## Feedback and contributions
We value your input on both the traditional and AI-native features. If you have suggestions, encounter issues,

View File

@ -12,6 +12,7 @@ Prerequisites:
While many extension features might work with earlier versions, they are unsupported.
- The GitLab Duo Code Suggestions feature requires GitLab version 16.8 or later.
- You have [Neovim](https://neovim.io/) version 0.9 or later.
- You have [NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) installed. NPM is required for the Code Suggestions install.
To install the extension, follow the installation steps for your chosen plugin manager:

View File

@ -55,7 +55,7 @@ You can find all those directories listed in the [Linux package configuration do
### Data access
- [Information exclusivity](information_exclusivity.md).
- [Security considerations for project membership](../user/project/members/_index.md#security-considerations).
- [Protecting and removing user file uploads](user_file_uploads.md).
- [Proxying linked images for user privacy](asset_proxy.md).

View File

@ -1,33 +1,13 @@
---
stage: Software Supply Chain Security
group: Authentication
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: Information exclusivity
redirect_to: '../user/project/members/_index.md#security-considerations'
remove_date: '2025-09-20'
---
{{< details >}}
<!-- markdownlint-disable -->
- Tier: Free, Premium, Ultimate
- Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated
This document was moved to [another location](../user/project/members/_index.md#security-considerations).
{{< /details >}}
Git is a distributed version control system (DVCS). This means that everyone
who works with the source code has a local copy of the complete repository.
In GitLab every project member that is not a guest (reporters, developers, and
maintainers) can clone the repository to create a local copy. After obtaining
a local copy, the user can upload the full repository anywhere, including to
another project that is under their control, or onto another server.
Therefore, it is impossible to build access controls that prevent the
intentional sharing of source code by users that have access to the source code.
This is an inherent feature of a DVCS. All Git management systems have this
limitation.
You can take steps to prevent unintentional sharing and information
destruction. This limitation is the reason why only certain people are allowed
to [add users to a project](../user/project/members/_index.md)
and why only a GitLab administrator can
[force push a protected branch](../user/project/repository/branches/protected.md).
<!-- This redirect file can be deleted after <YYYY-MM-DD>. -->
<!-- Redirects that point to other docs in the same project expire in three months. -->
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
<!-- Before deletion, see: https://docs.gitlab.com/development/documentation/redirects -->

View File

@ -140,8 +140,8 @@ URLs.
When a secret is detected a vulnerability is created for it. The vulnerability remains as "Still
detected" even if the secret is removed from the scanned file and pipeline secret detection has been
run again. This is because the secret remains in the Git repository's history. To remove a secret
from the Git repository's history, see
run again. This is because the leaked secret continues to be a security risk until it has been revoked.
Removed secrets also persist in the Git history. To remove a secret from the Git repository's history, see
[Redact text from repository](../../../project/merge_requests/revert_changes.md#redact-text-from-repository).
## Enable the analyzer

View File

@ -105,40 +105,40 @@ In this section, you'll build a simple Kubernetes manifest as an OCI artifact, t
1. We'll deploy NGINX as an example. Add the following YAML to `clusters/applications/nginx/nginx.yaml`:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-example
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: nginx-example
template:
metadata:
labels:
app: nginx-example
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: nginx-example
namespace: default
spec:
ports:
- port: 80
targetPort: 80
protocol: TCP
selector:
app: nginx-example
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-example
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: nginx-example
template:
metadata:
labels:
app: nginx-example
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: nginx-example
namespace: default
spec:
ports:
- port: 80
targetPort: 80
protocol: TCP
selector:
app: nginx-example
```
1. Now, let's package the previous YAML into an OCI image.

View File

@ -62,6 +62,43 @@ The following table lists the requirements supported by GitLab for ISO 27001 and
| 8.29 Security testing in development and acceptance | Security testing processes shall be defined and implemented in the development lifecycle. | <ul><li>Dependency scanning running</li><li>Container scanning running</li><li>SAST running</li><li>DAST running</li><li>API security running</li><li>Secret detection running</li><li>Fuzz testing running</li></ul> |
| 8.32 Change management | Changes to information processing facilities and information systems shall be subject to change management procedures. | <ul><li>Default branch protected</li></ul> |
## NIST 800-53 compliance requirements
The National Institute of Standards and Technology (NIST) Information Technology Laboratory (ITL) provides NIST 800-53 Revision 5.
NIST 800-53 Revision 5 compliance involves implementing security and privacy controls across various areas, including:
- Risk management
- Identification and authentication
- Incident response
- System and communications protection
The following table lists the requirements supported by GitLab for NIST 800-53 and the controls for the requirements.
| NIST 800-53 Revision 5 requirement | Description | Supported controls |
|:---------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------|
| AC-3(2): Dual Authorization | Enforce dual authorization for organization-defined privileged commands or other organization-defined actions. | <ul><li>At least two approvals</li><li>Author approved merge request is forbidden</li><li>Committers approved merge request is forbidden</li><li>Merge requests approval rules prevent editing</li></ul> |
| AC-5: Separation of Duties | Separate duties of individuals to prevent malevolent activity without collusion; document separation of duties; and define system access authorizations to support separation of duties. | <ul><li>At least two approvals</li><li>Author approved merge request is forbidden</li><li>Committers approved merge request is forbidden</li><li>Merge requests approval rules prevent editing</li></ul> |
| AU-9(5): Dual Authorization | Enforce dual authorization for the deletion or modification of organization-defined audit information. | <ul><li>At least two approvals</li><li>Author approved merge request is forbidden</li><li>Committers approved merge request is forbidden</li><li>Merge requests approval rules prevent editing</li></ul> |
| CM-3: Configuration Change Control | Determine and document the types of changes to the system that are configuration-controlled; review proposed configuration-controlled changes and approve or disapprove such changes with explicit consideration for security and privacy impact analyses; document configuration change decisions; implement approved configuration-controlled changes to the system; retain records of configuration-controlled changes to the system for organization-defined time period; monitor and review activities associated with configuration-controlled changes to the system; and coordinate and provide oversight for configuration change control activities through organization-defined configuration change control element. | <ul><li>Default branch protected</li><li>At least two approvals</li><li>Author approved merge request is forbidden</li><li>Committers approved merge request is forbidden</li><li>Merge requests approval rules prevent editing</li></ul> |
| CM-3(1): Automated Documentation, Notification, and Prohibition of Changes | Use automated mechanisms to document proposed changes to the system; notify organization-defined approval authorities; highlight change approvals that have not been received by organization-defined time period; prohibit changes to the system until designated approvals are received; and document all changes to the system. | <ul><li>At least two approvals</li><li>Author approved merge request is forbidden</li><li>Committers approved merge request is forbidden</li><li>Merge requests approval rules prevent editing</li></ul> |
| CM-5: Access Restrictions for Change | Define, document, approve, and enforce physical and logical access restrictions associated with changes to the system. | <ul><li>Default branch protected</li><li>At least two approvals</li><li>Author approved merge request is forbidden</li><li>Committers approved merge request is forbidden</li><li>Merge requests approval rules prevent editing</li></ul> |
| CM-5(4): Dual Authorization | Enforce dual authorization for implementing changes to organization-defined system components and system-level information. | <ul><li>At least two approvals</li><li>Author approved merge request is forbidden</li><li>Committers approved merge request is forbidden</li><li>Merge requests approval rules prevent editing</li></ul> |
| CM-6: Configuration Settings | Establish and document configuration settings for components employed in the system that reflect the most restrictive mode consistent with operational requirements using organization-defined common secure configurations; implement the configuration settings; identify, document, and approve any deviations from established configuration settings for organization-defined system components based on organization-defined operational requirements; and monitor and control changes to the configuration settings in accordance with organizational policies and procedures. | <ul><li>Author approved merge request is forbidden</li></ul> |
| CM-7: Least Functionality | Configure the system to provide only organization-defined mission essential capabilities; and prohibit or restrict organization-defined functions, system ports, protocols, software, or services. | <ul><li>Committers approved merge request is forbidden</li><li>Merge requests approval rules prevent editing</li></ul> |
| CM-9(1): Assignment of Responsibility | Assign responsibility for developing the configuration management process to organizational personnel that are not directly involved in system development. | <ul><li>Default branch protected</li></ul> |
| CM-10: Software Usage Restrictions | Use software and associated documentation in accordance with contract agreements and copyright laws; track the use of software and associated documentation protected by quantity licenses to control copying and distribution; and control and document the use of peer-to-peer file sharing technology to ensure that this capability is not used for the unauthorized distribution, display, performance, or reproduction of copyrighted work. | <ul><li>License compliance running</li></ul> |
| CP-9(7): Dual Authorization | Enforce dual authorization for the deletion or destruction of organization-defined backup information. | <ul><li>At least two approvals</li><li>Author approved merge request is forbidden</li><li>Committers approved merge request is forbidden</li><li>Merge requests approval rules prevent editing</li></ul> |
| IA-2(10): Single Sign-on | Provide a single sign-on capability for organization-defined system accounts and services. | <ul><li>Auth SSO enabled</li></ul> |
| IA-2(12): Acceptance of PIV Credentials | Accept and electronically verify Personal Identity Verification (PIV) credentials. | <ul><li>Auth SSO enabled</li></ul> |
| IA-5(7): No Embedded Unencrypted Static Authenticators | Ensure that unencrypted static authenticators are not embedded in applications or other forms of static storage. | <ul><li>Secret detection running</li></ul> |
| IA-5(9): Federated Credential Management | Use organization-defined external organizations to federate credentials. | <ul><li>Auth SSO enabled</li></ul> |
| IA-8(1): Acceptance of PIV Credentials From Other Agencies | Accept and electronically verify Personal Identity Verification (PIV) credentials from other federal agencies. | <ul><li>Auth SSO enabled</li></ul> |
| IA-8(5): Acceptance of PIV-I Credentials | Accept and verify Personal Identity Verification-I (PIV-I) credentials. | <ul><li>Auth SSO enabled</li></ul> |
| RA-5: Vulnerability Monitoring and Scanning | Scan for vulnerabilities in the system and hosted applications; employ vulnerability scanning tools and techniques; analyze vulnerability scan reports and results; remediate legitimate vulnerabilities; and share vulnerability information. | <ul><li>Dependency scanning running</li><li>Container scanning running</li><li>DAST running</li><li>API security running</li><li>Fuzz testing running</li></ul> |
| SA-11(1): Static Code Analysis | Require the developer of the system, system component, or system service to employ static code analysis tools to identify common flaws and document the results of the analysis. | <ul><li>SAST running</li></ul> |
| SA-11(8): Dynamic Code Analysis | Require the developer of the system, system component, or system service to employ dynamic code analysis tools to identify common flaws and document the results of the analysis. | <ul><li>DAST running</li><li>Fuzz testing running</li></ul> |
## NIST CSF 2.0 compliance requirements
NIST CSF is the Cybersecurity Framework from the National Institute of Standards and Technology.

View File

@ -3,7 +3,7 @@ stage: Tenant Scale
group: Organizations
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: Namespaces
description: Organization, hierarchy, and project grouping.
description: Learn more about different types of namespaces.
---
Namespaces organize projects in GitLab. Because each namespace is separate,
@ -26,7 +26,7 @@ GitLab has two types of namespaces:
- **User**: Your personal namespace is based on your username. In a personal namespace:
- You cannot create subgroups.
- Groups do not inherit your namespace permissions or group features.
- Groups you belong to do not inherit your personal namespace permissions or features.
- All the projects you create are under the scope of this namespace.
- Changes to your username also change project and namespace URLs. Before you change your username,
read about [repository redirects](../project/repository/_index.md#repository-path-changes).

View File

@ -79,6 +79,31 @@ In the previous example:
- **User 2** is an inherited shared member from the **Toolbox** group that is invited to the **demo** group.
- **User 3** is a direct member added to this project.
## Security considerations
Before adding members to your project, it's important to understand the security implications.
Git is a distributed version control system (DVCS). This means that everyone who works with the
source code has a local copy of the complete repository.
In GitLab, every project member with the Reporter role or higher can clone the repository to create
a local copy. After obtaining a local copy, users can upload the full repository anywhere, including:
- Another project under their control.
- A different server.
- External hosting services.
Access controls cannot prevent the intentional sharing of source code by users who already have access
to the repository. It is an inherent feature of a DVCS and applies to all Git management platforms.
### Mitigate risks
While you cannot prevent intentional sharing by authorized users, you can take steps to prevent
unintentional sharing and information destruction:
- Control who can [add users to a project](#add-users-to-a-project).
- Use [protected branches](../repository/branches/protected.md) to prevent unauthorized force pushes.
- Regularly review project membership and remove users who no longer require access.
## Add users to a project
{{< history >}}

View File

@ -31,12 +31,9 @@ module Gitlab
{
runner_id: runner.id,
runner_type: runner.runner_type,
sharding_key_id: runner.sharding_key_id
}.tap do |attrs|
next if Feature.disabled?(:populate_organization_id_in_runner_tables, runner.owner)
attrs.merge!(organization_id: runner.organization_id)
end
sharding_key_id: runner.sharding_key_id,
organization_id: runner.organization_id
}
end
def polymorphic_taggings?

View File

@ -42,3 +42,5 @@ module Sidebars
end
end
end
Sidebars::Projects::SuperSidebarPanel.prepend_mod_with('Sidebars::Projects::SuperSidebarPanel')

View File

@ -5714,6 +5714,12 @@ msgstr ""
msgid "Agent not found for provided id."
msgstr ""
msgid "Agents"
msgstr ""
msgid "Agents Platform Index"
msgstr ""
msgid "AiAgents|Agent Name"
msgstr ""
@ -23668,6 +23674,15 @@ msgstr ""
msgid "DuoTrialDiscover|Ask questions, explore concepts, test ideas, and receive instant feedback directly in your workflow."
msgstr ""
msgid "DuoWorkflowSettings|Model Context Protocol"
msgstr ""
msgid "DuoWorkflowSettings|Turn on MCP support for GitLab Duo Agentic Chat and GitLab Duo Workflow"
msgstr ""
msgid "DuoWorkflowSettings|Turn on Model Context Protocol (MCP) support"
msgstr ""
msgid "Duplicate entries found for compliance controls for the requirement."
msgstr ""
@ -32095,9 +32110,6 @@ msgstr ""
msgid "ImportProjects|Importing the project failed: %{reason}"
msgstr ""
msgid "ImportProjects|Namespace required"
msgstr ""
msgid "ImportProjects|Organization"
msgstr ""
@ -32119,15 +32131,6 @@ msgstr ""
msgid "ImportProjects|Requesting your %{provider} repositories failed"
msgstr ""
msgid "ImportProjects|Select a destination namespace for each repository before importing all."
msgstr ""
msgid "ImportProjects|Select a destination namespace."
msgstr ""
msgid "ImportProjects|Select namespace"
msgstr ""
msgid "ImportProjects|Select the repositories you want to import"
msgstr ""
@ -48544,9 +48547,6 @@ msgstr ""
msgid "ProjectSettings|Configure your instance"
msgstr ""
msgid "ProjectSettings|Contact an admin to change this setting."
msgstr ""
msgid "ProjectSettings|Container registry"
msgstr ""
@ -48925,13 +48925,10 @@ msgstr ""
msgid "ProjectSettings|This project can use Amazon Q."
msgstr ""
msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
msgid "ProjectSettings|This setting is on for the instance."
msgstr ""
msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
msgstr ""
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
msgid "ProjectSettings|This setting will be applied to all projects unless overridden for a project."
msgstr ""
msgid "ProjectSettings|Title regex"
@ -50347,6 +50344,15 @@ msgstr ""
msgid "PushRules|Commit messages cannot match this %{wiki_syntax_link_start}regular expression%{wiki_syntax_link_end}. If empty, commit messages are not rejected based on any expression."
msgstr ""
msgid "PushRules|Configure push rules for this project. Project settings override group and instance defaults."
msgstr ""
msgid "PushRules|Define push rules for newly created projects and groups. Some settings apply to new groups, all settings apply to new projects."
msgstr ""
msgid "PushRules|Define push rules for newly created projects in this group. Group settings override instance defaults."
msgstr ""
msgid "PushRules|Do not allow users to remove Git tags with %{code_block_start}git push%{code_block_end}"
msgstr ""
@ -50380,9 +50386,6 @@ msgstr ""
msgid "PushRules|Restrict commits to existing GitLab users."
msgstr ""
msgid "PushRules|Restrict push operations for this project."
msgstr ""
msgid "PushRules|Save push rules"
msgstr ""
@ -52296,12 +52299,6 @@ msgstr ""
msgid "Rule name is already taken."
msgstr ""
msgid "Rules that define what git pushes are accepted for a project in this group. All newly created projects in this group will use these settings."
msgstr ""
msgid "Rules that define what git pushes are accepted for a project. All newly created projects will use these settings."
msgstr ""
msgid "Run"
msgstr ""
@ -53586,6 +53583,9 @@ msgstr ""
msgid "Running"
msgstr ""
msgid "Runs"
msgstr ""
msgid "Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects."
msgstr ""
@ -70790,6 +70790,9 @@ msgstr ""
msgid "Workspaces|Available"
msgstr ""
msgid "Workspaces|Available agents for workspaces"
msgstr ""
msgid "Workspaces|Block agent"
msgstr ""
@ -70799,10 +70802,10 @@ msgstr ""
msgid "Workspaces|Blocked"
msgstr ""
msgid "Workspaces|Blocking an agent doesn't delete it. Agents can only be deleted in the project where they were created."
msgid "Workspaces|Blocked agents are not deleted and existing workspaces continue running. You can delete agents in their source project."
msgstr ""
msgid "Workspaces|Blocking an agent doesn't delete it. Agents can only be deleted in the project where they were created. In addition, existing workspaces using a blocked agent will continue to run."
msgid "Workspaces|Blocking an agent doesn't delete it. Agents can only be deleted in the project where they were created."
msgstr ""
msgid "Workspaces|Cancel"
@ -70811,7 +70814,7 @@ msgstr ""
msgid "Workspaces|Cluster agent"
msgstr ""
msgid "Workspaces|Configure which Kubernetes agents are available for new workspaces. These settings do not affect existing workspaces."
msgid "Workspaces|Configure which Kubernetes agents are available for new workspaces. %{learnMore}"
msgstr ""
msgid "Workspaces|Connected"
@ -70895,6 +70898,9 @@ msgstr ""
msgid "Workspaces|Instant development environments"
msgstr ""
msgid "Workspaces|Learn more about agents."
msgstr ""
msgid "Workspaces|Learn more."
msgstr ""
@ -70907,10 +70913,10 @@ msgstr ""
msgid "Workspaces|No active workspaces"
msgstr ""
msgid "Workspaces|No agents available"
msgid "Workspaces|No agents available to create workspaces. Please consult %{linkStart}Workspaces documentation%{linkEnd} for troubleshooting."
msgstr ""
msgid "Workspaces|No agents available to create workspaces. Please consult %{linkStart}Workspaces documentation%{linkEnd} for troubleshooting."
msgid "Workspaces|No agents found."
msgstr ""
msgid "Workspaces|No terminated workspaces"
@ -70976,9 +70982,6 @@ msgstr ""
msgid "Workspaces|This agent is already allowed."
msgstr ""
msgid "Workspaces|This agent is already available."
msgstr ""
msgid "Workspaces|This agent is already blocked."
msgstr ""
@ -70991,6 +70994,9 @@ msgstr ""
msgid "Workspaces|To create a workspace, add a devfile to this project. A devfile is a configuration file for your workspace."
msgstr ""
msgid "Workspaces|Unable to complete request. Please try again."
msgstr ""
msgid "Workspaces|Unable to complete this action"
msgstr ""
@ -71015,9 +71021,6 @@ msgstr ""
msgid "Workspaces|Workspaces"
msgstr ""
msgid "Workspaces|Workspaces Agent Availability"
msgstr ""
msgid "Workspaces|Workspaces Settings"
msgstr ""
@ -72485,6 +72488,9 @@ msgstr ""
msgid "already being used for another iteration within this cadence."
msgstr ""
msgid "already exists for the given recipe revision, package reference, and package revision"
msgstr ""
msgid "already has a \"created\" issue link"
msgstr ""

View File

@ -3,7 +3,7 @@
source 'https://rubygems.org'
gem 'gitlab-qa', '~> 15', '>= 15.5.0', require: 'gitlab/qa'
gem 'gitlab_quality-test_tooling', '~> 2.13.0', require: false
gem 'gitlab_quality-test_tooling', '~> 2.14.0', require: false
gem 'gitlab-utils', path: '../gems/gitlab-utils'
gem 'activesupport', '~> 7.1.5.1' # This should stay in sync with the root's Gemfile
gem 'allure-rspec', '~> 2.27.0'

View File

@ -139,7 +139,7 @@ GEM
rainbow (>= 3, < 4)
table_print (= 1.5.7)
zeitwerk (>= 2, < 3)
gitlab_quality-test_tooling (2.13.0)
gitlab_quality-test_tooling (2.14.0)
activesupport (>= 7.0, < 7.3)
amatch (~> 0.4.1)
fog-google (~> 1.24, >= 1.24.1)
@ -385,7 +385,7 @@ DEPENDENCIES
gitlab-orchestrator!
gitlab-qa (~> 15, >= 15.5.0)
gitlab-utils!
gitlab_quality-test_tooling (~> 2.13.0)
gitlab_quality-test_tooling (~> 2.14.0)
googleauth (~> 1.9.0)
influxdb-client (~> 3.2)
junit_merge (~> 0.1.2)

View File

@ -34,6 +34,7 @@ RSpec.describe 'Import multiple repositories by uploading a manifest file', :js,
page.within(second_row) do
click_on 'Import'
end
click_on 'Continue import'
wait_for_requests
@ -43,21 +44,6 @@ RSpec.describe 'Import multiple repositories by uploading a manifest file', :js,
end
end
it 'confirms user wishes to import all projects', :sidekiq_inline, :js do
visit new_import_manifest_path
attach_file('manifest', Rails.root.join('spec/fixtures/aosp_manifest.xml'))
click_on 'List available repositories'
wait_for_requests
click_on 'Import 660 repositories'
wait_for_requests
expect(page).to have_content 'Are you sure you want to import 660 repositories?'
end
it 'renders an error if the remote url scheme starts with javascript' do
visit new_import_manifest_path

View File

@ -39,6 +39,7 @@ describe('YourWorkGroupsApp', () => {
filteredSearchTermKey: FILTERED_SEARCH_TERM_KEY,
filteredSearchNamespace: FILTERED_SEARCH_NAMESPACE,
filteredSearchRecentSearchesStorageKey: RECENT_SEARCHES_STORAGE_KEY_GROUPS,
filteredSearchInputPlaceholder: 'Search',
sortOptions: SORT_OPTIONS,
defaultSortOption: SORT_OPTION_UPDATED,
timestampTypeMap: {

View File

@ -13,7 +13,17 @@ describe('your work groups resolver', () => {
const endpoint = '/dashboard/groups.json';
const makeQuery = () => {
const makeQuery = (apiResponse = dashboardGroupsWithChildrenResponse) => {
mockAxios = new MockAdapter(axios);
mockAxios.onGet(endpoint).reply(200, apiResponse, {
'x-per-page': 10,
'x-page': 2,
'x-total': 21,
'x-total-pages': 3,
'x-next-page': 3,
'x-prev-page': 1,
});
return mockApollo.clients.defaultClient.query({
query: groupsQuery,
variables: { search: 'foo', sort: 'created_desc', page: 2 },
@ -22,16 +32,6 @@ describe('your work groups resolver', () => {
beforeEach(() => {
mockApollo = createMockApollo([], resolvers(endpoint));
mockAxios = new MockAdapter(axios);
mockAxios.onGet(endpoint).reply(200, dashboardGroupsWithChildrenResponse, {
'x-per-page': 10,
'x-page': 2,
'x-total': 21,
'x-total-pages': 3,
'x-next-page': 3,
'x-prev-page': 1,
});
});
afterEach(() => {
@ -107,4 +107,44 @@ describe('your work groups resolver', () => {
previousPage: 1,
});
});
describe('when stats are undefined', () => {
it('returns null', async () => {
const {
data: {
groups: { nodes },
},
} = await makeQuery(
dashboardGroupsWithChildrenResponse.map((group) => ({
...group,
group_members_count: undefined,
subgroup_count: undefined,
project_count: undefined,
})),
);
expect(nodes[0]).toMatchObject({
descendantGroupsCount: null,
projectsCount: null,
groupMembersCount: null,
});
});
});
describe('when subgroup_count is undefined', () => {
it('returns 0 for childrenCount', async () => {
const {
data: {
groups: { nodes },
},
} = await makeQuery(
dashboardGroupsWithChildrenResponse.map((group) => ({
...group,
subgroup_count: undefined,
})),
);
expect(nodes[0].childrenCount).toBe(0);
});
});
});

View File

@ -331,6 +331,7 @@ describe('TabsWithList', () => {
filteredSearchNamespace: defaultPropsData.filteredSearchNamespace,
filteredSearchRecentSearchesStorageKey:
defaultPropsData.filteredSearchRecentSearchesStorageKey,
searchInputPlaceholder: 'Filter or search (3 character minimum)',
sortOptions: defaultPropsData.sortOptions,
activeSortOption: SORT_OPTION_CREATED,
isAscending: false,
@ -582,9 +583,9 @@ describe('TabsWithList', () => {
});
});
it('falls back to defaultSortOption prop ascending order', () => {
it('falls back to defaultSortOption prop descending order', () => {
expect(findTabView().props()).toMatchObject({
sort: `${defaultPropsData.defaultSortOption.value}_${SORT_DIRECTION_ASC}`,
sort: `${defaultPropsData.defaultSortOption.value}_${SORT_DIRECTION_DESC}`,
});
});
});

View File

@ -1,8 +1,8 @@
import { GlTabs, GlSearchBoxByClick } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import GithubStatusTable from '~/import_entities/import_projects/components/github_status_table.vue';
@ -14,10 +14,10 @@ import * as getters from '~/import_entities/import_projects/store/getters';
const ImportProjectsTableStub = stubComponent(ImportProjectsTable, {
importAllButtonText: 'IMPORT_ALL_TEXT',
methods: {
showModalHandler: jest.fn(),
showImportAllModal: jest.fn(),
},
template:
'<div><slot name="filter" v-bind="{ importAllButtonText: $options.importAllButtonText, showModalHandler }"></slot></div>',
'<div><slot name="filter" v-bind="{ importAllButtonText: $options.importAllButtonText, showImportAllModal }"></slot></div>',
});
Vue.use(Vuex);
@ -44,7 +44,7 @@ describe('GithubStatusTable', () => {
},
});
wrapper = mountExtended(GithubStatusTable, {
wrapper = mount(GithubStatusTable, {
store,
propsData: {
providerTitle: 'Github',

View File

@ -1,8 +1,8 @@
import { GlLoadingIcon, GlButton, GlIntersectionObserver, GlSearchBoxByClick } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { STATUSES } from '~/import_entities/constants';
import ImportProjectsTable from '~/import_entities/import_projects/components/import_projects_table.vue';
import ProviderRepoTableRow from '~/import_entities/import_projects/components/provider_repo_table_row.vue';
@ -13,6 +13,8 @@ import state from '~/import_entities/import_projects/store/state';
describe('ImportProjectsTable', () => {
let wrapper;
const USER_NAMESPACE = 'root';
const findFilterField = () =>
wrapper
.findAllComponents(GlSearchBoxByClick)
@ -31,25 +33,24 @@ describe('ImportProjectsTable', () => {
const findImportAllButton = () =>
wrapper.findAllComponents(GlButton).wrappers.find((w) => w.props('variant') === 'confirm');
const findImportAllModal = () => wrapper.findComponent({ ref: 'importAllModal' });
const findNamespaceRequiredModal = () => wrapper.findComponent({ ref: 'namespaceRequiredModal' });
const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
const importAllFn = jest.fn();
const importAllModalShowFn = jest.fn();
const fetchReposFn = jest.fn();
function createComponent({
state: initialState,
getters: customGetters = {},
getters: customGetters,
slots,
filterable,
paginatable,
optionalStages,
provider = 'test-provider',
} = {}) {
Vue.use(Vuex);
const store = new Vuex.Store({
state: { ...state(), ...initialState },
state: { ...state(), defaultTargetNamespace: USER_NAMESPACE, ...initialState },
getters: {
...getters,
...customGetters,
@ -64,16 +65,18 @@ describe('ImportProjectsTable', () => {
},
});
wrapper = shallowMountExtended(ImportProjectsTable, {
wrapper = shallowMount(ImportProjectsTable, {
store,
propsData: {
providerTitle,
filterable,
paginatable,
optionalStages,
provider,
},
slots,
stubs: {
GlModal: { template: '<div>Modal!</div>', methods: { show: importAllModalShowFn } },
},
});
}
@ -159,36 +162,13 @@ describe('ImportProjectsTable', () => {
expect(wrapper.text()).toContain(`No ${providerTitle} repositories found`);
});
it('requires namespace selection when `import all` button is clicked before selection', async () => {
it('opens confirmation modal when import all button is clicked', async () => {
createComponent({ state: { repositories: [providerRepo] } });
findImportAllButton().vm.$emit('click');
await nextTick();
const namespaceRequired = findNamespaceRequiredModal();
expect(namespaceRequired.props('title')).toBe('Namespace required');
expect(namespaceRequired.props('visible')).toBe(true);
expect(namespaceRequired.text()).toBe(
'Select a destination namespace for each repository before importing all.',
);
});
it('opens confirmation modal when `import all` button is clicked after namespace selection', async () => {
const mockGetImportTarget = jest.fn(() => () => ({
targetNamespace: 'some-namespace',
}));
createComponent({
state: { repositories: [providerRepo] },
getters: { getImportTarget: mockGetImportTarget },
});
findImportAllButton().vm.$emit('click');
await nextTick();
const verifyImport = findImportAllModal();
expect(verifyImport.props('visible')).toBe(true);
expect(importAllModalShowFn).toHaveBeenCalled();
});
it('triggers importAll action when modal is confirmed', async () => {

View File

@ -1,4 +1,4 @@
import { GlBadge, GlButton } from '@gitlab/ui';
import { GlBadge, GlButton, GlModal } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
@ -14,14 +14,14 @@ describe('ProviderRepoTableRow', () => {
const fetchImport = jest.fn();
const cancelImport = jest.fn();
const setImportTarget = jest.fn();
const defaultImportTarget = {
targetNamespace: null,
const groupImportTarget = {
targetNamespace: 'target',
newName: 'newName',
};
const userNamespace = 'root';
function initStore({ importTarget = defaultImportTarget } = {}) {
function initStore({ importTarget = groupImportTarget } = {}) {
const store = new Vuex.Store({
state: {},
getters: {
@ -45,8 +45,7 @@ describe('ProviderRepoTableRow', () => {
const findImportStatus = () => wrapper.findComponent(ImportStatus);
const findProviderLink = () => wrapper.findByTestId('provider-link');
const findMembershipsWarning = () => wrapper.findByTestId('memberships-warning');
const findMembershipsWarningModal = () => wrapper.findByTestId('memberships-warning-modal');
const findNamespaceRequiredWarning = () => wrapper.findByTestId('namespace-required-warning');
const findGlModal = () => wrapper.findComponent(GlModal);
const findCancelButton = () => {
const buttons = wrapper
@ -98,37 +97,12 @@ describe('ProviderRepoTableRow', () => {
expect(findImportTargetDropdown().exists()).toBe(true);
});
it('renders the dropdown without a default selected', () => {
expect(findImportTargetDropdown().props('selected')).toBe(null);
expect(findImportTargetDropdown().props().toggleText).toBe('Select namespace');
});
describe('when no namespace is selected', () => {
it('shows namespace required warning when import button is clicked', async () => {
findImportButton().vm.$emit('click');
await nextTick();
expect(findNamespaceRequiredWarning().text()).toBe('Select a destination namespace.');
});
it('does not trigger import when clicking import button', async () => {
findImportButton().vm.$emit('click');
await nextTick();
expect(fetchImport).not.toHaveBeenCalled();
});
});
describe('when user namespace is selected as import target', () => {
beforeEach(async () => {
beforeEach(() => {
mountComponent(
{ repo },
{ storeOptions: { importTarget: { targetNamespace: userNamespace } } },
);
const dropdown = findImportTargetDropdown();
dropdown.vm.$emit('select', userNamespace);
await nextTick();
});
it('shows memberships warning', () => {
@ -139,7 +113,7 @@ describe('ProviderRepoTableRow', () => {
findImportButton().vm.$emit('click');
await nextTick();
const modal = findMembershipsWarningModal();
const modal = findGlModal();
expect(modal.props('title')).toBe(
'Are you sure you want to import the project to a personal namespace?',
);
@ -148,15 +122,11 @@ describe('ProviderRepoTableRow', () => {
);
});
it('does not show `missing namespace` warning', () => {
expect(findNamespaceRequiredWarning().exists()).toBe(false);
});
it('triggers import when clicking modal primary button', async () => {
findImportButton().vm.$emit('click');
await nextTick();
findMembershipsWarningModal().vm.$emit('primary');
findGlModal().vm.$emit('primary');
expect(fetchImport).toHaveBeenCalledWith(expect.anything(), {
repoId: repo.importSource.id,
@ -166,68 +136,53 @@ describe('ProviderRepoTableRow', () => {
});
describe('when group namespace is selected as import target', () => {
beforeEach(async () => {
const dropdown = findImportTargetDropdown();
dropdown.vm.$emit('select', 'target');
await nextTick();
});
it('does not show memberships warning', () => {
expect(findMembershipsWarning().isVisible()).toBe(false);
});
it('does not show memberships modal when import button is clicked', async () => {
it('does not show modal when import button is clicked', async () => {
findImportButton().vm.$emit('click');
await nextTick();
expect(findMembershipsWarningModal().exists()).toBe(false);
expect(findGlModal().exists()).toBe(false);
});
});
it('renders import button', () => {
expect(findImportButton().exists()).toBe(true);
});
it('imports repo when clicking import button', async () => {
findImportButton().vm.$emit('click');
await nextTick();
expect(fetchImport).toHaveBeenCalledWith(expect.anything(), {
repoId: repo.importSource.id,
optionalStages: {},
});
});
it('includes optionalStages to import', async () => {
const OPTIONAL_STAGES = { stage1: true, stage2: false };
mountComponent({
repo,
optionalStages: OPTIONAL_STAGES,
});
it('does not show `missing namespace` warning when import button is clicked', async () => {
findImportButton().vm.$emit('click');
await nextTick();
expect(findNamespaceRequiredWarning().exists()).toBe(false);
findImportButton().vm.$emit('click');
await nextTick();
expect(fetchImport).toHaveBeenCalledWith(expect.anything(), {
repoId: repo.importSource.id,
optionalStages: OPTIONAL_STAGES,
});
});
it('renders import button', () => {
expect(findImportButton().exists()).toBe(true);
});
it('imports repo when clicking import button', async () => {
findImportButton().vm.$emit('click');
await nextTick();
expect(fetchImport).toHaveBeenCalledWith(expect.anything(), {
repoId: repo.importSource.id,
optionalStages: {},
});
});
it('includes optionalStages to import', async () => {
const OPTIONAL_STAGES = { stage1: true, stage2: false };
mountComponent({
repo,
optionalStages: OPTIONAL_STAGES,
});
const dropdown = findImportTargetDropdown();
dropdown.vm.$emit('select', 'target');
await nextTick();
findImportButton().vm.$emit('click');
await nextTick();
expect(fetchImport).toHaveBeenCalledWith(expect.anything(), {
repoId: repo.importSource.id,
optionalStages: OPTIONAL_STAGES,
});
});
it('does not render re-import button', () => {
expect(findReimportButton().exists()).toBe(false);
});
it('does not render re-import button', () => {
expect(findReimportButton().exists()).toBe(false);
});
});
@ -335,12 +290,7 @@ describe('ProviderRepoTableRow', () => {
await nextTick();
findImportTargetDropdown().vm.$emit('select', 'some-namespace');
await nextTick();
findReimportButton().vm.$emit('click');
await nextTick();
expect(findNamespaceRequiredWarning().exists()).toBe(false);
expect(fetchImport).toHaveBeenCalledWith(expect.anything(), {
repoId: repo.importSource.id,

View File

@ -55,7 +55,7 @@ describe('import_projects store actions', () => {
let localState;
const importRepoId = 1;
const otherImportRepoId = 2;
const defaultTargetNamespace = null;
const defaultTargetNamespace = 'default';
const sanitizedName = 'sanitizedName';
const defaultImportTarget = { newName: sanitizedName, targetNamespace: defaultTargetNamespace };

View File

@ -94,11 +94,12 @@ describe('import_projects store getters', () => {
describe('getImportTarget', () => {
it('returns default value if no custom target available', () => {
localState.defaultTargetNamespace = 'default';
localState.repositories = [IMPORTABLE_REPO];
expect(getImportTarget(localState)(IMPORTABLE_REPO.importSource.id)).toStrictEqual({
newName: IMPORTABLE_REPO.importSource.sanitizedName,
targetNamespace: null,
targetNamespace: localState.defaultTargetNamespace,
});
});

View File

@ -16,6 +16,26 @@ describe('IntegrationsList', () => {
it('provides correct `integrations` prop to the IntegrationsTable instance', () => {
createComponent({ integrations: [...mockInactiveIntegrations, ...mockActiveIntegrations] });
expect(findActiveIntegrationsTable().props('integrations')).toEqual(mockActiveIntegrations);
expect(findInactiveIntegrationsTable().props('integrations')).toEqual(mockInactiveIntegrations);
expect(findInactiveIntegrationsTable().props('inactive')).toBe(true);
});
it('filters out Amazon Q integration from this page since it is rendered in General Settings', () => {
const amazonQintegration = {
active: true,
configured: true,
title: 'Amazon Q',
description: 'Amazon Q integration',
updated_at: '2021-03-18T00:27:09.634Z',
edit_path:
'/gitlab-qa-sandbox-group/project_with_jenkins_6a55a67c-57c6ed0597c9319a/-/services/amazon_q/edit',
name: 'amazon_q',
};
const mockActiveIntegrationsWithAmazonQ = [...mockActiveIntegrations, amazonQintegration];
createComponent({
integrations: [...mockInactiveIntegrations, ...mockActiveIntegrationsWithAmazonQ],
});
expect(findActiveIntegrationsTable().props('integrations')).toEqual(mockActiveIntegrations);
expect(findInactiveIntegrationsTable().props('integrations')).toEqual(mockInactiveIntegrations);
expect(findInactiveIntegrationsTable().props('inactive')).toBe(true);

View File

@ -48,6 +48,7 @@ describe('YourWorkProjectsApp', () => {
filteredSearchTermKey: FILTERED_SEARCH_TERM_KEY,
filteredSearchNamespace: FILTERED_SEARCH_NAMESPACE,
filteredSearchRecentSearchesStorageKey: RECENT_SEARCHES_STORAGE_KEY_PROJECTS,
filteredSearchInputPlaceholder: 'Filter or search (3 character minimum)',
sortOptions: SORT_OPTIONS,
defaultSortOption: SORT_OPTION_UPDATED,
timestampTypeMap: {

View File

@ -68,6 +68,9 @@ describe('GroupsListItem', () => {
const findLeaveModal = () => wrapper.findComponent(GroupListItemLeaveModal);
const findAccessLevelBadge = () => wrapper.findByTestId('user-access-role');
const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip);
const findSubgroupCount = () => wrapper.findByTestId('subgroups-count');
const findProjectsCount = () => wrapper.findByTestId('projects-count');
const findMembersCount = () => wrapper.findByTestId('members-count');
const findInactiveBadge = () => wrapper.findComponent(GroupListItemInactiveBadge);
@ -117,33 +120,57 @@ describe('GroupsListItem', () => {
it('renders subgroup count', () => {
createComponent();
expect(wrapper.findByTestId('subgroups-count').props()).toMatchObject({
expect(findSubgroupCount().props()).toMatchObject({
tooltipText: 'Subgroups',
iconName: 'subgroup',
stat: group.descendantGroupsCount.toString(),
});
});
describe('when subgroup count is not available', () => {
it.each([undefined, null])('does not render subgroup count', (descendantGroupsCount) => {
createComponent({ propsData: { group: { ...group, descendantGroupsCount } } });
expect(findSubgroupCount().exists()).toBe(false);
});
});
it('renders projects count', () => {
createComponent();
expect(wrapper.findByTestId('projects-count').props()).toMatchObject({
expect(findProjectsCount().props()).toMatchObject({
tooltipText: 'Projects',
iconName: 'project',
stat: group.projectsCount.toString(),
});
});
describe('when projects count is not available', () => {
it.each([undefined, null])('does not render projects count', (projectsCount) => {
createComponent({ propsData: { group: { ...group, projectsCount } } });
expect(findProjectsCount().exists()).toBe(false);
});
});
it('renders members count', () => {
createComponent();
expect(wrapper.findByTestId('members-count').props()).toMatchObject({
expect(findMembersCount().props()).toMatchObject({
tooltipText: 'Direct members',
iconName: 'users',
stat: group.groupMembersCount.toString(),
});
});
describe('when members count is not available', () => {
it.each([undefined, null])('does not render members count', (groupMembersCount) => {
createComponent({ propsData: { group: { ...group, groupMembersCount } } });
expect(findMembersCount().exists()).toBe(false);
});
});
describe('when visibility is not provided', () => {
it('does not render visibility icon', () => {
const { visibility, ...groupWithoutVisibility } = group;

View File

@ -54,7 +54,7 @@ RSpec.describe Sidebars::Projects::SuperSidebarPanel, feature_category: :navigat
end
it "is exposed as a renderable menu" do
expect(subject.instance_variable_get(:@menus).map(&:class)).to eq(category_menu)
expect(subject.instance_variable_get(:@menus).map(&:class)).to include(*category_menu)
end
end

View File

@ -761,17 +761,6 @@ RSpec.describe Ci::RunnerManager, feature_category: :fleet_visibility, type: :mo
expect { runner_machine.save! }
.to change { runner_machine.organization_id }.from(nil).to(runner.organization_id)
end
context 'when populate_organization_id_in_runner_tables FF is disabled' do
before do
stub_feature_flags(populate_organization_id_in_runner_tables: false)
end
it 'does not populate organization_id from runner on save', :aggregate_failures do
expect { runner_machine.save! }
.not_to change { runner_machine.organization_id }.from(nil)
end
end
end
end
@ -791,17 +780,6 @@ RSpec.describe Ci::RunnerManager, feature_category: :fleet_visibility, type: :mo
expect { runner_machine.save! }
.to change { runner_machine.organization_id }.from(nil).to(runner.organization_id)
end
context 'when populate_organization_id_in_runner_tables FF is disabled' do
before do
stub_feature_flags(populate_organization_id_in_runner_tables: false)
end
it 'does not populate organization_id from runner on save', :aggregate_failures do
expect { runner_machine.save! }
.not_to change { runner_machine.organization_id }.from(nil)
end
end
end
end
end

View File

@ -2182,17 +2182,6 @@ RSpec.describe Ci::Runner, type: :model, factory_default: :keep, feature_categor
expect { runner.save! }
.to change { runner.organization_id }.from(nil).to(runner.owner.organization_id)
end
context 'when populate_organization_id_in_runner_tables FF is disabled' do
before do
stub_feature_flags(populate_organization_id_in_runner_tables: false)
end
it 'does not populate organization_id from owner on save', :aggregate_failures do
expect { runner.save! }
.not_to change { runner.organization_id }.from(nil)
end
end
end
end
@ -2213,17 +2202,6 @@ RSpec.describe Ci::Runner, type: :model, factory_default: :keep, feature_categor
expect { runner.save! }
.to change { runner.organization_id }.from(nil).to(runner.owner.organization_id)
end
context 'when populate_organization_id_in_runner_tables FF is disabled' do
before do
stub_feature_flags(populate_organization_id_in_runner_tables: false)
end
it 'does not populate organization_id from owner on save', :aggregate_failures do
expect { runner.save! }
.not_to change { runner.organization_id }.from(nil)
end
end
end
end
end

View File

@ -80,6 +80,90 @@ RSpec.describe Packages::PackageFile, type: :model, feature_category: :package_r
end
end
end
context 'with conan package' do
let_it_be(:package) { create(:conan_package, without_package_files: true) }
let_it_be(:recipe_revision) { package.conan_recipe_revisions.first }
let_it_be(:package_reference) { package.conan_package_references.first }
let_it_be(:package_revision) { package.conan_package_revisions.first }
let_it_be_with_reload(:existing_file) do
create(:conan_package_file, :conan_package, package: package, conan_recipe_revision: recipe_revision, conan_package_revision: package_revision)
end
context 'when creating a new file' do
let(:new_file) do
build(
:conan_package_file,
:conan_package,
package: package,
file_name: 'conan_package.tgz',
conan_file_metadatum: build(
:conan_file_metadatum,
:package_file,
recipe_revision: recipe_revision,
package_reference: package_reference,
package_revision: package_revision
)
)
end
it 'validates uniqueness of file name with same recipe revision, package reference and package revision' do
expect(new_file).not_to be_valid
expect(new_file.errors[:file_name]).to include('already exists for the given recipe revision, package reference, and package revision')
end
it 'allows same file name with different recipe revision' do
new_file.conan_file_metadatum.recipe_revision = build_stubbed(:conan_recipe_revision, package: package)
expect(new_file).to be_valid
end
it 'allows same file name with different package reference' do
new_file.conan_file_metadatum.package_reference = build_stubbed(:conan_package_reference, package: package)
expect(new_file).to be_valid
end
it 'allows same file name with different package revision' do
new_file.conan_file_metadatum.package_revision = build_stubbed(:conan_package_revision, package: package)
expect(new_file).to be_valid
end
it 'allows same file name without revision' do
new_file.conan_file_metadatum.recipe_revision = nil
new_file.conan_file_metadatum.package_revision = nil
expect(new_file).to be_valid
end
context 'when existing file is not installable' do
it 'allows same file name' do
existing_file.update!(status: :pending_destruction)
expect(new_file).to be_valid
end
end
context 'when both existing and new files have no revision' do
let_it_be(:recipe_revision) { nil }
let_it_be(:package_revision) { nil }
it 'allows same file name' do
expect(new_file).to be_valid
end
end
context 'when updating an existing file' do
it 'does not validate uniqueness on update' do
duplicate_file = create(
:conan_package_file,
:conan_package,
package: package,
file_name: 'duplicate_conan_package.tgz'
)
expect { existing_file.update!(file_name: duplicate_file.file_name) }.not_to raise_error
end
end
end
end
end
context 'with package filenames' do

View File

@ -81,22 +81,6 @@ RSpec.describe ::Ci::Runners::UpdateProjectRunnersOwnerService, '#execute', feat
expect(execute).to be_success
end
context 'when populate_organization_id_in_runner_tables FF is disabled' do
before do
stub_feature_flags(populate_organization_id_in_runner_tables: false)
end
it 'does not populate organization_id from owner on save', :aggregate_failures do
expect { execute }
# runners which had no organization_id
.to not_change { runner_without_org_id.reload.organization_id }.from(nil)
.and not_change { runner_manager_without_org_id.reload.organization_id }.from(nil)
.and not_change { tagging_org_id_for_runner(runner_without_org_id) }.from(nil)
expect(execute).to be_success
end
end
private
def tagging_sharding_key_id_for_runner(runner)

View File

@ -86,6 +86,16 @@ RSpec.shared_context 'project navbar structure' do
nav_item: _('Analyze'),
nav_sub_items: project_analytics_sub_nav_item
},
if Gitlab.ee?
{
nav_item: _('Agents'),
nav_sub_items: [
_('Runs')
]
}
end,
{
nav_item: _('Settings'),
nav_sub_items: [

View File

@ -44,30 +44,47 @@ RSpec.shared_examples 'creating wiki page meta record examples' do
end
end
context 'when a conflicting meta record exists' do
let!(:older_meta) do
create(:wiki_page_meta, container: container, canonical_slug: current_slug, created_at: 1.day.ago)
context 'when a conflicting meta record exists with the same slug' do
let!(:wiki_page_meta) do
create(:wiki_page_meta, container: container, canonical_slug: last_known_slug)
end
let!(:newer_meta) { create(:wiki_page_meta, container: container, canonical_slug: 'foobar') }
let!(:todo) { create(:todo, target: newer_meta) }
let!(:conflicting_record) { create(:wiki_page_meta, container: container, canonical_slug: 'foobar') }
let!(:todo) { create(:todo, target: conflicting_record) }
before do
slug = newer_meta.slugs.first
slug = conflicting_record.slugs.first
slug[:slug] = current_slug
slug.save!
end
it 'finds the older record' do
expect(find_record).to eq(older_meta)
it 'finds the record' do
expect(find_record).to eq(wiki_page_meta).or eq(conflicting_record)
end
it 'destroys the newer record' do
it 'destroys one of the records' do
expect { find_record }.to change { WikiPage::Meta.count }.by(-1)
end
end
context 'when a conflicting meta record exists with old slug' do
let!(:wiki_page_meta) do
create(:wiki_page_meta, container: container, canonical_slug: last_known_slug)
end
let!(:conflicting_record) { create(:wiki_page_meta, container: container, canonical_slug: current_slug) }
let!(:todo) { create(:todo, target: conflicting_record) }
it 'finds the record' do
expect(find_record).to eq(wiki_page_meta)
end
it 'destroys one of the records' do
expect { find_record }.to change { WikiPage::Meta.count }.by(-1)
end
it 'moves associated todos to the older record' do
expect { find_record }.to change { todo.reload.target }.from(newer_meta).to(older_meta)
it 'moves associated todos from the destroyed record' do
expect { find_record }.to change { todo.reload.target }.from(conflicting_record).to(wiki_page_meta)
end
end

View File

@ -919,7 +919,23 @@ RSpec.shared_examples 'workhorse recipe file upload endpoint' do |revision: fals
it_behaves_like 'handling validation error for package'
it_behaves_like 'protected package main example'
it { expect { request }.to change { Packages::Conan::RecipeRevision.count }.by(1) } if revision
if revision
it { expect { request }.to change { Packages::Conan::RecipeRevision.count }.by(1) }
context 'when the file already exists' do
let(:recipe_revision) { package.conan_recipe_revisions.first.revision }
let(:recipe_path_name) { package.name }
it 'does not upload the file again' do
expect { request }.not_to change { Packages::PackageFile.count }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq({
'message' => '400 Bad request - Validation failed: ' \
'File name already exists for the given recipe revision, package reference, and package revision'
})
end
end
end
end
RSpec.shared_examples 'workhorse package file upload endpoint' do |revision: false|
@ -952,6 +968,22 @@ RSpec.shared_examples 'workhorse package file upload endpoint' do |revision: fal
.to change { Packages::Conan::RecipeRevision.count }.by(1)
.and change { Packages::Conan::PackageRevision.count }.by(1)
end
context 'when the file already exists' do
let(:recipe_revision) { package.conan_recipe_revisions.first.revision }
let(:package_revision) { package.conan_package_revisions.first.revision }
let(:conan_package_reference) { package.conan_package_references.first.reference }
let(:recipe_path_name) { package.name }
it 'does not upload the file again' do
expect { request }.not_to change { Packages::PackageFile.count }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq({
'message' => '400 Bad request - Validation failed: ' \
'File name already exists for the given recipe revision, package reference, and package revision'
})
end
end
end
it { expect { request }.to change { Packages::Conan::PackageReference.count }.by(1) }