Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									24fb09b2eb
								
							
						
					
					
						commit
						533fed8bd8
					
				|  | @ -485,10 +485,19 @@ That's all of the required database changes. | |||
|       end | ||||
| 
 | ||||
|       trait :verification_succeeded do | ||||
|         synced | ||||
|         verification_checksum { 'e079a831cab27bcda7d81cd9b48296d0c3dd92ef' } | ||||
|         verification_state { Geo::CoolWidgetRegistry.verification_state_value(:verification_succeeded) } | ||||
|         verified_at { 5.days.ago } | ||||
|       end | ||||
| 
 | ||||
|       trait :verification_failed do | ||||
|         synced | ||||
|         verification_failure { 'Could not calculate the checksum' } | ||||
|         verification_state { Geo::CoolWidgetRegistry.verification_state_value(:verification_failed) } | ||||
|         verification_retry_count { 1 } | ||||
|         verification_retry_at { 2.hours.from_now } | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|   ``` | ||||
|  | @ -519,15 +528,15 @@ That's all of the required database changes. | |||
|   FactoryBot.modify do | ||||
|     factory :cool_widget do | ||||
|       trait :verification_succeeded do | ||||
|           repository | ||||
|           verification_checksum { 'abc' } | ||||
|           verification_state { CoolWidget.verification_state_value(:verification_succeeded) } | ||||
|         repository | ||||
|         verification_checksum { 'abc' } | ||||
|         verification_state { CoolWidget.verification_state_value(:verification_succeeded) } | ||||
|       end | ||||
| 
 | ||||
|       trait :verification_failed do | ||||
|           repository | ||||
|           verification_failure { 'Could not calculate the checksum' } | ||||
|           verification_state { CoolWidget.verification_state_value(:verification_failed) } | ||||
|         repository | ||||
|         verification_failure { 'Could not calculate the checksum' } | ||||
|         verification_state { CoolWidget.verification_state_value(:verification_failed) } | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -442,10 +442,19 @@ That's all of the required database changes. | |||
|       end | ||||
| 
 | ||||
|       trait :verification_succeeded do | ||||
|         synced | ||||
|         verification_checksum { 'e079a831cab27bcda7d81cd9b48296d0c3dd92ef' } | ||||
|         verification_state { Geo::CoolWidgetRegistry.verification_state_value(:verification_succeeded) } | ||||
|         verified_at { 5.days.ago } | ||||
|       end | ||||
| 
 | ||||
|       trait :verification_failed do | ||||
|         synced | ||||
|         verification_failure { 'Could not calculate the checksum' } | ||||
|         verification_state { Geo::CoolWidgetRegistry.verification_state_value(:verification_failed) } | ||||
|         verification_retry_count { 1 } | ||||
|         verification_retry_at { 2.hours.from_now } | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|   ``` | ||||
|  | @ -468,7 +477,7 @@ That's all of the required database changes. | |||
|   end | ||||
|   ``` | ||||
| 
 | ||||
| - [ ] Add the following to `spec/factories/cool_widgets.rb`: | ||||
| - [ ] Add the following to `ee/spec/factories/cool_widgets.rb`: | ||||
| 
 | ||||
|   ```ruby | ||||
|   # frozen_string_literal: true | ||||
|  | @ -476,15 +485,24 @@ That's all of the required database changes. | |||
|   FactoryBot.modify do | ||||
|     factory :cool_widget do | ||||
|       trait :verification_succeeded do | ||||
|           with_file | ||||
|           verification_checksum { 'abc' } | ||||
|           verification_state { CoolWidget.verification_state_value(:verification_succeeded) } | ||||
|         with_file | ||||
|         verification_checksum { 'abc' } | ||||
|         verification_state { CoolWidget.verification_state_value(:verification_succeeded) } | ||||
|       end | ||||
| 
 | ||||
|       trait :verification_failed do | ||||
|           with_file | ||||
|           verification_failure { 'Could not calculate the checksum' } | ||||
|           verification_state { CoolWidget.verification_state_value(:verification_failed) } | ||||
|         with_file | ||||
|         verification_failure { 'Could not calculate the checksum' } | ||||
|         verification_state { CoolWidget.verification_state_value(:verification_failed) } | ||||
| 
 | ||||
|         # | ||||
|         # Geo::VerifiableReplicator#after_verifiable_update tries to verify | ||||
|         # the replicable async and marks it as verification started when the | ||||
|         # model record is created/updated. | ||||
|         # | ||||
|         after(:create) do |instance, _| | ||||
|           instance.verification_failed! | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -217,7 +217,6 @@ Gitlab/StrongMemoizeAttr: | |||
|     - 'app/services/quick_actions/interpret_service.rb' | ||||
|     - 'app/services/releases/base_service.rb' | ||||
|     - 'app/services/resource_access_tokens/revoke_service.rb' | ||||
|     - 'app/services/resource_events/base_synthetic_notes_builder_service.rb' | ||||
|     - 'app/services/search/global_service.rb' | ||||
|     - 'app/services/search/project_service.rb' | ||||
|     - 'app/services/search_service.rb' | ||||
|  |  | |||
|  | @ -2573,7 +2573,6 @@ RSpec/FeatureCategory: | |||
|     - 'spec/lib/api/entities/application_setting_spec.rb' | ||||
|     - 'spec/lib/api/entities/branch_spec.rb' | ||||
|     - 'spec/lib/api/entities/bulk_import_spec.rb' | ||||
|     - 'spec/lib/api/entities/bulk_imports/entity_failure_spec.rb' | ||||
|     - 'spec/lib/api/entities/bulk_imports/entity_spec.rb' | ||||
|     - 'spec/lib/api/entities/bulk_imports/export_status_spec.rb' | ||||
|     - 'spec/lib/api/entities/changelog_spec.rb' | ||||
|  | @ -2695,9 +2694,7 @@ RSpec/FeatureCategory: | |||
|     - 'spec/lib/bulk_imports/projects/graphql/get_snippet_repository_query_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/pipelines/auto_devops_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/pipelines/ci_pipelines_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/pipelines/container_expiration_policy_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/pipelines/design_bundle_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/pipelines/external_pull_requests_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/pipelines/project_attributes_pipeline_spec.rb' | ||||
|  |  | |||
							
								
								
									
										2
									
								
								Gemfile
								
								
								
								
							
							
						
						
									
										2
									
								
								Gemfile
								
								
								
								
							|  | @ -196,7 +196,7 @@ gem 'seed-fu', '~> 2.3.7' # rubocop:todo Gemfile/MissingFeatureCategory | |||
