Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4c184f0a70
commit
645c20e091
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
7562adb6b7cc44f2997fe37692bbd9ff911cfd6d
|
||||
55f4fb94e39f60e1d4c76deac5d6f20c6202c68f
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -19,6 +19,6 @@ export const getImportTarget = (state) => (repoId) => {
|
|||
|
||||
return {
|
||||
newName: repo.importSource.sanitizedName,
|
||||
targetNamespace: null,
|
||||
targetNamespace: state.defaultTargetNamespace,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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: [] },
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
-->
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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 >}}
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -42,3 +42,5 @@ module Sidebars
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
Sidebars::Projects::SuperSidebarPanel.prepend_mod_with('Sidebars::Projects::SuperSidebarPanel')
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
Loading…
Reference in New Issue