summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-11-30 00:09:01 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-11-30 00:09:01 +0000
commit2fdee6d838d5615a24bfde9874a5c2d84a30d5bf (patch)
tree30ed88988118d43562d83ff493c7bee31b0e130c
parent8308674afc1f8636bcd2017e1573292d1500af9d (diff)
downloadgitlab-ce-2fdee6d838d5615a24bfde9874a5c2d84a30d5bf.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/global.gitlab-ci.yml2
-rw-r--r--.rubocop_todo/rspec/empty_line_after_example_group.yml35
-rw-r--r--app/controllers/concerns/preferred_language_switcher.rb2
-rw-r--r--app/models/project_export_job.rb15
-rw-r--r--app/services/ci/create_pipeline_service.rb2
-rw-r--r--app/services/projects/import_export/parallel_export_service.rb98
-rw-r--r--app/views/shared/_ref_switcher.html.haml2
-rw-r--r--app/workers/all_queues.yml9
-rw-r--r--app/workers/projects/import_export/parallel_project_export_worker.rb61
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--doc/api/bulk_imports.md2
-rw-r--r--doc/user/packages/yarn_repository/index.md248
-rw-r--r--doc/user/profile/notifications.md2
-rw-r--r--doc/user/project/deploy_tokens/index.md16
-rw-r--r--locale/gitlab.pot3
-rw-r--r--spec/commands/sidekiq_cluster/cli_spec.rb4
-rw-r--r--spec/controllers/explore/projects_controller_spec.rb1
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb1
-rw-r--r--spec/factories/project_export_jobs.rb16
-rw-r--r--spec/factories/projects/ci_feature_usages.rb1
-rw-r--r--spec/features/security/group/internal_access_spec.rb10
-rw-r--r--spec/features/security/group/private_access_spec.rb12
-rw-r--r--spec/features/security/group/public_access_spec.rb10
-rw-r--r--spec/helpers/blob_helper_spec.rb1
-rw-r--r--spec/helpers/git_helper_spec.rb2
-rw-r--r--spec/initializers/lograge_spec.rb2
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb1
-rw-r--r--spec/lib/gitlab/blob_helper_spec.rb1
-rw-r--r--spec/lib/gitlab/file_type_detection_spec.rb1
-rw-r--r--spec/lib/gitlab/instrumentation_helper_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/instrumentation_spec.rb6
-rw-r--r--spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb1
-rw-r--r--spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb1
-rw-r--r--spec/models/note_spec.rb1
-rw-r--r--spec/models/project_feature_spec.rb1
-rw-r--r--spec/models/user_spec.rb1
-rw-r--r--spec/models/zoom_meeting_spec.rb1
-rw-r--r--spec/requests/api/projects_spec.rb1
-rw-r--r--spec/routing/project_routing_spec.rb1
-rw-r--r--spec/services/projects/import_export/parallel_export_service_spec.rb98
-rw-r--r--spec/workers/projects/import_export/parallel_project_export_worker_spec.rb60
41 files changed, 668 insertions, 68 deletions
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index 203f4826ae4..8d5a2bef9d6 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -293,7 +293,7 @@
- name: postgres:12
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:6.0-alpine
- - name: elasticsearch:8.4.1
+ - name: elasticsearch:8.5.2
variables:
POSTGRES_HOST_AUTH_METHOD: trust
PG_VERSION: "12"
diff --git a/.rubocop_todo/rspec/empty_line_after_example_group.yml b/.rubocop_todo/rspec/empty_line_after_example_group.yml
deleted file mode 100644
index e298b1b89b8..00000000000
--- a/.rubocop_todo/rspec/empty_line_after_example_group.yml
+++ /dev/null
@@ -1,35 +0,0 @@
----
-# Cop supports --autocorrect.
-RSpec/EmptyLineAfterExampleGroup:
- Exclude:
- - 'ee/spec/controllers/groups/clusters_controller_spec.rb'
- - 'ee/spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb'
- - 'ee/spec/features/security/group/private_access_spec.rb'
- - 'ee/spec/lib/gitlab/vulnerabilities/container_scanning_vulnerability_spec.rb'
- - 'ee/spec/services/ee/gpg_keys/create_service_spec.rb'
- - 'ee/spec/services/ee/issues/create_from_vulnerability_data_service_spec.rb'
- - 'ee/spec/services/vulnerabilities/confirm_service_spec.rb'
- - 'ee/spec/services/vulnerabilities/dismiss_service_spec.rb'
- - 'ee/spec/services/vulnerabilities/resolve_service_spec.rb'
- - 'ee/spec/services/vulnerabilities/revert_to_detected_service_spec.rb'
- - 'ee/spec/services/vulnerability_issue_links/create_service_spec.rb'
- - 'ee/spec/services/vulnerability_issue_links/delete_service_spec.rb'
- - 'spec/controllers/explore/projects_controller_spec.rb'
- - 'spec/controllers/projects/notes_controller_spec.rb'
- - 'spec/factories/projects/ci_feature_usages.rb'
- - 'spec/features/security/group/internal_access_spec.rb'
- - 'spec/features/security/group/private_access_spec.rb'
- - 'spec/features/security/group/public_access_spec.rb'
- - 'spec/helpers/blob_helper_spec.rb'
- - 'spec/helpers/git_helper_spec.rb'
- - 'spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb'
- - 'spec/lib/gitlab/blob_helper_spec.rb'
- - 'spec/lib/gitlab/file_type_detection_spec.rb'
- - 'spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb'
- - 'spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb'
- - 'spec/models/note_spec.rb'
- - 'spec/models/project_feature_spec.rb'
- - 'spec/models/user_spec.rb'
- - 'spec/models/zoom_meeting_spec.rb'
- - 'spec/requests/api/projects_spec.rb'
- - 'spec/routing/project_routing_spec.rb'
diff --git a/app/controllers/concerns/preferred_language_switcher.rb b/app/controllers/concerns/preferred_language_switcher.rb
index 9711e57cf7a..00cd0f9d1d5 100644
--- a/app/controllers/concerns/preferred_language_switcher.rb
+++ b/app/controllers/concerns/preferred_language_switcher.rb
@@ -16,3 +16,5 @@ module PreferredLanguageSwitcher
Gitlab::CurrentSettings.default_preferred_language
end
end
+
+PreferredLanguageSwitcher.prepend_mod
diff --git a/app/models/project_export_job.rb b/app/models/project_export_job.rb
index decc71ee193..47be692d57a 100644
--- a/app/models/project_export_job.rb
+++ b/app/models/project_export_job.rb
@@ -6,6 +6,13 @@ class ProjectExportJob < ApplicationRecord
validates :project, :jid, :status, presence: true
+ STATUS = {
+ queued: 0,
+ started: 1,
+ finished: 2,
+ failed: 3
+ }.freeze
+
state_machine :status, initial: :queued do
event :start do
transition [:queued] => :started
@@ -19,9 +26,9 @@ class ProjectExportJob < ApplicationRecord
transition [:queued, :started] => :failed
end
- state :queued, value: 0
- state :started, value: 1
- state :finished, value: 2
- state :failed, value: 3
+ state :queued, value: STATUS[:queued]
+ state :started, value: STATUS[:started]
+ state :finished, value: STATUS[:finished]
+ state :failed, value: STATUS[:failed]
end
end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 43a2f4e9a71..9c3cc803587 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -4,8 +4,6 @@ module Ci
class CreatePipelineService < BaseService
attr_reader :pipeline, :logger
- CreateError = Class.new(StandardError)
-
LOG_MAX_DURATION_THRESHOLD = 3.seconds
LOG_MAX_PIPELINE_SIZE = 2_000
LOG_MAX_CREATION_THRESHOLD = 20.seconds
diff --git a/app/services/projects/import_export/parallel_export_service.rb b/app/services/projects/import_export/parallel_export_service.rb
new file mode 100644
index 00000000000..7e4c0279b06
--- /dev/null
+++ b/app/services/projects/import_export/parallel_export_service.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+module Projects
+ module ImportExport
+ class ParallelExportService
+ def initialize(export_job, current_user, after_export_strategy)
+ @export_job = export_job
+ @current_user = current_user
+ @after_export_strategy = after_export_strategy
+ @shared = project.import_export_shared
+ @logger = Gitlab::Export::Logger.build
+ end
+
+ def execute
+ log_info('Parallel project export started')
+
+ if save_exporters && save_export_archive
+ log_info('Parallel project export finished successfully')
+ execute_after_export_action(after_export_strategy)
+ else
+ notify_error
+ end
+
+ ensure
+ cleanup
+ end
+
+ private
+
+ attr_reader :export_job, :current_user, :after_export_strategy, :shared, :logger
+
+ delegate :project, to: :export_job
+
+ def execute_after_export_action(after_export_strategy)
+ return if after_export_strategy.execute(current_user, project)
+
+ notify_error
+ end
+
+ def exporters
+ [version_saver, exported_relations_merger]
+ end
+
+ def save_exporters
+ exporters.all? do |exporter|
+ log_info("Parallel project export - #{exporter.class.name} saver started")
+
+ exporter.save
+ end
+ end
+
+ def save_export_archive
+ Gitlab::ImportExport::Saver.save(exportable: project, shared: shared)
+ end
+
+ def version_saver
+ @version_saver ||= Gitlab::ImportExport::VersionSaver.new(shared: shared)
+ end
+
+ def exported_relations_merger
+ @relation_saver ||= Gitlab::ImportExport::Project::ExportedRelationsMerger.new(
+ export_job: export_job,
+ shared: shared)
+ end
+
+ def cleanup
+ FileUtils.rm_rf(shared.export_path) if File.exist?(shared.export_path)
+ FileUtils.rm_rf(shared.archive_path) if File.exist?(shared.archive_path)
+ end
+
+ def log_info(message)
+ logger.info(
+ message: message,
+ **log_base_data
+ )
+ end
+
+ def notify_error
+ logger.error(
+ message: 'Parallel project export error',
+ export_errors: shared.errors.join(', '),
+ export_job_id: export_job.id,
+ **log_base_data
+ )
+
+ NotificationService.new.project_not_exported(project, current_user, shared.errors)
+ end
+
+ def log_base_data
+ {
+ project_id: project.id,
+ project_name: project.name,
+ project_path: project.full_path
+ }
+ end
+ end
+ end
+end
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index 6a36f85daa4..fa718a9c907 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -2,7 +2,7 @@
- ref = local_assigns.fetch(:ref, @ref)
- form_path = local_assigns.fetch(:form_path, switch_project_refs_path(@project))
-- dropdown_toggle_text = @id || @project.default_branch
+- dropdown_toggle_text = ref || @project.default_branch
- field_name = local_assigns.fetch(:field_name, 'ref')
= form_tag form_path, method: :get, class: "project-refs-form" do
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 1b1b68320bc..8cce80e2771 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -3009,6 +3009,15 @@
:weight: 1
:idempotent: false
:tags: []
+- :name: projects_import_export_parallel_project_export
+ :worker_name: Projects::ImportExport::ParallelProjectExportWorker
+ :feature_category: :importers
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :memory
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: projects_import_export_relation_export
:worker_name: Projects::ImportExport::RelationExportWorker
:feature_category: :importers
diff --git a/app/workers/projects/import_export/parallel_project_export_worker.rb b/app/workers/projects/import_export/parallel_project_export_worker.rb
new file mode 100644
index 00000000000..ba4194fd4bc
--- /dev/null
+++ b/app/workers/projects/import_export/parallel_project_export_worker.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Projects
+ module ImportExport
+ class ParallelProjectExportWorker
+ include ApplicationWorker
+ include ExceptionBacktrace
+
+ idempotent!
+ data_consistency :always
+ deduplicate :until_executed
+ feature_category :importers
+ worker_resource_boundary :memory
+ urgency :low
+ loggable_arguments 1, 2
+ sidekiq_options retries: 3, dead: false, status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION
+
+ sidekiq_retries_exhausted do |job, exception|
+ export_job = ProjectExportJob.find(job['args'].first)
+
+ export_job.fail_op!
+ project = export_job.project
+
+ log_payload = {
+ message: 'Parallel project export error',
+ export_error: job['error_message'],
+ project_export_job_id: export_job.id,
+ project_name: project.name,
+ project_id: project.id
+ }
+ Gitlab::ExceptionLogFormatter.format!(exception, log_payload)
+ Gitlab::Export::Logger.error(log_payload)
+ end
+
+ def perform(project_export_job_id, user_id, after_export_strategy = {})
+ export_job = ProjectExportJob.find(project_export_job_id)
+
+ return if export_job.finished?
+
+ export_job.update_attribute(:jid, jid)
+ current_user = User.find(user_id)
+ after_export = build!(after_export_strategy)
+
+ export_service = ::Projects::ImportExport::ParallelExportService.new(export_job, current_user, after_export)
+ export_service.execute
+
+ export_job.finish!
+ rescue Gitlab::ImportExport::AfterExportStrategyBuilder::StrategyNotFoundError
+ export_job.fail_op!
+ end
+
+ private
+
+ def build!(after_export_strategy)
+ strategy_klass = after_export_strategy&.delete('klass')
+
+ Gitlab::ImportExport::AfterExportStrategyBuilder.build!(strategy_klass, after_export_strategy)
+ end
+ end
+ end
+end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 2862d13620a..46305512a7a 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -405,6 +405,8 @@
- 1
- - projects_git_garbage_collect
- 1
+- - projects_import_export_parallel_project_export
+ - 1
- - projects_import_export_relation_export
- 1
- - projects_inactive_projects_deletion_notification
diff --git a/doc/api/bulk_imports.md b/doc/api/bulk_imports.md
index 1e0096a6bdd..a438bc13818 100644
--- a/doc/api/bulk_imports.md
+++ b/doc/api/bulk_imports.md
@@ -29,7 +29,7 @@ POST /bulk_imports
| `entities[source_full_path]` | String | yes | Source full path of the entity to import. |
| `entities[destination_name]` | String | yes | Deprecated: Use :destination_slug instead. Destination slug for the entity. |
| `entities[destination_slug]` | String | yes | Destination slug for the entity. |
-| `entities[destination_namespace]` | String | no | Destination namespace for the entity. |
+| `entities[destination_namespace]` | String | yes | Destination namespace for the entity. |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/bulk_imports" \
diff --git a/doc/user/packages/yarn_repository/index.md b/doc/user/packages/yarn_repository/index.md
new file mode 100644
index 00000000000..7e2f45019cd
--- /dev/null
+++ b/doc/user/packages/yarn_repository/index.md
@@ -0,0 +1,248 @@
+---
+stage: Package
+group: Package Registry
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
+---
+
+# Publish packages with Yarn
+
+Publish npm packages in your project's Package Registry using Yarn. Then install the
+packages whenever you need to use them as a dependency.
+
+Learn how to build a [yarn](../workflows/build_packages.md#yarn) package.
+
+You can get started with Yarn 2 by following the [Yarn documentation](https://yarnpkg.com/getting-started/install/).
+
+## Publish to GitLab Package Registry
+
+### Authentication to the Package Registry
+
+You need a token to publish a package. Different tokens are available depending on what you're trying to
+achieve. For more information, review the [guidance on tokens](../../../user/packages/package_registry/index.md#authenticate-with-the-registry).
+
+- If your organization uses two-factor authentication (2FA), you must use a personal access token with the scope set to `api`.
+- If you publish a package via CI/CD pipelines, you must use a CI job token.
+
+Create a token and save it to use later in the process.
+
+### Naming convention
+
+Depending on how you install the package, you may need to adhere to the naming convention.
+
+You can use one of two API endpoints to install packages:
+
+- **Instance-level**: Use when you have many npm packages in different GitLab groups or in their own namespace.
+- **Project-level**: Use when you have a few npm packages, and they are not in the same GitLab group.
+
+If you plan to install a package through the [project level](#install-from-the-project-level), you do not have to
+adhere to the naming convention.
+
+If you plan to install a package through the [instance level](#install-from-the-instance-level), then you must name
+your package with a [scope](https://docs.npmjs.com/misc/scope/). Scoped packages begin with a `@` and have the
+`@owner/package-name` format. You can set up the scope for your package in the `.yarnrc.yml` file and by using the
+`publishConfig` option in the `package.json`.
+
+- The value used for the `@scope` is the root of the project that hosts the packages and not the root
+ of the project with the package's source code. The scope should be lowercase.
+- The package name can be anything you want
+
+| Project URL | Package Registry in | Scope | Full package name |
+| ------------------------------------------------------- | ------------------- | --------- | ---------------------- |
+| `https://gitlab.com/my-org/engineering-group/analytics` | Analytics | `@my-org` | `@my-org/package-name` |
+
+### Configuring `.yarnrc.yml` to publish from the project level
+
+To publish with the project-level npm endpoint, set the following configuration in
+`.yarnrc.yml`:
+
+```yaml
+npmScopes:
+ foo:
+ npmRegistryServer: 'https://<your_domain>/api/v4/projects/<your_project_id>/packages/npm/'
+ npmPublishRegistry: 'https://<your_domain>/api/v4/projects/<your_project_id>/packages/npm/'
+
+npmRegistries:
+ //gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:
+ npmAlwaysAuth: true
+ npmAuthToken: '<your_token>'
+```
+
+In this configuration:
+
+- Replace `<your_domain>` with your domain name.
+- Replace `<your_project_id>` with your project's ID, which you can find on the project's home page.
+- Replace `<your_token>` with a deploy token, group access token, project access token, or personal access token.
+
+### Configuring `.yarnrc.yml` to publish from the instance level
+
+For the instance-level npm endpoint, use this Yarn 2 configuration in `.yarnrc.yml`:
+
+```yaml
+npmScopes:
+ <scope>:
+ npmRegistryServer: 'https://<your_domain>/api/v4/packages/npm/'
+
+npmRegistries:
+ //gitlab.example.com/api/v4/packages/npm/:
+ npmAlwaysAuth: true
+ npmAuthToken: '<your_token>'
+```
+
+In this configuration:
+
+- Replace `<your_domain>` with your domain name.
+- Your scope is `<scope>`, without `@`.
+- Replace `<your_token>` with a deploy token, group access token, project access token, or personal access token.
+
+### Publishing a package via the command line
+
+Publish a package:
+
+```shell
+npm publish
+```
+
+Your package should now publish to the Package Registry.
+
+### Publishing via a CI/CD pipeline
+
+In the GitLab project that houses your `yarnrc.yml`, edit or create a `.gitlab-ci.yml` file. For example:
+
+```yaml
+image: node:latest
+
+stages:
+ - deploy
+
+deploy:
+ stage: deploy
+ script:
+ - npm publish
+```
+
+Your package should now publish to the Package Registry when the pipeline runs.
+
+## Install a package
+
+If multiple packages have the same name and version, the most recently-published package is retrieved when you install a package.
+
+You can install a package from a GitLab project or instance:
+
+- **Instance-level**: Use when you have many npm packages in different GitLab groups or in their own namespace.
+- **Project-level**: Use when you have a few npm packages, and they are not in the same GitLab group.
+
+### Install from the instance level
+
+WARNING:
+You must use packages published with the scoped [naming convention](#naming-convention) when you install a package from the instance level.
+
+1. Authenticate to the Package Registry
+
+ If you install a package from a private project, you must authenticate to the Package Registry. Skip this step if the project is not private.
+
+ ```shell
+ npm config set -- //your_domain_name/api/v4/packages/npm/:_authToken=your_token
+ ```
+
+ - Replace `your_domain_name` with your domain name, for example, `gitlab.com`.
+ - Replace `your_token` with a deploy token, group access token, project access token, or personal access token.
+
+1. Set the registry
+
+ ```shell
+ npm config set @scope:registry https://your_domain_name.com/api/v4/packages/npm/
+ ```
+
+ - Replace `@scope` with the [root level group](#naming-convention) of the project you're installing to the package from.
+ - Replace `your_domain_name` with your domain name, for example, `gitlab.com`.
+ - Replace `your_token` with a deploy token, group access token, project access token, or personal access token.
+
+1. Install the package
+
+ ```shell
+ yarn add @scope/my-package
+ ```
+
+### Install from the project level
+
+1. Authenticate to the Package Registry
+
+ If you install a package from a private project, you must authenticate to the Package Registry. Skip this step if the project is not private.
+
+ ```shell
+ npm config set -- //your_domain_name/api/v4/projects/your_project_id/packages/npm/:_authToken=your_token
+ ```
+
+ - Replace `your_domain_name` with your domain name, for example, `gitlab.com`.
+ - Replace `your_project_id` is your project ID, found on the project's home page.
+ - Replace `your_token` with a deploy token, group access token, project access token, or personal access token.
+
+1. Set the registry
+
+ ```shell
+ npm config set @scope:registry=https://your_domain_name/api/v4/projects/your_project_id/packages/npm/
+ ```
+
+ - Replace `@scope` with the [root level group](#naming-convention) of the project you're installing to the package from.
+ - Replace `your_domain_name` with your domain name, for example, `gitlab.com`.
+ - Replace `your_project_id` is your project ID, found on the project's home page.
+
+1. Install the package
+
+ ```shell
+ yarn add @scope/my-package
+ ```
+
+## Helpful hints
+
+For full helpful hints information, refer to the [npm documentation](../npm_registry/index.md#helpful-hints).
+
+### Supported CLI commands
+
+The GitLab npm repository supports the following commands for the npm CLI (`npm`) and yarn CLI
+(`yarn`):
+
+- `npm install`: Install npm packages.
+- `npm publish`: Publish an npm package to the registry.
+- `npm dist-tag add`: Add a dist-tag to an npm package.
+- `npm dist-tag ls`: List dist-tags for a package.
+- `npm dist-tag rm`: Delete a dist-tag.
+- `npm ci`: Install npm packages directly from your `package-lock.json` file.
+- `npm view`: Show package metadata.
+- `yarn add`: Install an npm package.
+- `yarn update`: Update your dependencies.
+
+## Troubleshooting
+
+For full troubleshooting information, refer to the [npm documentation](../npm_registry/index.md#troubleshooting).
+
+### Error running Yarn with the Package Registry for the npm registry
+
+If you are using [Yarn](https://classic.yarnpkg.com/en/) with the npm registry, you may get
+an error message like:
+
+```shell
+yarn install v1.15.2
+warning package.json: No license field
+info No lockfile found.
+warning XXX: No license field
+[1/4] 🔍 Resolving packages...
+[2/4] 🚚 Fetching packages...
+error An unexpected error occurred: "https://gitlab.example.com/api/v4/projects/XXX/packages/npm/XXX/XXX/-/XXX/XXX-X.X.X.tgz: Request failed \"404 Not Found\"".
+info If you think this is a bug, please open a bug report with the information provided in "/Users/XXX/gitlab-migration/module-util/yarn-error.log".
+info Visit https://classic.yarnpkg.com/en/docs/cli/install for documentation about this command
+```
+
+In this case, try adding this to your `.npmrc` file (and replace `<your_token>`
+with your personal access token or deploy token):
+
+```plaintext
+//gitlab.example.com/api/v4/projects/:_authToken=<your_token>
+```
+
+You can also use `yarn config` instead of `npm config` when setting your auth-token dynamically:
+
+```shell
+yarn config set '//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "<your_token>"
+yarn config set '//gitlab.example.com/api/v4/packages/npm/:_authToken' "<your_token>"
+```
diff --git a/doc/user/profile/notifications.md b/doc/user/profile/notifications.md
index 1deb4842107..d0a420a4bbd 100644
--- a/doc/user/profile/notifications.md
+++ b/doc/user/profile/notifications.md
@@ -76,7 +76,7 @@ For each project and group you can select one of the following levels:
| Participate | Receive notifications for threads you have participated in. |
| On mention | Receive notifications when you are [mentioned](../discussions/index.md#mentions) in a comment. |
| Disabled | Receive no notifications. |
-| Custom | Receive notifications for selected events. |
+| Custom | Receive notifications for selected events and threads you have participated in. |
### Global notification settings
diff --git a/doc/user/project/deploy_tokens/index.md b/doc/user/project/deploy_tokens/index.md
index aab72d4859e..cfe7a956bc3 100644
--- a/doc/user/project/deploy_tokens/index.md
+++ b/doc/user/project/deploy_tokens/index.md
@@ -214,19 +214,3 @@ Prerequisites:
- A deploy token with `read_registry` and `write_registry` scopes.
Follow the dependency proxy [authentication instructions](../../packages/dependency_proxy/index.md).
-
-## Troubleshooting
-
-### Error: `api error: Repository or object not found:`
-
-When using a group deploy token to clone from LFS objects, you might get `404 Not Found` responses
-and this error message. This occurs because of a bug, documented in
-[issue 235398](https://gitlab.com/gitlab-org/gitlab/-/issues/235398).
-
-```plaintext
-api error: Repository or object not found:
-https://<URL-with-token>.git/info/lfs/objects/batch
-Check that it exists and that you have proper access to it
-```
-
-The workaround is to use a project deploy token.
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 607cce02400..5c61b83fed7 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -22032,6 +22032,9 @@ msgstr ""
msgid "Integrations|Enable comments"
msgstr ""
+msgid "Integrations|Enable slash commands and notifications for a Slack workspace."
+msgstr ""
+
msgid "Integrations|Ensure your instance URL is correct and your instance is configured correctly. %{linkStart}Learn more%{linkEnd}."
msgstr ""
diff --git a/spec/commands/sidekiq_cluster/cli_spec.rb b/spec/commands/sidekiq_cluster/cli_spec.rb
index 4d1a07a6a75..0b73a62e1e0 100644
--- a/spec/commands/sidekiq_cluster/cli_spec.rb
+++ b/spec/commands/sidekiq_cluster/cli_spec.rb
@@ -245,9 +245,9 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo
it 'expands multiple queue groups correctly' do
expected_workers =
if Gitlab.ee?
- [%w[chat_notification], %w[project_export projects_import_export_relation_export project_template_export]]
+ [%w[chat_notification], %w[project_export projects_import_export_parallel_project_export projects_import_export_relation_export project_template_export]]
else
- [%w[chat_notification], %w[project_export projects_import_export_relation_export]]
+ [%w[chat_notification], %w[project_export projects_import_export_parallel_project_export projects_import_export_relation_export]]
end
expect(Gitlab::SidekiqCluster)
diff --git a/spec/controllers/explore/projects_controller_spec.rb b/spec/controllers/explore/projects_controller_spec.rb
index 5c977439af4..a79d9fa1276 100644
--- a/spec/controllers/explore/projects_controller_spec.rb
+++ b/spec/controllers/explore/projects_controller_spec.rb
@@ -101,6 +101,7 @@ RSpec.describe Explore::ProjectsController do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
context 'when topic exists' do
before do
create(:topic, name: 'topic1')
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 383e8112315..0afd2e10ea2 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -757,6 +757,7 @@ RSpec.describe Projects::NotesController do
expect { put :update, params: request_params }.to change { note.reload.note }
end
end
+
context "doesnt update the note" do
let(:issue) { create(:issue, :confidential, project: project) }
let(:note) { create(:note, noteable: issue, project: project) }
diff --git a/spec/factories/project_export_jobs.rb b/spec/factories/project_export_jobs.rb
index b2666555ea8..bf8cfd863ec 100644
--- a/spec/factories/project_export_jobs.rb
+++ b/spec/factories/project_export_jobs.rb
@@ -4,5 +4,21 @@ FactoryBot.define do
factory :project_export_job do
project
jid { SecureRandom.hex(8) }
+
+ trait :queued do
+ status { ProjectExportJob::STATUS[:queued] }
+ end
+
+ trait :started do
+ status { ProjectExportJob::STATUS[:started] }
+ end
+
+ trait :finished do
+ status { ProjectExportJob::STATUS[:finished] }
+ end
+
+ trait :failed do
+ status { ProjectExportJob::STATUS[:failed] }
+ end
end
end
diff --git a/spec/factories/projects/ci_feature_usages.rb b/spec/factories/projects/ci_feature_usages.rb
index 1ab1d82ef4b..48e5331afcc 100644
--- a/spec/factories/projects/ci_feature_usages.rb
+++ b/spec/factories/projects/ci_feature_usages.rb
@@ -4,6 +4,7 @@ FactoryBot.define do
factory :project_ci_feature_usage, class: 'Projects::CiFeatureUsage' do
project factory: :project
feature { :code_coverage } # rubocop: disable RSpec/EmptyExampleGroup
+
default_branch { false }
end
end
diff --git a/spec/features/security/group/internal_access_spec.rb b/spec/features/security/group/internal_access_spec.rb
index 904431b4a0f..ad2df4a1882 100644
--- a/spec/features/security/group/internal_access_spec.rb
+++ b/spec/features/security/group/internal_access_spec.rb
@@ -27,9 +27,11 @@ RSpec.describe 'Internal Group access', feature_category: :permissions do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_allowed_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -47,9 +49,11 @@ RSpec.describe 'Internal Group access', feature_category: :permissions do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_allowed_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -69,9 +73,11 @@ RSpec.describe 'Internal Group access', feature_category: :permissions do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_allowed_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -89,9 +95,11 @@ RSpec.describe 'Internal Group access', feature_category: :permissions do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_allowed_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -109,9 +117,11 @@ RSpec.describe 'Internal Group access', feature_category: :permissions do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_denied_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_denied_for(:maintainer).of(group) }
it { is_expected.to be_denied_for(:developer).of(group) }
diff --git a/spec/features/security/group/private_access_spec.rb b/spec/features/security/group/private_access_spec.rb
index 3d56468a1c9..2e7b7512b45 100644
--- a/spec/features/security/group/private_access_spec.rb
+++ b/spec/features/security/group/private_access_spec.rb
@@ -27,9 +27,11 @@ RSpec.describe 'Private Group access', feature_category: :permissions do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_denied_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -47,9 +49,11 @@ RSpec.describe 'Private Group access', feature_category: :permissions do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_denied_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -69,9 +73,11 @@ RSpec.describe 'Private Group access', feature_category: :permissions do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_denied_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -89,9 +95,11 @@ RSpec.describe 'Private Group access', feature_category: :permissions do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_denied_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -109,9 +117,11 @@ RSpec.describe 'Private Group access', feature_category: :permissions do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_denied_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_denied_for(:maintainer).of(group) }
it { is_expected.to be_denied_for(:developer).of(group) }
@@ -135,9 +145,11 @@ RSpec.describe 'Private Group access', feature_category: :permissions do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_denied_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
diff --git a/spec/features/security/group/public_access_spec.rb b/spec/features/security/group/public_access_spec.rb
index ac6b8a8ddd1..513c5710c8f 100644
--- a/spec/features/security/group/public_access_spec.rb
+++ b/spec/features/security/group/public_access_spec.rb
@@ -27,9 +27,11 @@ RSpec.describe 'Public Group access', feature_category: :permissions do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_allowed_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -47,9 +49,11 @@ RSpec.describe 'Public Group access', feature_category: :permissions do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_allowed_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -69,9 +73,11 @@ RSpec.describe 'Public Group access', feature_category: :permissions do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_allowed_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -89,9 +95,11 @@ RSpec.describe 'Public Group access', feature_category: :permissions do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_allowed_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -109,9 +117,11 @@ RSpec.describe 'Public Group access', feature_category: :permissions do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_denied_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_denied_for(:maintainer).of(group) }
it { is_expected.to be_denied_for(:developer).of(group) }
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index fe652e905cc..dac0d3fe182 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -80,6 +80,7 @@ RSpec.describe BlobHelper do
end
end
end
+
context 'viewer related' do
include FakeBlobHelpers
diff --git a/spec/helpers/git_helper_spec.rb b/spec/helpers/git_helper_spec.rb
index 0dd9eecb7f0..543b9ce7a82 100644
--- a/spec/helpers/git_helper_spec.rb
+++ b/spec/helpers/git_helper_spec.rb
@@ -15,11 +15,13 @@ RSpec.describe GitHelper do
it { expect(strip_signature).to eq("Version 1.69.0\n\n") }
end
+
context 'strips PGP MESSAGE' do
let(:strip_signature) { helper.strip_signature( pgp_message_tag ) }
it { expect(strip_signature).to eq("Version 1.69.0\n\n") }
end
+
context 'strips SIGNED MESSAGE' do
let(:strip_signature) { helper.strip_signature( x509_message_tag ) }
diff --git a/spec/initializers/lograge_spec.rb b/spec/initializers/lograge_spec.rb
index 0a794e8ebcd..5be99be61ae 100644
--- a/spec/initializers/lograge_spec.rb
+++ b/spec/initializers/lograge_spec.rb
@@ -96,7 +96,7 @@ RSpec.describe 'lograge', type: :request do
skip_memory_instrumentation!
end
- it 'logs memory usage metrics' do
+ it 'logs memory usage metrics', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/384081' do
expect(Lograge.formatter).to receive(:call)
.with(a_hash_including(:mem_objects))
.and_call_original
diff --git a/spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb
index 97fcddefd42..19e3a1fecc3 100644
--- a/spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb
@@ -89,6 +89,7 @@ RSpec.describe BulkImports::Projects::Pipelines::IssuesPipeline do
expect(award_emoji.user).to eq(user)
end
end
+
context 'issue state' do
let(:issue_attributes) { { 'state' => 'closed' } }
diff --git a/spec/lib/gitlab/blob_helper_spec.rb b/spec/lib/gitlab/blob_helper_spec.rb
index a2f20dcd4fc..e18277ba8d2 100644
--- a/spec/lib/gitlab/blob_helper_spec.rb
+++ b/spec/lib/gitlab/blob_helper_spec.rb
@@ -68,6 +68,7 @@ RSpec.describe Gitlab::BlobHelper do
expect(blob.image?).to be_falsey
end
end
+
context 'with a .webp file' do
it 'returns true' do
expect(webp_blob.image?).to be_truthy
diff --git a/spec/lib/gitlab/file_type_detection_spec.rb b/spec/lib/gitlab/file_type_detection_spec.rb
index c435d3f6097..1be0f7d53fa 100644
--- a/spec/lib/gitlab/file_type_detection_spec.rb
+++ b/spec/lib/gitlab/file_type_detection_spec.rb
@@ -31,6 +31,7 @@ RSpec.describe Gitlab::FileTypeDetection do
expect(described_class.extension_match?('my/file.foo', extensions)).to eq(true)
end
end
+
context 'when class is an uploader' do
let(:uploader) do
example_uploader = Class.new(CarrierWave::Uploader::Base) do
diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb
index 38607ce2752..f3df96fd38a 100644
--- a/spec/lib/gitlab/instrumentation_helper_spec.rb
+++ b/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -129,7 +129,7 @@ RSpec.describe Gitlab::InstrumentationHelper do
skip_memory_instrumentation!
end
- it 'logs memory usage metrics' do
+ it 'logs memory usage metrics', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/384081' do
subject
expect(payload).to include(
diff --git a/spec/lib/gitlab/memory/instrumentation_spec.rb b/spec/lib/gitlab/memory/instrumentation_spec.rb
index 069c45da18a..6489cda3978 100644
--- a/spec/lib/gitlab/memory/instrumentation_spec.rb
+++ b/spec/lib/gitlab/memory/instrumentation_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Memory::Instrumentation do
end
describe '.available?' do
- it 'returns true' do
+ it 'returns true', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/384081' do
expect(described_class).to be_available
end
end
@@ -18,7 +18,7 @@ RSpec.describe Gitlab::Memory::Instrumentation do
describe '.start_thread_memory_allocations' do
subject { described_class.start_thread_memory_allocations }
- it 'a hash is returned' do
+ it 'a hash is returned', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/384081' do
is_expected.to be_a(Hash)
end
@@ -47,7 +47,7 @@ RSpec.describe Gitlab::Memory::Instrumentation do
expect(described_class).to receive(:measure_thread_memory_allocations).and_call_original
end
- it 'a hash is returned' do
+ it 'a hash is returned', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/384081' do
result = subject
expect(result).to include(
mem_objects: be > 1000,
diff --git a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb
index 8c9a1abba5a..a2b79f7412e 100644
--- a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb
+++ b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb
@@ -452,6 +452,7 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do
expect(subject).to eq("current_rss(#{rss}) > soft_limit_rss(#{soft_limit}) longer than GRACE_BALLOON_SECONDS(#{grace_balloon_seconds})")
end
end
+
context 'deadline not exceeded' do
let(:deadline_exceeded) { false }
diff --git a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
index e0ebb86585a..2df86804f34 100644
--- a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
+++ b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
@@ -237,6 +237,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
expect(subject.set_token(instance, 'my-value')).to eq 'my-value'
end
end
+
context 'when encryption is optional' do
let(:options) { { encrypted: :optional } }
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 7c71080d63e..328d3ba7dda 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -579,6 +579,7 @@ RSpec.describe Note do
expect(commit_note.confidential?).to be_falsy
end
end
+
context 'when note is confidential' do
it 'is true even when a noteable is not confidential' do
issue = create(:issue, confidential: false)
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index dae0f84eda3..fb6aaffdf22 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -291,6 +291,7 @@ RSpec.describe ProjectFeature do
end
end
end
+
# rubocop:disable Gitlab/FeatureAvailableUsage
describe '#feature_available?' do
let(:features) { ProjectFeature::FEATURES }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 021083dc918..95014641abb 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -3217,6 +3217,7 @@ RSpec.describe User do
expect(described_class.find_by_full_path('unknown')).to eq(nil)
end
end
+
context 'with the follow_redirects option set to true' do
it 'returns nil' do
expect(described_class.find_by_full_path('unknown', follow_redirects: true)).to eq(nil)
diff --git a/spec/models/zoom_meeting_spec.rb b/spec/models/zoom_meeting_spec.rb
index 2b45533035d..d3d75a19fed 100644
--- a/spec/models/zoom_meeting_spec.rb
+++ b/spec/models/zoom_meeting_spec.rb
@@ -29,6 +29,7 @@ RSpec.describe ZoomMeeting do
expect(meetings_added).not_to include(removed_meeting.id)
end
end
+
describe '.removed_from_issue' do
it 'gets only removed meetings' do
meetings_removed = described_class.removed_from_issue.pluck(:id)
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 3831e8e1dfe..d8a0af4851b 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -4687,6 +4687,7 @@ RSpec.describe API::Projects do
end
end
end
+
describe 'PUT /projects/:id/transfer' do
context 'when authenticated as owner' do
let(:group) { create :group }
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 42196a7d8af..4448abe1638 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -301,6 +301,7 @@ RSpec.describe 'project routing' do
expect(get('/gitlab/gitlabhq/-/merge_requests/1/conflicts')).to route_to('projects/merge_requests/conflicts#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
end
end
+
# raw_project_snippet GET /:project_id/snippets/:id/raw(.:format) snippets#raw
# project_snippets GET /:project_id/snippets(.:format) snippets#index
# new_project_snippet GET /:project_id/snippets/new(.:format) snippets#new
diff --git a/spec/services/projects/import_export/parallel_export_service_spec.rb b/spec/services/projects/import_export/parallel_export_service_spec.rb
new file mode 100644
index 00000000000..b9f2867077c
--- /dev/null
+++ b/spec/services/projects/import_export/parallel_export_service_spec.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::ImportExport::ParallelExportService, feature_category: :importers do
+ let_it_be(:user) { create(:user) }
+
+ let(:export_job) { create(:project_export_job) }
+ let(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new }
+ let(:project) { export_job.project }
+
+ before do
+ allow_next_instance_of(Gitlab::ImportExport::Project::ExportedRelationsMerger) do |saver|
+ allow(saver).to receive(:save).and_return(true)
+ end
+
+ allow_next_instance_of(Gitlab::ImportExport::VersionSaver) do |saver|
+ allow(saver).to receive(:save).and_return(true)
+ end
+ end
+
+ describe '#execute' do
+ subject(:service) { described_class.new(export_job, user, after_export_strategy) }
+
+ it 'creates a project export archive file' do
+ expect(Gitlab::ImportExport::Saver).to receive(:save)
+ .with(exportable: project, shared: project.import_export_shared)
+
+ service.execute
+ end
+
+ it 'logs export progress' do
+ allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(true)
+
+ logger = service.instance_variable_get(:@logger)
+ messages = [
+ 'Parallel project export started',
+ 'Parallel project export - Gitlab::ImportExport::VersionSaver saver started',
+ 'Parallel project export - Gitlab::ImportExport::Project::ExportedRelationsMerger saver started',
+ 'Parallel project export finished successfully'
+ ]
+ messages.each do |message|
+ expect(logger).to receive(:info).ordered.with(hash_including(message: message))
+ end
+
+ service.execute
+ end
+
+ it 'executes after export stragegy on export success' do
+ allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(true)
+
+ expect(after_export_strategy).to receive(:execute)
+
+ service.execute
+ end
+
+ it 'ensures files are cleaned up' do
+ shared = project.import_export_shared
+ FileUtils.mkdir_p(shared.archive_path)
+ FileUtils.mkdir_p(shared.export_path)
+
+ allow(Gitlab::ImportExport::Saver).to receive(:save).and_raise(StandardError)
+
+ expect { service.execute }.to raise_error(StandardError)
+
+ expect(File.exist?(shared.export_path)).to eq(false)
+ expect(File.exist?(shared.archive_path)).to eq(false)
+ end
+
+ context 'when export fails' do
+ it 'notifies the error to the user' do
+ allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(false)
+
+ allow(project.import_export_shared).to receive(:errors).and_return(['Error'])
+
+ expect_next_instance_of(NotificationService) do |instance|
+ expect(instance).to receive(:project_not_exported).with(project, user, ['Error'])
+ end
+
+ service.execute
+ end
+ end
+
+ context 'when after export stragegy fails' do
+ it 'notifies the error to the user' do
+ allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(true)
+ allow(after_export_strategy).to receive(:execute).and_return(false)
+ allow(project.import_export_shared).to receive(:errors).and_return(['Error'])
+
+ expect_next_instance_of(NotificationService) do |instance|
+ expect(instance).to receive(:project_not_exported).with(project, user, ['Error'])
+ end
+
+ service.execute
+ end
+ end
+ end
+end
diff --git a/spec/workers/projects/import_export/parallel_project_export_worker_spec.rb b/spec/workers/projects/import_export/parallel_project_export_worker_spec.rb
new file mode 100644
index 00000000000..d3ac0a34295
--- /dev/null
+++ b/spec/workers/projects/import_export/parallel_project_export_worker_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::ImportExport::ParallelProjectExportWorker, feature_category: :importers do
+ let_it_be(:user) { create(:user) }
+
+ let(:export_job) { create(:project_export_job, :started) }
+ let(:after_export_strategy) { {} }
+ let(:job_args) { [export_job.id, user.id, after_export_strategy] }
+
+ before do
+ allow_next_instance_of(described_class) do |job|
+ allow(job).to receive(:jid) { SecureRandom.hex(8) }
+ end
+ end
+
+ describe '#perform' do
+ it_behaves_like 'an idempotent worker' do
+ it 'sets the export job status to finished' do
+ subject
+
+ expect(export_job.reload.finished?).to eq(true)
+ end
+ end
+
+ context 'when after export strategy does not exist' do
+ let(:after_export_strategy) { { 'klass' => 'InvalidStrategy' } }
+
+ it 'sets the export job status to failed' do
+ described_class.new.perform(*job_args)
+
+ expect(export_job.reload.failed?).to eq(true)
+ end
+ end
+ end
+
+ describe '.sidekiq_retries_exhausted' do
+ let(:job) { { 'args' => job_args, 'error_message' => 'Error message' } }
+
+ it 'sets export_job status to failed' do
+ described_class.sidekiq_retries_exhausted_block.call(job)
+
+ expect(export_job.reload.failed?).to eq(true)
+ end
+
+ it 'logs an error message' do
+ expect_next_instance_of(Gitlab::Export::Logger) do |logger|
+ expect(logger).to receive(:error).with(
+ hash_including(
+ message: 'Parallel project export error',
+ export_error: 'Error message'
+ )
+ )
+ end
+
+ described_class.sidekiq_retries_exhausted_block.call(job)
+ end
+ end
+end