diff options
53 files changed, 556 insertions, 143 deletions
diff --git a/.rubocop_todo/gettext/static_identifier.yml b/.rubocop_todo/gettext/static_identifier.yml index f14c4884102..2db2a2f130f 100644 --- a/.rubocop_todo/gettext/static_identifier.yml +++ b/.rubocop_todo/gettext/static_identifier.yml @@ -13,7 +13,6 @@ Gettext/StaticIdentifier: - 'app/services/users/banned_user_base_service.rb' - 'app/services/work_items/widgets/hierarchy_service/base_service.rb' - 'ee/app/controllers/admin/licenses_controller.rb' - - 'ee/app/controllers/subscriptions/groups_controller.rb' - 'ee/app/mailers/ee/emails/admin_notification.rb' - 'ee/app/mailers/emails/namespace_storage_usage_mailer.rb' - 'ee/app/models/ee/member.rb' diff --git a/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml b/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml index 7a1dd906310..62bf63d1bb9 100644 --- a/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml +++ b/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml @@ -78,7 +78,6 @@ Layout/LineEndStringConcatenationIndentation: - 'ee/lib/ee/gitlab/auth/ldap/access.rb' - 'ee/lib/ee/gitlab/ci/pipeline/quota/size.rb' - 'ee/lib/ee/gitlab/git_access.rb' - - 'ee/lib/ee/gitlab/namespace_storage_size_error_message.rb' - 'ee/lib/gitlab/manual_quarterly_co_term_banner.rb' - 'ee/lib/tasks/gitlab/geo.rake' - 'ee/spec/controllers/admin/licenses_controller_spec.rb' @@ -102,7 +101,6 @@ Layout/LineEndStringConcatenationIndentation: - 'ee/spec/lib/audit/group_merge_request_approval_setting_changes_auditor_spec.rb' - 'ee/spec/lib/ee/gitlab/ci/parsers/security/validators/schema_validator_spec.rb' - 'ee/spec/lib/ee/gitlab/ci/pipeline/quota/size_spec.rb' - - 'ee/spec/lib/ee/gitlab/namespace_storage_size_error_message_spec.rb' - 'ee/spec/lib/gitlab/ci/pipeline/chain/limit/size_spec.rb' - 'ee/spec/lib/gitlab/ci/templates/api_security_gitlab_ci_yaml_spec.rb' - 'ee/spec/lib/gitlab/ci/templates/api_security_latest_gitlab_ci_yaml_spec.rb' diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index 72f43c7779e..2b489861db7 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -1350,7 +1350,6 @@ Layout/LineLength: - 'ee/spec/controllers/projects/subscriptions_controller_spec.rb' - 'ee/spec/controllers/projects/vulnerability_feedback_controller_spec.rb' - 'ee/spec/controllers/projects_controller_spec.rb' - - 'ee/spec/controllers/subscriptions/groups_controller_spec.rb' - 'ee/spec/controllers/subscriptions_controller_spec.rb' - 'ee/spec/elastic/migrate/migration_shared_examples.rb' - 'ee/spec/factories/ci/builds.rb' diff --git a/.rubocop_todo/rspec/before_all_role_assignment.yml b/.rubocop_todo/rspec/before_all_role_assignment.yml index 8e3f8de920c..4b58f6fc52f 100644 --- a/.rubocop_todo/rspec/before_all_role_assignment.yml +++ b/.rubocop_todo/rspec/before_all_role_assignment.yml @@ -61,7 +61,6 @@ RSpec/BeforeAllRoleAssignment: - 'ee/spec/controllers/projects_controller_spec.rb' - 'ee/spec/controllers/registrations/groups_controller_spec.rb' - 'ee/spec/controllers/security/projects_controller_spec.rb' - - 'ee/spec/controllers/subscriptions/groups_controller_spec.rb' - 'ee/spec/controllers/subscriptions_controller_spec.rb' - 'ee/spec/features/admin/admin_sends_notification_spec.rb' - 'ee/spec/features/analytics/code_analytics_spec.rb' diff --git a/.rubocop_todo/style/empty_method.yml b/.rubocop_todo/style/empty_method.yml index cb3896f0e5a..723a3adf412 100644 --- a/.rubocop_todo/style/empty_method.yml +++ b/.rubocop_todo/style/empty_method.yml @@ -91,7 +91,6 @@ Style/EmptyMethod: - 'ee/app/controllers/projects/security/dast_scanner_profiles_controller.rb' - 'ee/app/controllers/projects/security/dast_site_profiles_controller.rb' - 'ee/app/controllers/projects/security/sast_configuration_controller.rb' - - 'ee/app/controllers/subscriptions/groups_controller.rb' - 'ee/app/models/ee/epic.rb' - 'ee/app/services/feature_flag_issues/destroy_service.rb' - 'ee/db/geo/migrate/20170906174622_remove_duplicates_from_project_registry.rb' diff --git a/.rubocop_todo/style/format_string.yml b/.rubocop_todo/style/format_string.yml index d03718c0d51..a45c6b39c25 100644 --- a/.rubocop_todo/style/format_string.yml +++ b/.rubocop_todo/style/format_string.yml @@ -170,7 +170,6 @@ Style/FormatString: - 'ee/app/controllers/groups/saml_group_links_controller.rb' - 'ee/app/controllers/groups/sso_controller.rb' - 'ee/app/controllers/projects/requirements_management/requirements_controller.rb' - - 'ee/app/controllers/subscriptions/groups_controller.rb' - 'ee/app/helpers/admin/emails_helper.rb' - 'ee/app/helpers/billing_plans_helper.rb' - 'ee/app/helpers/ee/application_helper.rb' diff --git a/.rubocop_todo/style/guard_clause.yml b/.rubocop_todo/style/guard_clause.yml index abd7fe7af98..302c477af53 100644 --- a/.rubocop_todo/style/guard_clause.yml +++ b/.rubocop_todo/style/guard_clause.yml @@ -372,7 +372,6 @@ Style/GuardClause: - 'ee/app/services/iterations/delete_service.rb' - 'ee/app/services/merge_trains/check_status_service.rb' - 'ee/app/services/merge_trains/refresh_merge_request_service.rb' - - 'ee/app/services/namespaces/storage/email_notification_service.rb' - 'ee/app/services/projects/update_mirror_service.rb' - 'ee/app/services/security/override_uuids_service.rb' - 'ee/app/services/timebox_report_service.rb' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 6bc06509d4b..cb201d05306 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -e8a0d0187f11908018a4f639d0b845501153eddb +6ca31a17e3225737d5cf9deba18c9b18951120a8 @@ -182,9 +182,9 @@ gem 'seed-fu', '~> 2.3.7' gem 'elasticsearch-model', '~> 7.2' gem 'elasticsearch-rails', '~> 7.2', require: 'elasticsearch/rails/instrumentation' gem 'elasticsearch-api', '7.13.3' -gem 'aws-sdk-core', '~> 3.176.1' +gem 'aws-sdk-core', '~> 3.177.0' gem 'aws-sdk-cloudformation', '~> 1' -gem 'aws-sdk-s3', '~> 1.127.0' +gem 'aws-sdk-s3', '~> 1.128.0' gem 'faraday_middleware-aws-sigv4', '~>0.3.0' gem 'typhoeus', '~> 1.4.0' # Used with Elasticsearch to support http keep-alive connections diff --git a/Gemfile.checksum b/Gemfile.checksum index ce558e0768a..9814160bc2f 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -37,9 +37,9 @@ {"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.176.1","platform":"ruby","checksum":"bc312a5fec3991896456653104ee996356ee6eaca6dd5eb6fb251270ed13be8a"}, +{"name":"aws-sdk-core","version":"3.177.0","platform":"ruby","checksum":"c884ac2c5c7b53c3ef5219451aa2d3313881afee0fa7d22ef16ad8b848ccd125"}, {"name":"aws-sdk-kms","version":"1.64.0","platform":"ruby","checksum":"40de596c95047bfc6e1aacea24f3df6241aa716b6f7ce08ac4c5f7e3120395ad"}, -{"name":"aws-sdk-s3","version":"1.127.0","platform":"ruby","checksum":"d3e0f22c505d1fee4648df0992b8e582d5440d0efcbc171a89c908ab21b68dc0"}, +{"name":"aws-sdk-s3","version":"1.128.0","platform":"ruby","checksum":"5b1420d5be9654a9b1b5c8309d75ce72592f3a1e29def15ea07a853b96999d85"}, {"name":"aws-sigv4","version":"1.6.0","platform":"ruby","checksum":"ca9e6a15cd424f1f32b524b9760995331459bc22e67d3daad4fcf0c0084b087d"}, {"name":"axe-core-api","version":"4.6.0","platform":"ruby","checksum":"1b0ddec3353f108dc10363baf2282f43a5ff7f13d4e25f99071294e78f8a6c62"}, {"name":"axe-core-rspec","version":"4.6.0","platform":"ruby","checksum":"11c25bc9dd388c137ba4e5e63d64d20092bf22c884d8ffc829a22acfbacd747f"}, diff --git a/Gemfile.lock b/Gemfile.lock index 888fe7feae6..eb62c3238ff 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -245,7 +245,7 @@ GEM aws-sdk-cloudformation (1.41.0) aws-sdk-core (~> 3, >= 3.99.0) aws-sigv4 (~> 1.1) - aws-sdk-core (3.176.1) + aws-sdk-core (3.177.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.5) @@ -253,8 +253,8 @@ GEM aws-sdk-kms (1.64.0) aws-sdk-core (~> 3, >= 3.165.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.127.0) - aws-sdk-core (~> 3, >= 3.176.0) + aws-sdk-s3 (1.128.0) + aws-sdk-core (~> 3, >= 3.177.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.6) aws-sigv4 (1.6.0) @@ -1730,8 +1730,8 @@ DEPENDENCIES autoprefixer-rails (= 10.2.5.1) awesome_print aws-sdk-cloudformation (~> 1) - aws-sdk-core (~> 3.176.1) - aws-sdk-s3 (~> 1.127.0) + aws-sdk-core (~> 3.177.0) + aws-sdk-s3 (~> 1.128.0) axe-core-rspec babosa (~> 2.0) base32 (~> 0.3.0) diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb index d312dcc3563..b368ba6e495 100644 --- a/app/controllers/admin/runners_controller.rb +++ b/app/controllers/admin/runners_controller.rb @@ -3,7 +3,7 @@ class Admin::RunnersController < Admin::ApplicationController include RunnerSetupScripts - before_action :runner, except: [:index, :new, :tag_list, :runner_setup_scripts] + before_action :runner, only: [:show, :edit, :register, :update] feature_category :runner urgency :low diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index d61760bd0fc..af0eee97481 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -148,7 +148,7 @@ module Ci end def manual_playable? - blocked? || skipped? + blocked? end # This will be removed with ci_remove_ensure_stage_service diff --git a/app/models/service_desk_setting.rb b/app/models/service_desk_setting.rb index 4216ad7e70f..6560b25b39c 100644 --- a/app/models/service_desk_setting.rb +++ b/app/models/service_desk_setting.rb @@ -21,6 +21,7 @@ class ServiceDeskSetting < ApplicationRecord validates :project_id, presence: true validate :valid_issue_template validate :valid_project_key + validate :custom_email_enabled_state validates :outgoing_name, length: { maximum: 255 }, allow_blank: true validates :project_key, length: { maximum: 255 }, @@ -86,6 +87,14 @@ class ServiceDeskSetting < ApplicationRecord end end + def custom_email_enabled_state + return unless custom_email_enabled? + + if custom_email_verification.blank? || !custom_email_verification.finished? + errors.add(:custom_email_enabled, 'cannot be enabled until verification process has finished.') + end + end + private def source_template_project diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb index cd473152b41..bc12d210334 100644 --- a/app/presenters/blob_presenter.rb +++ b/app/presenters/blob_presenter.rb @@ -86,7 +86,7 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated end def find_file_path - url_helpers.project_find_file_path(project, blob.commit_id) + url_helpers.project_find_file_path(project, commit_id, ref_type: ref_type) end def blame_path @@ -131,13 +131,13 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated end def can_modify_blob? - super(blob, project, blob.commit_id) + super(blob, project, commit_id) end def can_current_user_push_to_branch? - return false unless current_user && project.repository.branch_exists?(blob.commit_id) + return false unless current_user && project.repository.branch_exists?(commit_id) - user_access(project).can_push_to_branch?(blob.commit_id) + user_access(project).can_push_to_branch?(commit_id) end def archived? @@ -145,7 +145,7 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated end def ide_edit_path - super(project, blob.commit_id, blob.path) + super(project, commit_id, blob.path) end def external_storage_url @@ -159,7 +159,7 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated end def project_blob_path_root - project_blob_path(project, blob.commit_id) + project_blob_path(project, commit_id) end private @@ -181,7 +181,7 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated end def environment - environment_params = project.repository.branch_exists?(blob.commit_id) ? { ref: blob.commit_id } : { sha: blob.commit_id } + environment_params = project.repository.branch_exists?(commit_id) ? { ref: commit_id } : { sha: commit_id } environment_params[:find_latest] = true ::Environments::EnvironmentsByDeploymentsFinder.new(project, current_user, environment_params).execute.last end @@ -190,12 +190,13 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated blob.repository.project end - def ref_qualified_path + def commit_id # If `ref_type` is present the commit_id will include the ref qualifier e.g. `refs/heads/`. # We only accept/return unqualified refs so we need to remove the qualifier from the `commit_id`. + ExtractsRef.unqualify_ref(blob.commit_id, ref_type) + end - commit_id = ExtractsRef.unqualify_ref(blob.commit_id, ref_type) - + def ref_qualified_path File.join(commit_id, blob.path) end diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb index 7e7f7ea9810..df255a7ae24 100644 --- a/app/services/import/github_service.rb +++ b/app/services/import/github_service.rb @@ -16,7 +16,7 @@ module Import track_access_level('github') if project.persisted? - store_import_settings(project) + store_import_settings(project, access_params) success(project) elsif project.errors[:import_source_disabled].present? error(project.errors[:import_source_disabled], :forbidden) @@ -134,8 +134,13 @@ module Import error(translated_message, http_status) end - def store_import_settings(project) - Gitlab::GithubImport::Settings.new(project).write(params[:optional_stages]) + def store_import_settings(project, access_params) + Gitlab::GithubImport::Settings + .new(project) + .write( + optional_stages: params[:optional_stages], + additional_access_tokens: access_params[:additional_access_tokens] + ) end end end diff --git a/config/feature_flags/development/runners_dashboard.yml b/config/feature_flags/development/runners_dashboard.yml new file mode 100644 index 00000000000..dd773c5e337 --- /dev/null +++ b/config/feature_flags/development/runners_dashboard.yml @@ -0,0 +1,8 @@ +--- +name: runners_dashboard +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125301 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/417002 +milestone: '16.2' +type: development +group: group::runner +default_enabled: false diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 65c3072fa8b..8c21767f583 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -6881,6 +6881,31 @@ Input type: `UserSetNamespaceCommitEmailInput` | <a id="mutationusersetnamespacecommitemailerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationusersetnamespacecommitemailnamespacecommitemail"></a>`namespaceCommitEmail` | [`NamespaceCommitEmail`](#namespacecommitemail) | User namespace commit email after mutation. | +### `Mutation.vulnerabilitiesDismiss` + +WARNING: +**Introduced** in 16.2. +This feature is an Experiment. It can be changed or removed at any time. + +Input type: `VulnerabilitiesDismissInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationvulnerabilitiesdismissclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationvulnerabilitiesdismisscomment"></a>`comment` | [`String`](#string) | Comment why vulnerability was dismissed (maximum 50,000 characters). | +| <a id="mutationvulnerabilitiesdismissdismissalreason"></a>`dismissalReason` | [`VulnerabilityDismissalReason`](#vulnerabilitydismissalreason) | Reason why vulnerability should be dismissed. | +| <a id="mutationvulnerabilitiesdismissvulnerabilityids"></a>`vulnerabilityIds` | [`[VulnerabilityID!]!`](#vulnerabilityid) | IDs of the vulnerabilities to be dismissed. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationvulnerabilitiesdismissclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationvulnerabilitiesdismisserrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| <a id="mutationvulnerabilitiesdismissvulnerabilities"></a>`vulnerabilities` **{warning-solid}** | [`[Vulnerability!]!`](#vulnerability) | **Deprecated:** This feature is an Experiment. It can be changed or removed at any time. Introduced in 16.2. | + ### `Mutation.vulnerabilityConfirm` Input type: `VulnerabilityConfirmInput` diff --git a/doc/api/import.md b/doc/api/import.md index be70868cca5..7bbc19cb36a 100644 --- a/doc/api/import.md +++ b/doc/api/import.md @@ -26,14 +26,15 @@ Prerequisites: POST /import/github ``` -| Attribute | Type | Required | Description | -|-------------------------|---------|----------|-------------------------------------------------------------------------------------| -| `personal_access_token` | string | yes | GitHub personal access token | -| `repo_id` | integer | yes | GitHub repository ID | -| `new_name` | string | no | New repository name | -| `target_namespace` | string | yes | Namespace to import repository into. Supports subgroups like `/namespace/subgroup`. In GitLab 15.8 and later, must not be blank | -| `github_hostname` | string | no | Custom GitHub Enterprise hostname. Do not set for GitHub.com. | -| `optional_stages` | object | no | [Additional items to import](../user/project/import/github.md#select-additional-items-to-import). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/373705) in GitLab 15.5 | +| Attribute | Type | Required | Description | +|----------------------------|---------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `personal_access_token` | string | yes | GitHub personal access token | +| `repo_id` | integer | yes | GitHub repository ID | +| `new_name` | string | no | New repository name | +| `target_namespace` | string | yes | Namespace to import repository into. Supports subgroups like `/namespace/subgroup`. In GitLab 15.8 and later, must not be blank | +| `github_hostname` | string | no | Custom GitHub Enterprise hostname. Do not set for GitHub.com. | +| `optional_stages` | object | no | [Additional items to import](../user/project/import/github.md#select-additional-items-to-import). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/373705) in GitLab 15.5 | +| `additional_access_tokens` | string | no | Additional list of comma-separated personal access tokens. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/337232) in GitLab 16.2 | ```shell curl --request POST \ @@ -51,7 +52,8 @@ curl --request POST \ "single_endpoint_notes_import": true, "attachments_import": true, "collaborators_import": true - } + }, + "additional_access_tokens": "foo,bar" }' ``` @@ -64,6 +66,8 @@ The following keys are available for `optional_stages`: For more information, see [Select additional items to import](../user/project/import/github.md#select-additional-items-to-import). +You can supply multiple personal access tokens in `additional_access_tokens` from different user accounts to import projects faster. + Example response: ```json diff --git a/doc/ci/runners/index.md b/doc/ci/runners/index.md index 19c5be88c1b..99327b2de4f 100644 --- a/doc/ci/runners/index.md +++ b/doc/ci/runners/index.md @@ -17,15 +17,10 @@ Your jobs can run on: - [Windows runners](saas/windows_saas_runner.md) ([Beta](../../policy/experiment-beta-support.md#beta)) - [macOS runners](saas/macos_saas_runner.md) ([Beta](../../policy/experiment-beta-support.md#beta)) -Refer to the Compute [cost factor](../../ci/pipelines/cicd_minutes.md#cost-factor) for the cost factor applied to the machine type based on size. +Refer to the compute minutes [cost factor](../../ci/pipelines/cicd_minutes.md#cost-factor) for the cost factor applied to the machine type based on size. The number of minutes you can use on these runners depends on the [maximum number of units of compute](../pipelines/cicd_minutes.md) in your [subscription plan](https://about.gitlab.com/pricing/). -[Untagged](../../ci/runners/configure_runners.md#use-tags-to-control-which-jobs-a-runner-can-run) jobs automatically run in containers -on the `small` Linux runners. - -The objective is to make 90% of CI jobs start executing in 120 seconds or less. The error rate should be less than 0.5%. - ## How SaaS runners work When you use SaaS runners: @@ -35,6 +30,9 @@ When you use SaaS runners: - The virtual machine where your job runs has `sudo` access with no password. - The storage is shared by the operating system, the image with pre-installed software, and a copy of your cloned repository. This means that the available free disk space for your jobs to use is reduced. +- [Untagged](../../ci/runners/configure_runners.md#use-tags-to-control-which-jobs-a-runner-can-run) jobs automatically run in containers +on the `small` Linux runners. +- The objective is to make 90% of CI jobs start executing in 120 seconds or less. The error rate target will be less than 0.5%. NOTE: Jobs handled by SaaS runners on GitLab.com **time out after 3 hours**, regardless of the timeout configured in a project. diff --git a/doc/user/admin_area/moderate_users.md b/doc/user/admin_area/moderate_users.md index bbec556db88..9869fdfc5e6 100644 --- a/doc/user/admin_area/moderate_users.md +++ b/doc/user/admin_area/moderate_users.md @@ -178,6 +178,7 @@ Users can also be deactivated using the [GitLab API](../../api/users.md#deactiva ### Automatically deactivate dormant users > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/320875) in GitLab 14.0. +> - Exclusion of GitLab generate bots [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340346) in GitLab 14.5 > - Customizable time period [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/336747) in GitLab 15.4 > - The lower limit for inactive period set to 90 days [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/100793) in GitLab 15.5 @@ -200,6 +201,9 @@ When this feature is enabled, GitLab runs a job once a day to deactivate the dor A maximum of 100,000 users can be deactivated per day. +NOTE: +GitLab generated bots are excluded from the automatic deactivation of dormant users. + ### Automatically delete unconfirmed users **(PREMIUM SELF)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/352514) in GitLab 16.1 [with a flag](../../administration/feature_flags.md) named `delete_unconfirmed_users_setting`. Disabled by default. diff --git a/doc/user/project/ml/experiment_tracking/index.md b/doc/user/project/ml/experiment_tracking/index.md index 81c4bc20301..584a6c0df59 100644 --- a/doc/user/project/ml/experiment_tracking/index.md +++ b/doc/user/project/ml/experiment_tracking/index.md @@ -9,7 +9,7 @@ info: Machine Learning Experiment Tracking is a GitLab Incubation Engineering pr FLAG: On self-managed GitLab, model experiment tracking is disabled by default. To enable the feature, ask an administrator to [enable the feature flag](../../../../administration/feature_flags.md) named `ml_experiment_tracking`. -On GitLab.com, this feature is in private testing only. +On GitLab.com, this feature is enabled on all projects. NOTE: Model experiment tracking is an [experimental feature](../../../../policy/experiment-beta-support.md). Refer to <https://gitlab.com/gitlab-org/gitlab/-/issues/381660> for feedback and feature requests. diff --git a/lib/api/import_github.rb b/lib/api/import_github.rb index 6550808a563..ab7ac6624a8 100644 --- a/lib/api/import_github.rb +++ b/lib/api/import_github.rb @@ -20,7 +20,10 @@ module API end def access_params - { github_access_token: params[:personal_access_token] } + { + github_access_token: params[:personal_access_token], + additional_access_tokens: params[:additional_access_tokens] + } end def client_options @@ -59,6 +62,11 @@ module API requires :target_namespace, type: String, allow_blank: false, desc: 'Namespace or group to import repository into' optional :github_hostname, type: String, desc: 'Custom GitHub enterprise hostname' optional :optional_stages, type: Hash, desc: 'Optional stages of import to be performed' + optional :additional_access_tokens, + type: Array[String], + coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, + desc: 'Additional list of personal access tokens', + documentation: { example: 'foo,bar' } end post 'import/github' do result = Import::GithubService.new(client, current_user, params).execute(access_params, provider) diff --git a/lib/gitlab/github_import.rb b/lib/gitlab/github_import.rb index 9556a9e98ba..24e77363e1b 100644 --- a/lib/gitlab/github_import.rb +++ b/lib/gitlab/github_import.rb @@ -8,12 +8,18 @@ module Gitlab def self.new_client_for(project, token: nil, host: nil, parallel: true) token_to_use = token || project.import_data&.credentials&.fetch(:user) - Client.new( - token_to_use, + token_pool = project.import_data&.credentials&.dig(:additional_access_tokens) + options = { host: host.presence || self.formatted_import_url(project), per_page: self.per_page(project), parallel: parallel - ) + } + + if token_pool + ClientPool.new(token_pool: token_pool, **options) + else + Client.new(token_to_use, **options) + end end # Returns the ID of the ghost user. diff --git a/lib/gitlab/github_import/client_pool.rb b/lib/gitlab/github_import/client_pool.rb new file mode 100644 index 00000000000..e8414942d1b --- /dev/null +++ b/lib/gitlab/github_import/client_pool.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Gitlab + module GithubImport + class ClientPool + delegate_missing_to :best_client + + def initialize(token_pool:, per_page:, parallel:, host: nil) + @token_pool = token_pool + @host = host + @per_page = per_page + @parallel = parallel + end + + # Returns the client with the most remaining requests, or the client with + # the closest rate limit reset time, if all clients are rate limited. + def best_client + clients_with_requests_remaining = clients.select(&:requests_remaining?) + + return clients_with_requests_remaining.max_by(&:remaining_requests) if clients_with_requests_remaining.any? + + clients.min_by(&:rate_limit_resets_in) + end + + private + + def clients + @clients ||= @token_pool.map do |token| + Client.new( + token, + host: @host, + per_page: @per_page, + parallel: @parallel + ) + end + end + end + end +end diff --git a/lib/gitlab/github_import/settings.rb b/lib/gitlab/github_import/settings.rb index 0b883de8ed0..73a5f49a9e3 100644 --- a/lib/gitlab/github_import/settings.rb +++ b/lib/gitlab/github_import/settings.rb @@ -56,8 +56,16 @@ module Gitlab def write(user_settings) user_settings = user_settings.to_h.with_indifferent_access - optional_stages = fetch_stages_from_params(user_settings) - import_data = project.create_or_update_import_data(data: { optional_stages: optional_stages }) + optional_stages = fetch_stages_from_params(user_settings[:optional_stages]) + credentials = project.import_data&.credentials&.merge( + additional_access_tokens: user_settings[:additional_access_tokens] + ) + + import_data = project.create_or_update_import_data( + data: { optional_stages: optional_stages }, + credentials: credentials + ) + import_data.save! end @@ -74,6 +82,8 @@ module Gitlab attr_reader :project def fetch_stages_from_params(user_settings) + user_settings = user_settings.to_h.with_indifferent_access + OPTIONAL_STAGES.keys.to_h do |stage_name| enabled = Gitlab::Utils.to_boolean(user_settings[stage_name], default: false) [stage_name, enabled] diff --git a/lib/gitlab/sidekiq_logging/structured_logger.rb b/lib/gitlab/sidekiq_logging/structured_logger.rb index 64f1ab0554a..56762c0fb4b 100644 --- a/lib/gitlab/sidekiq_logging/structured_logger.rb +++ b/lib/gitlab/sidekiq_logging/structured_logger.rb @@ -96,6 +96,12 @@ module Gitlab db_duration = ActiveRecord::LogSubscriber.runtime payload['db_duration_s'] = Gitlab::Utils.ms_to_round_sec(db_duration) + job_urgency = payload['class'].safe_constantize&.get_urgency.to_s + unless job_urgency.empty? + payload['urgency'] = job_urgency + payload['target_duration_s'] = Gitlab::Metrics::SidekiqSlis.execution_duration_for_urgency(job_urgency) + end + payload end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 6d2c46303bd..ea64c1b5b42 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -39533,6 +39533,9 @@ msgstr "" msgid "Runners|Created %{timeAgo} by %{avatar}" msgstr "" +msgid "Runners|Dashboard" +msgstr "" + msgid "Runners|Delete %d runner" msgid_plural "Runners|Delete %d runners" msgstr[0] "" diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb index a1e75a94326..03ec72980e5 100644 --- a/spec/features/invites_spec.rb +++ b/spec/features/invites_spec.rb @@ -232,7 +232,8 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_cate end context 'when the user signs up for an account with the invitation email address' do - it 'redirects to the most recent membership activity page with all invitations automatically accepted' do + it 'redirects to the most recent membership activity page with all invitations automatically accepted', + quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/417092' do fill_in_sign_up_form(new_user) fill_in_welcome_form diff --git a/spec/features/issues/service_desk_spec.rb b/spec/features/issues/service_desk_spec.rb index 0319eaad478..8b52a0c2ab0 100644 --- a/spec/features/issues/service_desk_spec.rb +++ b/spec/features/issues/service_desk_spec.rb @@ -194,7 +194,7 @@ RSpec.describe 'Service Desk Issue Tracker', :js, feature_category: :team_planni visit service_desk_project_issues_path(project) end - it 'only displays issues created by support bot' do + it 'only displays issues created by support bot', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/417200' do expect(page).to have_selector('.issues-list .issue', count: 1) expect(page).to have_text('Help from email') expect(page).not_to have_text('Unrelated issue') diff --git a/spec/frontend/vue_compat_test_setup.js b/spec/frontend/vue_compat_test_setup.js index 6eba9465c80..ddf75bcc017 100644 --- a/spec/frontend/vue_compat_test_setup.js +++ b/spec/frontend/vue_compat_test_setup.js @@ -76,9 +76,57 @@ if (global.document) { Vue.configureCompat(compatConfig); installVTUCompat(VTU, fullCompatConfig, compatH); + + jest.mock('vue', () => { + const actualVue = jest.requireActual('vue'); + actualVue.configureCompat(compatConfig); + return actualVue; + }); + + jest.mock('portal-vue', () => ({ + __esModule: true, + default: { + install: jest.fn(), + }, + Portal: {}, + PortalTarget: {}, + MountingPortal: { + template: '<h1>MOUNTING-PORTAL</h1>', + }, + Wormhole: {}, + })); + VTU.config.global.renderStubDefaultSlot = true; const noop = () => {}; + const invalidProperties = new Set(); + + const getDescriptor = (root, prop) => { + let obj = root; + while (obj != null) { + const desc = Object.getOwnPropertyDescriptor(obj, prop); + if (desc) { + return desc; + } + obj = Object.getPrototypeOf(obj); + } + return null; + }; + + const isPropertyValidOnDomNode = (prop) => { + if (invalidProperties.has(prop)) { + return false; + } + + const domNode = document.createElement('anonymous-stub'); + const descriptor = getDescriptor(domNode, prop); + if (descriptor && descriptor.get && !descriptor.set) { + invalidProperties.add(prop); + return false; + } + + return true; + }; VTU.config.plugins.createStubs = ({ name, component: rawComponent, registerStub }) => { const component = unwrapLegacyVueExtendComponent(rawComponent); @@ -126,7 +174,11 @@ if (global.document) { .filter(Boolean) : renderSlotByName('default'); - return Vue.h(`${hyphenatedName || 'anonymous'}-stub`, this.$props, slotContents); + const props = Object.fromEntries( + Object.entries(this.$props).filter(([prop]) => isPropertyValidOnDomNode(prop)), + ); + + return Vue.h(`${hyphenatedName || 'anonymous'}-stub`, props, slotContents); }, }); diff --git a/spec/graphql/types/ci/detailed_status_type_spec.rb b/spec/graphql/types/ci/detailed_status_type_spec.rb index 81ab1b52552..69fb2bc43c0 100644 --- a/spec/graphql/types/ci/detailed_status_type_spec.rb +++ b/spec/graphql/types/ci/detailed_status_type_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Types::Ci::DetailedStatusType do include GraphqlHelpers - let_it_be(:stage) { create(:ci_stage, status: :skipped) } + let_it_be(:stage) { create(:ci_stage, status: :manual) } specify { expect(described_class.graphql_name).to eq('DetailedStatus') } diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb index 35d44281072..702341a7ea7 100644 --- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Status::Stage::Factory do +RSpec.describe Gitlab::Ci::Status::Stage::Factory, feature_category: :continuous_integration do let(:user) { create(:user) } let(:project) { create(:project) } let(:pipeline) { create(:ci_empty_pipeline, project: project) } @@ -62,7 +62,7 @@ RSpec.describe Gitlab::Ci::Status::Stage::Factory do end context 'when stage has manual builds' do - (Ci::HasStatus::BLOCKED_STATUS + ['skipped']).each do |core_status| + Ci::HasStatus::BLOCKED_STATUS.each do |core_status| context "when status is #{core_status}" do let(:stage) { create(:ci_stage, pipeline: pipeline, status: core_status) } diff --git a/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb b/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb index 9fdaddc083e..e23645c106b 100644 --- a/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Status::Stage::PlayManual do +RSpec.describe Gitlab::Ci::Status::Stage::PlayManual, feature_category: :continuous_integration do let(:stage) { double('stage') } let(:play_manual) { described_class.new(stage) } @@ -48,7 +48,7 @@ RSpec.describe Gitlab::Ci::Status::Stage::PlayManual do context 'when stage is skipped' do let(:stage) { create(:ci_stage, status: :skipped) } - it { is_expected.to be_truthy } + it { is_expected.to be_falsy } end context 'when stage is manual' do diff --git a/spec/lib/gitlab/github_import/client_pool_spec.rb b/spec/lib/gitlab/github_import/client_pool_spec.rb new file mode 100644 index 00000000000..aabb47c2cf1 --- /dev/null +++ b/spec/lib/gitlab/github_import/client_pool_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::ClientPool, feature_category: :importers do + subject(:pool) { described_class.new(token_pool: %w[foo bar], per_page: 1, parallel: true) } + + describe '#best_client' do + it 'returns the client with the most remaining requests' do + allow(Gitlab::GithubImport::Client).to receive(:new).and_return( + instance_double( + Gitlab::GithubImport::Client, + requests_remaining?: true, remaining_requests: 10, rate_limit_resets_in: 1 + ), + instance_double( + Gitlab::GithubImport::Client, + requests_remaining?: true, remaining_requests: 20, rate_limit_resets_in: 2 + ) + ) + + expect(pool.best_client.remaining_requests).to eq(20) + end + + context 'when all clients are rate limited' do + it 'returns the client with the closest rate limit reset time' do + allow(Gitlab::GithubImport::Client).to receive(:new).and_return( + instance_double( + Gitlab::GithubImport::Client, + requests_remaining?: false, remaining_requests: 10, rate_limit_resets_in: 10 + ), + instance_double( + Gitlab::GithubImport::Client, + requests_remaining?: false, remaining_requests: 20, rate_limit_resets_in: 20 + ) + ) + + expect(pool.best_client.rate_limit_resets_in).to eq(10) + end + end + end +end diff --git a/spec/lib/gitlab/github_import/settings_spec.rb b/spec/lib/gitlab/github_import/settings_spec.rb index 43e096863b8..d670aaea482 100644 --- a/spec/lib/gitlab/github_import/settings_spec.rb +++ b/spec/lib/gitlab/github_import/settings_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::GithubImport::Settings do +RSpec.describe Gitlab::GithubImport::Settings, feature_category: :importers do subject(:settings) { described_class.new(project) } let_it_be(:project) { create(:project) } @@ -55,19 +55,26 @@ RSpec.describe Gitlab::GithubImport::Settings do describe '#write' do let(:data_input) do { - single_endpoint_issue_events_import: true, - single_endpoint_notes_import: 'false', - attachments_import: nil, - collaborators_import: false, - foo: :bar + optional_stages: { + single_endpoint_issue_events_import: true, + single_endpoint_notes_import: 'false', + attachments_import: nil, + collaborators_import: false, + foo: :bar + }, + additional_access_tokens: %w[foo bar] }.stringify_keys end - it 'puts optional steps flags into projects import_data' do + it 'puts optional steps & access tokens into projects import_data' do + project.create_or_update_import_data(credentials: { user: 'token' }) + settings.write(data_input) expect(project.import_data.data['optional_stages']) .to eq optional_stages.stringify_keys + expect(project.import_data.credentials.fetch(:additional_access_tokens)) + .to eq(data_input['additional_access_tokens']) end end diff --git a/spec/lib/gitlab/github_import_spec.rb b/spec/lib/gitlab/github_import_spec.rb index 1ea9f003098..c4ed4b09f04 100644 --- a/spec/lib/gitlab/github_import_spec.rb +++ b/spec/lib/gitlab/github_import_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::GithubImport do +RSpec.describe Gitlab::GithubImport, feature_category: :importers do before do stub_feature_flags(github_importer_lower_per_page_limit: false) end @@ -11,6 +11,8 @@ RSpec.describe Gitlab::GithubImport do let(:project) { double(:project, import_url: 'http://t0ken@github.com/user/repo.git', id: 1, group: nil) } it 'returns a new Client with a custom token' do + allow(project).to receive(:import_data) + expect(described_class::Client) .to receive(:new) .with('123', host: nil, parallel: true, per_page: 100) @@ -24,6 +26,7 @@ RSpec.describe Gitlab::GithubImport do expect(project) .to receive(:import_data) .and_return(import_data) + .twice expect(described_class::Client) .to receive(:new) @@ -46,12 +49,31 @@ RSpec.describe Gitlab::GithubImport do described_class.ghost_user_id end end + + context 'when there are additional access tokens' do + it 'returns a new ClientPool containing all tokens' do + import_data = double(:import_data, credentials: { user: '123', additional_access_tokens: %w[foo bar] }) + + expect(project) + .to receive(:import_data) + .and_return(import_data) + .twice + + expect(described_class::ClientPool) + .to receive(:new) + .with(token_pool: %w[foo bar], host: nil, parallel: true, per_page: 100) + + described_class.new_client_for(project) + end + end end context 'GitHub Enterprise' do let(:project) { double(:project, import_url: 'http://t0ken@github.another-domain.com/repo-org/repo.git', group: nil) } it 'returns a new Client with a custom token' do + allow(project).to receive(:import_data) + expect(described_class::Client) .to receive(:new) .with('123', host: 'http://github.another-domain.com/api/v3', parallel: true, per_page: 100) @@ -65,6 +87,7 @@ RSpec.describe Gitlab::GithubImport do expect(project) .to receive(:import_data) .and_return(import_data) + .twice expect(described_class::Client) .to receive(:new) diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb index e1c7cf3226b..4e46a26e89f 100644 --- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb +++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb @@ -456,6 +456,28 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do end end end + + context 'with a real worker' do + let(:worker_class) { AuthorizedKeysWorker.name } + + let(:expected_end_payload) do + end_payload.merge( + 'urgency' => 'high', + 'target_duration_s' => 10 + ) + end + + it 'logs job done with urgency and target_duration_s fields' do + travel_to(timestamp) do + expect(logger).to receive(:info).with(start_payload).ordered + expect(logger).to receive(:info).with(expected_end_payload).ordered + expect(subject).to receive(:log_job_start).and_call_original + expect(subject).to receive(:log_job_done).and_call_original + + call_subject(job, 'test_queue') {} + end + end + end end describe '#add_time_keys!' do diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 372808b64d3..76771360e1f 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -1572,16 +1572,22 @@ RSpec.describe Notify do context 'when custom email is enabled' do let_it_be(:credentials) { create(:service_desk_custom_email_credential, project: project) } + let_it_be(:verification) { create(:service_desk_custom_email_verification, project: project) } let_it_be(:settings) do create( :service_desk_setting, project: project, - custom_email_enabled: true, custom_email: 'supersupport@example.com' ) end + before_all do + verification.mark_as_finished! + project.reset + settings.update!(custom_email_enabled: true) + end + it 'uses custom email and service bot name in "from" header' do expect_sender(User.support_bot, sender_email: 'supersupport@example.com') end @@ -1630,22 +1636,23 @@ RSpec.describe Notify do end context 'when custom email is enabled' do - let_it_be(:credentials) do - create( - :service_desk_custom_email_credential, - project: project - ) - end + let_it_be(:credentials) { create(:service_desk_custom_email_credential, project: project) } + let_it_be(:verification) { create(:service_desk_custom_email_verification, project: project) } let_it_be(:settings) do create( :service_desk_setting, project: project, - custom_email_enabled: true, custom_email: 'supersupport@example.com' ) end + before_all do + verification.mark_as_finished! + project.reset + settings.update!(custom_email_enabled: true) + end + it 'uses custom email and author\'s name in "from" header' do expect_sender(first_note.author, sender_email: project.service_desk_setting.custom_email) end diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index 79e92082ee1..1be50083cd4 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::Stage, :models do +RSpec.describe Ci::Stage, :models, feature_category: :continuous_integration do let_it_be(:pipeline) { create(:ci_empty_pipeline) } let(:stage) { create(:ci_stage, pipeline: pipeline, project: pipeline.project) } diff --git a/spec/models/service_desk_setting_spec.rb b/spec/models/service_desk_setting_spec.rb index b9679b82bd0..34165fc2bf3 100644 --- a/spec/models/service_desk_setting_spec.rb +++ b/spec/models/service_desk_setting_spec.rb @@ -3,12 +3,9 @@ require 'spec_helper' RSpec.describe ServiceDeskSetting, feature_category: :service_desk do - let(:verification) { build(:service_desk_custom_email_verification) } - let(:project) { build(:project) } + subject(:setting) { build(:service_desk_setting) } describe 'validations' do - subject(:service_desk_setting) { create(:service_desk_setting) } - it { is_expected.to validate_presence_of(:project_id) } it { is_expected.to validate_length_of(:outgoing_name).is_at_most(255) } it { is_expected.to validate_length_of(:project_key).is_at_most(255) } @@ -18,14 +15,56 @@ RSpec.describe ServiceDeskSetting, feature_category: :service_desk do it { is_expected.to validate_length_of(:custom_email).is_at_most(255) } describe '#custom_email_enabled' do - it { expect(subject.custom_email_enabled).to be_falsey } + it { expect(setting.custom_email_enabled).to be_falsey } it { expect(described_class.new(custom_email_enabled: true).custom_email_enabled).to be_truthy } + + context 'when set to true' do + let(:expected_error_part) { 'cannot be enabled until verification process has finished.' } + + before do + setting.custom_email = 'user@example.com' + setting.custom_email_enabled = true + end + + it 'is not valid' do + is_expected.not_to be_valid + expect(setting.errors[:custom_email_enabled].join).to include(expected_error_part) + end + + context 'when custom email records exist' do + let_it_be(:project) { create(:project) } + let_it_be(:credential) { create(:service_desk_custom_email_credential, project: project) } + + let!(:verification) { create(:service_desk_custom_email_verification, project: project) } + + subject(:setting) { build_stubbed(:service_desk_setting, project: project) } + + before do + project.reset + end + + context 'when custom email verification started' do + it 'is not valid' do + is_expected.not_to be_valid + expect(setting.errors[:custom_email_enabled].join).to include(expected_error_part) + end + end + + context 'when custom email verification has been finished' do + before do + verification.mark_as_finished! + end + + it { is_expected.to be_valid } + end + end + end end context 'when custom_email_enabled is true' do before do # Test without ServiceDesk::CustomEmailVerification for simplicity - subject.custom_email_enabled = true + setting.custom_email_enabled = true end it { is_expected.to validate_presence_of(:custom_email) } @@ -66,13 +105,13 @@ RSpec.describe ServiceDeskSetting, feature_category: :service_desk do describe '#custom_email_address_for_verification' do it 'returns nil' do - expect(subject.custom_email_address_for_verification).to be_nil + expect(setting.custom_email_address_for_verification).to be_nil end context 'when custom_email exists' do it 'returns correct verification address' do - subject.custom_email = 'support@example.com' - expect(subject.custom_email_address_for_verification).to eq('support+verify@example.com') + setting.custom_email = 'support@example.com' + expect(setting.custom_email_address_for_verification).to eq('support+verify@example.com') end end end @@ -114,6 +153,8 @@ RSpec.describe ServiceDeskSetting, feature_category: :service_desk do end describe 'associations' do + let(:project) { build(:project) } + let(:verification) { build(:service_desk_custom_email_verification) } let(:custom_email_settings) do build_stubbed( :service_desk_setting, diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb index e776716bd2d..150c7bd5f3e 100644 --- a/spec/presenters/blob_presenter_spec.rb +++ b/spec/presenters/blob_presenter_spec.rb @@ -7,28 +7,52 @@ RSpec.describe BlobPresenter do let_it_be(:user) { project.first_owner } let(:repository) { project.repository } - let(:blob) { repository.blob_at('HEAD', 'files/ruby/regex.rb') } + let(:blob) { repository.blob_at(ref, path) } + let(:ref) { 'HEAD' } + let(:path) { 'files/ruby/regex.rb' } subject(:presenter) { described_class.new(blob, current_user: user) } describe '#web_url' do - it { expect(presenter.web_url).to eq("http://localhost/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}") } + it { expect(presenter.web_url).to eq("http://localhost/#{project.full_path}/-/blob/#{ref}/#{path}") } end describe '#web_path' do - it { expect(presenter.web_path).to eq("/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}") } + it { expect(presenter.web_path).to eq("/#{project.full_path}/-/blob/#{ref}/#{path}") } end describe '#edit_blob_path' do - it { expect(presenter.edit_blob_path).to eq("/#{project.full_path}/-/edit/#{blob.commit_id}/#{blob.path}") } + it { expect(presenter.edit_blob_path).to eq("/#{project.full_path}/-/edit/#{ref}/#{path}") } end describe '#raw_path' do - it { expect(presenter.raw_path).to eq("/#{project.full_path}/-/raw/#{blob.commit_id}/#{blob.path}") } + it { expect(presenter.raw_path).to eq("/#{project.full_path}/-/raw/#{ref}/#{path}") } end describe '#replace_path' do - it { expect(presenter.replace_path).to eq("/#{project.full_path}/-/update/#{blob.commit_id}/#{blob.path}") } + it { expect(presenter.replace_path).to eq("/#{project.full_path}/-/update/#{ref}/#{path}") } + end + + shared_examples_for '#can_current_user_push_to_branch?' do + let(:branch_exists) { true } + + before do + allow(project.repository).to receive(:branch_exists?).with(blob.commit_id).and_return(branch_exists) + end + + it { expect(presenter.can_current_user_push_to_branch?).to eq(true) } + + context 'current_user is nil' do + let(:user) { nil } + + it { expect(presenter.can_current_user_push_to_branch?).to eq(false) } + end + + context 'branch does not exist' do + let(:branch_exists) { false } + + it { expect(presenter.can_current_user_push_to_branch?).to eq(false) } + end end context 'when blob has ref_type' do @@ -37,46 +61,67 @@ RSpec.describe BlobPresenter do end describe '#web_url' do - it { expect(presenter.web_url).to eq("http://localhost/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}?ref_type=heads") } + it { expect(presenter.web_url).to eq("http://localhost/#{project.full_path}/-/blob/#{ref}/#{path}?ref_type=heads") } end describe '#web_path' do - it { expect(presenter.web_path).to eq("/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}?ref_type=heads") } + it { expect(presenter.web_path).to eq("/#{project.full_path}/-/blob/#{ref}/#{path}?ref_type=heads") } end describe '#edit_blob_path' do - it { expect(presenter.edit_blob_path).to eq("/#{project.full_path}/-/edit/#{blob.commit_id}/#{blob.path}?ref_type=heads") } + it { expect(presenter.edit_blob_path).to eq("/#{project.full_path}/-/edit/#{ref}/#{path}?ref_type=heads") } end describe '#raw_path' do - it { expect(presenter.raw_path).to eq("/#{project.full_path}/-/raw/#{blob.commit_id}/#{blob.path}?ref_type=heads") } + it { expect(presenter.raw_path).to eq("/#{project.full_path}/-/raw/#{ref}/#{path}?ref_type=heads") } end describe '#replace_path' do - it { expect(presenter.replace_path).to eq("/#{project.full_path}/-/update/#{blob.commit_id}/#{blob.path}?ref_type=heads") } + it { expect(presenter.replace_path).to eq("/#{project.full_path}/-/update/#{ref}/#{path}?ref_type=heads") } end + + it_behaves_like '#can_current_user_push_to_branch?' end - describe '#can_current_user_push_to_branch' do - let(:branch_exists) { true } + describe '#can_modify_blob?' do + context 'when blob is store externally' do + before do + allow(blob).to receive(:stored_externally?).and_return(true) + end - before do - allow(project.repository).to receive(:branch_exists?).with(blob.commit_id).and_return(branch_exists) + it { expect(presenter.can_modify_blob?).to be_falsey } end - it { expect(presenter.can_current_user_push_to_branch?).to eq(true) } + context 'when the user cannot edit the tree' do + before do + allow(presenter).to receive(:can_edit_tree?).with(project, ref).and_return(false) + end - context 'current_user is nil' do - let(:user) { nil } + it { expect(presenter.can_modify_blob?).to be_falsey } + end - it { expect(presenter.can_current_user_push_to_branch?).to eq(false) } + context 'when ref is a branch' do + let(:ref) { 'feature' } + + it { expect(presenter.can_modify_blob?).to be_truthy } end + end - context 'branch does not exist' do - let(:branch_exists) { false } + describe '#can_current_user_push_to_branch?' do + context 'when ref is a branch' do + let(:ref) { 'feature' } - it { expect(presenter.can_current_user_push_to_branch?).to eq(false) } + it 'delegates to UserAccess' do + allow_next_instance_of(Gitlab::UserAccess) do |instance| + expect(instance).to receive(:can_push_to_branch?).with(ref).and_call_original + end + expect(presenter.can_current_user_push_to_branch?).to be_truthy + end end + + it_behaves_like '#can_current_user_push_to_branch?' + + it { expect(presenter.can_current_user_push_to_branch?).to be_falsey } end describe '#archived?' do @@ -95,9 +140,10 @@ RSpec.describe BlobPresenter do ) end - let(:blob) { repository.blob_at('main', '.gitlab-ci.yml') } + let(:ref) { 'main' } + let(:path) { '.gitlab-ci.yml' } - it { expect(presenter.pipeline_editor_path).to eq("/#{project.full_path}/-/ci/editor?branch_name=#{blob.commit_id}") } + it { expect(presenter.pipeline_editor_path).to eq("/#{project.full_path}/-/ci/editor?branch_name=#{ref}") } end end @@ -114,7 +160,7 @@ RSpec.describe BlobPresenter do context 'Gitpod enabled for application and user' do describe '#gitpod_blob_url' do - it { expect(presenter.gitpod_blob_url).to eq("#{gitpod_url}##{"http://localhost/#{project.full_path}/-/tree/#{blob.commit_id}/#{blob.path}"}") } + it { expect(presenter.gitpod_blob_url).to eq("#{gitpod_url}##{"http://localhost/#{project.full_path}/-/tree/#{ref}/#{path}"}") } end end @@ -157,7 +203,7 @@ RSpec.describe BlobPresenter do let!(:deployment) { create(:deployment, :success, environment: environment, project: project, sha: blob.commit_id) } before do - allow(project).to receive(:public_path_for_source_path).with(blob.path, blob.commit_id).and_return(blob.path) + allow(project).to receive(:public_path_for_source_path).with(path, blob.commit_id).and_return(path) end describe '#environment_formatted_external_url' do @@ -165,7 +211,7 @@ RSpec.describe BlobPresenter do end describe '#environment_external_url_for_route_map' do - it { expect(presenter.environment_external_url_for_route_map).to eq("#{external_url}/#{blob.path}") } + it { expect(presenter.environment_external_url_for_route_map).to eq("#{external_url}/#{path}") } end describe 'chooses the latest deployed environment for #environment_formatted_external_url and #environment_external_url_for_route_map' do @@ -174,7 +220,7 @@ RSpec.describe BlobPresenter do let!(:another_deployment) { create(:deployment, :success, environment: another_environment, project: project, sha: blob.commit_id) } it { expect(presenter.environment_formatted_external_url).to eq("another.environment") } - it { expect(presenter.environment_external_url_for_route_map).to eq("#{another_external_url}/#{blob.path}") } + it { expect(presenter.environment_external_url_for_route_map).to eq("#{another_external_url}/#{path}") } end end @@ -219,7 +265,7 @@ RSpec.describe BlobPresenter do end describe '#code_navigation_path' do - let(:code_navigation_path) { Gitlab::CodeNavigationPath.new(project, blob.commit_id).full_json_path_for(blob.path) } + let(:code_navigation_path) { Gitlab::CodeNavigationPath.new(project, blob.commit_id).full_json_path_for(path) } it { expect(presenter.code_navigation_path).to eq(code_navigation_path) } end @@ -232,11 +278,11 @@ RSpec.describe BlobPresenter do let(:blob) { Gitlab::Graphql::Representation::TreeEntry.new(super(), repository) } describe '#web_url' do - it { expect(presenter.web_url).to eq("http://localhost/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}") } + it { expect(presenter.web_url).to eq("http://localhost/#{project.full_path}/-/blob/#{ref}/#{path}") } end describe '#web_path' do - it { expect(presenter.web_path).to eq("/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}") } + it { expect(presenter.web_path).to eq("/#{project.full_path}/-/blob/#{ref}/#{path}") } end end diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb index 9b5ae72526c..e394b92c0a2 100644 --- a/spec/requests/api/import_github_spec.rb +++ b/spec/requests/api/import_github_spec.rb @@ -5,7 +5,8 @@ require 'spec_helper' RSpec.describe API::ImportGithub, feature_category: :importers do let(:token) { "asdasd12345" } let(:provider) { :github } - let(:access_params) { { github_access_token: token } } + let(:access_params) { { github_access_token: token, additional_access_tokens: additional_access_tokens } } + let(:additional_access_tokens) { nil } let(:provider_username) { user.username } let(:provider_user) { double('provider', login: provider_username).as_null_object } let(:provider_repo) do @@ -51,7 +52,7 @@ RSpec.describe API::ImportGithub, feature_category: :importers do it 'returns 201 response when the project is imported successfully' do allow(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo[:name], user.namespace, user, type: provider, **access_params) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post api("/import/github", user), params: { target_namespace: user.namespace_path, @@ -120,6 +121,28 @@ RSpec.describe API::ImportGithub, feature_category: :importers do expect(response).to have_gitlab_http_status(:unauthorized) end end + + context 'when additional access tokens are provided' do + let(:additional_access_tokens) { 'token1,token2' } + + it 'returns 201' do + expected_access_params = { github_access_token: token, additional_access_tokens: %w[token1 token2] } + + expect(Gitlab::LegacyGithubImport::ProjectCreator) + .to receive(:new) + .with(provider_repo, provider_repo[:name], user.namespace, user, type: provider, **expected_access_params) + .and_return(double(execute: project)) + + post api("/import/github", user), params: { + target_namespace: user.namespace_path, + personal_access_token: token, + repo_id: non_existing_record_id, + additional_access_tokens: 'token1,token2' + } + + expect(response).to have_gitlab_http_status(:created) + end + end end describe "POST /import/github/cancel" do diff --git a/spec/serializers/stage_entity_spec.rb b/spec/serializers/stage_entity_spec.rb index 5cb5724ebdc..fe8ee027245 100644 --- a/spec/serializers/stage_entity_spec.rb +++ b/spec/serializers/stage_entity_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe StageEntity do +RSpec.describe StageEntity, feature_category: :continuous_integration do let(:pipeline) { create(:ci_pipeline) } let(:request) { double('request') } let(:user) { create(:user) } @@ -76,8 +76,8 @@ RSpec.describe StageEntity do context 'with a skipped stage ' do let(:stage) { create(:ci_stage, status: 'skipped') } - it 'contains play_all_manual' do - expect(subject[:status][:action]).to be_present + it 'does not contain play_all_manual' do + expect(subject[:status][:action]).not_to be_present end end diff --git a/spec/services/import/github_service_spec.rb b/spec/services/import/github_service_spec.rb index 21dc24e28f6..982b8b11383 100644 --- a/spec/services/import/github_service_spec.rb +++ b/spec/services/import/github_service_spec.rb @@ -5,7 +5,13 @@ require 'spec_helper' RSpec.describe Import::GithubService, feature_category: :importers do let_it_be(:user) { create(:user) } let_it_be(:token) { 'complex-token' } - let_it_be(:access_params) { { github_access_token: 'github-complex-token' } } + let_it_be(:access_params) do + { + github_access_token: 'github-complex-token', + additional_access_tokens: %w[foo bar] + } + end + let(:settings) { instance_double(Gitlab::GithubImport::Settings) } let(:user_namespace_path) { user.namespace_path } let(:optional_stages) { nil } @@ -26,7 +32,12 @@ RSpec.describe Import::GithubService, feature_category: :importers do before do allow(Gitlab::GithubImport::Settings).to receive(:new).with(project_double).and_return(settings) - allow(settings).to receive(:write).with(optional_stages) + allow(settings) + .to receive(:write) + .with( + optional_stages: optional_stages, + additional_access_tokens: access_params[:additional_access_tokens] + ) end context 'do not raise an exception on input error' do @@ -82,7 +93,9 @@ RSpec.describe Import::GithubService, feature_category: :importers do context 'when there is no repository size limit defined' do it 'skips the check, succeeds, and tracks an access level' do expect(subject.execute(access_params, :github)).to include(status: :success) - expect(settings).to have_received(:write).with(nil) + expect(settings) + .to have_received(:write) + .with(optional_stages: nil, additional_access_tokens: access_params[:additional_access_tokens]) expect_snowplow_event( category: 'Import::GithubService', action: 'create', @@ -102,7 +115,9 @@ RSpec.describe Import::GithubService, feature_category: :importers do it 'succeeds when the repository is smaller than the limit' do expect(subject.execute(access_params, :github)).to include(status: :success) - expect(settings).to have_received(:write).with(nil) + expect(settings) + .to have_received(:write) + .with(optional_stages: nil, additional_access_tokens: access_params[:additional_access_tokens]) expect_snowplow_event( category: 'Import::GithubService', action: 'create', @@ -129,7 +144,9 @@ RSpec.describe Import::GithubService, feature_category: :importers do context 'when application size limit is defined' do it 'succeeds when the repository is smaller than the limit' do expect(subject.execute(access_params, :github)).to include(status: :success) - expect(settings).to have_received(:write).with(nil) + expect(settings) + .to have_received(:write) + .with(optional_stages: nil, additional_access_tokens: access_params[:additional_access_tokens]) expect_snowplow_event( category: 'Import::GithubService', action: 'create', @@ -160,7 +177,22 @@ RSpec.describe Import::GithubService, feature_category: :importers do it 'saves optional stages choice to import_data' do subject.execute(access_params, :github) - expect(settings).to have_received(:write).with(optional_stages) + expect(settings) + .to have_received(:write) + .with( + optional_stages: optional_stages, + additional_access_tokens: access_params[:additional_access_tokens] + ) + end + end + + context 'when additional access tokens are present' do + it 'saves additional access tokens to import_data' do + subject.execute(access_params, :github) + + expect(settings) + .to have_received(:write) + .with(optional_stages: optional_stages, additional_access_tokens: %w[foo bar]) end end end diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index 0f91ff5f12c..9197f2ec236 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -166,7 +166,6 @@ - './ee/spec/controllers/security/vulnerabilities_controller_spec.rb' - './ee/spec/controllers/sitemap_controller_spec.rb' - './ee/spec/controllers/subscriptions_controller_spec.rb' -- './ee/spec/controllers/subscriptions/groups_controller_spec.rb' - './ee/spec/controllers/trial_registrations_controller_spec.rb' - './ee/spec/controllers/users_controller_spec.rb' - './ee/spec/db/production/license_spec.rb' diff --git a/spec/support/shared_contexts/lib/gitlab/sidekiq_logging/structured_logger_shared_context.rb b/spec/support/shared_contexts/lib/gitlab/sidekiq_logging/structured_logger_shared_context.rb index f1dce9fa8cd..69c20a00c5a 100644 --- a/spec/support/shared_contexts/lib/gitlab/sidekiq_logging/structured_logger_shared_context.rb +++ b/spec/support/shared_contexts/lib/gitlab/sidekiq_logging/structured_logger_shared_context.rb @@ -4,10 +4,11 @@ RSpec.shared_context 'structured_logger' do let(:timestamp) { Time.iso8601('2018-01-01T12:00:00.000Z') } let(:created_at) { timestamp - 1.second } let(:scheduling_latency_s) { 1.0 } + let(:worker_class) { "TestWorker" } let(:job) do { - "class" => "TestWorker", + "class" => worker_class, "args" => [1234, 'hello', { 'key' => 'value' }], "retry" => false, "queue" => "cronjob:test_queue", @@ -31,7 +32,7 @@ RSpec.shared_context 'structured_logger' do job.except( 'exception.backtrace', 'exception.class', 'exception.message' ).merge( - 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: start', + 'message' => "#{worker_class} JID-da883554ee4fe414012f5f42: start", 'job_status' => 'start', 'pid' => Process.pid, 'created_at' => created_at.to_f, @@ -55,7 +56,7 @@ RSpec.shared_context 'structured_logger' do let(:end_payload) do start_payload.merge(db_payload_defaults).merge( - 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: done: 0.0 sec', + 'message' => "#{worker_class} JID-da883554ee4fe414012f5f42: done: 0.0 sec", 'job_status' => 'done', 'duration_s' => 0.0, 'completed_at' => timestamp.to_f, @@ -67,7 +68,7 @@ RSpec.shared_context 'structured_logger' do let(:deferred_payload) do end_payload.merge( - 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: deferred: 0.0 sec', + 'message' => "#{worker_class} JID-da883554ee4fe414012f5f42: deferred: 0.0 sec", 'job_status' => 'deferred', 'job_deferred_by' => :feature_flag, 'deferred_count' => 1 @@ -83,7 +84,7 @@ RSpec.shared_context 'structured_logger' do let(:exception_payload) do end_payload.merge( - 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: fail: 0.0 sec', + 'message' => "#{worker_class} JID-da883554ee4fe414012f5f42: fail: 0.0 sec", 'job_status' => 'fail', 'exception.class' => 'ArgumentError', 'exception.message' => 'Something went wrong', diff --git a/spec/support/shared_examples/ci/stage_shared_examples.rb b/spec/support/shared_examples/ci/stage_shared_examples.rb index a2849e00d27..cdb1058e584 100644 --- a/spec/support/shared_examples/ci/stage_shared_examples.rb +++ b/spec/support/shared_examples/ci/stage_shared_examples.rb @@ -21,7 +21,7 @@ RSpec.shared_examples 'manual playable stage' do |stage_type| context 'when is skipped' do let(:status) { 'skipped' } - it { is_expected.to be_truthy } + it { is_expected.to be_falsy } end end end diff --git a/spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb index 2945bcbe641..e385a5aaf3f 100644 --- a/spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb +++ b/spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportAttachmentsWorker, feature_cat let(:stage_enabled) { true } before do - settings.write({ attachments_import: stage_enabled }) + settings.write({ optional_stages: { attachments_import: stage_enabled } }) end describe '#import' do diff --git a/spec/workers/gitlab/github_import/stage/import_collaborators_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_collaborators_worker_spec.rb index 33ecf848997..808f6e827ed 100644 --- a/spec/workers/gitlab/github_import/stage/import_collaborators_worker_spec.rb +++ b/spec/workers/gitlab/github_import/stage/import_collaborators_worker_spec.rb @@ -16,7 +16,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportCollaboratorsWorker, feature_c let(:push_rights_granted) { true } before do - settings.write({ collaborators_import: stage_enabled }) + settings.write({ optional_stages: { collaborators_import: stage_enabled } }) allow(client).to receive(:repository).with(project.import_source) .and_return({ permissions: { push: push_rights_granted } }) end diff --git a/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb index c70ee0250e8..7b0cf77bbbe 100644 --- a/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb +++ b/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportIssueEventsWorker, feature_cat let(:stage_enabled) { true } before do - settings.write({ single_endpoint_issue_events_import: stage_enabled }) + settings.write({ optional_stages: { single_endpoint_issue_events_import: stage_enabled } }) end describe '#import' do diff --git a/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb index 872201ece93..188cf3530f7 100644 --- a/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb +++ b/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportIssuesAndDiffNotesWorker, feat let(:single_endpoint_optional_stage) { true } before do - settings.write({ single_endpoint_notes_import: single_endpoint_optional_stage }) + settings.write({ optional_stages: { single_endpoint_notes_import: single_endpoint_optional_stage } }) end describe '#import' do diff --git a/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb index 8c0004c0f3f..dcceeb1d6c2 100644 --- a/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb +++ b/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportNotesWorker, feature_category: let(:single_endpoint_optional_stage) { true } before do - settings.write({ single_endpoint_notes_import: single_endpoint_optional_stage }) + settings.write({ optional_stages: { single_endpoint_notes_import: single_endpoint_optional_stage } }) end describe '#import' do |