diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 0b55d4b9956..02c710fa92c 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -1061,7 +1061,7 @@ rspec unit pg17:
rspec integration pg17:
extends:
- - .rspec-base-pg16
+ - .rspec-base-pg17
- .rails:rules:default-branch-schedule-nightly--code-backstage
- .rspec-integration-parallel
diff --git a/GITLAB_KAS_VERSION b/GITLAB_KAS_VERSION
index c448ab2fdf4..e2c978ceb81 100644
--- a/GITLAB_KAS_VERSION
+++ b/GITLAB_KAS_VERSION
@@ -1 +1 @@
-7b5e739ea26a4d484d2986595626caf7cf02b002
+9f9e01592618d35f9acd9573970fc39a1177fbc3
diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index 5b458a92b44..0b89a6021e1 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -143,6 +143,11 @@
"CiDeletedNamespace",
"Namespace"
],
+ "NamespacesLinkPaths": [
+ "GroupNamespaceLinks",
+ "ProjectNamespaceLinks",
+ "UserNamespaceLinks"
+ ],
"NoteableInterface": [
"AlertManagementAlert",
"BoardEpic",
diff --git a/app/assets/javascripts/work_items/components/create_work_item.vue b/app/assets/javascripts/work_items/components/create_work_item.vue
index 7796df302a9..cfd35e2a289 100644
--- a/app/assets/javascripts/work_items/components/create_work_item.vue
+++ b/app/assets/javascripts/work_items/components/create_work_item.vue
@@ -11,6 +11,7 @@ import {
GlIcon,
} from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
import { createAlert } from '~/alert';
import { clearDraft } from '~/lib/utils/autosave';
import { isMetaEnterKeyPair, parseBoolean } from '~/lib/utils/common_utils';
@@ -30,7 +31,11 @@ import {
getNewWorkItemAutoSaveKey,
newWorkItemFullPath,
} from '~/work_items/utils';
-import { TYPENAME_MERGE_REQUEST, TYPENAME_VULNERABILITY } from '~/graphql_shared/constants';
+import {
+ TYPENAME_MERGE_REQUEST,
+ TYPENAME_VULNERABILITY,
+ TYPENAME_GROUP,
+} from '~/graphql_shared/constants';
import {
I18N_WORK_ITEM_ERROR_CREATING,
i18n,
@@ -198,7 +203,7 @@ export default {
localTitle: this.title || '',
error: null,
workItem: {},
- workItemTypes: [],
+ namespace: null,
selectedProjectFullPath: this.initialSelectedProject(),
selectedWorkItemTypeId: null,
loading: false,
@@ -234,7 +239,7 @@ export default {
this.error = i18n.fetchError;
},
},
- workItemTypes: {
+ namespace: {
query() {
return namespaceWorkItemTypesQuery;
},
@@ -244,7 +249,7 @@ export default {
};
},
update(data) {
- return data.workspace?.workItemTypes?.nodes;
+ return data.workspace;
},
async result() {
this.initialLoadingWorkItemTypes = false;
@@ -291,6 +296,9 @@ export default {
},
},
computed: {
+ workItemTypes() {
+ return this.namespace?.workItemTypes?.nodes ?? [];
+ },
newWorkItemPath() {
return newWorkItemFullPath(this.selectedProjectFullPath, this.selectedWorkItemTypeName);
},
@@ -532,6 +540,16 @@ export default {
showWorkItemStatus() {
return this.workItemStatus && this.glFeatures.workItemStatusFeatureFlag;
},
+ isGroupWorkItem() {
+ return this.namespace?.id.includes(TYPENAME_GROUP);
+ },
+ uploadsPath() {
+ const rootPath = this.namespace?.webUrl;
+ if (!rootPath) {
+ return window.uploads_path;
+ }
+ return this.isGroupWorkItem ? `${rootPath}/-/uploads` : `${rootPath}/uploads`;
+ },
},
watch: {
shouldDiscardDraft: {
@@ -871,7 +889,7 @@ export default {
/>
-
+
diff --git a/app/assets/javascripts/work_items/graphql/namespace_work_item_types.query.graphql b/app/assets/javascripts/work_items/graphql/namespace_work_item_types.query.graphql
index 55264d23185..586686a66a9 100644
--- a/app/assets/javascripts/work_items/graphql/namespace_work_item_types.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/namespace_work_item_types.query.graphql
@@ -3,6 +3,7 @@
query namespaceWorkItemTypes($fullPath: ID!, $name: IssueType) {
workspace: namespace(fullPath: $fullPath) {
id
+ webUrl
workItemTypes(name: $name) {
nodes {
...WorkItemTypeFragment
diff --git a/app/graphql/mutations/ci/runner/assign_to_project.rb b/app/graphql/mutations/ci/runner/assign_to_project.rb
new file mode 100644
index 00000000000..b086b6edd09
--- /dev/null
+++ b/app/graphql/mutations/ci/runner/assign_to_project.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ module Runner
+ class AssignToProject < BaseMutation
+ graphql_name 'RunnerAssignToProject'
+
+ authorize :assign_runner
+
+ argument :runner_id, ::Types::GlobalIDType[::Ci::Runner],
+ required: true,
+ description: 'ID of the runner to assign to the project .'
+
+ argument :project_path, GraphQL::Types::ID,
+ required: true,
+ description: 'Full path of the project to which the runner will be assigned.'
+
+ def resolve(**args)
+ project, runner = find_project_and_runner!(args)
+ result = ::Ci::Runners::AssignRunnerService.new(runner, project, current_user).execute
+
+ { errors: result.errors }
+ end
+
+ def find_project_and_runner!(args)
+ project = ::Project.find_by_full_path(args[:project_path])
+ raise_resource_not_available_error! unless project
+
+ runner = authorized_find!(id: args[:runner_id])
+ raise_resource_not_available_error!("Runner is not a project runner") unless runner.project_type?
+
+ [project, runner]
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/ci/runner/unassign_from_project.rb b/app/graphql/mutations/ci/runner/unassign_from_project.rb
new file mode 100644
index 00000000000..2cd6843a68f
--- /dev/null
+++ b/app/graphql/mutations/ci/runner/unassign_from_project.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ module Runner
+ class UnassignFromProject < BaseMutation
+ graphql_name 'RunnerUnassignFromProject'
+
+ include FindsProject
+
+ authorize :admin_project_runners
+
+ argument :runner_id, ::Types::GlobalIDType[::Ci::Runner],
+ required: true,
+ description: 'ID of the runner to unassign from the project.'
+
+ argument :project_path, GraphQL::Types::ID,
+ required: true,
+ description: 'Full path of the project from which the runner will be unassigned.'
+
+ def resolve(**args)
+ project = authorized_find!(args[:project_path])
+ runner_id = GitlabSchema.parse_gid(args[:runner_id], expected_type: ::Ci::Runner).model_id
+ runner_project = project.runner_projects.find_by_runner_id(runner_id)
+
+ unless runner_project&.runner
+ raise_resource_not_available_error! "Runner does not exist or is not assigned to this project"
+ end
+
+ result = ::Ci::Runners::UnassignRunnerService.new(runner_project, current_user).execute
+
+ { errors: result.errors }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index b0d673c8241..0a1efdee237 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -208,6 +208,8 @@ module Types
mount_mutation Mutations::Ci::Runner::Create, experiment: { milestone: '15.10' }
mount_mutation Mutations::Ci::Runner::Delete
mount_mutation Mutations::Ci::Runner::Update
+ mount_mutation Mutations::Ci::Runner::AssignToProject, experiment: { milestone: '18.1' }
+ mount_mutation Mutations::Ci::Runner::UnassignFromProject, experiment: { milestone: '18.1' }
mount_mutation Mutations::Ci::RunnersRegistrationToken::Reset, deprecated: {
reason: 'Underlying feature was deprecated in 15.6 and will be removed in 18.0',
milestone: '17.7'
diff --git a/app/graphql/types/namespace_type.rb b/app/graphql/types/namespace_type.rb
index 3ac3607fcc8..cc0674fc93f 100644
--- a/app/graphql/types/namespace_type.rb
+++ b/app/graphql/types/namespace_type.rb
@@ -133,6 +133,13 @@ module Types
calls_gitaly: true,
description: 'Work item description templates available to the namespace.'
+ field :link_paths,
+ Types::Namespaces::LinkPaths,
+ null: true,
+ description: 'Namespace relevant paths to create links on the UI.',
+ method: :itself,
+ experiment: { milestone: '18.1' }
+
markdown_field :description_html, null: true
def achievements_path
diff --git a/app/graphql/types/namespaces/link_paths.rb b/app/graphql/types/namespaces/link_paths.rb
new file mode 100644
index 00000000000..7f1645ddad5
--- /dev/null
+++ b/app/graphql/types/namespaces/link_paths.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+module Types
+ module Namespaces
+ module LinkPaths
+ include ::Types::BaseInterface
+ # required for the new_comment_template_paths
+ include ::IssuablesHelper
+
+ graphql_name 'NamespacesLinkPaths'
+
+ TYPE_MAPPINGS = {
+ ::Group => ::Types::Namespaces::LinkPaths::GroupNamespaceLinksType,
+ ::Namespaces::ProjectNamespace => ::Types::Namespaces::LinkPaths::ProjectNamespaceLinksType,
+ ::Namespaces::UserNamespace => ::Types::Namespaces::LinkPaths::UserNamespaceLinksType
+ }.freeze
+
+ field :issues_list,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Namespace issues_list.',
+ fallback_value: nil
+
+ field :labels_manage,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Namespace labels_manage.',
+ fallback_value: nil
+
+ field :new_project,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Namespace new_project.',
+ fallback_value: nil
+
+ field :new_comment_template,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Namespace new_comment_template_paths.',
+ fallback_value: nil
+
+ field :register,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Namespace register_path.'
+
+ field :report_abuse,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Namespace report_abuse.'
+
+ field :sign_in,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Namespace sign_in_path.'
+
+ def self.type_mappings
+ TYPE_MAPPINGS
+ end
+
+ def self.resolve_type(object, _context)
+ type_mappings[object.class] || raise("Unknown GraphQL type for namespace type #{object.class}")
+ end
+
+ orphan_types(*type_mappings.values)
+
+ def register
+ url_helpers.new_user_registration_path(redirect_to_referer: 'yes')
+ end
+
+ def report_abuse
+ url_helpers.add_category_abuse_reports_path
+ end
+
+ def sign_in
+ url_helpers.new_user_session_path(redirect_to_referer: 'yes')
+ end
+
+ private
+
+ def url_helpers
+ Gitlab::Routing.url_helpers
+ end
+ end
+ end
+end
+
+::Types::Namespaces::LinkPaths.prepend_mod
diff --git a/app/graphql/types/namespaces/link_paths/group_namespace_links_type.rb b/app/graphql/types/namespaces/link_paths/group_namespace_links_type.rb
new file mode 100644
index 00000000000..b09bbba3da1
--- /dev/null
+++ b/app/graphql/types/namespaces/link_paths/group_namespace_links_type.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Types
+ module Namespaces
+ module LinkPaths
+ class GroupNamespaceLinksType < BaseObject # rubocop:disable Graphql/AuthorizeTypes -- parent is already authorized
+ graphql_name 'GroupNamespaceLinks'
+ implements ::Types::Namespaces::LinkPaths
+
+ alias_method :group, :object
+
+ def issues_list
+ url_helpers.issues_group_path(group)
+ end
+
+ def labels_manage
+ url_helpers.group_labels_path(group)
+ end
+
+ def new_project
+ url_helpers.new_project_path(namespace_id: group&.id)
+ end
+
+ def report_abuse
+ url_helpers.add_category_abuse_reports_path
+ end
+
+ def new_comment_template
+ new_comment_template_paths(group)&.dig(0, :href)
+ end
+ end
+ end
+ end
+end
+
+::Types::Namespaces::LinkPaths::GroupNamespaceLinksType.prepend_mod
diff --git a/app/graphql/types/namespaces/link_paths/project_namespace_links_type.rb b/app/graphql/types/namespaces/link_paths/project_namespace_links_type.rb
new file mode 100644
index 00000000000..73607993b8a
--- /dev/null
+++ b/app/graphql/types/namespaces/link_paths/project_namespace_links_type.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Types
+ module Namespaces
+ module LinkPaths
+ class ProjectNamespaceLinksType < BaseObject # rubocop:disable Graphql/AuthorizeTypes -- parent is already authorized
+ graphql_name 'ProjectNamespaceLinks'
+ implements ::Types::Namespaces::LinkPaths
+
+ def issues_list
+ url_helpers.project_issues_path(project)
+ end
+
+ def labels_manage
+ url_helpers.project_labels_path(project)
+ end
+
+ def new_project
+ url_helpers.new_project_path(namespace_id: group&.id)
+ end
+
+ def new_comment_template
+ new_comment_template_paths(group, project)&.dig(0, :href)
+ end
+
+ private
+
+ def project
+ @project ||= object.project
+ end
+
+ def group
+ @group ||= project.group
+ end
+ end
+ end
+ end
+end
+
+::Types::Namespaces::LinkPaths::ProjectNamespaceLinksType.prepend_mod
diff --git a/app/graphql/types/namespaces/link_paths/user_namespace_links_type.rb b/app/graphql/types/namespaces/link_paths/user_namespace_links_type.rb
new file mode 100644
index 00000000000..9092a799f02
--- /dev/null
+++ b/app/graphql/types/namespaces/link_paths/user_namespace_links_type.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Types
+ module Namespaces
+ module LinkPaths
+ class UserNamespaceLinksType < BaseObject # rubocop:disable Graphql/AuthorizeTypes -- parent is already authorized
+ graphql_name 'UserNamespaceLinks'
+ implements ::Types::Namespaces::LinkPaths
+ end
+ end
+ end
+end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index ea7ea845647..2699fa221b7 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -408,7 +408,7 @@ module IssuablesHelper
def new_comment_template_paths(group, project = nil)
[{
text: _('Your comment templates'),
- href: profile_comment_templates_path
+ href: ::Gitlab::Routing.url_helpers.profile_comment_templates_path
}]
end
end
diff --git a/app/services/container_registry/protection/concerns/tag_rule.rb b/app/services/container_registry/protection/concerns/tag_rule.rb
new file mode 100644
index 00000000000..a5719924987
--- /dev/null
+++ b/app/services/container_registry/protection/concerns/tag_rule.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module ContainerRegistry
+ module Protection
+ module Concerns
+ module TagRule
+ extend ActiveSupport::Concern
+
+ private
+
+ def protected_patterns_for_delete(project:, current_user: nil)
+ tag_rules = ContainerRegistry::Protection::TagRule.tag_name_patterns_for_project(project.id)
+
+ if current_user
+ return if current_user.can_admin_all_resources?
+
+ user_access_level = project.team.max_member_access(current_user.id)
+ tag_rules = tag_rules.for_delete_and_access(user_access_level)
+ end
+
+ return if tag_rules.blank?
+
+ tag_rules.map { |rule| ::Gitlab::UntrustedRegexp.new(rule.tag_name_pattern) }
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/projects/container_repository/cleanup_tags_base_service.rb b/app/services/projects/container_repository/cleanup_tags_base_service.rb
index b9d52f5dbb2..1f0eac0c735 100644
--- a/app/services/projects/container_repository/cleanup_tags_base_service.rb
+++ b/app/services/projects/container_repository/cleanup_tags_base_service.rb
@@ -5,6 +5,8 @@ module Projects
class CleanupTagsBaseService < BaseContainerRepositoryService
private
+ include ContainerRegistry::Protection::Concerns::TagRule
+
def filter_out_latest!(tags)
return unless keep_latest
@@ -22,18 +24,8 @@ module Projects
end
def filter_out_protected!(tags)
- tag_rules = ::ContainerRegistry::Protection::TagRule.tag_name_patterns_for_project(project.id)
-
- if current_user
- return if current_user.can_admin_all_resources?
-
- user_access_level = project.team.max_member_access(current_user.id)
- tag_rules = tag_rules.for_delete_and_access(user_access_level)
- end
-
- return if tag_rules.blank?
-
- patterns = tag_rules.map { |rule| ::Gitlab::UntrustedRegexp.new(rule.tag_name_pattern) }
+ patterns = protected_patterns_for_delete(project:, current_user:)
+ return if patterns.blank?
tags.reject! do |tag|
patterns.detect do |pattern|
diff --git a/app/services/projects/container_repository/gitlab/delete_tags_service.rb b/app/services/projects/container_repository/gitlab/delete_tags_service.rb
index ed7f3326f6a..5bdf61da6c6 100644
--- a/app/services/projects/container_repository/gitlab/delete_tags_service.rb
+++ b/app/services/projects/container_repository/gitlab/delete_tags_service.rb
@@ -7,6 +7,7 @@ module Projects
include BaseServiceUtility
include ::Gitlab::Utils::StrongMemoize
include ::Projects::ContainerRepository::Gitlab::Timeoutable
+ include ContainerRegistry::Protection::Concerns::TagRule
PROTECTED_TAGS_ERROR_MESSAGE = 'cannot delete protected tag(s)'
@@ -52,18 +53,8 @@ module Projects
end
def filter_out_protected!
- tag_rules = ::ContainerRegistry::Protection::TagRule.tag_name_patterns_for_project(project.id)
-
- if current_user
- return if current_user.can_admin_all_resources?
-
- user_access_level = project.team.max_member_access(current_user.id)
- tag_rules = tag_rules.for_delete_and_access(user_access_level)
- end
-
- return if tag_rules.blank?
-
- patterns = tag_rules.map { |rule| ::Gitlab::UntrustedRegexp.new(rule.tag_name_pattern) }
+ patterns = protected_patterns_for_delete(project:, current_user:)
+ return if patterns.blank?
tag_names.reject! do |tag_name|
patterns.detect do |pattern|
diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb
index 7855f399c8f..d024cb651a1 100644
--- a/config/initializers/7_prometheus_metrics.rb
+++ b/config/initializers/7_prometheus_metrics.rb
@@ -99,9 +99,19 @@ Gitlab::Cluster::LifecycleEvents.on_worker_start do
end
if Gitlab::Runtime.sidekiq?
- Gitlab::Metrics::Samplers::ConcurrencyLimitSampler.instance(logger: logger).start
- Gitlab::Metrics::Samplers::StatActivitySampler.instance(logger: logger).start
- Gitlab::Metrics::Samplers::GlobalSearchSampler.instance(logger: logger).start if Gitlab.ee?
+ @samplers_started = false
+
+ Rails.application.config.after_routes_loaded do
+ # Rails will reload this hook every time routes are changed.
+ unless @samplers_started
+ # These samplers may attempt to retrieve database connections (e.g. for feature flag checks)
+ # in the background, so wait until all the code is loaded before starting.
+ Gitlab::Metrics::Samplers::ConcurrencyLimitSampler.instance(logger: logger).start
+ Gitlab::Metrics::Samplers::StatActivitySampler.instance(logger: logger).start
+ Gitlab::Metrics::Samplers::GlobalSearchSampler.instance(logger: logger).start if Gitlab.ee?
+ @samplers_started = true
+ end
+ end
end
Gitlab::Ci::Parsers.instrument!
diff --git a/db/docs/batched_background_migrations/backfill_resource_link_events_namespace_id.yml b/db/docs/batched_background_migrations/backfill_resource_link_events_namespace_id.yml
index 3571385de89..1d2e4a0df03 100644
--- a/db/docs/batched_background_migrations/backfill_resource_link_events_namespace_id.yml
+++ b/db/docs/batched_background_migrations/backfill_resource_link_events_namespace_id.yml
@@ -5,4 +5,4 @@ feature_category: team_planning
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/174402
milestone: '17.7'
queued_migration_version: 20241202141411
-finalized_by: # version of the migration that finalized this BBM
+finalized_by: '20250511231623'
diff --git a/db/post_migrate/20250511231623_finalize_hk_backfill_resource_link_events_namespace_id.rb b/db/post_migrate/20250511231623_finalize_hk_backfill_resource_link_events_namespace_id.rb
new file mode 100644
index 00000000000..6b01ebf6a9d
--- /dev/null
+++ b/db/post_migrate/20250511231623_finalize_hk_backfill_resource_link_events_namespace_id.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class FinalizeHkBackfillResourceLinkEventsNamespaceId < Gitlab::Database::Migration[2.3]
+ milestone '18.0'
+
+ disable_ddl_transaction!
+
+ restrict_gitlab_migration gitlab_schema: :gitlab_main_cell
+
+ def up
+ ensure_batched_background_migration_is_finished(
+ job_class_name: 'BackfillResourceLinkEventsNamespaceId',
+ table_name: :resource_link_events,
+ column_name: :id,
+ job_arguments: [:namespace_id, :issues, :namespace_id, :issue_id],
+ finalize: true
+ )
+ end
+
+ def down; end
+end
diff --git a/db/schema_migrations/20250511231623 b/db/schema_migrations/20250511231623
new file mode 100644
index 00000000000..30889a81aac
--- /dev/null
+++ b/db/schema_migrations/20250511231623
@@ -0,0 +1 @@
+54cc277ec4f8dd21e5627c0d5d1bfd42dca452e40675bec5e55a0045c338ce4f
\ No newline at end of file
diff --git a/doc/administration/get_started.md b/doc/administration/get_started.md
index 2914070a69c..2e31644844d 100644
--- a/doc/administration/get_started.md
+++ b/doc/administration/get_started.md
@@ -170,8 +170,6 @@ All backups are encrypted. After 90 days, backups are deleted.
- Labels
- Additional items
-For more information about GitLab SaaS backups, see our [Backup FAQ page](https://handbook.gitlab.com/handbook/engineering/infrastructure/faq/#gitlabcom-backups).
-
{{< alert type="note" >}}
You should not use [direct transfer](../user/group/import/_index.md) or
@@ -306,7 +304,6 @@ You can learn more about how to administer GitLab.
### Paid GitLab training
- GitLab education services: Learn more about [GitLab and DevOps best practices](https://about.gitlab.com/services/education/) through our specialized training courses. See our full course catalog.
-- GitLab technical certifications: Explore our [certification options](https://handbook.gitlab.com/handbook/customer-success/professional-services-engineering/gitlab-technical-certifications/) that focus on key GitLab and DevOps skills.
### Free GitLab training
diff --git a/doc/api/discussions.md b/doc/api/discussions.md
index b6b0af10821..2a29b8d3b15 100644
--- a/doc/api/discussions.md
+++ b/doc/api/discussions.md
@@ -502,7 +502,7 @@ curl --request DELETE \
The Epics REST API was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/460668) in GitLab 17.0
and is planned for removal in v5 of the API.
In GitLab 17.4 or later, if [the new look for epics](../user/group/epics/epic_work_items.md) is enabled, use the
-[Work Items API](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/work_items/) instead. For more information, see the [guide how to migrate your existing APIs](graphql/epic_work_items_api_migration_guide.md).
+Work Items API instead. For more information, see the [guide how to migrate your existing APIs](graphql/epic_work_items_api_migration_guide.md).
This change is a breaking change.
{{< /alert >}}
diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md
index 7740b54d20f..1af8364c1dc 100644
--- a/doc/api/graphql/reference/_index.md
+++ b/doc/api/graphql/reference/_index.md
@@ -10054,6 +10054,30 @@ Input type: `RestorePagesDeploymentInput`
| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| `pagesDeployment` | [`PagesDeployment!`](#pagesdeployment) | Restored Pages Deployment. |
+### `Mutation.runnerAssignToProject`
+
+{{< details >}}
+**Introduced** in GitLab 18.1.
+**Status**: Experiment.
+{{< /details >}}
+
+Input type: `RunnerAssignToProjectInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `projectPath` | [`ID!`](#id) | Full path of the project to which the runner will be assigned. |
+| `runnerId` | [`CiRunnerID!`](#cirunnerid) | ID of the runner to assign to the project . |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+
### `Mutation.runnerBulkPause`
{{< details >}}
@@ -10152,6 +10176,30 @@ Input type: `RunnerDeleteInput`
| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+### `Mutation.runnerUnassignFromProject`
+
+{{< details >}}
+**Introduced** in GitLab 18.1.
+**Status**: Experiment.
+{{< /details >}}
+
+Input type: `RunnerUnassignFromProjectInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `projectPath` | [`ID!`](#id) | Full path of the project from which the runner will be unassigned. |
+| `runnerId` | [`CiRunnerID!`](#cirunnerid) | ID of the runner to unassign from the project. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+
### `Mutation.runnerUpdate`
Input type: `RunnerUpdateInput`
@@ -27678,6 +27726,7 @@ GPG signature for a signed commit.
| `isAdjournedDeletionEnabled` {{< icon name="warning-solid" >}} | [`Boolean!`](#boolean) | **Introduced** in GitLab 16.11. **Status**: Experiment. Indicates if delayed group deletion is enabled. |
| `isLinkedToSubscription` | [`Boolean`](#boolean) | Indicates if group is linked to a subscription. |
| `lfsEnabled` | [`Boolean`](#boolean) | Indicates if Large File Storage (LFS) is enabled for namespace. |
+| `linkPaths` {{< icon name="warning-solid" >}} | [`NamespacesLinkPaths`](#namespaceslinkpaths) | **Introduced** in GitLab 18.1. **Status**: Experiment. Namespace relevant paths to create links on the UI. |
| `lockDuoFeaturesEnabled` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Introduced** in GitLab 16.10. **Status**: Experiment. Indicates if the GitLab Duo features enabled setting is enforced for all subgroups. |
| `lockMathRenderingLimitsEnabled` | [`Boolean`](#boolean) | Indicates if math rendering limits are locked for all descendant groups. |
| `markedForDeletionOn` {{< icon name="warning-solid" >}} | [`Time`](#time) | **Introduced** in GitLab 16.11. **Status**: Experiment. Date when group was scheduled to be deleted. |
@@ -29603,6 +29652,23 @@ Limited group data accessible to users without full group read access (e.g. non-
| `name` | [`String!`](#string) | Name of the group. |
| `webUrl` | [`String`](#string) | Web URL of the group. |
+### `GroupNamespaceLinks`
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `epicsList` | [`String`](#string) | Namespace epics_list. |
+| `groupIssues` | [`String`](#string) | Namespace group_issues. |
+| `issuesList` | [`String`](#string) | Namespace issues_list. |
+| `labelsFetch` | [`String`](#string) | Namespace labels_fetch. |
+| `labelsManage` | [`String`](#string) | Namespace labels_manage. |
+| `newCommentTemplate` | [`String`](#string) | Namespace new_comment_template_paths. |
+| `newProject` | [`String`](#string) | Namespace new_project. |
+| `register` | [`String`](#string) | Namespace register_path. |
+| `reportAbuse` | [`String`](#string) | Namespace report_abuse. |
+| `signIn` | [`String`](#string) | Namespace sign_in_path. |
+
### `GroupPermissions`
#### Fields
@@ -33156,6 +33222,7 @@ Product analytics events for a specific month and year.
| `fullPath` | [`ID!`](#id) | Full path of the namespace. |
| `id` | [`ID!`](#id) | ID of the namespace. |
| `lfsEnabled` | [`Boolean`](#boolean) | Indicates if Large File Storage (LFS) is enabled for namespace. |
+| `linkPaths` {{< icon name="warning-solid" >}} | [`NamespacesLinkPaths`](#namespaceslinkpaths) | **Introduced** in GitLab 18.1. **Status**: Experiment. Namespace relevant paths to create links on the UI. |
| `name` | [`String!`](#string) | Name of the namespace. |
| `packageSettings` | [`PackageSettings`](#packagesettings) | Package settings for the namespace. |
| `path` | [`String!`](#string) | Path of the namespace. |
@@ -37513,6 +37580,23 @@ Returns [`UserMergeRequestInteraction`](#usermergerequestinteraction).
| `nameWithNamespace` | [`String!`](#string) | Name of the project including the namespace. |
| `webUrl` | [`String`](#string) | Web URL of the project. |
+### `ProjectNamespaceLinks`
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `epicsList` | [`String`](#string) | Namespace epics_list. |
+| `groupIssues` | [`String`](#string) | Namespace group_issues. |
+| `issuesList` | [`String`](#string) | Namespace issues_list. |
+| `labelsFetch` | [`String`](#string) | Namespace labels_fetch. |
+| `labelsManage` | [`String`](#string) | Namespace labels_manage. |
+| `newCommentTemplate` | [`String`](#string) | Namespace new_comment_template_paths. |
+| `newProject` | [`String`](#string) | Namespace new_project. |
+| `register` | [`String`](#string) | Namespace register_path. |
+| `reportAbuse` | [`String`](#string) | Namespace report_abuse. |
+| `signIn` | [`String`](#string) | Namespace sign_in_path. |
+
### `ProjectPermissions`
#### Fields
@@ -40280,6 +40364,23 @@ fields relate to interactions between the two entities.
| `reviewState` | [`MergeRequestReviewState`](#mergerequestreviewstate) | State of the review by the user. |
| `reviewed` | [`Boolean!`](#boolean) | Whether the user has provided a review for the merge request. |
+### `UserNamespaceLinks`
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `epicsList` | [`String`](#string) | Namespace epics_list. |
+| `groupIssues` | [`String`](#string) | Namespace group_issues. |
+| `issuesList` | [`String`](#string) | Namespace issues_list. |
+| `labelsFetch` | [`String`](#string) | Namespace labels_fetch. |
+| `labelsManage` | [`String`](#string) | Namespace labels_manage. |
+| `newCommentTemplate` | [`String`](#string) | Namespace new_comment_template_paths. |
+| `newProject` | [`String`](#string) | Namespace new_project. |
+| `register` | [`String`](#string) | Namespace register_path. |
+| `reportAbuse` | [`String`](#string) | Namespace report_abuse. |
+| `signIn` | [`String`](#string) | Namespace sign_in_path. |
+
### `UserPermissions`
#### Fields
@@ -48099,6 +48200,29 @@ Returns [`UserMergeRequestInteraction`](#usermergerequestinteraction).
| ---- | ---- | ----------- |
| `id` | [`MergeRequestID!`](#mergerequestid) | Global ID of the merge request. |
+#### `NamespacesLinkPaths`
+
+Implementations:
+
+- [`GroupNamespaceLinks`](#groupnamespacelinks)
+- [`ProjectNamespaceLinks`](#projectnamespacelinks)
+- [`UserNamespaceLinks`](#usernamespacelinks)
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `epicsList` | [`String`](#string) | Namespace epics_list. |
+| `groupIssues` | [`String`](#string) | Namespace group_issues. |
+| `issuesList` | [`String`](#string) | Namespace issues_list. |
+| `labelsFetch` | [`String`](#string) | Namespace labels_fetch. |
+| `labelsManage` | [`String`](#string) | Namespace labels_manage. |
+| `newCommentTemplate` | [`String`](#string) | Namespace new_comment_template_paths. |
+| `newProject` | [`String`](#string) | Namespace new_project. |
+| `register` | [`String`](#string) | Namespace register_path. |
+| `reportAbuse` | [`String`](#string) | Namespace report_abuse. |
+| `signIn` | [`String`](#string) | Namespace sign_in_path. |
+
#### `NoteableInterface`
Implementations:
diff --git a/doc/editor_extensions/eclipse/_index.md b/doc/editor_extensions/eclipse/_index.md
index 3c4d80c3d8a..f743ba3550a 100644
--- a/doc/editor_extensions/eclipse/_index.md
+++ b/doc/editor_extensions/eclipse/_index.md
@@ -46,6 +46,5 @@ Use the `Bug` or `Feature Proposal` template.
- [Code Suggestions](../../user/project/repository/code_suggestions/_index.md)
- [Eclipse troubleshooting](troubleshooting.md)
- [GitLab Language Server documentation](../language_server/_index.md)
-- [About the Create:Editor Extensions Group](https://handbook.gitlab.com/handbook/engineering/development/dev/create/editor-extensions/)
- [Open issues for this plugin](https://gitlab.com/gitlab-org/editor-extensions/gitlab-eclipse-plugin/-/issues/)
- [View source code](https://gitlab.com/gitlab-org/editor-extensions/gitlab-eclipse-plugin)
diff --git a/doc/editor_extensions/jetbrains_ide/_index.md b/doc/editor_extensions/jetbrains_ide/_index.md
index 19d5ca14bc9..5909fdf2210 100644
--- a/doc/editor_extensions/jetbrains_ide/_index.md
+++ b/doc/editor_extensions/jetbrains_ide/_index.md
@@ -98,7 +98,6 @@ built-in error reporting tool:
- [Code Suggestions](../../user/project/repository/code_suggestions/_index.md)
- [JetBrains troubleshooting](jetbrains_troubleshooting.md)
- [GitLab Language Server documentation](../language_server/_index.md)
-- [About the Create:Editor Extensions Group](https://handbook.gitlab.com/handbook/engineering/development/dev/create/editor-extensions/)
- [Open issues for this plugin](https://gitlab.com/gitlab-org/editor-extensions/gitlab-jetbrains-plugin/-/issues/)
- [Plugin documentation](https://gitlab.com/gitlab-org/editor-extensions/gitlab-jetbrains-plugin/-/blob/main/README.md)
- [View source code](https://gitlab.com/gitlab-org/editor-extensions/gitlab-jetbrains-plugin)
diff --git a/doc/editor_extensions/visual_studio/_index.md b/doc/editor_extensions/visual_studio/_index.md
index d84bbf5e5b0..eac80b72088 100644
--- a/doc/editor_extensions/visual_studio/_index.md
+++ b/doc/editor_extensions/visual_studio/_index.md
@@ -27,7 +27,6 @@ To update your extension to the latest version:
## Related topics
-- [About the Create:Editor Extensions Group](https://handbook.gitlab.com/handbook/engineering/development/dev/create/editor-extensions/)
- [Open issues for this plugin](https://gitlab.com/gitlab-org/editor-extensions/gitlab-visual-studio-extension/-/issues/)
- [View source code](https://gitlab.com/gitlab-org/editor-extensions/gitlab-visual-studio-extension)
- [GitLab Language Server documentation](../language_server/_index.md)
diff --git a/doc/integration/advanced_search/elasticsearch.md b/doc/integration/advanced_search/elasticsearch.md
index 25c8f83a82a..fe191a5808b 100644
--- a/doc/integration/advanced_search/elasticsearch.md
+++ b/doc/integration/advanced_search/elasticsearch.md
@@ -82,8 +82,6 @@ Advanced search works with the following versions of Elasticsearch.
| GitLab 14.0 to 14.10 | Elasticsearch 6.8 to 7.x |
Advanced search follows the [Elasticsearch end-of-life policy](https://www.elastic.co/support/eol).
-When we change Elasticsearch supported versions in GitLab, we announce them in [deprecation notes](https://handbook.gitlab.com/handbook/marketing/blog/release-posts/#update-the-deprecations-doc) in monthly release posts
-before we remove them.
#### OpenSearch
diff --git a/doc/solutions/cloud/aws/gitaly_sre_for_aws.md b/doc/solutions/cloud/aws/gitaly_sre_for_aws.md
index 05fc15c4bbb..859f715643c 100644
--- a/doc/solutions/cloud/aws/gitaly_sre_for_aws.md
+++ b/doc/solutions/cloud/aws/gitaly_sre_for_aws.md
@@ -46,7 +46,7 @@ All recommendations are for production configurations, including performance tes
#### Overall recommendations
-- Production-grade Gitaly must be implemented on instance compute due to all of the above and below characteristics.
+- Production-grade Gitaly must be implemented on instance compute due to all of the previous and following characteristics.
- Never use [burstable instance types](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/burstable-performance-instances.html) (such as `t2`, `t3`, `t4g`) for Gitaly.
- Always use at least the [AWS Nitro generation of instances](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html#ec2-nitro-instances) to ensure many of the below concerns are automatically handled.
- Use Amazon Linux 2 to ensure that all [AWS oriented hardware and OS optimizations](https://aws.amazon.com/amazon-linux-2/faqs/) are maximized without additional configuration or SRE management.
diff --git a/doc/tutorials/reviews/_index.md b/doc/tutorials/reviews/_index.md
index 8e4f65a5d5b..ef05f121345 100644
--- a/doc/tutorials/reviews/_index.md
+++ b/doc/tutorials/reviews/_index.md
@@ -383,6 +383,4 @@ After you provide your feedback, tidy up.
## Related topics
- [Conventional comments](https://conventionalcomments.org/) provide helpful structure for comments.
-- [Code review guidelines](https://handbook.gitlab.com/handbook/engineering/workflow/code-review/) in the GitLab handbook
-- [Merge request coaches](https://handbook.gitlab.com/job-families/expert/merge-request-coach/) in the GitLab handbook
- [Efficient code review tips](https://about.gitlab.com/blog/2020/09/08/efficient-code-review-tips/)
diff --git a/doc/update/terminology.md b/doc/update/terminology.md
index 99c1359b730..36fb5928b85 100644
--- a/doc/update/terminology.md
+++ b/doc/update/terminology.md
@@ -14,7 +14,7 @@ title: Deprecation terms
- Begins after a deprecation announcement outlining an end-of-support or removal date.
- Ends after the end-of-support date or removal date has passed.
-## End of Support
+## End of support
- Optional step before removal.
- Feature usage strongly discouraged.
@@ -23,7 +23,7 @@ title: Deprecation terms
- Will be removed in a future major release.
- Begins after an end-of-support date has passed.
-[Announcing an End of Support period](https://handbook.gitlab.com/handbook/marketing/blog/release-posts/#announcing-an-end-of-support-period)
+Announcing an end-of-support period
should only be used in special circumstances and is not recommended for general use.
Most features should be deprecated and then removed.
diff --git a/doc/user/gitlab_com/_index.md b/doc/user/gitlab_com/_index.md
index 43203f3148b..0ffef97ba46 100644
--- a/doc/user/gitlab_com/_index.md
+++ b/doc/user/gitlab_com/_index.md
@@ -49,8 +49,6 @@ this limit. Repository limits apply to both public and private projects.
## Backups
-[See our backup strategy](https://handbook.gitlab.com/handbook/engineering/infrastructure/production/#backups).
-
To back up an entire project on GitLab.com, you can export it:
- [Through the UI](../project/settings/import_export.md).
@@ -506,9 +504,7 @@ More details are available on the rate limits for
GitLab can rate-limit requests at several layers. The rate limits listed here
are configured in the application. These limits are the most
-restrictive for each IP address. For more information about the rate limits
-for GitLab.com, see
-[the documentation in the handbook](https://handbook.gitlab.com/handbook/engineering/infrastructure/rate-limiting).
+restrictive for each IP address.
### Group and project import by uploading export files
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 0308802ae02..bd836fdd8d5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -27878,9 +27878,15 @@ msgstr ""
msgid "GetStarted|Download the extension to access GitLab features and GitLab Duo AI capabilities to handle everyday tasks."
msgstr ""
+msgid "GetStarted|There was a problem trying to end the tutorial. Please try again."
+msgstr ""
+
msgid "GetStarted|Use GitLab Duo locally"
msgstr ""
+msgid "GetStarted|You've ended the tutorial."
+msgstr ""
+
msgid "GiB"
msgstr ""
@@ -35866,6 +35872,9 @@ msgstr ""
msgid "LearnGitLab|Enable require merge approvals"
msgstr ""
+msgid "LearnGitLab|End tutorial"
+msgstr ""
+
msgid "LearnGitLab|Enroll"
msgstr ""
diff --git a/qa/qa/support/wait_for_requests.rb b/qa/qa/support/wait_for_requests.rb
index a132a5fc52e..a5dd106752b 100644
--- a/qa/qa/support/wait_for_requests.rb
+++ b/qa/qa/support/wait_for_requests.rb
@@ -24,7 +24,7 @@ module QA
end
script = requests.join(' || ')
- Capybara.page.evaluate_script(script).zero? # rubocop:disable Style/NumericPredicate
+ Capybara.page.evaluate_script(script).to_i == 0
end
def spinner_exists?
@@ -32,11 +32,6 @@ module QA
end
def finished_loading?(wait: DEFAULT_MAX_WAIT_TIME)
- # The number of selectors should be able to be reduced after
- # migration to the new spinner is complete.
- # https://gitlab.com/groups/gitlab-org/-/epics/956
- # retry_on_exception added here due to `StaleElementReferenceError`. See: https://gitlab.com/gitlab-org/gitlab/-/issues/232485
-
Capybara.page.has_no_css?('.gl-spinner', wait: wait)
rescue Selenium::WebDriver::Error::StaleElementReferenceError => e
QA::Runtime::Logger.error(".gl-spinner reference has become stale: #{e}")
diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb
index ec9e5b5d510..fed96db7403 100644
--- a/qa/spec/page/logging_spec.rb
+++ b/qa/spec/page/logging_spec.rb
@@ -14,6 +14,8 @@ RSpec.describe QA::Support::Page::Logging do
allow(page).to receive(:find).and_return(page)
allow(page).to receive(:current_url).and_return('http://current-url')
allow(page).to receive(:has_css?).with(any_args).and_return(true)
+ allow(subject).to receive(:wait_for_requests).and_return(true)
+
allow(QA::Support::PageErrorChecker).to receive(:check_page_for_error_code).and_return(0)
end
diff --git a/qa/tasks/ci.rake b/qa/tasks/ci.rake
index d9b71a15c2d..7c8fb9abfef 100644
--- a/qa/tasks/ci.rake
+++ b/qa/tasks/ci.rake
@@ -77,16 +77,23 @@ namespace :ci do
pipelines_for_selective_improved = [:test_on_gdk]
logger.warn("*** Recreating #{pipelines_for_selective_improved} using spec list based on coverage mappings ***")
tests_from_mapping = qa_changes.qa_tests(from_code_path_mapping: true)
- properties = {
- label: tests_from_mapping.nil? || tests_from_mapping.empty? ? 'non-selective' : 'selective',
- value: tests_from_mapping.nil? || tests_from_mapping.empty? ? 0 : tests_from_mapping.count
- }
- Tooling::Events::TrackPipelineEvents.new(
- event_name: "e2e_tests_selected_for_execution_gitlab_pipeline",
- properties: properties
- ).send_event
+
logger.info("Following specs were selected for execution: '#{tests_from_mapping}'")
- QA::Tools::Ci::PipelineCreator.new(tests_from_mapping, **creator_args).create(pipelines_for_selective_improved)
+ begin
+ QA::Tools::Ci::PipelineCreator.new(tests_from_mapping, **creator_args).create(pipelines_for_selective_improved)
+ properties = {
+ label: tests_from_mapping.nil? || tests_from_mapping.empty? ? 'non-selective' : 'selective',
+ value: tests_from_mapping.nil? || tests_from_mapping.empty? ? 0 : tests_from_mapping.count
+ }
+ Tooling::Events::TrackPipelineEvents.new(
+ event_name: "e2e_tests_selected_for_execution_gitlab_pipeline",
+ properties: properties
+ ).send_event
+ rescue StandardError => e
+ logger.warn("*** Error while creating pipeline with selected specs: #{e.backtrace} ****")
+ logger.info("*** Running full suite ***")
+ QA::Tools::Ci::PipelineCreator.new([], **creator_args).create
+ end
end
desc "Export test run metrics to influxdb"
diff --git a/spec/frontend/work_items/components/create_work_item_spec.js b/spec/frontend/work_items/components/create_work_item_spec.js
index 9414e12d682..ca58112b84b 100644
--- a/spec/frontend/work_items/components/create_work_item_spec.js
+++ b/spec/frontend/work_items/components/create_work_item_spec.js
@@ -2,6 +2,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlAlert, GlButton, GlFormSelect, GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { cloneDeep } from 'lodash';
import namespaceWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_items/project_namespace_work_item_types.query.graphql.json';
import { setHTMLFixture } from 'helpers/fixtures';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
@@ -59,9 +60,9 @@ describe('Create work item component', () => {
const mutationErrorHandler = jest.fn().mockResolvedValue(createWorkItemMutationErrorResponse);
const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
const workItemQuerySuccessHandler = jest.fn().mockResolvedValue(createWorkItemQueryResponse());
- const namespaceWorkItemTypesHandler = jest
- .fn()
- .mockResolvedValue(namespaceWorkItemTypesQueryResponse);
+ const namespaceWorkItemTypes =
+ namespaceWorkItemTypesQueryResponse.data.workspace.workItemTypes.nodes;
+ const { webUrl: namespaceWebUrl } = namespaceWorkItemTypesQueryResponse.data.workspace;
const findFormTitle = () => wrapper.find('h1');
const findAlert = () => wrapper.findComponent(GlAlert);
@@ -86,14 +87,20 @@ describe('Create work item component', () => {
const findResolveDiscussionLink = () =>
wrapper.find('[data-testid="work-item-resolve-discussion"]').findComponent(GlLink);
- const namespaceWorkItemTypes =
- namespaceWorkItemTypesQueryResponse.data.workspace.workItemTypes.nodes;
-
const createComponent = ({
props = {},
mutationHandler = createWorkItemSuccessHandler,
preselectedWorkItemType = WORK_ITEM_TYPE_NAME_EPIC,
+ isGroupWorkItem = false,
} = {}) => {
+ const namespaceResponseCopy = cloneDeep(namespaceWorkItemTypesQueryResponse);
+ namespaceResponseCopy.data.workspace.id = 'gid://gitlab/Group/33';
+ const namespaceResponse = isGroupWorkItem
+ ? namespaceResponseCopy
+ : namespaceWorkItemTypesQueryResponse;
+
+ const namespaceWorkItemTypesHandler = jest.fn().mockResolvedValue(namespaceResponse);
+
mockApollo = createMockApollo(
[
[workItemByIidQuery, workItemQuerySuccessHandler],
@@ -898,4 +905,18 @@ describe('Create work item component', () => {
});
});
});
+
+ it.each`
+ isGroupWorkItem | uploadsPath
+ ${true} | ${`${namespaceWebUrl}/-/uploads`}
+ ${false} | ${`${namespaceWebUrl}/uploads`}
+ `(
+ 'passes correct uploads path for markdown editor when isGroupWorkItem is $isGroupWorkItem',
+ async ({ isGroupWorkItem, uploadsPath }) => {
+ createComponent({ isGroupWorkItem });
+ await waitForPromises();
+
+ expect(findDescriptionWidget().props('uploadsPath')).toBe(uploadsPath);
+ },
+ );
});
diff --git a/spec/graphql/types/namespaces/link_paths/group_namespace_links_type_spec.rb b/spec/graphql/types/namespaces/link_paths/group_namespace_links_type_spec.rb
new file mode 100644
index 00000000000..8138df5a800
--- /dev/null
+++ b/spec/graphql/types/namespaces/link_paths/group_namespace_links_type_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Types::Namespaces::LinkPaths::GroupNamespaceLinksType, feature_category: :shared do
+ it_behaves_like 'expose all link paths fields for the namespace'
+end
diff --git a/spec/graphql/types/namespaces/link_paths/project_namespace_links_type_spec.rb b/spec/graphql/types/namespaces/link_paths/project_namespace_links_type_spec.rb
new file mode 100644
index 00000000000..c733097f053
--- /dev/null
+++ b/spec/graphql/types/namespaces/link_paths/project_namespace_links_type_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Types::Namespaces::LinkPaths::ProjectNamespaceLinksType, feature_category: :shared do
+ it_behaves_like 'expose all link paths fields for the namespace'
+end
diff --git a/spec/graphql/types/namespaces/link_paths/user_namespace_links_type_spec.rb b/spec/graphql/types/namespaces/link_paths/user_namespace_links_type_spec.rb
new file mode 100644
index 00000000000..238cc7b1ec5
--- /dev/null
+++ b/spec/graphql/types/namespaces/link_paths/user_namespace_links_type_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Types::Namespaces::LinkPaths::UserNamespaceLinksType, feature_category: :shared do
+ it_behaves_like 'expose all link paths fields for the namespace'
+end
diff --git a/spec/graphql/types/namespaces/link_paths_spec.rb b/spec/graphql/types/namespaces/link_paths_spec.rb
new file mode 100644
index 00000000000..bea5bc3865e
--- /dev/null
+++ b/spec/graphql/types/namespaces/link_paths_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Types::Namespaces::LinkPaths, feature_category: :shared do
+ include GraphqlHelpers
+ using RSpec::Parameterized::TableSyntax
+
+ where(:namespace_class, :namespace_type_name) do
+ ::Group | ::Types::Namespaces::LinkPaths::GroupNamespaceLinksType
+ ::Namespaces::ProjectNamespace | ::Types::Namespaces::LinkPaths::ProjectNamespaceLinksType
+ ::Namespaces::UserNamespace | ::Types::Namespaces::LinkPaths::UserNamespaceLinksType
+ end
+
+ with_them do
+ describe ".resolve_type" do
+ it 'knows the correct type for objects' do
+ namespace = namespace_class.new
+
+ expect(described_class.resolve_type(namespace, {}))
+ .to eq(namespace_type_name)
+ end
+ end
+
+ describe '.orphan_types' do
+ it 'includes the type' do
+ expect(described_class.orphan_types).to include(namespace_type_name)
+ end
+ end
+ end
+
+ it 'raises an error for an unknown type' do
+ namespace = build(:project)
+
+ expect { described_class.resolve_type(namespace, {}) }
+ .to raise_error("Unknown GraphQL type for namespace type #{namespace.class}")
+ end
+
+ it_behaves_like 'expose all link paths fields for the namespace'
+end
diff --git a/spec/requests/api/graphql/mutations/ci/runner/assign_to_project_spec.rb b/spec/requests/api/graphql/mutations/ci/runner/assign_to_project_spec.rb
new file mode 100644
index 00000000000..57b5fa5fd25
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/runner/assign_to_project_spec.rb
@@ -0,0 +1,188 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Ci::Runner::AssignToProject, feature_category: :runner do
+ include GraphqlHelpers
+
+ let_it_be(:group_owner) { create(:user) }
+ let_it_be(:project_owner) { create(:user) }
+ let_it_be(:group) { create(:group, owners: group_owner) }
+ let_it_be(:project) { create(:project, namespace: group, owners: project_owner) }
+ let_it_be(:project2) { create(:project, namespace: group, owners: project_owner) }
+ let_it_be(:project_with_org) { create(:project, organization: create(:organization), owners: project_owner) }
+ let_it_be(:runner) { create(:ci_runner, :project, projects: [project2]) }
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:non_accessible_user) { create(:user) }
+
+ let(:mutation_params) do
+ {
+ project_path: project.full_path,
+ runner_id: runner.to_global_id
+ }
+ end
+
+ let(:mutation) do
+ graphql_mutation(
+ :runner_assign_to_project,
+ mutation_params,
+ 'errors'
+ )
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:runner_assign_to_project) }
+
+ specify { expect(described_class).to require_graphql_authorizations(:assign_runner) }
+
+ context 'with invalid parameters' do
+ context 'when project_path is not given' do
+ let(:mutation_params) do
+ {
+ runner_id: runner.to_global_id
+ }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: group_owner)
+ expect_graphql_errors_to_include("invalid value for projectPath")
+ end
+ end
+
+ context 'when project_path is invalid' do
+ let(:mutation_params) do
+ {
+ runner_id: runner.to_global_id,
+ project_path: 'non/existing/project/path'
+ }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: group_owner)
+ expect_graphql_errors_to_include("The resource that you are attempting to access does not exist or you " \
+ "don't have permission to perform this action")
+ end
+ end
+
+ context 'when runner_id is invalid' do
+ let(:mutation_params) do
+ {
+ runner_id: "gid://gitlab/Ci::Runner/#{non_existing_record_id}",
+ project_path: project.full_path
+ }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: group_owner)
+ expect_graphql_errors_to_include("The resource that you are attempting to access does not exist or you " \
+ "don't have permission to perform this action")
+ end
+ end
+
+ context 'when runner_id is missing' do
+ let(:mutation_params) do
+ {
+ project_path: project.full_path
+ }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: group_owner)
+ expect_graphql_errors_to_include('was provided invalid value for runnerId')
+ end
+ end
+ end
+
+ context 'with runner type constraints' do
+ context 'when the runner is not a project runner' do
+ let(:runner) { create(:ci_runner, :group, groups: [group]) }
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: group_owner)
+ expect_graphql_errors_to_include('Runner is not a project runner')
+ end
+ end
+ end
+
+ context 'with organization constraints' do
+ context "when project organization_id is not the same as the runner's" do
+ let(:mutation_params) do
+ {
+ project_path: project_with_org.full_path,
+ runner_id: runner.to_global_id
+ }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: project_owner)
+ expect(graphql_mutation_response(:runner_assign_to_project)['errors'])
+ .to include("runner can only be assigned to projects in the same organization")
+ expect(runner.reload.projects).not_to include(project)
+ end
+ end
+ end
+
+ context 'with permission checks' do
+ context 'when user does not have necessary permissions' do
+ it 'does not allow non-accessible user to assign a project to a runner' do
+ post_graphql_mutation(mutation, current_user: non_accessible_user)
+ expect_graphql_errors_to_include("The resource that you are attempting to access does not exist or you " \
+ "don't have permission to perform this action")
+ expect(runner.reload.projects).not_to include(project)
+ end
+ end
+
+ context 'when user has necessary permissions' do
+ context 'when the user is group owner' do
+ it 'allows accessible user to assign a project to a runner' do
+ post_graphql_mutation(mutation, current_user: group_owner)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(runner.reload.projects).to include(project)
+ end
+
+ context 'when the runner is locked' do
+ let_it_be(:runner) { create(:ci_runner, :project, :locked, projects: [project2]) }
+
+ it 'returns an error' do
+ # this case is not explicitly handled in the mutation definition or service,
+ # this is handled in the policy itself (app/policies/ci/runner_policy.rb)
+ post_graphql_mutation(mutation, current_user: group_owner)
+ expect_graphql_errors_to_include("The resource that you are attempting to access does not exist or you " \
+ "don't have permission to perform this action")
+ expect(runner.reload.projects).not_to include(project)
+ end
+ end
+
+ context 'when the runner is already assigned to the project' do
+ it 'assigns the project to the runner and does not duplicate the assignment' do
+ post_graphql_mutation(mutation, current_user: group_owner)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(runner.reload.projects).to include(project)
+
+ # Check for duplicate assignments
+ project_count = runner.reload.projects.count
+ post_graphql_mutation(mutation, current_user: group_owner)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(runner.reload.projects).to include(project)
+ expect(runner.reload.projects.count).to eq(project_count)
+ end
+ end
+ end
+
+ context 'when user is admin', :enable_admin_mode do
+ it 'allows accessible user to assign a project to a runner' do
+ post_graphql_mutation(mutation, current_user: admin)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(runner.reload.projects).to include(project)
+ end
+ end
+
+ context 'when the user is project owner' do
+ it 'allows accessible user to assign a project to a runner' do
+ post_graphql_mutation(mutation, current_user: project_owner)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(runner.reload.projects).to include(project)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/ci/runner/unassign_from_project_spec.rb b/spec/requests/api/graphql/mutations/ci/runner/unassign_from_project_spec.rb
new file mode 100644
index 00000000000..806c631d246
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/runner/unassign_from_project_spec.rb
@@ -0,0 +1,200 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Ci::Runner::UnassignFromProject, feature_category: :runner do
+ include GraphqlHelpers
+
+ let_it_be(:group_owner) { create(:user) }
+ let_it_be(:project_owner) { create(:user) }
+ let_it_be(:group) { create(:group, owners: group_owner) }
+ let_it_be(:owner_project) { create(:project, namespace: group, owners: project_owner) }
+ let_it_be(:project) { create(:project, namespace: group, owners: project_owner) }
+ let_it_be(:runner) { create(:ci_runner, :project, projects: [owner_project, project]) }
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:non_accessible_user) { create(:user) }
+
+ let(:mutation_params) do
+ {
+ project_path: project.full_path,
+ runner_id: runner.to_global_id
+ }
+ end
+
+ let(:mutation) do
+ graphql_mutation(
+ :runner_unassign_from_project,
+ mutation_params,
+ 'errors'
+ )
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:runner_unassign_from_project) }
+
+ specify { expect(described_class).to require_graphql_authorizations(:admin_project_runners) }
+
+ context 'with invalid parameters' do
+ context 'when project_path is missing' do
+ let(:mutation_params) do
+ {
+ runner_id: runner.to_global_id
+ }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: group_owner)
+ expect_graphql_errors_to_include("invalid value for projectPath")
+ end
+ end
+
+ context 'when project_path is invalid' do
+ let(:mutation_params) do
+ {
+ runner_id: runner.to_global_id,
+ project_path: 'non/existing/project/path'
+ }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: group_owner)
+ expect_graphql_errors_to_include("The resource that you are attempting to access does not exist or you " \
+ "don't have permission to perform this action")
+ end
+ end
+
+ context 'when runner_id is missing' do
+ let(:mutation_params) do
+ {
+ project_path: project.full_path
+ }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: group_owner)
+ expect_graphql_errors_to_include('invalid value for runnerId')
+ end
+ end
+
+ context 'when runner_id is invalid' do
+ let(:mutation_params) do
+ {
+ runner_id: "gid://gitlab/Ci::Runner/#{non_existing_record_id}",
+ project_path: project.full_path
+ }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: group_owner)
+ expect_graphql_errors_to_include("Runner does not exist or is not assigned to this project")
+ end
+ end
+ end
+
+ context 'with permission checks' do
+ context 'when user does not have necessary permissions' do
+ it 'does not allow non-accessible user to unassign a runner from a project' do
+ post_graphql_mutation(mutation, current_user: non_accessible_user)
+ expect_graphql_errors_to_include("The resource that you are attempting to access does not exist or you " \
+ "don't have permission to perform this action")
+ expect(runner.reload.projects).to include(project)
+ end
+ end
+
+ context 'when user has necessary permissions' do
+ context 'when the user is group owner' do
+ it 'allows group owner to unassign a runner from a project' do
+ post_graphql_mutation(mutation, current_user: group_owner)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(runner.reload.projects).not_to include(project)
+ end
+ end
+
+ context 'when user is admin', :enable_admin_mode do
+ it 'allows admin to unassign a runner from a project' do
+ post_graphql_mutation(mutation, current_user: admin)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(runner.reload.projects).not_to include(project)
+ end
+ end
+
+ context 'when the user is project owner' do
+ it 'allows project owner to unassign a runner from a project' do
+ post_graphql_mutation(mutation, current_user: project_owner)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(runner.reload.projects).not_to include(project)
+ end
+ end
+ end
+ end
+
+ context 'with runner assignment scenarios' do
+ context 'when the runner is not assigned to the project' do
+ let_it_be(:project2) { create(:project, namespace: group) }
+ let(:mutation_params) do
+ {
+ project_path: project2.full_path,
+ runner_id: runner.to_global_id
+ }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: group_owner)
+ expect(response).to have_gitlab_http_status(:success)
+ expect_graphql_errors_to_include("Runner does not exist or is not assigned to this project")
+ end
+ end
+
+ context 'when the runner is the last one assigned to the project' do
+ it 'successfully unassigns the runner' do
+ expect(project.runners.count).to eq(1)
+ post_graphql_mutation(mutation, current_user: project_owner)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(project.reload.runners.count).to eq(0)
+ end
+ end
+
+ context 'when the project has multiple runners' do
+ let_it_be(:another_runner) { create(:ci_runner, :project, projects: [project]) }
+
+ it 'only unassigns the specified runner' do
+ expect(project.runners.count).to eq(2)
+ post_graphql_mutation(mutation, current_user: project_owner)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(project.reload.runners.count).to eq(1)
+ expect(project.runners).to include(another_runner)
+ expect(project.runners).not_to include(runner)
+ end
+ end
+
+ context 'when unassigning a owner project from the runner' do
+ let(:mutation_params) do
+ {
+ project_path: owner_project.full_path,
+ runner_id: runner.to_global_id
+ }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: group_owner)
+ expect(mutation_response['errors']).to include("You cannot unassign a runner from the owner project. " \
+ "Delete the runner instead")
+ end
+ end
+ end
+
+ context 'when service returns an error' do
+ before do
+ service = instance_double(::Ci::Runners::UnassignRunnerService)
+ result = ServiceResponse.error(message: 'Custom error message')
+
+ allow(::Ci::Runners::UnassignRunnerService).to receive(:new).and_return(service)
+ allow(service).to receive(:execute).and_return(result)
+ end
+
+ it 'returns the error from the service' do
+ post_graphql_mutation(mutation, current_user: project_owner)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors']).to include('Custom error message')
+ end
+ end
+end
diff --git a/spec/services/container_registry/protection/concerns/tag_rule_spec.rb b/spec/services/container_registry/protection/concerns/tag_rule_spec.rb
new file mode 100644
index 00000000000..95311a15131
--- /dev/null
+++ b/spec/services/container_registry/protection/concerns/tag_rule_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ContainerRegistry::Protection::Concerns::TagRule, feature_category: :container_registry do
+ using RSpec::Parameterized::TableSyntax
+
+ # Create a test class that includes our service concern
+ let(:test_class) do
+ Class.new do
+ include ContainerRegistry::Protection::Concerns::TagRule
+
+ # Make the private method public for testing
+ public :protected_patterns_for_delete
+ end
+ end
+
+ # Create an instance of the test class to use in our tests
+ let(:service) { test_class.new }
+
+ describe '#protected_patterns_for_delete' do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
+ subject(:tag_name_patterns) { service.protected_patterns_for_delete(project: project, current_user: current_user) }
+
+ context 'when the project has no tag protection rules' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'when the project has tag protection rules' do
+ def create_rule(access_level, tag_name_pattern)
+ create(
+ :container_registry_protection_tag_rule,
+ project: project,
+ tag_name_pattern: tag_name_pattern,
+ minimum_access_level_for_delete: access_level
+ )
+ end
+
+ let_it_be(:rule1) { create_rule(:owner, 'owner_pattern') }
+ let_it_be(:rule2) { create_rule(:admin, 'admin_pattern') }
+ let_it_be(:rule3) { create_rule(:maintainer, 'maintainer_pattern') }
+
+ context 'when current user is nil' do
+ let_it_be(:current_user) { nil }
+ let(:expected_tag_name_pattern) { [rule1, rule2, rule3].map(&:tag_name_pattern) }
+
+ it 'returns all tag rules' do
+ expect(tag_name_patterns.all?(Gitlab::UntrustedRegexp)).to be(true)
+ expect(tag_name_patterns.map(&:source)).to match_array(expected_tag_name_pattern)
+ end
+ end
+
+ context 'when current user is supplied' do
+ context 'when current user is an admin', :enable_admin_mode do
+ let(:current_user) { build_stubbed(:admin) }
+
+ it { is_expected.to be_nil }
+ end
+
+ where(:user_role, :expected_patterns) do
+ :developer | %w[admin_pattern maintainer_pattern owner_pattern]
+ :maintainer | %w[admin_pattern owner_pattern]
+ :owner | %w[admin_pattern]
+ end
+
+ with_them do
+ before do
+ project.send(:"add_#{user_role}", current_user)
+ end
+
+ it 'returns the tag name patterns with access levels that are above the user' do
+ expect(tag_name_patterns.all?(Gitlab::UntrustedRegexp)).to be(true)
+ expect(tag_name_patterns.map(&:source)).to match_array(expected_patterns)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb b/spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb
index de781778c9f..edb98ab9ec3 100644
--- a/spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb
@@ -87,28 +87,12 @@ RSpec.describe Projects::ContainerRepository::Gitlab::CleanupTagsService, featur
delete_expectations: [%w[Bb], %w[C]]
it_behaves_like 'with protected rule having pattern ^\d{1,2}-\d{1,2}-stable$',
- delete_expectations: [%w[A], %w[Ba Bb], %w[C D], %w[17-8-stable]]
-
- context 'with admin minimum_access_level_for_delete' do
- it_behaves_like 'with protected rule having pattern ^\d{1,2}-\d{1,2}-stable$',
- delete_expectations: [%w[A], %w[Ba Bb], %w[C D]],
- minimum_access_level_for_delete: :admin
- end
-
- context 'without user' do
- let(:user) { nil }
-
- it_behaves_like 'with protected rule having pattern ^\d{1,2}-\d{1,2}-stable$',
- delete_expectations: [%w[A], %w[Ba Bb], %w[C D]]
- end
+ delete_expectations: [%w[A], %w[Ba Bb], %w[C D]]
context 'with the skip_protected_tags param' do
- let(:params) do
- { 'skip_protected_tags' => true }
- end
-
it_behaves_like 'with protected rule having pattern ^\d{1,2}-\d{1,2}-stable$',
- delete_expectations: [%w[A], %w[Ba Bb], %w[C D], %w[17-8-stable]]
+ delete_expectations: [%w[A], %w[Ba Bb], %w[C D], %w[17-8-stable]],
+ extra_params: { 'skip_protected_tags' => true }
end
context 'with a timeout' do
@@ -174,28 +158,12 @@ RSpec.describe Projects::ContainerRepository::Gitlab::CleanupTagsService, featur
delete_expectations: [%w[Ba Bb C]]
it_behaves_like 'with protected rule having pattern ^\d{1,2}-\d{1,2}-stable$',
- delete_expectations: [%w[A Ba Bb C D 17-8-stable]]
-
- context 'with admin minimum_access_level_for_delete' do
- it_behaves_like 'with protected rule having pattern ^\d{1,2}-\d{1,2}-stable$',
- delete_expectations: [%w[A Ba Bb C D]],
- minimum_access_level_for_delete: :admin
- end
-
- context 'without user' do
- let(:user) { nil }
-
- it_behaves_like 'with protected rule having pattern ^\d{1,2}-\d{1,2}-stable$',
- delete_expectations: [%w[A Ba Bb C D]]
- end
+ delete_expectations: [%w[A Ba Bb C D]]
context 'with the skip_protected_tags param' do
- let(:params) do
- { 'skip_protected_tags' => true }
- end
-
it_behaves_like 'with protected rule having pattern ^\d{1,2}-\d{1,2}-stable$',
- delete_expectations: [%w[A Ba Bb C D 17-8-stable]]
+ delete_expectations: [%w[A Ba Bb C D 17-8-stable]],
+ extra_params: { 'skip_protected_tags' => true }
end
end
diff --git a/spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb b/spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb
index eb281a8455b..8ce0f1e5635 100644
--- a/spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb
@@ -45,18 +45,18 @@ RSpec.describe Projects::ContainerRepository::Gitlab::DeleteTagsService, feature
end
context 'with tag protection rules' do
- let_it_be(:rule1) do
- create(:container_registry_protection_tag_rule, project: project, tag_name_pattern: 'A')
- end
-
let(:tag_names) { %w[A Ba Bb C D] }
+ let(:protected_patterns) { nil }
before do
+ allow(service).to receive(:protected_patterns_for_delete).and_return(protected_patterns)
allow(repository.client).to receive(:supports_tag_delete?).and_return(true)
stub_delete_reference_requests(tag_names)
end
context 'when not all tags are protected' do
+ let(:protected_patterns) { %w[A].map { |pattern| ::Gitlab::UntrustedRegexp.new(pattern) } }
+
before do
expect_delete_tags(%w[Ba Bb C D])
end
@@ -65,68 +65,18 @@ RSpec.describe Projects::ContainerRepository::Gitlab::DeleteTagsService, feature
end
context 'when all tags are protected' do
- before do
- create(:container_registry_protection_tag_rule, project: project, tag_name_pattern: 'B')
- create(:container_registry_protection_tag_rule, project: project, tag_name_pattern: 'C')
- create(:container_registry_protection_tag_rule, project: project, tag_name_pattern: 'D')
- end
+ let(:protected_patterns) { %w[A B C D].map { |pattern| ::Gitlab::UntrustedRegexp.new(pattern) } }
it { is_expected.to include(status: :error, message: 'cannot delete protected tag(s)') }
end
- context 'when the user has admin permissions' do
- before do
- allow(user).to receive(:can_admin_all_resources?).and_return(true)
- end
-
- it 'deletes tags including protected ones' do
+ context 'when no tags are protected' do
+ it 'deletes all tags' do
expect_delete_tags(tag_names)
is_expected.to include(status: :success)
end
end
-
- context 'when the user has no admin permissions' do
- before do
- create(:container_registry_protection_tag_rule,
- project: project,
- tag_name_pattern: 'B',
- minimum_access_level_for_delete: :maintainer)
- create(:container_registry_protection_tag_rule,
- project: project,
- tag_name_pattern: 'C',
- minimum_access_level_for_delete: :owner)
-
- project.add_maintainer(user)
- end
-
- it 'applies tag protection rules based on the user access level' do
- expect_delete_tags(%w[A Ba Bb D])
-
- is_expected.to include(status: :success)
- end
- end
-
- context 'when there is no user, run by a cleanup policy' do
- let(:user) { nil }
-
- before do
- create(:container_registry_protection_tag_rule,
- project: project,
- tag_name_pattern: 'B',
- minimum_access_level_for_delete: :maintainer)
- create(:container_registry_protection_tag_rule,
- project: project,
- tag_name_pattern: 'C',
- minimum_access_level_for_delete: :owner)
-
- expect_delete_tags(%w[D])
- end
-
- it 'uses all the tag protection rules' do
- is_expected.to include(status: :success)
- end
- end
end
context 'with failures' do
diff --git a/spec/support/shared_examples/graphql/types/namespaces/link_paths_shared_examples.rb b/spec/support/shared_examples/graphql/types/namespaces/link_paths_shared_examples.rb
new file mode 100644
index 00000000000..9850980963c
--- /dev/null
+++ b/spec/support/shared_examples/graphql/types/namespaces/link_paths_shared_examples.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'expose all link paths fields for the namespace' do
+ include GraphqlHelpers
+
+ specify do
+ expected_fields = %i[
+ issuesList
+ labelsManage
+ newCommentTemplate
+ newProject
+ register
+ reportAbuse
+ signIn
+ ]
+
+ if Gitlab.ee?
+ expected_fields.push(*%i[
+ labelsFetch
+ epicsList
+ groupIssues
+ ])
+ end
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/support/shared_examples/product_usage_data_collection_shared_examples.rb b/spec/support/shared_examples/product_usage_data_collection_shared_examples.rb
index 703279371c9..0908c663826 100644
--- a/spec/support/shared_examples/product_usage_data_collection_shared_examples.rb
+++ b/spec/support/shared_examples/product_usage_data_collection_shared_examples.rb
@@ -17,6 +17,8 @@ RSpec.shared_examples 'page with product usage data collection banner' do
allow(user).to receive(:dismissed_callout?).and_return(false)
visit page_path
+ wait_for_requests
+
expect(page).to have_selector '[data-testid="product-usage-data-collection-banner"]'
page.within('[data-testid="product-usage-data-collection-banner"]') do
diff --git a/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb b/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb
index 94b7fe37195..bbc4a50c7fa 100644
--- a/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb
+++ b/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb
@@ -195,19 +195,15 @@ RSpec.shared_examples 'when running a container_expiration_policy' do
end
RSpec.shared_examples 'with protected rule having pattern ^\d{1,2}-\d{1,2}-stable$' do
- |delete_expectations:, service_response_extra: {}, supports_caching: false,
- minimum_access_level_for_delete: :maintainer|
- let_it_be(:rule) do
- create(
- :container_registry_protection_tag_rule,
- tag_name_pattern: '^\d{1,2}-\d{1,2}-stable$',
- project: project,
- minimum_access_level_for_delete: minimum_access_level_for_delete
- )
+ |delete_expectations:, service_response_extra: {}, supports_caching: false, extra_params: {}|
+
+ before do
+ patterns = [::Gitlab::UntrustedRegexp.new('^\d{1,2}-\d{1,2}-stable$')]
+ allow(service).to receive(:protected_patterns_for_delete).and_return(patterns)
end
let(:params) do
- { 'name_regex_delete' => '.*' }
+ { 'name_regex_delete' => '.*' }.merge(extra_params)
end
it_behaves_like 'removing the expected tags',