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