| gem 'elasticsearch-model', '~> 7.2' # rubocop:todo Gemfile/MissingFeatureCategory | ||||
| gem 'elasticsearch-rails', '~> 7.2', require: 'elasticsearch/rails/instrumentation' # rubocop:todo Gemfile/MissingFeatureCategory | ||||
| gem 'elasticsearch-api',   '7.13.3' # rubocop:todo Gemfile/MissingFeatureCategory | ||||
| gem 'aws-sdk-core', '~> 3.185.1' # rubocop:todo Gemfile/MissingFeatureCategory | ||||
| gem 'aws-sdk-core', '~> 3.185.2' # rubocop:todo Gemfile/MissingFeatureCategory | ||||
| gem 'aws-sdk-cloudformation', '~> 1' # rubocop:todo Gemfile/MissingFeatureCategory | ||||
| gem 'aws-sdk-s3', '~> 1.136.0' # rubocop:todo Gemfile/MissingFeatureCategory | ||||
| gem 'faraday_middleware-aws-sigv4', '~>0.3.0' # rubocop:todo Gemfile/MissingFeatureCategory | ||||
|  |  | |||
|  | @ -36,7 +36,7 @@ | |||
| {"name":"aws-eventstream","version":"1.2.0","platform":"ruby","checksum":"ffa53482c92880b001ff2fb06919b9bb82fd847cbb0fa244985d2ebb6dd0d1df"}, | ||||
| {"name":"aws-partitions","version":"1.761.0","platform":"ruby","checksum":"291e444e1edfc92c5521a6dbdd1236ccc3f122b3520163b2be6ec5b6ef350ef2"}, | ||||
| {"name":"aws-sdk-cloudformation","version":"1.41.0","platform":"ruby","checksum":"31e47539719734413671edf9b1a31f8673fbf9688549f50c41affabbcb1c6b26"}, | ||||
| {"name":"aws-sdk-core","version":"3.185.1","platform":"ruby","checksum":"572ada4eaf8393a9999d9a50adc2dcb78cc742c26a5727248c27f02cdaf97973"}, | ||||
| {"name":"aws-sdk-core","version":"3.185.2","platform":"ruby","checksum":"75878c00df67750de85537cc851b1281770f2270392de73b9dedcecba314b0ce"}, | ||||
| {"name":"aws-sdk-kms","version":"1.64.0","platform":"ruby","checksum":"40de596c95047bfc6e1aacea24f3df6241aa716b6f7ce08ac4c5f7e3120395ad"}, | ||||
| {"name":"aws-sdk-s3","version":"1.136.0","platform":"ruby","checksum":"3547302a85d51de6cc75b48fb37d328f65f6526e7fc73a27a5b1b871f99a8d63"}, | ||||
| {"name":"aws-sigv4","version":"1.6.0","platform":"ruby","checksum":"ca9e6a15cd424f1f32b524b9760995331459bc22e67d3daad4fcf0c0084b087d"}, | ||||
|  |  | |||
|  | @ -270,7 +270,7 @@ GEM | |||
|     aws-sdk-cloudformation (1.41.0) | ||||
|       aws-sdk-core (~> 3, >= 3.99.0) | ||||
|       aws-sigv4 (~> 1.1) | ||||
|     aws-sdk-core (3.185.1) | ||||
|     aws-sdk-core (3.185.2) | ||||
|       aws-eventstream (~> 1, >= 1.0.2) | ||||
|       aws-partitions (~> 1, >= 1.651.0) | ||||
|       aws-sigv4 (~> 1.5) | ||||
|  | @ -1749,7 +1749,7 @@ DEPENDENCIES | |||
|   autoprefixer-rails (= 10.2.5.1) | ||||
|   awesome_print | ||||
|   aws-sdk-cloudformation (~> 1) | ||||
|   aws-sdk-core (~> 3.185.1) | ||||
|   aws-sdk-core (~> 3.185.2) | ||||
|   aws-sdk-s3 (~> 1.136.0) | ||||
|   axe-core-rspec | ||||
|   babosa (~> 2.0) | ||||
|  |  | |||
|  | @ -371,7 +371,6 @@ export default { | |||
|         :label-text="$options.i18n.key" | ||||
|         class="gl-border-none gl-pb-0! gl-mb-n5" | ||||
|         data-testid="ci-variable-key" | ||||
|         data-qa-selector="ci_variable_key_field" | ||||
|       /> | ||||
|       <gl-form-group | ||||
|         :label="$options.i18n.value" | ||||
|  | @ -388,7 +387,6 @@ export default { | |||
|           rows="3" | ||||
|           max-rows="10" | ||||
|           data-testid="ci-variable-value" | ||||
|           data-qa-selector="ci_variable_value_field" | ||||
|           spellcheck="false" | ||||
|         /> | ||||
|         <p | ||||
|  | @ -419,15 +417,14 @@ export default { | |||
|           variant="danger" | ||||
|           category="secondary" | ||||
|           class="gl-mr-3" | ||||
|           data-testid="ci-variable-delete-btn" | ||||
|           data-testid="ci-variable-delete-button" | ||||
|           >{{ $options.i18n.deleteVariable }}</gl-button | ||||
|         > | ||||
|         <gl-button | ||||
|           category="primary" | ||||
|           variant="confirm" | ||||
|           :disabled="!canSubmit" | ||||
|           data-testid="ci-variable-confirm-btn" | ||||
|           data-qa-selector="ci_variable_save_button" | ||||
|           data-testid="ci-variable-confirm-button" | ||||
|           @click="submit" | ||||
|           >{{ modalActionText }} | ||||
|         </gl-button> | ||||
|  |  | |||
|  | @ -243,7 +243,6 @@ export default { | |||
|           <gl-button | ||||
|             size="small" | ||||
|             :disabled="exceedsVariableLimit" | ||||
|             data-qa-selector="add_ci_variable_button" | ||||
|             data-testid="add-ci-variable-button" | ||||
|             @click="setSelectedVariable()" | ||||
|             >{{ $options.i18n.addButton }}</gl-button | ||||
|  | @ -376,7 +375,7 @@ export default { | |||
|               size="small" | ||||
|               class="gl-mr-3" | ||||
|               :aria-label="$options.i18n.editButton" | ||||
|               data-qa-selector="edit_ci_variable_button" | ||||
|               data-testid="edit-ci-variable-button" | ||||
|               @click="setSelectedVariable(item.index)" | ||||
|             /> | ||||
|             <gl-button | ||||
|  |  | |||
|  | @ -184,8 +184,8 @@ module Integrations | |||
|       options | ||||
|     end | ||||
| 
 | ||||
|     def client | ||||
|       @client ||= JIRA::Client.new(options).tap do |client| | ||||
|     def client(additional_options = {}) | ||||
|       JIRA::Client.new(options.merge(additional_options)).tap do |client| | ||||
|         # Replaces JIRA default http client with our implementation | ||||
|         client.request_client = Gitlab::Jira::HttpClient.new(client.options) | ||||
|       end | ||||
|  |  | |||
|  | @ -112,7 +112,7 @@ class ResourceLabelEvent < ResourceEvent | |||
|   end | ||||
| 
 | ||||
|   def resource_parent | ||||
|     issuable.project || issuable.group | ||||
|     issuable.try(:resource_parent) || issuable.project || issuable.group | ||||
|   end | ||||
| 
 | ||||
|   def discussion_id_key | ||||
|  |  | |||
|  | @ -44,10 +44,9 @@ module ResourceEvents | |||
|     end | ||||
| 
 | ||||
|     def resource_parent | ||||
|       strong_memoize(:resource_parent) do | ||||
|         resource.project || resource.group | ||||
|       end | ||||
|       resource.try(:resource_parent) || resource.project || resource.group | ||||
|     end | ||||
|     strong_memoize_attr :resource_parent | ||||
| 
 | ||||
|     def table_name | ||||
|       raise NotImplementedError | ||||
|  |  | |||
|  | @ -37,4 +37,4 @@ module ResourceEvents | |||
|   end | ||||
| end | ||||
| 
 | ||||
| ResourceEvents::MergeIntoNotesService.prepend_mod_with('ResourceEvents::MergeIntoNotesService') | ||||
| ResourceEvents::MergeIntoNotesService.prepend_mod | ||||
|  |  | |||
|  | @ -9,7 +9,14 @@ module Gitlab | |||
|         private | ||||
| 
 | ||||
|         def import(project) | ||||
|           jobs_waiter = Gitlab::JiraImport::IssuesImporter.new(project).execute | ||||
|           jira_client = if Feature.enabled?(:increase_jira_import_issues_timeout) | ||||
|                           project.jira_integration.client(read_timeout: 2.minutes) | ||||
|                         end | ||||
| 
 | ||||
|           jobs_waiter = Gitlab::JiraImport::IssuesImporter.new( | ||||
|             project, | ||||
|             jira_client | ||||
|           ).execute | ||||
| 
 | ||||
|           project.latest_jira_import.refresh_jid_expiration | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,8 @@ | |||
| --- | ||||
| name: increase_jira_import_issues_timeout | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135050 | ||||
| rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429293 | ||||
| milestone: '16.6' | ||||
| type: development | ||||
| group: group::project management | ||||
| default_enabled: false | ||||
|  | @ -0,0 +1,12 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class AddFieldsToBulkImportFailures < Gitlab::Database::Migration[2.2] | ||||
|   milestone '16.6' | ||||
| 
 | ||||
|   # rubocop:disable Migration/AddLimitToTextColumns | ||||
|   def change | ||||
|     add_column :bulk_import_failures, :source_url, :text | ||||
|     add_column :bulk_import_failures, :source_title, :text | ||||
|   end | ||||
|   # rubocop:enable Migration/AddLimitToTextColumns | ||||
| end | ||||
|  | @ -0,0 +1,16 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class AddTextLimitToBulkImportFailures < Gitlab::Database::Migration[2.2] | ||||
|   milestone '16.6' | ||||
|   disable_ddl_transaction! | ||||
| 
 | ||||
|   def up | ||||
|     add_text_limit :bulk_import_failures, :source_url, 255 | ||||
|     add_text_limit :bulk_import_failures, :source_title, 255 | ||||
|   end | ||||
| 
 | ||||
|   def down | ||||
|     remove_text_limit :bulk_import_failures, :source_url | ||||
|     remove_text_limit :bulk_import_failures, :source_title | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1 @@ | |||
| 6103bd075183ce4196dee2b140cb960f075cc7d3f4fc4f370bb6217c3ff1e758 | ||||
|  | @ -0,0 +1 @@ | |||
| 9627d5af229e51bee8a5a8c47beedf5bd0b3b2ce89f4cc209fe96089e662c749 | ||||
|  | @ -13118,10 +13118,14 @@ CREATE TABLE bulk_import_failures ( | |||
|     exception_message text NOT NULL, | ||||
|     correlation_id_value text, | ||||
|     pipeline_step text, | ||||
|     source_url text, | ||||
|     source_title text, | ||||
|     CONSTRAINT check_053d65c7a4 CHECK ((char_length(pipeline_class) <= 255)), | ||||
|     CONSTRAINT check_6eca8f972e CHECK ((char_length(exception_message) <= 255)), | ||||
|     CONSTRAINT check_721a422375 CHECK ((char_length(pipeline_step) <= 255)), | ||||
|     CONSTRAINT check_74414228d4 CHECK ((char_length(source_title) <= 255)), | ||||
|     CONSTRAINT check_c7dba8398e CHECK ((char_length(exception_class) <= 255)), | ||||
|     CONSTRAINT check_e035a720ad CHECK ((char_length(source_url) <= 255)), | ||||
|     CONSTRAINT check_e787285882 CHECK ((char_length(correlation_id_value) <= 255)) | ||||
| ); | ||||
| 
 | ||||
|  |  | |||
|  | @ -257,3 +257,24 @@ curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab | |||
|   "updated_at": "2021-06-18T09:46:27.003Z" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ## Get list of failed import records for group or project migration entity | ||||
| 
 | ||||
| ```plaintext | ||||
| GET /bulk_imports/:id/entities/:entity_id/failures | ||||
| ``` | ||||
| 
 | ||||
| ```shell | ||||
| curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/bulk_imports/1/entities/2/failures" | ||||
| ``` | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "relation": "issues", | ||||
|   "exception_message": "Error!", | ||||
|   "exception_class": "StandardError", | ||||
|   "correlation_id_value": "06289e4b064329a69de7bb2d7a1b5a97", | ||||
|   "source_url": "https://gitlab.example/project/full/path/-/issues/1", | ||||
|   "source_title": "Issue title" | ||||
| } | ||||
| ``` | ||||
|  |  | |||
|  | @ -29,10 +29,9 @@ To enable Continuous Vulnerability Scanning: | |||
| 
 | ||||
| - Enable the Continuous Vulnerability Scanning setting in the project's [security configuration](../configuration/index.md). | ||||
| - Enable [Dependency Scanning](../dependency_scanning/index.md#configuration) and ensure that its prerequisites are met. | ||||
| - On GitLab self-managed only, you can [choose package registry metadata to synchronize](../../../administration/settings/security_and_compliance.md#choose-package-registry-metadata-to-sync) in the Admin Area for the GitLab instance. For this data synchronization to work, you must allow outbound network traffic from your GitLab instance to the domain `storage.googleapis.com`. If you have limited or no network connectivity then please refer to the documentation section [running in an offline environment](#running-in-an-offline-environment) for further guidance. | ||||
| 
 | ||||
| On GitLab self-managed only, you can [choose package registry metadata to sync](../../../administration/settings/security_and_compliance.md#choose-package-registry-metadata-to-sync) in the Admin Area for the GitLab instance. | ||||
| 
 | ||||
| ### Requirements for offline environments | ||||
| ### Running in an offline environment | ||||
| 
 | ||||
| For self-managed GitLab instances in an environment with limited, restricted, or intermittent access to external resources through the internet, | ||||
| some adjustments are required to successfully scan CycloneDX reports for vulnerabilities. | ||||
|  |  | |||
|  | @ -259,7 +259,7 @@ A finding's primary identifier is a value that is unique to each finding. The ex | |||
| of the finding's [first identifier](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/v2.4.0-rc1/dist/sast-report-format.json#L228) | ||||
| combine to create the value. | ||||
| 
 | ||||
| Examples of primary identifiers include `PluginID` for OWASP Zed Attack Proxy (ZAP), or `CVE` for | ||||
| Examples of primary identifiers include `PluginID` for Zed Attack Proxy (ZAP), or `CVE` for | ||||
| Trivy. The identifier must be stable. Subsequent scans must return the same value for the | ||||
| same finding, even if the location has slightly changed. | ||||
| 
 | ||||
|  |  | |||
|  | @ -22,16 +22,11 @@ Licenses not in the SPDX list are reported as "Unknown". License information can | |||
| 
 | ||||
| ## Configuration | ||||
| 
 | ||||
| Prerequisites: | ||||
| To enable License scanning of CycloneDX files: | ||||
| 
 | ||||
| - On GitLab self-managed only, enable [Synchronization with the GitLab License Database](../../../administration/settings/security_and_compliance.md#choose-package-registry-metadata-to-sync) in the Admin Area for the GitLab instance. On GitLab SaaS this step has already been completed. | ||||
| - Enable [Dependency Scanning](../../application_security/dependency_scanning/index.md#enabling-the-analyzer) | ||||
|   and ensure that its prerequisites are met. | ||||
| 
 | ||||
| From the `.gitlab-ci.yml` file, remove the deprecated line `Jobs/License-Scanning.gitlab-ci.yml`, if | ||||
| it's present. | ||||
| 
 | ||||
| On GitLab self-managed only, you can [choose package registry metadata to sync](../../../administration/settings/security_and_compliance.md#choose-package-registry-metadata-to-sync) in the Admin Area for the GitLab instance. | ||||
| - On GitLab self-managed only, you can [choose package registry metadata to synchronize](../../../administration/settings/security_and_compliance.md#choose-package-registry-metadata-to-sync) in the Admin Area for the GitLab instance. For this data synchronization to work, you must allow outbound network traffic from your GitLab instance to the domain `storage.googleapis.com`. If you have limited or no network connectivity then please refer to the documentation section [running in an offline environment](#running-in-an-offline-environment) for further guidance. | ||||
| 
 | ||||
| ## Supported languages and package managers | ||||
| 
 | ||||
|  |  | |||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 18 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 8.3 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 14 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 18 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 43 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 11 KiB | 
|  | @ -192,7 +192,7 @@ To add an internal note: | |||
| 1. Below the comment, select the **Make this an internal note** checkbox. | ||||
| 1. Select **Add internal note**. | ||||
| 
 | ||||
|  | ||||
|  | ||||
| 
 | ||||
| You can also mark an [issue as confidential](../project/issues/confidential_issues.md). | ||||
| 
 | ||||
|  | @ -233,7 +233,7 @@ You can assign an issue to a user who made a comment. | |||
| 
 | ||||
| 1. In the comment, select the **More Actions** (**{ellipsis_v}**) menu. | ||||
| 1. Select **Assign to commenting user**: | ||||
|     | ||||
|     | ||||
| 1. To unassign the commenter, select the button again. | ||||
| 
 | ||||
| ## Create a thread by replying to a standard comment | ||||
|  | @ -272,9 +272,9 @@ To create a thread: | |||
| 1. From the list, select **Start thread**. | ||||
| 1. Select **Start thread** again. | ||||
| 
 | ||||
| A threaded comment is created. | ||||
|  | ||||
| 
 | ||||
|  | ||||
| A threaded comment is created. | ||||
| 
 | ||||
| ## Resolve a thread | ||||
| 
 | ||||
|  |  | |||
|  | @ -88,7 +88,8 @@ Create a deploy token to automate deployment tasks that can run independently of | |||
| 
 | ||||
| Prerequisites: | ||||
| 
 | ||||
| - You must have at least the Maintainer role for the project or group. | ||||
| - To create a group deploy token, you must have the Owner role for the group. | ||||
| - To create a project deploy token, you must have at least the Maintainer role for the project. | ||||
| 
 | ||||
| 1. On the left sidebar, select **Search or go to** and find your project or group. | ||||
| 1. Select **Settings > Repository**. | ||||
|  | @ -106,7 +107,8 @@ Revoke a token when it's no longer required. | |||
| 
 | ||||
| Prerequisites: | ||||
| 
 | ||||
| - You must have at least the Maintainer role for the project or group. | ||||
| - To revoke a group deploy token, you must have the Owner role for the group. | ||||
| - To revoke a project deploy token, you must have at least the Maintainer role for the project. | ||||
| 
 | ||||
| To revoke a deploy token: | ||||
| 
 | ||||
|  |  | |||
|  | @ -214,6 +214,23 @@ module API | |||
|       get ':import_id/entities/:entity_id' do | ||||
|         present bulk_import_entity, with: Entities::BulkImports::Entity | ||||
|       end | ||||
| 
 | ||||
|       desc 'Get GitLab Migration entity failures' do | ||||
|         detail 'This feature was introduced in GitLab 16.6' | ||||
|         success code: 200, model: Entities::BulkImports::EntityFailure | ||||
|         failure [ | ||||
|           { code: 401, message: 'Unauthorized' }, | ||||
|           { code: 404, message: 'Not found' }, | ||||
|           { code: 503, message: 'Service unavailable' } | ||||
|         ] | ||||
|       end | ||||
|       params do | ||||
|         requires :import_id, type: Integer, desc: "The ID of user's GitLab Migration" | ||||
|         requires :entity_id, type: Integer, desc: "The ID of GitLab Migration entity" | ||||
|       end | ||||
|       get ':import_id/entities/:entity_id/failures' do | ||||
|         present paginate(bulk_import_entity.failures), with: Entities::BulkImports::EntityFailure | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -4,18 +4,14 @@ module API | |||
|   module Entities | ||||
|     module BulkImports | ||||
|       class EntityFailure < Grape::Entity | ||||
|         expose :relation, documentation: { type: 'string', example: 'group' } | ||||
|         expose :pipeline_step, as: :step, documentation: { type: 'string', example: 'extractor' } | ||||
|         expose :relation, documentation: { type: 'string', example: 'label' } | ||||
|         expose :exception_message, documentation: { type: 'string', example: 'error message' } do |failure| | ||||
|           ::Projects::ImportErrorFilter.filter_message(failure.exception_message.truncate(72)) | ||||
|         end | ||||
|         expose :exception_class, documentation: { type: 'string', example: 'Exception' } | ||||
|         expose :correlation_id_value, documentation: { type: 'string', example: 'dfcf583058ed4508e4c7c617bd7f0edd' } | ||||
|         expose :created_at, documentation: { type: 'dateTime', example: '2012-05-28T04:42:42-07:00' } | ||||
|         expose :pipeline_class, documentation: { | ||||
|           type: 'string', example: 'BulkImports::Groups::Pipelines::GroupPipeline' | ||||
|         } | ||||
|         expose :pipeline_step, documentation: { type: 'string', example: 'extractor' } | ||||
|         expose :source_url, documentation: { type: 'string', example: 'https://source.gitlab.com/group/-/epics/1' } | ||||
|         expose :source_title, documentation: { type: 'string', example: 'title' } | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -25,7 +25,7 @@ module BulkImports | |||
|               end | ||||
|             end | ||||
| 
 | ||||
|             run_pipeline_step(:loader, loader.class.name) do | ||||
|             run_pipeline_step(:loader, loader.class.name, entry) do | ||||
|               loader.load(context, entry) | ||||
|             end | ||||
| 
 | ||||
|  | @ -49,7 +49,7 @@ module BulkImports | |||
| 
 | ||||
|       private # rubocop:disable Lint/UselessAccessModifier | ||||
| 
 | ||||
|       def run_pipeline_step(step, class_name = nil) | ||||
|       def run_pipeline_step(step, class_name = nil, entry = nil) | ||||
|         raise MarkedAsFailedError if context.entity.failed? | ||||
| 
 | ||||
|         info(pipeline_step: step, step_class: class_name) | ||||
|  | @ -65,11 +65,11 @@ module BulkImports | |||
|       rescue BulkImports::NetworkError => e | ||||
|         raise BulkImports::RetryPipelineError.new(e.message, e.retry_delay) if e.retriable?(context.tracker) | ||||
| 
 | ||||
|         log_and_fail(e, step) | ||||
|         log_and_fail(e, step, entry) | ||||
|       rescue BulkImports::RetryPipelineError | ||||
|         raise | ||||
|       rescue StandardError => e | ||||
|         log_and_fail(e, step) | ||||
|         log_and_fail(e, step, entry) | ||||
|       end | ||||
| 
 | ||||
|       def extracted_data_from | ||||
|  | @ -95,8 +95,8 @@ module BulkImports | |||
|         run if extracted_data.has_next_page? | ||||
|       end | ||||
| 
 | ||||
|       def log_and_fail(exception, step) | ||||
|         log_import_failure(exception, step) | ||||
|       def log_and_fail(exception, step, entry = nil) | ||||
|         log_import_failure(exception, step, entry) | ||||
| 
 | ||||
|         if abort_on_failure? | ||||
|           tracker.fail_op! | ||||
|  | @ -114,7 +114,7 @@ module BulkImports | |||
|         tracker.skip! | ||||
|       end | ||||
| 
 | ||||
|       def log_import_failure(exception, step) | ||||
|       def log_import_failure(exception, step, entry) | ||||
|         failure_attributes = { | ||||
|           bulk_import_entity_id: context.entity.id, | ||||
|           pipeline_class: pipeline, | ||||
|  | @ -124,6 +124,11 @@ module BulkImports | |||
|           correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id | ||||
|         } | ||||
| 
 | ||||
|         if entry | ||||
|           failure_attributes[:source_url] = BulkImports::SourceUrlBuilder.new(context, entry).url | ||||
|           failure_attributes[:source_title] = entry.try(:title) || entry.try(:name) | ||||
|         end | ||||
| 
 | ||||
|         log_exception( | ||||
|           exception, | ||||
|           log_params( | ||||
|  |  | |||
|  | @ -0,0 +1,57 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module BulkImports | ||||
|   class SourceUrlBuilder | ||||
|     ALLOWED_RELATIONS = %w[ | ||||
|       issues | ||||
|       merge_requests | ||||
|       epics | ||||
|       milestones | ||||
|     ].freeze | ||||
| 
 | ||||
|     attr_reader :context, :entity, :entry | ||||
| 
 | ||||
|     # @param [BulkImports::Pipeline::Context] context | ||||
|     # @param [ApplicationRecord] entry | ||||
|     def initialize(context, entry) | ||||
|       @context = context | ||||
|       @entity = context.entity | ||||
|       @entry = entry | ||||
|     end | ||||
| 
 | ||||
|     # Builds a source URL for the given entry if iid is present | ||||
|     def url | ||||
|       return unless entry.is_a?(ApplicationRecord) | ||||
|       return unless iid | ||||
|       return unless ALLOWED_RELATIONS.include?(relation) | ||||
| 
 | ||||
|       File.join(source_instance_url, group_prefix, source_full_path, '-', relation, iid.to_s) | ||||
|     end | ||||
| 
 | ||||
|     private | ||||
| 
 | ||||
|     def iid | ||||
|       @iid ||= entry.try(:iid) | ||||
|     end | ||||
| 
 | ||||
|     def relation | ||||
|       @relation ||= context.tracker.pipeline_class.relation | ||||
|     end | ||||
| 
 | ||||
|     def source_instance_url | ||||
|       @source_instance_url ||= context.bulk_import.configuration.url | ||||
|     end | ||||
| 
 | ||||
|     def source_full_path | ||||
|       @source_full_path ||= entity.source_full_path | ||||
|     end | ||||
| 
 | ||||
|     # Group milestone (or epic) url is /groups/:group_path/-/milestones/:iid | ||||
|     # Project milestone url is /:project_path/-/milestones/:iid | ||||
|     def group_prefix | ||||
|       return '' if entity.project? | ||||
| 
 | ||||
|       entity.pluralized_name | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -34,6 +34,17 @@ module Gitlab | |||
|         request_params[:headers][:Cookie] = get_cookies if options[:use_cookies] | ||||
|         request_params[:base_uri] = uri.to_s | ||||
|         request_params.merge!(auth_params) | ||||
|         # Setting defaults here so we can also set `timeout` which prevents setting defaults in the HTTP gem's code | ||||
|         request_params[:open_timeout] = options[:open_timeout] || default_timeout_for(:open_timeout) | ||||
|         request_params[:read_timeout] = options[:read_timeout] || default_timeout_for(:read_timeout) | ||||
|         request_params[:write_timeout] = options[:write_timeout] || default_timeout_for(:write_timeout) | ||||
|         # Global timeout. Needs to be at least as high as the maximum defined in other timeouts | ||||
|         request_params[:timeout] = [ | ||||
|           Gitlab::HTTP::DEFAULT_READ_TOTAL_TIMEOUT, | ||||
|           request_params[:open_timeout], | ||||
|           request_params[:read_timeout], | ||||
|           request_params[:write_timeout] | ||||
|         ].max | ||||
| 
 | ||||
|         result = Gitlab::HTTP.public_send(http_method, path, **request_params) # rubocop:disable GitlabSecurity/PublicSend | ||||
|         @authenticated = result.response.is_a?(Net::HTTPOK) | ||||
|  | @ -52,6 +63,10 @@ module Gitlab | |||
| 
 | ||||
|       private | ||||
| 
 | ||||
|       def default_timeout_for(param) | ||||
|         Gitlab::HTTP::DEFAULT_TIMEOUT_OPTIONS[param] | ||||
|       end | ||||
| 
 | ||||
|       def auth_params | ||||
|         return {} unless @options[:username] && @options[:password] | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ module Gitlab | |||
|     class BaseImporter | ||||
|       attr_reader :project, :client, :formatter, :jira_project_key, :running_import | ||||
| 
 | ||||
|       def initialize(project) | ||||
|       def initialize(project, client = nil) | ||||
|         Gitlab::JiraImport.validate_project_settings!(project) | ||||
| 
 | ||||
|         @running_import = project.latest_jira_import | ||||
|  | @ -14,7 +14,7 @@ module Gitlab | |||
|         raise Projects::ImportService::Error, _('Unable to find Jira project to import data from.') unless @jira_project_key | ||||
| 
 | ||||
|         @project = project | ||||
|         @client = project.jira_integration.client | ||||
|         @client = client || project.jira_integration.client | ||||
|         @formatter = Gitlab::ImportFormatter.new | ||||
|       end | ||||
| 
 | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ module Gitlab | |||
| 
 | ||||
|       attr_reader :imported_items_cache_key, :start_at, :job_waiter | ||||
| 
 | ||||
|       def initialize(project) | ||||
|       def initialize(project, client = nil) | ||||
|         super | ||||
|         # get cached start_at value, or zero if not cached yet | ||||
|         @start_at = Gitlab::JiraImport.get_issues_next_start_at(project.id) | ||||
|  |  | |||
|  | @ -8,33 +8,33 @@ module QA | |||
|           include QA::Page::Settings::Common | ||||
| 
 | ||||
|           view 'app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue' do | ||||
|             element :ci_variable_key_field | ||||
|             element :ci_variable_value_field | ||||
|             element :ci_variable_save_button | ||||
|             element 'ci-variable-key' | ||||
|             element 'ci-variable-value' | ||||
|             element 'ci-variable-confirm-button' | ||||
|           end | ||||
| 
 | ||||
|           def fill_variable(key, value, masked = false) | ||||
|             within_element(:ci_variable_key_field) { find('input').set key } | ||||
|             fill_element :ci_variable_value_field, value | ||||
|             within_element('ci-variable-key') { find('input').set key } | ||||
|             fill_element 'ci-variable-value', value | ||||
|             click_ci_variable_save_button | ||||
| 
 | ||||
|             wait_until(reload: false) do | ||||
|               within_element('ci-variable-table') { has_element?(:edit_ci_variable_button) } | ||||
|               within_element('ci-variable-table') { has_element?('edit-ci-variable-button') } | ||||
|             end | ||||
|           end | ||||
| 
 | ||||
|           def click_add_variable | ||||
|             click_element :add_ci_variable_button | ||||
|             click_element 'add-ci-variable-button' | ||||
|           end | ||||
| 
 | ||||
|           def click_edit_ci_variable | ||||
|             within_element('ci-variable-table') do | ||||
|               click_element :edit_ci_variable_button | ||||
|               click_element 'edit-ci-variable-button' | ||||
|             end | ||||
|           end | ||||
| 
 | ||||
|           def click_ci_variable_save_button | ||||
|             click_element :ci_variable_save_button | ||||
|             click_element 'ci-variable-confirm-button' | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|  |  | |||
|  | @ -123,7 +123,12 @@ module QA | |||
|           access_token: ENV['QA_LARGE_IMPORT_GH_TOKEN'] || Runtime::Env.github_access_token, | ||||
|           per_page: 100, | ||||
|           middleware: Faraday::RackBuilder.new do |builder| | ||||
|             builder.use(Faraday::Retry::Middleware, exceptions: [Octokit::InternalServerError, Octokit::ServerError]) | ||||
|             builder.use(Faraday::Retry::Middleware, | ||||
|               max: 3, | ||||
|               interval: 1, | ||||
|               retry_block: ->(exception:, **) { logger.warn("Request to GitHub failed: '#{exception}', retrying") }, | ||||
|               exceptions: [Octokit::InternalServerError, Octokit::ServerError] | ||||
|             ) | ||||
|             builder.use(Faraday::Response::RaiseError) # faraday retry swallows errors, so it needs to be re-raised | ||||
|           end | ||||
|         ) | ||||
|  | @ -161,52 +166,33 @@ module QA | |||
|       end | ||||
| 
 | ||||
|       let(:gh_issues) do | ||||
|         issues = gh_all_issues.reject(&:pull_request).each_with_object({}) do |issue, hash| | ||||
|         gh_all_issues.reject(&:pull_request).each_with_object({}) do |issue, hash| | ||||
|           id = issue.number | ||||
|           logger.debug("- Fetching comments and events for issue #{id} -") | ||||
|           hash[id] = { | ||||
|             url: issue.html_url, | ||||
|             title: issue.title, | ||||
|             body: issue.body || '', | ||||
|             comments: gh_issue_comments[id] | ||||
|             comments: fetch_issuable_comments(id, "issue"), | ||||
|             events: fetch_issuable_events(id) | ||||
|           } | ||||
|         end | ||||
| 
 | ||||
|         fetch_github_events(issues, "issue") | ||||
|       end | ||||
| 
 | ||||
|       let(:gh_prs) do | ||||
|         prs = gh_all_issues.select(&:pull_request).each_with_object({}) do |pr, hash| | ||||
|         gh_all_issues.select(&:pull_request).each_with_object({}) do |pr, hash| | ||||
|           id = pr.number | ||||
|           logger.debug("- Fetching comments and events for pr #{id} -") | ||||
|           hash[id] = { | ||||
|             url: pr.html_url, | ||||
|             title: pr.title, | ||||
|             body: pr.body || '', | ||||
|             comments: [*gh_pr_comments[id], *gh_issue_comments[id]].compact | ||||
|             comments: fetch_issuable_comments(id, "pr"), | ||||
|             events: fetch_issuable_events(id) | ||||
|           } | ||||
|         end | ||||
| 
 | ||||
|         fetch_github_events(prs, "pr") | ||||
|       end | ||||
| 
 | ||||
|       # rubocop:disable Layout/LineLength | ||||
|       let(:gh_issue_comments) do | ||||
|         logger.info("- Fetching issue comments -") | ||||
|         with_paginated_request { github_client.issues_comments(github_repo) }.each_with_object(Hash.new { |h, k| h[k] = [] }) do |c, hash| | ||||
|           hash[id_from_url(c.html_url)] << c.body&.gsub(gh_link_pattern, dummy_url) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       let(:gh_pr_comments) do | ||||
|         logger.info("- Fetching pr comments -") | ||||
|         with_paginated_request { github_client.pull_requests_comments(github_repo) }.each_with_object(Hash.new { |h, k| h[k] = [] }) do |c, hash| | ||||
|           hash[id_from_url(c.html_url)] << c.body | ||||
|             # some suggestions can contain extra whitespaces which gitlab will remove | ||||
|             &.gsub(/suggestion\s+\r/, "suggestion\r") | ||||
|             &.gsub(gh_link_pattern, dummy_url) | ||||
|         end | ||||
|       end | ||||
|       # rubocop:enable Layout/LineLength | ||||
| 
 | ||||
|       let(:imported_project) do | ||||
|         Resource::ProjectImportedFromGithub.fabricate_via_api! do |project| | ||||
|           project.add_name_uuid = false | ||||
|  | @ -282,7 +268,7 @@ module QA | |||
|               issue_events: gl_issues.sum { |_k, v| v[:events].length } | ||||
|             } | ||||
|           }, | ||||
|           not_imported: { | ||||
|           diff: { | ||||
|             mrs: @mr_diff, | ||||
|             issues: @issue_diff | ||||
|           } | ||||
|  | @ -415,24 +401,35 @@ module QA | |||
|       # | ||||
|       private | ||||
| 
 | ||||
|       # Fetch github events and add to issue object | ||||
|       # Fetch issuable object comments | ||||
|       # | ||||
|       # @param [Hash] issuables | ||||
|       # @param [Integer] id | ||||
|       # @param [String] type | ||||
|       # @return [Hash] | ||||
|       def fetch_github_events(issuables, type) | ||||
|         logger.info("- Fetching #{type} events -") | ||||
|         issuables.to_h do |id, issuable| | ||||
|           logger.debug("Fetching events for #{type} !#{id}") | ||||
|           events = with_paginated_request { github_client.issue_events(github_repo, id) } | ||||
|             .map { |event| event[:event] } | ||||
|             .reject { |event| unsupported_events.include?(event) } | ||||
|       # @return [Array] | ||||
|       def fetch_issuable_comments(id, type) | ||||
|         pr = type == "pr" | ||||
|         comments = [] | ||||
|         # every pr is also an issue, so when fetching pr comments, issue endpoint has to be used as well | ||||
|         comments.push(*with_paginated_request { github_client.issue_comments(github_repo, id) }) | ||||
|         comments.push(*with_paginated_request { github_client.pull_request_comments(github_repo, id) }) if pr | ||||
|         comments.map! { |comment| comment.body&.gsub(gh_link_pattern, dummy_url) } | ||||
|         return comments unless pr | ||||
| 
 | ||||
|           [id, issuable.merge({ events: events })] | ||||
|         end | ||||
|         # some suggestions can contain extra whitespaces which gitlab will remove | ||||
|         comments.map { |comment| comment.gsub(/suggestion\s+\r/, "suggestion\r") } | ||||
|       end | ||||
| 
 | ||||
|       # Verify imported mrs or issues and return missing items | ||||
|       # Fetch issuable object events | ||||
|       # | ||||
|       # @param [Integer] id | ||||
|       # @return [Array] | ||||
|       def fetch_issuable_events(id) | ||||
|         with_paginated_request { github_client.issue_events(github_repo, id) } | ||||
|           .map { |event| event[:event] } | ||||
|           .reject { |event| unsupported_events.include?(event) } | ||||
|       end | ||||
| 
 | ||||
|       # Verify imported mrs or issues and return content diff | ||||
|       # | ||||
|       # @param [String] type verification object, 'mrs' or 'issues' | ||||
|       # @return [Hash] | ||||
|  | @ -443,18 +440,20 @@ module QA | |||
|         actual = type == 'mr' ? mrs : gl_issues | ||||
| 
 | ||||
|         missing_objects = (expected.keys - actual.keys).map { |it| expected[it].slice(:title, :url) } | ||||
|         extra_objects = (actual.keys - expected.keys).map { |it| actual[it].slice(:title, :url) } | ||||
|         count_msg = <<~MSG | ||||
|           Expected to contain all of GitHub's #{type}s. Gitlab: #{actual.length}, Github: #{expected.length}. | ||||
|           Missing: #{missing_objects.map { |it| it[:url] }} | ||||
|         MSG | ||||
|         expect(expected.length <= actual.length).to be_truthy, count_msg | ||||
| 
 | ||||
|         missing_content = verify_comments_and_events(type, actual, expected) | ||||
|         content_diff = verify_comments_and_events(type, actual, expected) | ||||
| 
 | ||||
|         { | ||||
|           "#{type}s": missing_objects.empty? ? nil : missing_objects, | ||||
|           "#{type}_content": missing_content.empty? ? nil : missing_content | ||||
|         }.compact | ||||
|           "extra_#{type}s": extra_objects, | ||||
|           "missing_#{type}s": missing_objects, | ||||
|           "#{type}_content_diff": content_diff | ||||
|         }.compact_blank | ||||
|       end | ||||
| 
 | ||||
|       # Verify imported comments and events | ||||
|  | @ -464,7 +463,7 @@ module QA | |||
|       # @param [Hash] expected | ||||
|       # @return [Hash] | ||||
|       def verify_comments_and_events(type, actual, expected) | ||||
|         actual.each_with_object([]) do |(key, actual_item), missing_content| | ||||
|         actual.each_with_object([]) do |(key, actual_item), content_diff| | ||||
|           expected_item = expected[key] | ||||
|           title = actual_item[:title] | ||||
|           msg = "expected #{type} with iid '#{key}' to have" | ||||
|  | @ -498,19 +497,23 @@ module QA | |||
|           MSG | ||||
|           expect(actual_events).to include(*expected_events), event_count_msg | ||||
| 
 | ||||
|           # Save missing comments and events | ||||
|           # Save comment and event diff | ||||
|           # | ||||
|           comment_diff = expected_comments - actual_comments | ||||
|           event_diff = expected_events - actual_events | ||||
|           next if comment_diff.empty? && event_diff.empty? | ||||
|           missing_comments = expected_comments - actual_comments | ||||
|           extra_comments = actual_comments - expected_comments | ||||
|           missing_events = expected_events - actual_events | ||||
|           extra_events = actual_events - expected_events | ||||
|           next if [missing_comments, missing_events, extra_comments, extra_events].all?(&:empty?) | ||||
| 
 | ||||
|           missing_content << { | ||||
|           content_diff << { | ||||
|             title: title, | ||||
|             github_url: expected_item[:url], | ||||
|             gitlab_url: actual_item[:url], | ||||
|             missing_comments: comment_diff.empty? ? nil : comment_diff, | ||||
|             missing_events: event_diff.empty? ? nil : event_diff | ||||
|           }.compact | ||||
|             missing_comments: missing_comments, | ||||
|             extra_comments: extra_comments, | ||||
|             missing_events: missing_events, | ||||
|             extra_events: extra_events | ||||
|           }.compact_blank | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|  | @ -671,16 +674,6 @@ module QA | |||
|         File.open("tmp/github-import-data.json", "w") { |file| file.write(JSON.pretty_generate(json)) } | ||||
|       end | ||||
| 
 | ||||
|       # Extract id number from web url of issue or pull request | ||||
|       # | ||||
|       # Some endpoints don't return object id as separate parameter so web url can be used as a workaround | ||||
|       # | ||||
|       # @param [String] url | ||||
|       # @return [Integer] | ||||
|       def id_from_url(url) | ||||
|         url.match(%r{(?<type>issues|pull)/(?<id>\d+)})&.named_captures&.fetch("id", nil).to_i | ||||
|       end | ||||
| 
 | ||||
|       # Custom pagination for github requests | ||||
|       # | ||||
|       # Default autopagination doesn't work correctly with rate limit | ||||
|  |  | |||
|  | @ -71,7 +71,10 @@ module QA | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     describe 'OIDC' do | ||||
|     describe 'OIDC', quarantine: { | ||||
|       issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/429723', | ||||
|       type: :flaky | ||||
|     } do | ||||
|       let(:consumer_name) { 'gitlab-oidc-consumer' } | ||||
|       let(:redirect_uri) { "#{consumer_host}/users/auth/openid_connect/callback" } | ||||
|       let(:scopes) { %w[openid profile email] } | ||||
|  |  | |||
|  | @ -22,14 +22,6 @@ FactoryBot.define do | |||
|       locked { :unlocked } | ||||
|     end | ||||
| 
 | ||||
|     trait :checksummed do | ||||
|       verification_checksum { 'abc' } | ||||
|     end | ||||
| 
 | ||||
|     trait :checksum_failure do | ||||
|       verification_failure { 'Could not calculate the checksum' } | ||||
|     end | ||||
| 
 | ||||
|     trait :expired do | ||||
|       expire_at { Date.yesterday } | ||||
|     end | ||||
|  |  | |||
|  | @ -8,13 +8,5 @@ FactoryBot.define do | |||
|       snippet_repository.shard_name = snippet_repository.snippet.repository_storage | ||||
|       snippet_repository.disk_path  = snippet_repository.snippet.disk_path | ||||
|     end | ||||
| 
 | ||||
|     trait(:checksummed) do | ||||
|       verification_checksum { 'abc' } | ||||
|     end | ||||
| 
 | ||||
|     trait(:checksum_failure) do | ||||
|       verification_failure { 'Could not calculate the checksum' } | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -8,13 +8,5 @@ FactoryBot.define do | |||
| 
 | ||||
|     sequence(:version) | ||||
|     file { fixture_file_upload('spec/fixtures/terraform/terraform.tfstate', 'application/json') } | ||||
| 
 | ||||
|     trait(:checksummed) do | ||||
|       verification_checksum { 'abc' } | ||||
|     end | ||||
| 
 | ||||
|     trait(:checksum_failure) do | ||||
|       verification_failure { 'Could not calculate the checksum' } | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -67,9 +67,9 @@ describe('CI Variable Drawer', () => { | |||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   const findConfirmBtn = () => wrapper.findByTestId('ci-variable-confirm-btn'); | ||||
|   const findConfirmBtn = () => wrapper.findByTestId('ci-variable-confirm-button'); | ||||
|   const findConfirmDeleteModal = () => wrapper.findComponent(GlModal); | ||||
|   const findDeleteBtn = () => wrapper.findByTestId('ci-variable-delete-btn'); | ||||
|   const findDeleteBtn = () => wrapper.findByTestId('ci-variable-delete-button'); | ||||
|   const findDisabledEnvironmentScopeDropdown = () => wrapper.findComponent(GlFormInput); | ||||
|   const findDrawer = () => wrapper.findComponent(GlDrawer); | ||||
|   const findEnvironmentScopeDropdown = () => wrapper.findComponent(CiEnvironmentsDropdown); | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe API::Entities::BulkImports::EntityFailure do | ||||
| RSpec.describe API::Entities::BulkImports::EntityFailure, feature_category: :importers do | ||||
|   let_it_be(:failure) { create(:bulk_import_failure) } | ||||
| 
 | ||||
|   subject { described_class.new(failure).as_json } | ||||
|  | @ -10,11 +10,11 @@ RSpec.describe API::Entities::BulkImports::EntityFailure do | |||
|   it 'has the correct attributes' do | ||||
|     expect(subject).to include( | ||||
|       :relation, | ||||
|       :step, | ||||
|       :exception_class, | ||||
|       :exception_message, | ||||
|       :exception_class, | ||||
|       :correlation_id_value, | ||||
|       :created_at | ||||
|       :source_url, | ||||
|       :source_title | ||||
|     ) | ||||
|   end | ||||
| 
 | ||||
|  |  | |||
|  | @ -43,7 +43,9 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do | |||
|     stub_const('BulkImports::MyPipeline', pipeline) | ||||
|   end | ||||
| 
 | ||||
|   let_it_be_with_reload(:entity) { create(:bulk_import_entity) } | ||||
|   let_it_be(:bulk_import) { create(:bulk_import) } | ||||
|   let_it_be(:configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) } | ||||
|   let_it_be_with_reload(:entity) { create(:bulk_import_entity, bulk_import: bulk_import) } | ||||
| 
 | ||||
|   let(:tracker) { create(:bulk_import_tracker, entity: entity) } | ||||
|   let(:context) { BulkImports::Pipeline::Context.new(tracker, extra: :data) } | ||||
|  | @ -119,6 +121,56 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do | |||
|         expect(entity.failed?).to eq(false) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when failure happens during loader' do | ||||
|       before do | ||||
|         allow(tracker).to receive(:pipeline_class).and_return(BulkImports::MyPipeline) | ||||
|         allow(BulkImports::MyPipeline).to receive(:relation).and_return(relation) | ||||
| 
 | ||||
|         allow_next_instance_of(BulkImports::Extractor) do |extractor| | ||||
|           allow(extractor).to receive(:extract).with(context).and_return(extracted_data) | ||||
|         end | ||||
| 
 | ||||
|         allow_next_instance_of(BulkImports::Transformer) do |transformer| | ||||
|           allow(transformer).to receive(:transform).with(context, extracted_data.data.first).and_return(entry) | ||||
|         end | ||||
| 
 | ||||
|         allow_next_instance_of(BulkImports::Loader) do |loader| | ||||
|           allow(loader).to receive(:load).with(context, entry).and_raise(StandardError, 'Error!') | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when entry has title' do | ||||
|         let(:relation) { 'issues' } | ||||
|         let(:entry) { Issue.new(iid: 1, title: 'hello world') } | ||||
| 
 | ||||
|         it 'creates failure record with source url and title' do | ||||
|           subject.run | ||||
| 
 | ||||
|           failure = entity.failures.first | ||||
|           expected_source_url = File.join(configuration.url, 'groups', entity.source_full_path, '-', 'issues', '1') | ||||
| 
 | ||||
|           expect(failure).to be_present | ||||
|           expect(failure.source_url).to eq(expected_source_url) | ||||
|           expect(failure.source_title).to eq('hello world') | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when entry has name' do | ||||
|         let(:relation) { 'boards' } | ||||
|         let(:entry) { Board.new(name: 'hello world') } | ||||
| 
 | ||||
|         it 'creates failure record with name' do | ||||
|           subject.run | ||||
| 
 | ||||
|           failure = entity.failures.first | ||||
| 
 | ||||
|           expect(failure).to be_present | ||||
|           expect(failure.source_url).to be_nil | ||||
|           expect(failure.source_title).to eq('hello world') | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe 'pipeline runner' do | ||||
|  | @ -363,7 +415,11 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do | |||
| 
 | ||||
|     def extracted_data(has_next_page: false) | ||||
|       BulkImports::Pipeline::ExtractedData.new( | ||||
|         data: { foo: :bar }, | ||||
|         data: { | ||||
|           'foo' => 'bar', | ||||
|           'title' => 'hello world', | ||||
|           'iid' => 1 | ||||
|         }, | ||||
|         page_info: { | ||||
|           'has_next_page' => has_next_page, | ||||
|           'next_page' => has_next_page ? 'cursor' : nil | ||||
|  |  | |||
|  | @ -2,10 +2,10 @@ | |||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe BulkImports::Projects::Pipelines::ContainerExpirationPolicyPipeline do | ||||
| RSpec.describe BulkImports::Projects::Pipelines::ContainerExpirationPolicyPipeline, feature_category: :importers do | ||||
|   let_it_be(:project) { create(:project) } | ||||
|   let_it_be(:entity) { create(:bulk_import_entity, :project_entity, project: project) } | ||||
|   let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } | ||||
|   let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity, pipeline_name: described_class) } | ||||
|   let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) } | ||||
| 
 | ||||
|   let_it_be(:policy) do | ||||
|  |  | |||
|  | @ -2,11 +2,11 @@ | |||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe BulkImports::Projects::Pipelines::ExternalPullRequestsPipeline do | ||||
| RSpec.describe BulkImports::Projects::Pipelines::ExternalPullRequestsPipeline, feature_category: :importers do | ||||
|   let_it_be(:project) { create(:project) } | ||||
|   let_it_be(:bulk_import) { create(:bulk_import) } | ||||
|   let_it_be(:entity) { create(:bulk_import_entity, :project_entity, project: project, bulk_import: bulk_import) } | ||||
|   let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } | ||||
|   let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity, pipeline_name: described_class) } | ||||
|   let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) } | ||||
| 
 | ||||
|   let(:attributes) { {} } | ||||
|  |  | |||
|  | @ -0,0 +1,78 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe BulkImports::SourceUrlBuilder, feature_category: :importers do | ||||
|   let_it_be(:bulk_import) { create(:bulk_import) } | ||||
|   let_it_be(:configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) } | ||||
| 
 | ||||
|   let(:entity) { create(:bulk_import_entity, bulk_import: bulk_import) } | ||||
|   let(:tracker) { create(:bulk_import_tracker, entity: entity) } | ||||
|   let(:context) { BulkImports::Pipeline::Context.new(tracker) } | ||||
|   let(:entry) { Issue.new(iid: 1, title: 'hello world') } | ||||
| 
 | ||||
|   describe '#url' do | ||||
|     subject { described_class.new(context, entry) } | ||||
| 
 | ||||
|     before do | ||||
|       allow(subject).to receive(:relation).and_return('issues') | ||||
|     end | ||||
| 
 | ||||
|     context 'when relation is allowed' do | ||||
|       context 'when entity is a group' do | ||||
|         it 'returns the url specific to groups' do | ||||
|           expected_url = File.join( | ||||
|             configuration.url, | ||||
|             'groups', | ||||
|             entity.source_full_path, | ||||
|             '-', | ||||
|             'issues', | ||||
|             '1' | ||||
|           ) | ||||
| 
 | ||||
|           expect(subject.url).to eq(expected_url) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when entity is a project' do | ||||
|         let(:entity) { create(:bulk_import_entity, :project_entity, bulk_import: bulk_import) } | ||||
| 
 | ||||
|         it 'returns the url' do | ||||
|           expected_url = File.join( | ||||
|             configuration.url, | ||||
|             entity.source_full_path, | ||||
|             '-', | ||||
|             'issues', | ||||
|             '1' | ||||
|           ) | ||||
| 
 | ||||
|           expect(subject.url).to eq(expected_url) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when entry is not an ApplicationRecord' do | ||||
|       let(:entry) { 'not an ApplicationRecord' } | ||||
| 
 | ||||
|       it 'returns nil' do | ||||
|         expect(subject.url).to be_nil | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when relation is not allowed' do | ||||
|       it 'returns nil' do | ||||
|         allow(subject).to receive(:relation).and_return('not_allowed') | ||||
| 
 | ||||
|         expect(subject.url).to be_nil | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when entry has no iid' do | ||||
|       let(:entry) { Issue.new } | ||||
| 
 | ||||
|       it 'returns nil' do | ||||
|         expect(subject.url).to be_nil | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -603,6 +603,17 @@ RSpec.describe Integrations::Jira, feature_category: :integrations do | |||
|       jira_integration.client.get('/foo') | ||||
|     end | ||||
| 
 | ||||
|     context 'when a custom read_timeout option is passed as an argument' do | ||||
|       it 'uses the default GitLab::HTTP timeouts plus a custom read_timeout' do | ||||
|         expected_timeouts = Gitlab::HTTP::DEFAULT_TIMEOUT_OPTIONS.merge(read_timeout: 2.minutes, timeout: 2.minutes) | ||||
| 
 | ||||
|         expect(Gitlab::HTTP_V2::Client).to receive(:httparty_perform_request) | ||||
|           .with(Net::HTTP::Get, '/foo', hash_including(expected_timeouts)).and_call_original | ||||
| 
 | ||||
|         jira_integration.client(read_timeout: 2.minutes).get('/foo') | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'with basic auth' do | ||||
|       before do | ||||
|         jira_integration.jira_auth_type = 0 | ||||
|  |  | |||
|  | @ -394,7 +394,7 @@ RSpec.describe API::BulkImports, feature_category: :importers do | |||
| 
 | ||||
|       expect(response).to have_gitlab_http_status(:ok) | ||||
|       expect(json_response.pluck('id')).to contain_exactly(entity_3.id) | ||||
|       expect(json_response.first['failures'].first['exception_class']).to eq(failure_3.exception_class) | ||||
|       expect(json_response.first['failures'].first['exception_message']).to eq(failure_3.exception_message) | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'disabled feature' | ||||
|  | @ -420,4 +420,17 @@ RSpec.describe API::BulkImports, feature_category: :importers do | |||
|       expect(response).to have_gitlab_http_status(:unauthorized) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe 'GET /bulk_imports/:id/entities/:entity_id/failures' do | ||||
|     let(:request) { get api("/bulk_imports/#{import_2.id}/entities/#{entity_3.id}/failures", user) } | ||||
| 
 | ||||
|     it 'returns specified entity failures' do | ||||
|       request | ||||
| 
 | ||||
|       expect(response).to have_gitlab_http_status(:ok) | ||||
|       expect(json_response.first['exception_message']).to eq(failure_3.exception_message) | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'disabled feature' | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -30,6 +30,12 @@ RSpec.describe Issuable::DiscussionsListService, feature_category: :team_plannin | |||
|         expect(discussions_service.execute).to be_empty | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when issue exists at the group level' do | ||||
|       let_it_be(:issuable) { create(:issue, :group_level, namespace: group) } | ||||
| 
 | ||||
|       it_behaves_like 'listing issuable discussions', :guest, 1, 7 | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe 'fetching notes for merge requests' do | ||||
|  |  | |||
|  | @ -25,7 +25,11 @@ RSpec.describe Gitlab::JiraImport::Stage::ImportIssuesWorker, feature_category: | |||
|     end | ||||
| 
 | ||||
|     context 'when import started', :clean_gitlab_redis_cache do | ||||
|       let_it_be(:jira_integration) { create(:jira_integration, project: project) } | ||||
|       let(:job_waiter) { Gitlab::JobWaiter.new(2, 'some-job-key') } | ||||
| 
 | ||||
|       before_all do | ||||
|         create(:jira_integration, project: project) | ||||
|       end | ||||
| 
 | ||||
|       before do | ||||
|         jira_import.start! | ||||
|  | @ -34,6 +38,40 @@ RSpec.describe Gitlab::JiraImport::Stage::ImportIssuesWorker, feature_category: | |||
|         end | ||||
|       end | ||||
| 
 | ||||
|       it 'uses a custom http client for the issues importer' do | ||||
|         jira_integration = project.jira_integration | ||||
|         client = instance_double(JIRA::Client) | ||||
|         issue_importer = instance_double(Gitlab::JiraImport::IssuesImporter) | ||||
| 
 | ||||
|         allow(Project).to receive(:find_by_id).with(project.id).and_return(project) | ||||
|         allow(issue_importer).to receive(:execute).and_return(job_waiter) | ||||
| 
 | ||||
|         expect(jira_integration).to receive(:client).with(read_timeout: 2.minutes).and_return(client) | ||||
|         expect(Gitlab::JiraImport::IssuesImporter).to receive(:new).with( | ||||
|           project, | ||||
|           client | ||||
|         ).and_return(issue_importer) | ||||
| 
 | ||||
|         described_class.new.perform(project.id) | ||||
|       end | ||||
| 
 | ||||
|       context 'when increase_jira_import_issues_timeout feature flag is disabled' do | ||||
|         before do | ||||
|           stub_feature_flags(increase_jira_import_issues_timeout: false) | ||||
|         end | ||||
| 
 | ||||
|         it 'does not provide a custom client to IssuesImporter' do | ||||
|           issue_importer = instance_double(Gitlab::JiraImport::IssuesImporter) | ||||
|           expect(Gitlab::JiraImport::IssuesImporter).to receive(:new).with( | ||||
|             instance_of(Project), | ||||
|             nil | ||||
|           ).and_return(issue_importer) | ||||
|           allow(issue_importer).to receive(:execute).and_return(job_waiter) | ||||
| 
 | ||||
|           described_class.new.perform(project.id) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when start_at is nil' do | ||||
|         it_behaves_like 'advance to next stage', :attachments | ||||
|       end | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue