diff --git a/.gitlab/ci/test-on-cng/main.gitlab-ci.yml b/.gitlab/ci/test-on-cng/main.gitlab-ci.yml
index 95d86995d9d..4a21528afea 100644
--- a/.gitlab/ci/test-on-cng/main.gitlab-ci.yml
+++ b/.gitlab/ci/test-on-cng/main.gitlab-ci.yml
@@ -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
diff --git a/.rubocop_todo/gitlab/bounded_contexts.yml b/.rubocop_todo/gitlab/bounded_contexts.yml
index 7ed64340f2d..9881dbba34d 100644
--- a/.rubocop_todo/gitlab/bounded_contexts.yml
+++ b/.rubocop_todo/gitlab/bounded_contexts.yml
@@ -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'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 24a3014832e..cbcee7d4b72 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-7562adb6b7cc44f2997fe37692bbd9ff911cfd6d
+55f4fb94e39f60e1d4c76deac5d6f20c6202c68f
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 80d62e3e764..3798f7b00f7 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -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"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 5bc60ae7f95..aa2e20c14c8 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -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)
diff --git a/Gemfile.next.checksum b/Gemfile.next.checksum
index 87826b5e3c8..adb518c94cb 100644
--- a/Gemfile.next.checksum
+++ b/Gemfile.next.checksum
@@ -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"},
diff --git a/Gemfile.next.lock b/Gemfile.next.lock
index 9e8191568ed..8acd7a24357 100644
--- a/Gemfile.next.lock
+++ b/Gemfile.next.lock
@@ -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)
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index f155f05cc8c..230b5cd5e3b 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -317,7 +317,7 @@ export default {
-
+
({
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,
});
diff --git a/app/assets/javascripts/groups_projects/components/tabs_with_list.vue b/app/assets/javascripts/groups_projects/components/tabs_with_list.vue
index d6c4a408d7d..d14e3700dd2 100644
--- a/app/assets/javascripts/groups_projects/components/tabs_with_list.vue
+++ b/app/assets/javascripts/groups_projects/components/tabs_with_list.vue
@@ -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"
diff --git a/app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue b/app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue
index 6ac74d3c8d2..7e3b60bcad2 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue
@@ -64,7 +64,7 @@ export default {
-
+
{{ importAllButtonText }}
diff --git a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
index c7f0e31a128..9a0c94a7d00 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
@@ -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') },
};
@@ -171,14 +138,14 @@ export default {
-
+
{{ importAllButtonText }}
@@ -203,7 +170,6 @@ export default {
/>
-
- {{
- s__(
- 'ImportProjects|Select a destination namespace for each repository before importing all.',
- )
- }}
-
diff --git a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
index 6de37173535..ab166b8fba1 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
@@ -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 {
@@ -246,14 +224,6 @@ export default {
data-testid="project-path-field"
/>
-
- {{ s__('ImportProjects|Select a destination namespace.') }}
-
{{ displayFullPath }}
@@ -294,11 +264,9 @@ export default {
(repoId) => {
return {
newName: repo.importSource.sanitizedName,
- targetNamespace: null,
+ targetNamespace: state.defaultTargetNamespace,
};
};
diff --git a/app/assets/javascripts/integrations/index/components/integrations_list.vue b/app/assets/javascripts/integrations/index/components/integrations_list.vue
index 79b5ec757f3..5334a2ea6fc 100644
--- a/app/assets/javascripts/integrations/index/components/integrations_list.vue
+++ b/app/assets/javascripts/integrations/index/components/integrations_list.vue
@@ -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: [] },
diff --git a/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue b/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue
index bf474550052..dc5d69a1dbc 100644
--- a/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue
@@ -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 {
{ !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')
diff --git a/app/models/wiki_page/meta.rb b/app/models/wiki_page/meta.rb
index e90a90d6517..a72fa7260f1 100644
--- a/app/models/wiki_page/meta.rb
+++ b/app/models/wiki_page/meta.rb
@@ -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)
diff --git a/app/services/ci/runners/update_project_runners_owner_service.rb b/app/services/ci/runners/update_project_runners_owner_service.rb
index 151e74743a7..508f1eb657a 100644
--- a/app/services/ci/runners/update_project_runners_owner_service.rb
+++ b/app/services/ci/runners/update_project_runners_owner_service.rb
@@ -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
diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml
index b6d4dc6b9c9..ad68a0587f2 100644
--- a/app/views/import/_githubish_status.html.haml
+++ b/app/views/import/_githubish_status.html.haml
@@ -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))
diff --git a/config/feature_flags/gitlab_com_derisk/populate_organization_id_in_runner_tables.yml b/config/feature_flags/gitlab_com_derisk/populate_organization_id_in_runner_tables.yml
deleted file mode 100644
index 08a15c6fb25..00000000000
--- a/config/feature_flags/gitlab_com_derisk/populate_organization_id_in_runner_tables.yml
+++ /dev/null
@@ -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
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 56b1a68f25f..ebf46b408b5 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -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
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index aa86ff815e2..8725ce0ffce 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -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
diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md
index a5b35f47203..b8249921fe3 100644
--- a/doc/api/graphql/reference/_index.md
+++ b/doc/api/graphql/reference/_index.md
@@ -28242,7 +28242,7 @@ GPG signature for a signed commit.
| `markdownPaths` {{< icon name="warning-solid" >}} | [`MarkdownPaths`](#markdownpaths) | **Introduced** in GitLab 18.1. **Status**: Experiment. Namespace relevant paths to create markdown links on the UI. |
| `markedForDeletionOn` {{< icon name="warning-solid" >}} | [`Time`](#time) | **Introduced** in GitLab 16.11. **Status**: Experiment. Date when group was scheduled to be deleted. |
| `mathRenderingLimitsEnabled` | [`Boolean`](#boolean) | Indicates if math rendering limits are used for the group. |
-| `mavenVirtualRegistries` {{< icon name="warning-solid" >}} | [`MavenVirtualRegistryConnection`](#mavenvirtualregistryconnection) | **Introduced** in GitLab 18.1. **Status**: Experiment. Maven virtual registries registered to the group. |
+| `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. |
| `maxAccessLevel` | [`AccessLevel!`](#accesslevel) | Maximum access level of the current user in the group. |
| `mentionsDisabled` | [`Boolean`](#boolean) | Indicates if a group is disabled from getting mentioned. |
| `name` | [`String`](#string) | Name of the group. |
@@ -31444,6 +31444,22 @@ Maven metadata.
| `path` | [`String!`](#string) | Path of the Maven package. |
| `updatedAt` | [`Time!`](#time) | Date of most recent update. |
+### `MavenUpstream`
+
+Represents the upstream registries of a Maven virtual registry.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `cacheValidityHours` {{< icon name="warning-solid" >}} | [`Int!`](#int) | **Introduced** in GitLab 18.1. **Status**: Experiment. Time before the cache expires for the upstream registry. |
+| `description` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.1. **Status**: Experiment. Description of the upstream registry. |
+| `id` {{< icon name="warning-solid" >}} | [`ID!`](#id) | **Introduced** in GitLab 18.1. **Status**: Experiment. ID of the upstream registry. |
+| `name` {{< icon name="warning-solid" >}} | [`String!`](#string) | **Introduced** in GitLab 18.1. **Status**: Experiment. Name of the upstream registry. |
+| `password` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.1. **Status**: Experiment. Password to sign in to the upstream registry. |
+| `url` {{< icon name="warning-solid" >}} | [`String!`](#string) | **Introduced** in GitLab 18.1. **Status**: Experiment. URL of the upstream registry. |
+| `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.
| `description` | [`String`](#string) | Description of the virtual registry. |
| `id` | [`ID!`](#id) | ID of the virtual registry. |
| `name` | [`String!`](#string) | Name of the virtual registry. |
+| `upstreams` {{< icon name="warning-solid" >}} | [`[MavenUpstream!]`](#mavenupstream) | **Introduced** in GitLab 18.1. **Status**: Experiment. List of upstream registries for the Maven virtual registry. |
### `MemberApproval`
diff --git a/doc/api/groups.md b/doc/api/groups.md
index c0b3097b7df..7fab1fff2ab 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -2119,6 +2119,7 @@ Example response:
]
```
+
diff --git a/doc/editor_extensions/_index.md b/doc/editor_extensions/_index.md
index 9b999292b8c..e3ec6fb2c9d 100644
--- a/doc/editor_extensions/_index.md
+++ b/doc/editor_extensions/_index.md
@@ -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,
diff --git a/doc/editor_extensions/neovim/setup.md b/doc/editor_extensions/neovim/setup.md
index 02c49d6a3e2..b7b16ace06b 100644
--- a/doc/editor_extensions/neovim/setup.md
+++ b/doc/editor_extensions/neovim/setup.md
@@ -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:
diff --git a/doc/security/_index.md b/doc/security/_index.md
index ceadc1dcb9d..06f6bba689a 100644
--- a/doc/security/_index.md
+++ b/doc/security/_index.md
@@ -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).
diff --git a/doc/security/information_exclusivity.md b/doc/security/information_exclusivity.md
index cc4f1ff53b0..81946bb013d 100644
--- a/doc/security/information_exclusivity.md
+++ b/doc/security/information_exclusivity.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 >}}
+
-- 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).
+
+
+
+
diff --git a/doc/user/application_security/secret_detection/pipeline/_index.md b/doc/user/application_security/secret_detection/pipeline/_index.md
index 4094548f27c..8622626abf8 100644
--- a/doc/user/application_security/secret_detection/pipeline/_index.md
+++ b/doc/user/application_security/secret_detection/pipeline/_index.md
@@ -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
diff --git a/doc/user/clusters/agent/getting_started_deployments.md b/doc/user/clusters/agent/getting_started_deployments.md
index d5cde547c56..ba532798780 100644
--- a/doc/user/clusters/agent/getting_started_deployments.md
+++ b/doc/user/clusters/agent/getting_started_deployments.md
@@ -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.
diff --git a/doc/user/compliance/compliance_frameworks/compliance_standards.md b/doc/user/compliance/compliance_frameworks/compliance_standards.md
index 2afc296c168..fedc363f85a 100644
--- a/doc/user/compliance/compliance_frameworks/compliance_standards.md
+++ b/doc/user/compliance/compliance_frameworks/compliance_standards.md
@@ -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. | - Dependency scanning running
- Container scanning running
- SAST running
- DAST running
- API security running
- Secret detection running
- Fuzz testing running
|
| 8.32 Change management | Changes to information processing facilities and information systems shall be subject to change management procedures. | |
+## 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. | - At least two approvals
- Author approved merge request is forbidden
- Committers approved merge request is forbidden
- Merge requests approval rules prevent editing
|
+| 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. | - At least two approvals
- Author approved merge request is forbidden
- Committers approved merge request is forbidden
- Merge requests approval rules prevent editing
|
+| AU-9(5): Dual Authorization | Enforce dual authorization for the deletion or modification of organization-defined audit information. | - At least two approvals
- Author approved merge request is forbidden
- Committers approved merge request is forbidden
- Merge requests approval rules prevent editing
|
+| 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. | - Default branch protected
- At least two approvals
- Author approved merge request is forbidden
- Committers approved merge request is forbidden
- Merge requests approval rules prevent editing
|
+| 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. | - At least two approvals
- Author approved merge request is forbidden
- Committers approved merge request is forbidden
- Merge requests approval rules prevent editing
|
+| CM-5: Access Restrictions for Change | Define, document, approve, and enforce physical and logical access restrictions associated with changes to the system. | - Default branch protected
- At least two approvals
- Author approved merge request is forbidden
- Committers approved merge request is forbidden
- Merge requests approval rules prevent editing
|
+| CM-5(4): Dual Authorization | Enforce dual authorization for implementing changes to organization-defined system components and system-level information. | - At least two approvals
- Author approved merge request is forbidden
- Committers approved merge request is forbidden
- Merge requests approval rules prevent editing
|
+| 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. | - Author approved merge request is forbidden
|
+| 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. | - Committers approved merge request is forbidden
- Merge requests approval rules prevent editing
|
+| 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. | |
+| 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. | - License compliance running
|
+| CP-9(7): Dual Authorization | Enforce dual authorization for the deletion or destruction of organization-defined backup information. | - At least two approvals
- Author approved merge request is forbidden
- Committers approved merge request is forbidden
- Merge requests approval rules prevent editing
|
+| IA-2(10): Single Sign-on | Provide a single sign-on capability for organization-defined system accounts and services. | |
+| IA-2(12): Acceptance of PIV Credentials | Accept and electronically verify Personal Identity Verification (PIV) credentials. | |
+| IA-5(7): No Embedded Unencrypted Static Authenticators | Ensure that unencrypted static authenticators are not embedded in applications or other forms of static storage. | |
+| IA-5(9): Federated Credential Management | Use organization-defined external organizations to federate credentials. | |
+| IA-8(1): Acceptance of PIV Credentials From Other Agencies | Accept and electronically verify Personal Identity Verification (PIV) credentials from other federal agencies. | |
+| IA-8(5): Acceptance of PIV-I Credentials | Accept and verify Personal Identity Verification-I (PIV-I) credentials. | |
+| 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. | - Dependency scanning running
- Container scanning running
- DAST running
- API security running
- Fuzz testing running
|
+| 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. | |
+| 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. | - DAST running
- Fuzz testing running
|
+
## NIST CSF 2.0 compliance requirements
NIST CSF is the Cybersecurity Framework from the National Institute of Standards and Technology.
diff --git a/doc/user/namespace/_index.md b/doc/user/namespace/_index.md
index 2a95088fda1..b4bf572c52d 100644
--- a/doc/user/namespace/_index.md
+++ b/doc/user/namespace/_index.md
@@ -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).
diff --git a/doc/user/project/members/_index.md b/doc/user/project/members/_index.md
index 6b393608caa..58c7f438e13 100644
--- a/doc/user/project/members/_index.md
+++ b/doc/user/project/members/_index.md
@@ -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 >}}
diff --git a/lib/gitlab/ci/tags/bulk_insert/runner_taggings_configuration.rb b/lib/gitlab/ci/tags/bulk_insert/runner_taggings_configuration.rb
index 600eec3e533..2612bdfca64 100644
--- a/lib/gitlab/ci/tags/bulk_insert/runner_taggings_configuration.rb
+++ b/lib/gitlab/ci/tags/bulk_insert/runner_taggings_configuration.rb
@@ -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?
diff --git a/lib/sidebars/projects/super_sidebar_panel.rb b/lib/sidebars/projects/super_sidebar_panel.rb
index 57959318582..77463dc8e92 100644
--- a/lib/sidebars/projects/super_sidebar_panel.rb
+++ b/lib/sidebars/projects/super_sidebar_panel.rb
@@ -42,3 +42,5 @@ module Sidebars
end
end
end
+
+Sidebars::Projects::SuperSidebarPanel.prepend_mod_with('Sidebars::Projects::SuperSidebarPanel')
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 024baf95597..d791b5fa91c 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/qa/Gemfile b/qa/Gemfile
index 5366d6af999..fba96c21afc 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -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'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index ef61f8f54e5..fcb5e4325c9 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -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)
diff --git a/spec/features/import/manifest_import_spec.rb b/spec/features/import/manifest_import_spec.rb
index ed2e8cfbbb2..175ae2fee8c 100644
--- a/spec/features/import/manifest_import_spec.rb
+++ b/spec/features/import/manifest_import_spec.rb
@@ -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
diff --git a/spec/frontend/groups/your_work/components/app_spec.js b/spec/frontend/groups/your_work/components/app_spec.js
index 97baf5c826e..3a793386859 100644
--- a/spec/frontend/groups/your_work/components/app_spec.js
+++ b/spec/frontend/groups/your_work/components/app_spec.js
@@ -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: {
diff --git a/spec/frontend/groups/your_work/graphql/resolvers_spec.js b/spec/frontend/groups/your_work/graphql/resolvers_spec.js
index ce95b88e426..37d603f86be 100644
--- a/spec/frontend/groups/your_work/graphql/resolvers_spec.js
+++ b/spec/frontend/groups/your_work/graphql/resolvers_spec.js
@@ -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);
+ });
+ });
});
diff --git a/spec/frontend/groups_projects/components/tabs_with_list_spec.js b/spec/frontend/groups_projects/components/tabs_with_list_spec.js
index 75d43925d43..d5a7e0eb3b2 100644
--- a/spec/frontend/groups_projects/components/tabs_with_list_spec.js
+++ b/spec/frontend/groups_projects/components/tabs_with_list_spec.js
@@ -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}`,
});
});
});
diff --git a/spec/frontend/import_entities/import_projects/components/github_status_table_spec.js b/spec/frontend/import_entities/import_projects/components/github_status_table_spec.js
index 67c7681fe1c..1560ea68ea4 100644
--- a/spec/frontend/import_entities/import_projects/components/github_status_table_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/github_status_table_spec.js
@@ -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:
- '
',
+ '
',
});
Vue.use(Vuex);
@@ -44,7 +44,7 @@ describe('GithubStatusTable', () => {
},
});
- wrapper = mountExtended(GithubStatusTable, {
+ wrapper = mount(GithubStatusTable, {
store,
propsData: {
providerTitle: 'Github',
diff --git a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
index e8ab436531b..f8a9bb8af9b 100644
--- a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
@@ -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: 'Modal!
', 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 () => {
diff --git a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
index 9f5c1bca94f..a1355706566 100644
--- a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
@@ -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,
diff --git a/spec/frontend/import_entities/import_projects/store/actions_spec.js b/spec/frontend/import_entities/import_projects/store/actions_spec.js
index aa57ae9f041..918821dfa59 100644
--- a/spec/frontend/import_entities/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/actions_spec.js
@@ -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 };
diff --git a/spec/frontend/import_entities/import_projects/store/getters_spec.js b/spec/frontend/import_entities/import_projects/store/getters_spec.js
index 55bac7e3f5f..fced5670f25 100644
--- a/spec/frontend/import_entities/import_projects/store/getters_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/getters_spec.js
@@ -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,
});
});
diff --git a/spec/frontend/integrations/index/components/integrations_list_spec.js b/spec/frontend/integrations/index/components/integrations_list_spec.js
index 2d2a9b845a3..7d88f278cb1 100644
--- a/spec/frontend/integrations/index/components/integrations_list_spec.js
+++ b/spec/frontend/integrations/index/components/integrations_list_spec.js
@@ -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);
diff --git a/spec/frontend/projects/your_work/components/app_spec.js b/spec/frontend/projects/your_work/components/app_spec.js
index df0bd2db3b8..e4d774bf2d6 100644
--- a/spec/frontend/projects/your_work/components/app_spec.js
+++ b/spec/frontend/projects/your_work/components/app_spec.js
@@ -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: {
diff --git a/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js b/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js
index 285cbbbc81a..328619e9f6c 100644
--- a/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js
+++ b/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js
@@ -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;
diff --git a/spec/lib/sidebars/projects/super_sidebar_panel_spec.rb b/spec/lib/sidebars/projects/super_sidebar_panel_spec.rb
index c07d7824307..e57713d7238 100644
--- a/spec/lib/sidebars/projects/super_sidebar_panel_spec.rb
+++ b/spec/lib/sidebars/projects/super_sidebar_panel_spec.rb
@@ -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
diff --git a/spec/models/ci/runner_manager_spec.rb b/spec/models/ci/runner_manager_spec.rb
index e50271a87ca..b5add7c119a 100644
--- a/spec/models/ci/runner_manager_spec.rb
+++ b/spec/models/ci/runner_manager_spec.rb
@@ -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
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index bdb098cdd7e..817f2ddfe1e 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -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
diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb
index 39672ed0ab0..023ec6a4200 100644
--- a/spec/models/packages/package_file_spec.rb
+++ b/spec/models/packages/package_file_spec.rb
@@ -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
diff --git a/spec/services/ci/runners/update_project_runners_owner_service_spec.rb b/spec/services/ci/runners/update_project_runners_owner_service_spec.rb
index 6f679a75ba9..8b40c0bfae6 100644
--- a/spec/services/ci/runners/update_project_runners_owner_service_spec.rb
+++ b/spec/services/ci/runners/update_project_runners_owner_service_spec.rb
@@ -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)
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index 5adca24f8e6..b86ac9448e9 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -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: [
diff --git a/spec/support/shared_examples/models/wiki_page/meta_shared_examples.rb b/spec/support/shared_examples/models/wiki_page/meta_shared_examples.rb
index 1f08960d62a..4e0974a0d60 100644
--- a/spec/support/shared_examples/models/wiki_page/meta_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_page/meta_shared_examples.rb
@@ -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
diff --git a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
index bafeb456ddf..5858ec21656 100644
--- a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
@@ -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) }