Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--app/controllers/concerns/service_params.rb2
-rw-r--r--app/controllers/projects/branches_controller.rb3
-rw-r--r--app/graphql/resolvers/blobs_resolver.rb2
-rw-r--r--app/graphql/resolvers/ci/test_suite_resolver.rb40
-rw-r--r--app/graphql/types/ci/pipeline_type.rb6
-rw-r--r--app/graphql/types/ci/recent_failures_type.rb20
-rw-r--r--app/graphql/types/ci/test_case_status_enum.rb15
-rw-r--r--app/graphql/types/ci/test_case_type.rb41
-rw-r--r--app/graphql/types/ci/test_suite_type.rb41
-rw-r--r--app/graphql/types/repository/blob_type.rb40
-rw-r--r--app/graphql/types/repository_type.rb2
-rw-r--r--app/helpers/branches_helper.rb4
-rw-r--r--app/helpers/ci/pipelines_helper.rb66
-rw-r--r--app/helpers/projects_helper.rb8
-rw-r--r--app/helpers/whats_new_helper.rb4
-rw-r--r--app/models/ci/pipeline.rb6
-rw-r--r--app/models/commit_status.rb6
-rw-r--r--app/models/concerns/issuable.rb8
-rw-r--r--app/models/concerns/milestoneable.rb4
-rw-r--r--app/models/concerns/sidebars/container_with_html_options.rb7
-rw-r--r--app/models/concerns/vulnerability_finding_helpers.rb7
-rw-r--r--app/models/concerns/vulnerability_finding_signature_helpers.rb7
-rw-r--r--app/models/pages_deployment.rb2
-rw-r--r--app/models/sidebars/projects/menus/project_overview/menu.rb45
-rw-r--r--app/models/sidebars/projects/menus/project_overview/menu_items/activity.rb35
-rw-r--r--app/models/sidebars/projects/menus/project_overview/menu_items/details.rb36
-rw-r--r--app/models/sidebars/projects/menus/project_overview/menu_items/releases.rb40
-rw-r--r--app/models/sidebars/projects/panel.rb2
-rw-r--r--app/services/ci/process_pipeline_service.rb2
-rw-r--r--app/services/issuable/bulk_update_service.rb13
-rw-r--r--app/services/merge_requests/update_service.rb56
-rw-r--r--app/services/namespaces/in_product_marketing_emails_service.rb8
-rw-r--r--app/services/pages/migrate_from_legacy_storage_service.rb38
-rw-r--r--app/views/layouts/header/_default.html.haml3
-rw-r--r--app/views/layouts/header/_whats_new_dropdown_item.html.haml11
-rw-r--r--app/views/layouts/nav/sidebar/_project_menus.html.haml26
-rw-r--r--app/views/projects/branches/index.html.haml20
-rw-r--r--app/views/shared/nav/_sidebar.html.haml2
-rw-r--r--app/views/shared/nav/_sidebar_menu.html.haml27
-rw-r--r--app/views/shared/nav/_sidebar_menu_item.html.haml8
-rw-r--r--changelogs/unreleased/21068-optimize-issueable-updates.yml5
-rw-r--r--changelogs/unreleased/300121-fix-jenkins-ce.yml5
-rw-r--r--changelogs/unreleased/322001-poc-for-migrating-pages-to-zip-storage-in-the-background.yml5
-rw-r--r--changelogs/unreleased/325285-rake-pages-deployments.yml5
-rw-r--r--changelogs/unreleased/be-test-suite-graphql.yml5
-rw-r--r--changelogs/unreleased/jivanvl-remove-gldropdown-branches-ff.yml5
-rw-r--r--changelogs/unreleased/jswain_whats_new_self_managed_authenticated.yml5
-rw-r--r--changelogs/unreleased/kerrizor-use-specialized-service-for-assignee-updates.yml5
-rw-r--r--changelogs/unreleased/qmnguyen0711-add-queue-to-background-transaction.yml5
-rw-r--r--changelogs/unreleased/upgrade-pages-1-38.yml5
-rw-r--r--config/feature_flags/development/load_balancing_atomic_replica.yml (renamed from config/feature_flags/development/gldropdown_branches.yml)10
-rw-r--r--db/post_migrate/20210302150310_schedule_migrate_pages_to_zip_storage.rb36
-rw-r--r--db/schema_migrations/202103021503101
-rw-r--r--doc/administration/whats-new.md29
-rw-r--r--doc/api/api_resources.md3
-rw-r--r--doc/api/graphql/reference/index.md112
-rw-r--r--doc/api/usage_data.md56
-rw-r--r--doc/development/usage_ping/index.md31
-rw-r--r--doc/integration/jira/connect-app.md2
-rw-r--r--doc/integration/jira/dvcs.md19
-rw-r--r--doc/user/clusters/agent/index.md34
-rw-r--r--doc/user/project/integrations/webhooks.md1
-rw-r--r--doc/user/project/merge_requests/creating_merge_requests.md21
-rw-r--r--doc/user/project/settings/index.md1
-rw-r--r--lib/gitlab/background_migration/migrate_pages_to_zip_storage.rb19
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb7
-rw-r--r--lib/gitlab/ci/pipeline/chain/helpers.rb30
-rw-r--r--lib/gitlab/ci/pipeline/chain/metrics.rb2
-rw-r--r--lib/gitlab/ci/pipeline/metrics.rb72
-rw-r--r--lib/gitlab/database/background_migration/batched_migration.rb7
-rw-r--r--lib/gitlab/database/background_migration/batched_migration_wrapper.rb62
-rw-r--r--lib/gitlab/metrics/background_transaction.rb20
-rw-r--r--lib/gitlab/pages/migration_helper.rb53
-rw-r--r--lib/gitlab/usage_data_non_sql_metrics.rb6
-rw-r--r--lib/tasks/gitlab/pages.rake33
-rw-r--r--locale/gitlab.pot15
-rw-r--r--qa/qa/page/project/menu.rb7
-rw-r--r--qa/qa/page/project/sub_menus/project.rb6
-rw-r--r--spec/features/projects/branches/user_deletes_branch_spec.rb25
-rw-r--r--spec/features/projects/branches_spec.rb106
-rw-r--r--spec/features/protected_branches_spec.rb47
-rw-r--r--spec/features/whats_new_spec.rb62
-rw-r--r--spec/graphql/resolvers/ci/test_suite_resolver_spec.rb54
-rw-r--r--spec/graphql/types/ci/pipeline_type_spec.rb2
-rw-r--r--spec/graphql/types/ci/recent_failures_type_spec.rb15
-rw-r--r--spec/graphql/types/ci/test_case_status_enum_spec.rb13
-rw-r--r--spec/graphql/types/ci/test_case_type_spec.rb15
-rw-r--r--spec/graphql/types/ci/test_suite_type_spec.rb15
-rw-r--r--spec/graphql/types/repository/blob_type_spec.rb9
-rw-r--r--spec/helpers/branches_helper_spec.rb15
-rw-r--r--spec/helpers/whats_new_helper_spec.rb28
-rw-r--r--spec/lib/gitlab/background_migration/migrate_pages_to_zip_storage_spec.rb43
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/command_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb13
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb5
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_spec.rb13
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb65
-rw-r--r--spec/lib/gitlab/metrics/background_transaction_spec.rb51
-rw-r--r--spec/lib/gitlab/usage_data_non_sql_metrics_spec.rb18
-rw-r--r--spec/migrations/schedule_migrate_pages_to_zip_storage_spec.rb46
-rw-r--r--spec/models/bulk_imports/stage_spec.rb8
-rw-r--r--spec/models/ci/pipeline_spec.rb10
-rw-r--r--spec/models/commit_status_spec.rb17
-rw-r--r--spec/models/concerns/issuable_spec.rb17
-rw-r--r--spec/models/concerns/milestoneable_spec.rb14
-rw-r--r--spec/models/pages_deployment_spec.rb26
-rw-r--r--spec/models/sidebars/projects/menus/project_overview/menu_items/releases_spec.rb38
-rw-r--r--spec/models/sidebars/projects/menus/project_overview/menu_spec.rb18
-rw-r--r--spec/requests/api/graphql/project/pipeline_spec.rb47
-rw-r--r--spec/requests/api/graphql/project/repository/blobs_spec.rb36
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb19
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb52
-rw-r--r--spec/services/merge_requests/update_service_spec.rb34
-rw-r--r--spec/services/namespaces/in_product_marketing_emails_service_spec.rb46
-rw-r--r--spec/services/pages/migrate_from_legacy_storage_service_spec.rb159
-rw-r--r--spec/tasks/gitlab/pages_rake_spec.rb96
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb49
119 files changed, 2208 insertions, 621 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 9657f8ff598..8c745bb4253 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-e505e98295e765f945719f289125a98e671a7e19
+6904387a86815c80988d87f23af9d3fe1e2d4c85
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index bf50e910e62..ebeef2f2d61 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-1.37.0
+1.38.0
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index 6262d29a734..7c57d321c80 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -44,6 +44,7 @@ module ServiceParams
# make those event names plural as special case.
:issues_events,
:issues_url,
+ :jenkins_url,
:jira_issue_transition_automatic,
:jira_issue_transition_id,
:manual_configuration,
@@ -56,6 +57,7 @@ module ServiceParams
:password,
:priority,
:project_key,
+ :project_name,
:project_url,
:recipients,
:restrict_to_branch,
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 12510b4eb33..f522dffdf3e 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -12,9 +12,6 @@ class Projects::BranchesController < Projects::ApplicationController
# Support legacy URLs
before_action :redirect_for_legacy_index_sort_or_search, only: [:index]
before_action :limit_diverging_commit_counts!, only: [:diverging_commit_counts]
- before_action do
- push_frontend_feature_flag(:gldropdown_branches, default_enabled: :yaml)
- end
feature_category :source_code_management
diff --git a/app/graphql/resolvers/blobs_resolver.rb b/app/graphql/resolvers/blobs_resolver.rb
index 521e0482759..d006769bd4b 100644
--- a/app/graphql/resolvers/blobs_resolver.rb
+++ b/app/graphql/resolvers/blobs_resolver.rb
@@ -21,7 +21,7 @@ module Resolvers
# We fetch blobs from Gitaly efficiently but it still scales O(N) with the
# number of paths being fetched, so apply a scaling limit to that.
def self.resolver_complexity(args, child_complexity:)
- super + args.fetch(:paths, []).size
+ super + (args[:paths] || []).size
end
def resolve(paths:, ref:)
diff --git a/app/graphql/resolvers/ci/test_suite_resolver.rb b/app/graphql/resolvers/ci/test_suite_resolver.rb
new file mode 100644
index 00000000000..90cc30b1281
--- /dev/null
+++ b/app/graphql/resolvers/ci/test_suite_resolver.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ class TestSuiteResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type ::Types::Ci::TestSuiteType, null: true
+ authorize :read_build
+ authorizes_object!
+
+ alias_method :pipeline, :object
+
+ argument :build_ids, [GraphQL::ID_TYPE],
+ required: true,
+ description: 'IDs of the builds used to run the test suite.'
+
+ def resolve(build_ids:)
+ builds = pipeline.latest_builds.id_in(build_ids).presence
+ return unless builds
+
+ TestSuiteSerializer
+ .new(project: pipeline.project, current_user: @current_user)
+ .represent(load_test_suite_data(builds), details: true)
+ end
+
+ private
+
+ def load_test_suite_data(builds)
+ suite = builds.sum do |build|
+ build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new)
+ end
+
+ Gitlab::Ci::Reports::TestFailureHistory.new(suite.failed.values, pipeline.project).load!
+
+ suite
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb
index 79b1271c19f..2e83f6c1f5a 100644
--- a/app/graphql/types/ci/pipeline_type.rb
+++ b/app/graphql/types/ci/pipeline_type.rb
@@ -128,6 +128,12 @@ module Types
description: 'Summary of the test report generated by the pipeline.',
resolver: Resolvers::Ci::TestReportSummaryResolver
+ field :test_suite,
+ Types::Ci::TestSuiteType,
+ null: true,
+ description: 'A specific test suite in a pipeline test report.',
+ resolver: Resolvers::Ci::TestSuiteResolver
+
def detailed_status
object.detailed_status(current_user)
end
diff --git a/app/graphql/types/ci/recent_failures_type.rb b/app/graphql/types/ci/recent_failures_type.rb
new file mode 100644
index 00000000000..eeff7222762
--- /dev/null
+++ b/app/graphql/types/ci/recent_failures_type.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ class RecentFailuresType < BaseObject
+ graphql_name 'RecentFailures'
+ description 'Recent failure history of a test case.'
+
+ connection_type_class(Types::CountableConnectionType)
+
+ field :count, GraphQL::INT_TYPE, null: true,
+ description: 'Number of times the test case has failed in the past 14 days.'
+
+ field :base_branch, GraphQL::STRING_TYPE, null: true,
+ description: 'Name of the base branch of the project.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/ci/test_case_status_enum.rb b/app/graphql/types/ci/test_case_status_enum.rb
new file mode 100644
index 00000000000..6a5f8bc2a59
--- /dev/null
+++ b/app/graphql/types/ci/test_case_status_enum.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class TestCaseStatusEnum < BaseEnum
+ graphql_name 'TestCaseStatus'
+
+ ::Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status|
+ value status,
+ description: "Test case that has a status of #{status}.",
+ value: status
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/test_case_type.rb b/app/graphql/types/ci/test_case_type.rb
new file mode 100644
index 00000000000..9cc3f918125
--- /dev/null
+++ b/app/graphql/types/ci/test_case_type.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ class TestCaseType < BaseObject
+ graphql_name 'TestCase'
+ description 'Test case in pipeline test report.'
+
+ connection_type_class(Types::CountableConnectionType)
+
+ field :status, Types::Ci::TestCaseStatusEnum, null: true,
+ description: "Status of the test case (#{::Gitlab::Ci::Reports::TestCase::STATUS_TYPES.join(', ')})."
+
+ field :name, GraphQL::STRING_TYPE, null: true,
+ description: 'Name of the test case.'
+
+ field :classname, GraphQL::STRING_TYPE, null: true,
+ description: 'Classname of the test case.'
+
+ field :execution_time, GraphQL::FLOAT_TYPE, null: true,
+ description: 'Test case execution time in seconds.'
+
+ field :file, GraphQL::STRING_TYPE, null: true,
+ description: 'Path to the file of the test case.'
+
+ field :attachment_url, GraphQL::STRING_TYPE, null: true,
+ description: 'URL of the test case attachment file.'
+
+ field :system_output, GraphQL::STRING_TYPE, null: true,
+ description: 'System output of the test case.'
+
+ field :stack_trace, GraphQL::STRING_TYPE, null: true,
+ description: 'Stack trace of the test case.'
+
+ field :recent_failures, Types::Ci::RecentFailuresType, null: true,
+ description: 'Recent failure history of the test case on the base branch.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/ci/test_suite_type.rb b/app/graphql/types/ci/test_suite_type.rb
new file mode 100644
index 00000000000..7d4c01da81b
--- /dev/null
+++ b/app/graphql/types/ci/test_suite_type.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ class TestSuiteType < BaseObject
+ graphql_name 'TestSuite'
+ description 'Test suite in a pipeline test report.'
+
+ connection_type_class(Types::CountableConnectionType)
+
+ field :name, GraphQL::STRING_TYPE, null: true,
+ description: 'Name of the test suite.'
+
+ field :total_time, GraphQL::FLOAT_TYPE, null: true,
+ description: 'Total duration of the tests in the test suite.'
+
+ field :total_count, GraphQL::INT_TYPE, null: true,
+ description: 'Total number of the test cases in the test suite.'
+
+ field :success_count, GraphQL::INT_TYPE, null: true,
+ description: 'Total number of test cases that succeeded in the test suite.'
+
+ field :failed_count, GraphQL::INT_TYPE, null: true,
+ description: 'Total number of test cases that failed in the test suite.'
+
+ field :skipped_count, GraphQL::INT_TYPE, null: true,
+ description: 'Total number of test cases that were skipped in the test suite.'
+
+ field :error_count, GraphQL::INT_TYPE, null: true,
+ description: 'Total number of test cases that had an error.'
+
+ field :suite_error, GraphQL::STRING_TYPE, null: true,
+ description: 'Test suite error message.'
+
+ field :test_cases, Types::Ci::TestCaseType.connection_type, null: true,
+ description: 'Test cases in the test suite.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/repository/blob_type.rb b/app/graphql/types/repository/blob_type.rb
new file mode 100644
index 00000000000..912fc5f643a
--- /dev/null
+++ b/app/graphql/types/repository/blob_type.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+module Types
+ module Repository
+ # rubocop: disable Graphql/AuthorizeTypes
+ # This is presented through `Repository` that has its own authorization
+ class BlobType < BaseObject
+ present_using BlobPresenter
+
+ graphql_name 'RepositoryBlob'
+
+ field :id, GraphQL::ID_TYPE, null: false,
+ description: 'ID of the blob.'
+
+ field :oid, GraphQL::STRING_TYPE, null: false, method: :id,
+ description: 'OID of the blob.'
+
+ field :path, GraphQL::STRING_TYPE, null: false,
+ description: 'Path of the blob.'
+
+ field :name, GraphQL::STRING_TYPE,
+ description: 'Blob name.',
+ null: true
+
+ field :mode, type: GraphQL::STRING_TYPE,
+ description: 'Blob mode.',
+ null: true
+
+ field :lfs_oid, GraphQL::STRING_TYPE, null: true,
+ calls_gitaly: true,
+ description: 'LFS OID of the blob.'
+
+ field :web_path, GraphQL::STRING_TYPE, null: true,
+ description: 'Web path of the blob.'
+
+ def lfs_oid
+ Gitlab::Graphql::Loaders::BatchLfsOidLoader.new(object.repository, object.id).find
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/repository_type.rb b/app/graphql/types/repository_type.rb
index d426868b129..963a4296c4f 100644
--- a/app/graphql/types/repository_type.rb
+++ b/app/graphql/types/repository_type.rb
@@ -14,7 +14,7 @@ module Types
description: 'Indicates a corresponding Git repository exists on disk.'
field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver, calls_gitaly: true,
description: 'Tree of the repository.'
- field :blobs, Types::Tree::BlobType.connection_type, null: true, resolver: Resolvers::BlobsResolver, calls_gitaly: true,
+ field :blobs, Types::Repository::BlobType.connection_type, null: true, resolver: Resolvers::BlobsResolver, calls_gitaly: true,
description: 'Blobs contained within the repository'
field :branch_names, [GraphQL::STRING_TYPE], null: true, calls_gitaly: true,
complexity: 170, description: 'Names of branches available in this repository that match the search pattern.',
diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb
index ef855718c43..8f87cd5bfe0 100644
--- a/app/helpers/branches_helper.rb
+++ b/app/helpers/branches_helper.rb
@@ -20,10 +20,6 @@ module BranchesHelper
end
end
end
-
- def gldropdrown_branches_enabled?
- Feature.enabled?(:gldropdown_branches, default_enabled: :yaml)
- end
end
BranchesHelper.prepend_if_ee('EE::BranchesHelper')
diff --git a/app/helpers/ci/pipelines_helper.rb b/app/helpers/ci/pipelines_helper.rb
index 766c46249eb..cabb43f45fd 100644
--- a/app/helpers/ci/pipelines_helper.rb
+++ b/app/helpers/ci/pipelines_helper.rb
@@ -34,39 +34,39 @@ module Ci
# and will be cleaned up with https://gitlab.com/gitlab-org/gitlab/-/issues/326299
def experiment_suggested_ci_templates
[
- { name: 'Android', logo: image_path('/assets/illustrations/logos/android.svg') },
- { name: 'Bash', logo: image_path('/assets/illustrations/logos/bash.svg') },
- { name: 'C++', logo: image_path('/assets/illustrations/logos/c_plus_plus.svg') },
- { name: 'Clojure', logo: image_path('/assets/illustrations/logos/clojure.svg') },
- { name: 'Composer', logo: image_path('/assets/illustrations/logos/composer.svg') },
- { name: 'Crystal', logo: image_path('/assets/illustrations/logos/crystal.svg') },
- { name: 'Dart', logo: image_path('/assets/illustrations/logos/dart.svg') },
- { name: 'Django', logo: image_path('/assets/illustrations/logos/django.svg') },
- { name: 'Docker', logo: image_path('/assets/illustrations/logos/docker.svg') },
- { name: 'Elixir', logo: image_path('/assets/illustrations/logos/elixir.svg') },
- { name: 'iOS-Fastlane', logo: image_path('/assets/illustrations/logos/fastlane.svg') },
- { name: 'Flutter', logo: image_path('/assets/illustrations/logos/flutter.svg') },
- { name: 'Go', logo: image_path('/assets/illustrations/logos/go_logo.svg') },
- { name: 'Gradle', logo: image_path('/assets/illustrations/logos/gradle.svg') },
- { name: 'Grails', logo: image_path('/assets/illustrations/logos/grails.svg') },
- { name: 'dotNET', logo: image_path('/assets/illustrations/logos/dotnet.svg') },
- { name: 'Rails', logo: image_path('/assets/illustrations/logos/rails.svg') },
- { name: 'Julia', logo: image_path('/assets/illustrations/logos/julia.svg') },
- { name: 'Laravel', logo: image_path('/assets/illustrations/logos/laravel.svg') },
- { name: 'Latex', logo: image_path('/assets/illustrations/logos/latex.svg') },
- { name: 'Maven', logo: image_path('/assets/illustrations/logos/maven.svg') },
- { name: 'Mono', logo: image_path('/assets/illustrations/logos/mono.svg') },
- { name: 'Nodejs', logo: image_path('/assets/illustrations/logos/node_js.svg') },
- { name: 'npm', logo: image_path('/assets/illustrations/logos/npm.svg') },
- { name: 'OpenShift', logo: image_path('/assets/illustrations/logos/openshift.svg') },
- { name: 'Packer', logo: image_path('/assets/illustrations/logos/packer.svg') },
- { name: 'PHP', logo: image_path('/assets/illustrations/logos/php.svg') },
- { name: 'Python', logo: image_path('/assets/illustrations/logos/python.svg') },
- { name: 'Ruby', logo: image_path('/assets/illustrations/logos/ruby.svg') },
- { name: 'Rust', logo: image_path('/assets/illustrations/logos/rust.svg') },
- { name: 'Scala', logo: image_path('/assets/illustrations/logos/scala.svg') },
- { name: 'Swift', logo: image_path('/assets/illustrations/logos/swift.svg') },
- { name: 'Terraform', logo: image_path('/assets/illustrations/logos/terraform.svg') }
+ { name: 'Android', logo: image_path('illustrations/logos/android.svg') },
+ { name: 'Bash', logo: image_path('illustrations/logos/bash.svg') },
+ { name: 'C++', logo: image_path('illustrations/logos/c_plus_plus.svg') },
+ { name: 'Clojure', logo: image_path('illustrations/logos/clojure.svg') },
+ { name: 'Composer', logo: image_path('illustrations/logos/composer.svg') },
+ { name: 'Crystal', logo: image_path('illustrations/logos/crystal.svg') },
+ { name: 'Dart', logo: image_path('illustrations/logos/dart.svg') },
+ { name: 'Django', logo: image_path('illustrations/logos/django.svg') },
+ { name: 'Docker', logo: image_path('illustrations/logos/docker.svg') },
+ { name: 'Elixir', logo: image_path('illustrations/logos/elixir.svg') },
+ { name: 'iOS-Fastlane', logo: image_path('illustrations/logos/fastlane.svg') },
+ { name: 'Flutter', logo: image_path('illustrations/logos/flutter.svg') },
+ { name: 'Go', logo: image_path('illustrations/logos/go_logo.svg') },
+ { name: 'Gradle', logo: image_path('illustrations/logos/gradle.svg') },
+ { name: 'Grails', logo: image_path('illustrations/logos/grails.svg') },
+ { name: 'dotNET', logo: image_path('illustrations/logos/dotnet.svg') },
+ { name: 'Rails', logo: image_path('illustrations/logos/rails.svg') },
+ { name: 'Julia', logo: image_path('illustrations/logos/julia.svg') },
+ { name: 'Laravel', logo: image_path('illustrations/logos/laravel.svg') },
+ { name: 'Latex', logo: image_path('illustrations/logos/latex.svg') },
+ { name: 'Maven', logo: image_path('illustrations/logos/maven.svg') },
+ { name: 'Mono', logo: image_path('illustrations/logos/mono.svg') },
+ { name: 'Nodejs', logo: image_path('illustrations/logos/node_js.svg') },
+ { name: 'npm', logo: image_path('illustrations/logos/npm.svg') },
+ { name: 'OpenShift', logo: image_path('illustrations/logos/openshift.svg') },
+ { name: 'Packer', logo: image_path('illustrations/logos/packer.svg') },
+ { name: 'PHP', logo: image_path('illustrations/logos/php.svg') },
+ { name: 'Python', logo: image_path('illustrations/logos/python.svg') },
+ { name: 'Ruby', logo: image_path('illustrations/logos/ruby.svg') },
+ { name: 'Rust', logo: image_path('illustrations/logos/rust.svg') },
+ { name: 'Scala', logo: image_path('illustrations/logos/scala.svg') },
+ { name: 'Swift', logo: image_path('illustrations/logos/swift.svg') },
+ { name: 'Terraform', logo: image_path('illustrations/logos/terraform.svg') }
]
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 70c094c7ec6..68c3738d19e 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -729,14 +729,6 @@ module ProjectsHelper
]
end
- def sidebar_projects_paths
- %w[
- projects#show
- projects#activity
- releases#index
- ]
- end
-
def sidebar_settings_paths
%w[
projects#edit
diff --git a/app/helpers/whats_new_helper.rb b/app/helpers/whats_new_helper.rb
index 23ed2fc987c..9362ae1491f 100644
--- a/app/helpers/whats_new_helper.rb
+++ b/app/helpers/whats_new_helper.rb
@@ -8,4 +8,8 @@ module WhatsNewHelper
def whats_new_version_digest
ReleaseHighlight.most_recent_version_digest
end
+
+ def display_whats_new?
+ Gitlab.dev_env_org_or_com? || user_signed_in?
+ end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 0e55841f5fa..c9ab69317e1 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -286,9 +286,11 @@ module Ci
end
after_transition any => [:failed] do |pipeline|
- next unless pipeline.auto_devops_source?
+ pipeline.run_after_commit do
+ ::Gitlab::Ci::Pipeline::Metrics.pipeline_failure_reason_counter.increment(reason: pipeline.failure_reason)
- pipeline.run_after_commit { AutoDevops::DisableWorker.perform_async(pipeline.id) }
+ AutoDevops::DisableWorker.perform_async(pipeline.id) if pipeline.auto_devops_source?
+ end
end
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index c89a132bdd6..e989129209a 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -179,6 +179,12 @@ class CommitStatus < ApplicationRecord
ExpireJobCacheWorker.perform_async(id)
end
end
+
+ after_transition any => :failed do |commit_status|
+ commit_status.run_after_commit do
+ ::Gitlab::Ci::Pipeline::Metrics.job_failure_reason_counter.increment(reason: commit_status.failure_reason)
+ end
+ end
end
def self.names
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 478c7cd156f..1e44321e148 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -137,6 +137,14 @@ module Issuable
scope :references_project, -> { references(:project) }
scope :non_archived, -> { join_project.where(projects: { archived: false }) }
+ scope :includes_for_bulk_update, -> do
+ associations = %i[author assignees epic group labels metrics project source_project target_project].select do |association|
+ reflect_on_association(association)
+ end
+
+ includes(*associations)
+ end
+
attr_mentionable :title, pipeline: :single_line
attr_mentionable :description
diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb
index ccb334343ff..d42417bb6c1 100644
--- a/app/models/concerns/milestoneable.rb
+++ b/app/models/concerns/milestoneable.rb
@@ -39,11 +39,13 @@ module Milestoneable
private
def milestone_is_valid
- errors.add(:milestone_id, 'is invalid') if respond_to?(:milestone_id) && milestone_id.present? && !milestone_available?
+ errors.add(:milestone_id, 'is invalid') if respond_to?(:milestone_id) && !milestone_available?
end
end
def milestone_available?
+ return true if milestone_id.blank?
+
project_id == milestone&.project_id || project.ancestors_upto.compact.include?(milestone&.group)
end
diff --git a/app/models/concerns/sidebars/container_with_html_options.rb b/app/models/concerns/sidebars/container_with_html_options.rb
index 911540f25f8..8cb2fc7d6b2 100644
--- a/app/models/concerns/sidebars/container_with_html_options.rb
+++ b/app/models/concerns/sidebars/container_with_html_options.rb
@@ -17,6 +17,13 @@ module Sidebars
{}
end
+ # Attributes to pass to the html_options attribute
+ # in the helper method that sets the active class
+ # on each element.
+ def nav_link_html_options
+ {}
+ end
+
def title
raise NotImplementedError
end
diff --git a/app/models/concerns/vulnerability_finding_helpers.rb b/app/models/concerns/vulnerability_finding_helpers.rb
new file mode 100644
index 00000000000..cf50305faab
--- /dev/null
+++ b/app/models/concerns/vulnerability_finding_helpers.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module VulnerabilityFindingHelpers
+ extend ActiveSupport::Concern
+end
+
+VulnerabilityFindingHelpers.prepend_if_ee('EE::VulnerabilityFindingHelpers')
diff --git a/app/models/concerns/vulnerability_finding_signature_helpers.rb b/app/models/concerns/vulnerability_finding_signature_helpers.rb
new file mode 100644
index 00000000000..f57e3cb0bfb
--- /dev/null
+++ b/app/models/concerns/vulnerability_finding_signature_helpers.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module VulnerabilityFindingSignatureHelpers
+ extend ActiveSupport::Concern
+end
+
+VulnerabilityFindingSignatureHelpers.prepend_if_ee('EE::VulnerabilityFindingSignatureHelpers')
diff --git a/app/models/pages_deployment.rb b/app/models/pages_deployment.rb
index d67a92af6af..294a4e85d1f 100644
--- a/app/models/pages_deployment.rb
+++ b/app/models/pages_deployment.rb
@@ -14,6 +14,8 @@ class PagesDeployment < ApplicationRecord
scope :older_than, -> (id) { where('id < ?', id) }
scope :migrated_from_legacy_storage, -> { where(file: MIGRATED_FILE_NAME) }
+ scope :with_files_stored_locally, -> { where(file_store: ::ObjectStorage::Store::LOCAL) }
+ scope :with_files_stored_remotely, -> { where(file_store: ::ObjectStorage::Store::REMOTE) }
validates :file, presence: true
validates :file_store, presence: true, inclusion: { in: ObjectStorage::SUPPORTED_STORES }
diff --git a/app/models/sidebars/projects/menus/project_overview/menu.rb b/app/models/sidebars/projects/menus/project_overview/menu.rb
new file mode 100644
index 00000000000..e6aa8ed159f
--- /dev/null
+++ b/app/models/sidebars/projects/menus/project_overview/menu.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ module ProjectOverview
+ class Menu < ::Sidebars::Menu
+ override :configure_menu_items
+ def configure_menu_items
+ add_item(MenuItems::Details.new(context))
+ add_item(MenuItems::Activity.new(context))
+ add_item(MenuItems::Releases.new(context))
+ end
+
+ override :link
+ def link
+ project_path(context.project)
+ end
+
+ override :extra_container_html_options
+ def extra_container_html_options
+ {
+ class: 'shortcuts-project rspec-project-link'
+ }
+ end
+
+ override :extra_container_html_options
+ def nav_link_html_options
+ { class: 'home' }
+ end
+
+ override :title
+ def title
+ _('Project overview')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'home'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/sidebars/projects/menus/project_overview/menu_items/activity.rb b/app/models/sidebars/projects/menus/project_overview/menu_items/activity.rb
new file mode 100644
index 00000000000..46d0f0bc43b
--- /dev/null
+++ b/app/models/sidebars/projects/menus/project_overview/menu_items/activity.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ module ProjectOverview
+ module MenuItems
+ class Activity < ::Sidebars::MenuItem
+ override :link
+ def link
+ activity_project_path(context.project)
+ end
+
+ override :extra_container_html_options
+ def extra_container_html_options
+ {
+ class: 'shortcuts-project-activity'
+ }
+ end
+
+ override :active_routes
+ def active_routes
+ { path: 'projects#activity' }
+ end
+
+ override :title
+ def title
+ _('Activity')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/sidebars/projects/menus/project_overview/menu_items/details.rb b/app/models/sidebars/projects/menus/project_overview/menu_items/details.rb
new file mode 100644
index 00000000000..c8cf5d503ab
--- /dev/null
+++ b/app/models/sidebars/projects/menus/project_overview/menu_items/details.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ module ProjectOverview
+ module MenuItems
+ class Details < ::Sidebars::MenuItem
+ override :link
+ def link
+ project_path(context.project)
+ end
+
+ override :extra_container_html_options
+ def extra_container_html_options
+ {
+ title: _('Project details'),
+ class: 'shortcuts-project'
+ }
+ end
+
+ override :active_routes
+ def active_routes
+ { path: 'projects#show' }
+ end
+
+ override :title
+ def title
+ _('Details')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/sidebars/projects/menus/project_overview/menu_items/releases.rb b/app/models/sidebars/projects/menus/project_overview/menu_items/releases.rb
new file mode 100644
index 00000000000..5e8348f4398
--- /dev/null
+++ b/app/models/sidebars/projects/menus/project_overview/menu_items/releases.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ module ProjectOverview
+ module MenuItems
+ class Releases < ::Sidebars::MenuItem
+ override :link
+ def link
+ project_releases_path(context.project)
+ end
+
+ override :extra_container_html_options
+ def extra_container_html_options
+ {
+ class: 'shortcuts-project-releases'
+ }
+ end
+
+ override :render?
+ def render?
+ can?(context.current_user, :read_release, context.project) && !context.project.empty_repo?
+ end
+
+ override :active_routes
+ def active_routes
+ { controller: :releases }
+ end
+
+ override :title
+ def title
+ _('Releases')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/sidebars/projects/panel.rb b/app/models/sidebars/projects/panel.rb
index 2532a151ed6..5f4c7f32164 100644
--- a/app/models/sidebars/projects/panel.rb
+++ b/app/models/sidebars/projects/panel.rb
@@ -6,6 +6,8 @@ module Sidebars
override :configure_menus
def configure_menus
set_scope_menu(Sidebars::Projects::Menus::Scope::Menu.new(context))
+
+ add_menu(Sidebars::Projects::Menus::ProjectOverview::Menu.new(context))
end
override :render_raw_menus_partial
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 970652b4da3..6c69df0c616 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -19,7 +19,7 @@ module Ci
end
def metrics
- @metrics ||= ::Gitlab::Ci::Pipeline::Metrics.new
+ @metrics ||= ::Gitlab::Ci::Pipeline::Metrics
end
private
diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb
index d3d543edcd7..13e289716ef 100644
--- a/app/services/issuable/bulk_update_service.rb
+++ b/app/services/issuable/bulk_update_service.rb
@@ -15,7 +15,7 @@ module Issuable
set_update_params(type)
items = update_issuables(type, ids)
- response_success(payload: { count: items.count })
+ response_success(payload: { count: items.size })
rescue ArgumentError => e
response_error(e.message, 422)
end
@@ -59,10 +59,17 @@ module Issuable
def find_issuables(parent, model_class, ids)
if parent.is_a?(Project)
- model_class.id_in(ids).of_projects(parent)
+ projects = parent
elsif parent.is_a?(Group)
- model_class.id_in(ids).of_projects(parent.all_projects)
+ projects = parent.all_projects
+ else
+ return
end
+
+ model_class
+ .id_in(ids)
+ .of_projects(projects)
+ .includes_for_bulk_update
end
def response_success(message: nil, payload: nil)
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 89202edd0d4..8995c5f2411 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -11,18 +11,7 @@ module MergeRequests
end
def execute(merge_request)
- # We don't allow change of source/target projects and source branch
- # after merge request was created
- params.delete(:source_project_id)
- params.delete(:target_project_id)
- params.delete(:source_branch)
-
- if merge_request.closed_or_merged_without_fork?
- params.delete(:target_branch)
- params.delete(:force_remove_source_branch)
- end
-
- update_task_event(merge_request) || update(merge_request)
+ update_merge_request_with_specialized_service(merge_request) || general_fallback(merge_request)
end
def handle_changes(merge_request, options)
@@ -86,6 +75,21 @@ module MergeRequests
attr_reader :target_branch_was_deleted
+ def general_fallback(merge_request)
+ # We don't allow change of source/target projects and source branch
+ # after merge request was created
+ params.delete(:source_project_id)
+ params.delete(:target_project_id)
+ params.delete(:source_branch)
+
+ if merge_request.closed_or_merged_without_fork?
+ params.delete(:target_branch)
+ params.delete(:force_remove_source_branch)
+ end
+
+ update_task_event(merge_request) || update(merge_request)
+ end
+
def track_title_and_desc_edits(changed_fields)
tracked_fields = %w(title description)
@@ -272,6 +276,34 @@ module MergeRequests
def quick_action_options
{ merge_request_diff_head_sha: params.delete(:merge_request_diff_head_sha) }
end
+
+ def update_merge_request_with_specialized_service(merge_request)
+ return unless params.delete(:use_specialized_service)
+
+ # If we're attempting to modify only a single attribute, look up whether
+ # we have a specialized, targeted service we should use instead. We may
+ # in the future extend this to include specialized services that operate
+ # on multiple attributes, but for now limit to only single attribute
+ # updates.
+ #
+ return unless params.one?
+
+ attempt_specialized_update_services(merge_request, params.each_key.first.to_sym)
+ end
+
+ def attempt_specialized_update_services(merge_request, attribute)
+ case attribute
+ when :assignee_ids
+ assignees_service.execute(merge_request)
+ else
+ nil
+ end
+ end
+
+ def assignees_service
+ @assignees_service ||= ::MergeRequests::UpdateAssigneesService
+ .new(project, current_user, params)
+ end
end
end
diff --git a/app/services/namespaces/in_product_marketing_emails_service.rb b/app/services/namespaces/in_product_marketing_emails_service.rb
index 2b3c0c382a8..eb81253bc08 100644
--- a/app/services/namespaces/in_product_marketing_emails_service.rb
+++ b/app/services/namespaces/in_product_marketing_emails_service.rb
@@ -41,9 +41,11 @@ module Namespaces
attr_reader :track, :interval, :in_product_marketing_email_records
def send_email_for_group(group)
- experiment_enabled_for_group = experiment_enabled_for_group?(group)
- experiment_add_group(group, experiment_enabled_for_group)
- return unless experiment_enabled_for_group
+ if Gitlab.com?
+ experiment_enabled_for_group = experiment_enabled_for_group?(group)
+ experiment_add_group(group, experiment_enabled_for_group)
+ return unless experiment_enabled_for_group
+ end
users_for_group(group).each do |user|
if can_perform_action?(user, group)
diff --git a/app/services/pages/migrate_from_legacy_storage_service.rb b/app/services/pages/migrate_from_legacy_storage_service.rb
index d649505f27d..b6aa08bba01 100644
--- a/app/services/pages/migrate_from_legacy_storage_service.rb
+++ b/app/services/pages/migrate_from_legacy_storage_service.rb
@@ -2,10 +2,8 @@
module Pages
class MigrateFromLegacyStorageService
- def initialize(logger, migration_threads:, batch_size:, ignore_invalid_entries:, mark_projects_as_not_deployed:)
+ def initialize(logger, ignore_invalid_entries:, mark_projects_as_not_deployed:)
@logger = logger
- @migration_threads = migration_threads
- @batch_size = batch_size
@ignore_invalid_entries = ignore_invalid_entries
@mark_projects_as_not_deployed = mark_projects_as_not_deployed
@@ -14,25 +12,35 @@ module Pages
@counters_lock = Mutex.new
end
- def execute
+ def execute_with_threads(threads:, batch_size:)
@queue = SizedQueue.new(1)
- threads = start_migration_threads
+ migration_threads = start_migration_threads(threads)
- ProjectPagesMetadatum.only_on_legacy_storage.each_batch(of: @batch_size) do |batch|
+ ProjectPagesMetadatum.only_on_legacy_storage.each_batch(of: batch_size) do |batch|
@queue.push(batch)
end
@queue.close
- @logger.info("Waiting for threads to finish...")
- threads.each(&:join)
+ @logger.info(message: "Pages legacy storage migration: Waiting for threads to finish...")
+ migration_threads.each(&:join)
{ migrated: @migrated, errored: @errored }
end
- def start_migration_threads
- Array.new(@migration_threads) do
+ def execute_for_batch(project_ids)
+ batch = ProjectPagesMetadatum.only_on_legacy_storage.where(project_id: project_ids) # rubocop: disable CodeReuse/ActiveRecord
+
+ process_batch(batch)
+
+ { migrated: @migrated, errored: @errored }
+ end
+
+ private
+
+ def start_migration_threads(count)
+ Array.new(count) do
Thread.new do
while batch = @queue.pop
Rails.application.executor.wrap do
@@ -50,12 +58,12 @@ module Pages
migrate_project(project)
end
- @logger.info("#{@migrated} projects are migrated successfully, #{@errored} projects failed to be migrated")
+ @logger.info(message: "Pages legacy storage migration: batch processed", migrated: @migrated, errored: @errored)
rescue => e
# This method should never raise exception otherwise all threads might be killed
# and this will result in queue starving (and deadlock)
Gitlab::ErrorTracking.track_exception(e)
- @logger.error("failed processing a batch: #{e.message}")
+ @logger.error(message: "Pages legacy storage migration: failed processing a batch: #{e.message}")
end
def migrate_project(project)
@@ -67,15 +75,15 @@ module Pages
end
if result[:status] == :success
- @logger.info("project_id: #{project.id} #{project.pages_path} has been migrated in #{time.round(2)} seconds: #{result[:message]}")
+ @logger.info(message: "Pages legacy storage migration: project migrated: #{result[:message]}", project_id: project.id, pages_path: project.pages_path, duration: time.round(2))
@counters_lock.synchronize { @migrated += 1 }
else
- @logger.error("project_id: #{project.id} #{project.pages_path} failed to be migrated in #{time.round(2)} seconds: #{result[:message]}")
+ @logger.error(message: "Pages legacy storage migration: project failed to be migrated: #{result[:message]}", project_id: project.id, pages_path: project.pages_path, duration: time.round(2))
@counters_lock.synchronize { @errored += 1 }
end
rescue => e
@counters_lock.synchronize { @errored += 1 }
- @logger.error("project_id: #{project&.id} #{project&.pages_path} failed to be migrated: #{e.message}")
+ @logger.error(message: "Pages legacy storage migration: project failed to be migrated: #{result[:message]}", project_id: project&.id, pages_path: project&.pages_path)
Gitlab::ErrorTracking.track_exception(e, project_id: project&.id)
end
end
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 3225dad5d57..481e83c9701 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -120,7 +120,8 @@
= sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right')
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
-#whats-new-app{ data: { version_digest: whats_new_version_digest } }
+- if display_whats_new?
+ #whats-new-app{ data: { version_digest: whats_new_version_digest } }
- if can?(current_user, :update_user_status, current_user)
.js-set-status-modal-wrapper{ data: user_status_data }
diff --git a/app/views/layouts/header/_whats_new_dropdown_item.html.haml b/app/views/layouts/header/_whats_new_dropdown_item.html.haml
index 61fe2f1e711..9fe98a54aae 100644
--- a/app/views/layouts/header/_whats_new_dropdown_item.html.haml
+++ b/app/views/layouts/header/_whats_new_dropdown_item.html.haml
@@ -1,5 +1,6 @@
-%li
- %button.gl-justify-content-space-between.gl-align-items-center.js-whats-new-trigger{ type: 'button', class: 'gl-display-flex!' }
- = _("What's new")
- %span.js-whats-new-notification-count.whats-new-notification-count
- = whats_new_most_recent_release_items_count
+- if display_whats_new?
+ %li
+ %button.gl-justify-content-space-between.gl-align-items-center.js-whats-new-trigger{ type: 'button', class: 'gl-display-flex!' }
+ = _("What's new")
+ %span.js-whats-new-notification-count.whats-new-notification-count
+ = whats_new_most_recent_release_items_count
diff --git a/app/views/layouts/nav/sidebar/_project_menus.html.haml b/app/views/layouts/nav/sidebar/_project_menus.html.haml
index 79a7150e030..aee506ead6c 100644
--- a/app/views/layouts/nav/sidebar/_project_menus.html.haml
+++ b/app/views/layouts/nav/sidebar/_project_menus.html.haml
@@ -1,29 +1,3 @@
-= nav_link(path: sidebar_projects_paths, html_options: { class: 'home' }) do
- = link_to project_path(@project), class: 'shortcuts-project rspec-project-link', data: { qa_selector: 'project_link' } do
- .nav-icon-container
- = sprite_icon('home')
- %span.nav-item-name
- = _('Project overview')
-
- %ul.sidebar-sub-level-items
- = nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do
- = link_to project_path(@project) do
- %strong.fly-out-top-item-name
- = _('Project overview')
- %li.divider.fly-out-top-item
- = nav_link(path: 'projects#show') do
- = link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do
- %span= _('Details')
-
- = nav_link(path: 'projects#activity') do
- = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity', data: { qa_selector: 'activity_link' } do
- %span= _('Activity')
-
- - if project_nav_tab?(:releases)
- = nav_link(controller: :releases) do
- = link_to project_releases_path(@project), title: _('Releases'), class: 'shortcuts-project-releases' do
- %span= _('Releases')
-
- if project_nav_tab? :learn_gitlab
= nav_link(controller: :learn_gitlab, html_options: { class: 'home' }) do
= link_to project_learn_gitlab_path(@project) do
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index c8a5908018d..129b207a26f 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -16,25 +16,7 @@
= link_to s_('Branches|All'), project_branches_filtered_path(@project, state: 'all'), title: s_('Branches|Show all branches')
.nav-controls
- - if !gldropdrown_branches_enabled?
- = form_tag(project_branches_filtered_path(@project, state: 'all'), method: :get) do
- = search_field_tag :search, params[:search], { placeholder: s_('Branches|Filter by branch name'), id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false }
-
- - unless @mode == 'overview'
- .dropdown.inline>
- %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
- %span.light
- = branches_sort_options_hash[@sort]
- = sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3")
- %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
- %li.dropdown-header
- = s_('Branches|Sort by')
- - branches_sort_options_hash.each do |value, title|
- %li
- = link_to title, project_branches_filtered_path(@project, state: 'all', search: params[:search], sort: value), class: ("is-active" if @sort == value)
-
- - else
- #js-branches-sort-dropdown{ data: { project_branches_filtered_path: project_branches_path(@project, state: 'all'), sort_options: branches_sort_options_hash.to_json, mode: @mode } }
+ #js-branches-sort-dropdown{ data: { project_branches_filtered_path: project_branches_path(@project, state: 'all'), sort_options: branches_sort_options_hash.to_json, mode: @mode } }
- if can? current_user, :push_code, @project
= link_to project_merged_branches_path(@project),
diff --git a/app/views/shared/nav/_sidebar.html.haml b/app/views/shared/nav/_sidebar.html.haml
index ab198edb63b..1c06fc9eebf 100644
--- a/app/views/shared/nav/_sidebar.html.haml
+++ b/app/views/shared/nav/_sidebar.html.haml
@@ -6,6 +6,8 @@
= render sidebar.render_raw_scope_menu_partial
%ul.sidebar-top-level-items.qa-project-sidebar
+ - if sidebar.renderable_menus.any?
+ = render partial: 'shared/nav/sidebar_menu', collection: sidebar.renderable_menus
- if sidebar.render_raw_menus_partial
= render sidebar.render_raw_menus_partial
diff --git a/app/views/shared/nav/_sidebar_menu.html.haml b/app/views/shared/nav/_sidebar_menu.html.haml
new file mode 100644
index 00000000000..cd3222294f3
--- /dev/null
+++ b/app/views/shared/nav/_sidebar_menu.html.haml
@@ -0,0 +1,27 @@
+= nav_link(**sidebar_menu.all_active_routes, html_options: sidebar_menu.nav_link_html_options) do
+ = link_to sidebar_menu.link, **sidebar_menu.container_html_options, data: { qa_selector: 'sidebar_menu_link', qa_menu_item: sidebar_menu.title } do
+ - if sidebar_menu.icon_or_image?
+ .nav-icon-container
+ - if sidebar_menu.image_path
+ = image_tag(sidebar_menu.image_path, **sidebar_menu.image_html_options)
+ - elsif sidebar_menu.sprite_icon
+ = sprite_icon(sidebar_menu.sprite_icon, **sidebar_menu.sprite_icon_html_options)
+
+ %span.nav-item-name{ **sidebar_menu.title_html_options }
+ = sidebar_menu.title
+ - if sidebar_menu.has_pill?
+ %span.badge.badge-pill.count{ **sidebar_menu.pill_html_options }
+ = number_with_delimiter(sidebar_menu.pill_count)
+
+ %ul.sidebar-sub-level-items{ class: ('is-fly-out-only' unless sidebar_menu.has_items?) }
+ = nav_link(**sidebar_menu.all_active_routes, html_options: { class: 'fly-out-top-item' } ) do
+ = link_to sidebar_menu.link, title: sidebar_menu.title do
+ %strong.fly-out-top-item-name
+ = sidebar_menu.title
+ - if sidebar_menu.has_pill?
+ %span.badge.badge-pill.count.fly-out-badge{ **sidebar_menu.pill_html_options }
+ = number_with_delimiter(sidebar_menu.pill_count)
+
+ - if sidebar_menu.has_renderable_items?
+ %li.divider.fly-out-top-item
+ = render partial: 'shared/nav/sidebar_menu_item', collection: sidebar_menu.renderable_items
diff --git a/app/views/shared/nav/_sidebar_menu_item.html.haml b/app/views/shared/nav/_sidebar_menu_item.html.haml
new file mode 100644
index 00000000000..0b0e4c7aec9
--- /dev/null
+++ b/app/views/shared/nav/_sidebar_menu_item.html.haml
@@ -0,0 +1,8 @@
+= nav_link(**sidebar_menu_item.active_routes) do
+ = link_to sidebar_menu_item.link, **sidebar_menu_item.container_html_options, data: { qa_selector: 'sidebar_menu_item_link', qa_menu_item: sidebar_menu_item.title } do
+ %span
+ = sidebar_menu_item.title
+ - if sidebar_menu_item.sprite_icon
+ = sprite_icon(sidebar_menu_item.sprite_icon, **sidebar_menu_item.sprite_icon_html_options)
+ - if sidebar_menu_item.show_hint?
+ .js-feature-highlight{ **sidebar_menu_item.hint_html_options }
diff --git a/changelogs/unreleased/21068-optimize-issueable-updates.yml b/changelogs/unreleased/21068-optimize-issueable-updates.yml
new file mode 100644
index 00000000000..225604a9917
--- /dev/null
+++ b/changelogs/unreleased/21068-optimize-issueable-updates.yml
@@ -0,0 +1,5 @@
+---
+title: Optimize issuable updates
+merge_request: 59468
+author:
+type: performance
diff --git a/changelogs/unreleased/300121-fix-jenkins-ce.yml b/changelogs/unreleased/300121-fix-jenkins-ce.yml
new file mode 100644
index 00000000000..03a7efce5fb
--- /dev/null
+++ b/changelogs/unreleased/300121-fix-jenkins-ce.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Jenkins integration for GitLab FOSS
+merge_request: 59476
+author:
+type: fixed
diff --git a/changelogs/unreleased/322001-poc-for-migrating-pages-to-zip-storage-in-the-background.yml b/changelogs/unreleased/322001-poc-for-migrating-pages-to-zip-storage-in-the-background.yml
new file mode 100644
index 00000000000..cfa15a3319f
--- /dev/null
+++ b/changelogs/unreleased/322001-poc-for-migrating-pages-to-zip-storage-in-the-background.yml
@@ -0,0 +1,5 @@
+---
+title: Automatically try to migrate gitlab pages to zip storage
+merge_request: 54578
+author:
+type: added
diff --git a/changelogs/unreleased/325285-rake-pages-deployments.yml b/changelogs/unreleased/325285-rake-pages-deployments.yml
new file mode 100644
index 00000000000..4647ac4c43f
--- /dev/null
+++ b/changelogs/unreleased/325285-rake-pages-deployments.yml
@@ -0,0 +1,5 @@
+---
+title: Add rake tasks for Pages deployment migration
+merge_request: 57120
+author:
+type: added
diff --git a/changelogs/unreleased/be-test-suite-graphql.yml b/changelogs/unreleased/be-test-suite-graphql.yml
new file mode 100644
index 00000000000..e333ea84512
--- /dev/null
+++ b/changelogs/unreleased/be-test-suite-graphql.yml
@@ -0,0 +1,5 @@
+---
+title: Add GraphQL endpoint for a specific test suite in pipelines
+merge_request: 58924
+author:
+type: added
diff --git a/changelogs/unreleased/jivanvl-remove-gldropdown-branches-ff.yml b/changelogs/unreleased/jivanvl-remove-gldropdown-branches-ff.yml
new file mode 100644
index 00000000000..344642e281b
--- /dev/null
+++ b/changelogs/unreleased/jivanvl-remove-gldropdown-branches-ff.yml
@@ -0,0 +1,5 @@
+---
+title: Remove gldropdown_branches feature flag
+merge_request: 59179
+author:
+type: changed
diff --git a/changelogs/unreleased/jswain_whats_new_self_managed_authenticated.yml b/changelogs/unreleased/jswain_whats_new_self_managed_authenticated.yml
new file mode 100644
index 00000000000..e7e91dd8580
--- /dev/null
+++ b/changelogs/unreleased/jswain_whats_new_self_managed_authenticated.yml
@@ -0,0 +1,5 @@
+---
+title: Hide What's New for unauthenticated users
+merge_request: 59330
+author:
+type: changed
diff --git a/changelogs/unreleased/kerrizor-use-specialized-service-for-assignee-updates.yml b/changelogs/unreleased/kerrizor-use-specialized-service-for-assignee-updates.yml
new file mode 100644
index 00000000000..d251af4a6f9
--- /dev/null
+++ b/changelogs/unreleased/kerrizor-use-specialized-service-for-assignee-updates.yml
@@ -0,0 +1,5 @@
+---
+title: Add framework for using specialized services to improve performance of MergeRequests::UpdateService
+merge_request: 58836
+author:
+type: performance
diff --git a/changelogs/unreleased/qmnguyen0711-add-queue-to-background-transaction.yml b/changelogs/unreleased/qmnguyen0711-add-queue-to-background-transaction.yml
new file mode 100644
index 00000000000..9052df1fa01
--- /dev/null
+++ b/changelogs/unreleased/qmnguyen0711-add-queue-to-background-transaction.yml
@@ -0,0 +1,5 @@
+---
+title: Add queue label to metrics dispatched by background transaction
+merge_request: 59344
+author:
+type: changed
diff --git a/changelogs/unreleased/upgrade-pages-1-38.yml b/changelogs/unreleased/upgrade-pages-1-38.yml
new file mode 100644
index 00000000000..ec132f0a508
--- /dev/null
+++ b/changelogs/unreleased/upgrade-pages-1-38.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade GitLab Pages to 1.38.0
+merge_request: 59464
+author:
+type: added
diff --git a/config/feature_flags/development/gldropdown_branches.yml b/config/feature_flags/development/load_balancing_atomic_replica.yml
index 67d52a495a8..fb0707849d4 100644
--- a/config/feature_flags/development/gldropdown_branches.yml
+++ b/config/feature_flags/development/load_balancing_atomic_replica.yml
@@ -1,8 +1,8 @@
---
-name: gldropdown_branches
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57760
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/326549
+name: load_balancing_atomic_replica
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49294
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/291193
milestone: '13.11'
type: development
-group: group::continuous integration
-default_enabled: true
+group:
+default_enabled: false
diff --git a/db/post_migrate/20210302150310_schedule_migrate_pages_to_zip_storage.rb b/db/post_migrate/20210302150310_schedule_migrate_pages_to_zip_storage.rb
new file mode 100644
index 00000000000..7f6d7ffe9b7
--- /dev/null
+++ b/db/post_migrate/20210302150310_schedule_migrate_pages_to_zip_storage.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+class ScheduleMigratePagesToZipStorage < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ MIGRATION = 'MigratePagesToZipStorage'
+ BATCH_SIZE = 10
+ BATCH_TIME = 5.minutes
+
+ disable_ddl_transaction!
+
+ class ProjectPagesMetadatum < ActiveRecord::Base
+ extend SuppressCompositePrimaryKeyWarning
+
+ include EachBatch
+
+ self.primary_key = :project_id
+ self.table_name = 'project_pages_metadata'
+ self.inheritance_column = :_type_disabled
+
+ scope :deployed, -> { where(deployed: true) }
+ scope :only_on_legacy_storage, -> { deployed.where(pages_deployment_id: nil) }
+ end
+
+ def up
+ queue_background_migration_jobs_by_range_at_intervals(
+ ProjectPagesMetadatum.only_on_legacy_storage,
+ MIGRATION,
+ BATCH_TIME,
+ batch_size: BATCH_SIZE,
+ primary_column_name: :project_id
+ )
+ end
+end
diff --git a/db/schema_migrations/20210302150310 b/db/schema_migrations/20210302150310
new file mode 100644
index 00000000000..251fdb0ba8e
--- /dev/null
+++ b/db/schema_migrations/20210302150310
@@ -0,0 +1 @@
+7c562d43801c18af48dc526dc6574aebd11689b62bad864b107580d341ba64a1 \ No newline at end of file
diff --git a/doc/administration/whats-new.md b/doc/administration/whats-new.md
new file mode 100644
index 00000000000..4cbb0b854ae
--- /dev/null
+++ b/doc/administration/whats-new.md
@@ -0,0 +1,29 @@
+---
+stage: Growth
+group: Adoption
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+---
+
+# What's new **(FREE)**
+
+With each monthly release, GitLab includes some of the highlights from the last 10
+GitLab versions in the **What's new** feature. To access it:
+
+1. In the top navigation bar, select the **{question}** icon.
+1. Select **What's new** from the menu.
+
+The **What's new** describes new features available in multiple
+[GitLab tiers](https://about.gitlab.com/pricing). While all users can see the
+feature list, the feature list is tailored to your subscription type:
+
+- Features only available to self-managed installations are not shown on GitLab.com.
+- Features only available on GitLab.com are not shown to self-managed installations.
+
+The **What's new** feature cannot be disabled, but
+[is planned](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59011) for a future release.
+
+## Self-managed installations
+
+Due to our release post process, the content for **What's new** is not yet finalized
+when a new version (`.0` release) is cut. The updated **What's new** is included
+in the first patch release, such as `13.10.1`.
diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md
index b14d28d6ec0..ce07bdd2966 100644
--- a/doc/api/api_resources.md
+++ b/doc/api/api_resources.md
@@ -167,7 +167,8 @@ The following API resources are available outside of project and group contexts
| [Sidekiq metrics](sidekiq_metrics.md) **(FREE SELF)** | `/sidekiq` |
| [Suggestions](suggestions.md) | `/suggestions` |
| [System hooks](system_hooks.md) | `/hooks` |
-| [To-dos](todos.md) | `/todos` |
+| [To-dos](todos.md) | `/todos` |
+| [Usage data](usage_data.md) | `/usage_data` (For GitLab instance [Administrator](../user/permissions.md) users only) |
| [Users](users.md) | `/users` |
| [Validate `.gitlab-ci.yml` file](lint.md) | `/lint` |
| [Version](version.md) | `/version` |
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 2bef0260565..c803c888449 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -631,7 +631,7 @@ Parsed field from an alert used for custom mappings.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `label` | [`String`](#string) | Human-readable label of the payload path. |
-| `path` | [`[String!]`](#string) | Path to value inside payload JSON. |
+| `path` | [`[PayloadAlertFieldPathSegment!]`](#payloadalertfieldpathsegment) | Path to value inside payload JSON. |
| `type` | [`AlertManagementPayloadAlertFieldType`](#alertmanagementpayloadalertfieldtype) | Type of the parsed value. |
### `AlertManagementPayloadAlertMappingField`
@@ -642,7 +642,7 @@ Parsed field (with its name) from an alert used for custom mappings.
| ----- | ---- | ----------- |
| `fieldName` | [`AlertManagementPayloadAlertFieldName`](#alertmanagementpayloadalertfieldname) | A GitLab alert field name. |
| `label` | [`String`](#string) | Human-readable label of the payload path. |
-| `path` | [`[String!]`](#string) | Path to value inside payload JSON. |
+| `path` | [`[PayloadAlertFieldPathSegment!]`](#payloadalertfieldpathsegment) | Path to value inside payload JSON. |
| `type` | [`AlertManagementPayloadAlertFieldType`](#alertmanagementpayloadalertfieldtype) | Type of the parsed value. |
### `AlertManagementPrometheusIntegration`
@@ -4827,6 +4827,7 @@ Information about pagination in a connection.
| `startedAt` | [`Time`](#time) | Timestamp when the pipeline was started. |
| `status` | [`PipelineStatusEnum!`](#pipelinestatusenum) | Status of the pipeline (CREATED, WAITING_FOR_RESOURCE, PREPARING, PENDING, RUNNING, FAILED, SUCCESS, CANCELED, SKIPPED, MANUAL, SCHEDULED). |
| `testReportSummary` | [`TestReportSummary!`](#testreportsummary) | Summary of the test report generated by the pipeline. |
+| `testSuite` | [`TestSuite`](#testsuite) | A specific test suite in a pipeline test report. |
| `updatedAt` | [`Time!`](#time) | Timestamp of the pipeline's last activity. |
| `upstream` | [`Pipeline`](#pipeline) | Pipeline that triggered the pipeline. |
| `user` | [`User`](#user) | Pipeline user. |
@@ -5276,6 +5277,15 @@ Represents rules that commit pushes must follow.
| ----- | ---- | ----------- |
| `rejectUnsignedCommits` | [`Boolean!`](#boolean) | Indicates whether commits not signed through GPG will be rejected. |
+### `RecentFailures`
+
+Recent failure history of a test case.
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `baseBranch` | [`String`](#string) | Name of the base branch of the project. |
+| `count` | [`Int`](#int) | Number of times the test case has failed in the past 14 days. |
+
### `Release`
Represents a release.
@@ -5522,13 +5532,44 @@ Autogenerated return type of RepositionImageDiffNote.
| Field | Type | Description |
| ----- | ---- | ----------- |
-| `blobs` | [`BlobConnection`](#blobconnection) | Blobs contained within the repository. |
+| `blobs` | [`RepositoryBlobConnection`](#repositoryblobconnection) | Blobs contained within the repository. |
| `branchNames` | [`[String!]`](#string) | Names of branches available in this repository that match the search pattern. |
| `empty` | [`Boolean!`](#boolean) | Indicates repository has no visible content. |
| `exists` | [`Boolean!`](#boolean) | Indicates a corresponding Git repository exists on disk. |
| `rootRef` | [`String`](#string) | Default branch of the repository. |
| `tree` | [`Tree`](#tree) | Tree of the repository. |
+### `RepositoryBlob`
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `id` | [`ID!`](#id) | ID of the blob. |
+| `lfsOid` | [`String`](#string) | LFS OID of the blob. |
+| `mode` | [`String`](#string) | Blob mode. |
+| `name` | [`String`](#string) | Blob name. |
+| `oid` | [`String!`](#string) | OID of the blob. |
+| `path` | [`String!`](#string) | Path of the blob. |
+| `webPath` | [`String`](#string) | Web path of the blob. |
+
+### `RepositoryBlobConnection`
+
+The connection type for RepositoryBlob.
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `edges` | [`[RepositoryBlobEdge]`](#repositoryblobedge) | A list of edges. |
+| `nodes` | [`[RepositoryBlob]`](#repositoryblob) | A list of nodes. |
+| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+### `RepositoryBlobEdge`
+
+An edge in a connection.
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| `node` | [`RepositoryBlob`](#repositoryblob) | The item at the end of the edge. |
+
### `Requirement`
Represents a requirement.
@@ -6333,6 +6374,42 @@ An edge in a connection.
| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
| `node` | [`TerraformStateVersionRegistry`](#terraformstateversionregistry) | The item at the end of the edge. |
+### `TestCase`
+
+Test case in pipeline test report.
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `attachmentUrl` | [`String`](#string) | URL of the test case attachment file. |
+| `classname` | [`String`](#string) | Classname of the test case. |
+| `executionTime` | [`Float`](#float) | Test case execution time in seconds. |
+| `file` | [`String`](#string) | Path to the file of the test case. |
+| `name` | [`String`](#string) | Name of the test case. |
+| `recentFailures` | [`RecentFailures`](#recentfailures) | Recent failure history of the test case on the base branch. |
+| `stackTrace` | [`String`](#string) | Stack trace of the test case. |
+| `status` | [`TestCaseStatus`](#testcasestatus) | Status of the test case (error, failed, success, skipped). |
+| `systemOutput` | [`String`](#string) | System output of the test case. |
+
+### `TestCaseConnection`
+
+The connection type for TestCase.
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `count` | [`Int!`](#int) | Total count of collection. |
+| `edges` | [`[TestCaseEdge]`](#testcaseedge) | A list of edges. |
+| `nodes` | [`[TestCase]`](#testcase) | A list of nodes. |
+| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+### `TestCaseEdge`
+
+An edge in a connection.
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| `node` | [`TestCase`](#testcase) | The item at the end of the edge. |
+
### `TestReport`
Represents a requirement test report.
@@ -6386,6 +6463,22 @@ Total test report statistics.
| `suiteError` | [`String`](#string) | Test suite error message. |
| `time` | [`Float`](#float) | Total duration of the tests. |
+### `TestSuite`
+
+Test suite in a pipeline test report.
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `errorCount` | [`Int`](#int) | Total number of test cases that had an error. |
+| `failedCount` | [`Int`](#int) | Total number of test cases that failed in the test suite. |
+| `name` | [`String`](#string) | Name of the test suite. |
+| `skippedCount` | [`Int`](#int) | Total number of test cases that were skipped in the test suite. |
+| `successCount` | [`Int`](#int) | Total number of test cases that succeeded in the test suite. |
+| `suiteError` | [`String`](#string) | Test suite error message. |
+| `testCases` | [`TestCaseConnection`](#testcaseconnection) | Test cases in the test suite. |
+| `totalCount` | [`Int`](#int) | Total number of the test cases in the test suite. |
+| `totalTime` | [`Float`](#float) | Total duration of the tests in the test suite. |
+
### `TestSuiteSummary`
Test suite summary in a pipeline test report.
@@ -8424,6 +8517,15 @@ Common sort values.
| `updated_asc` **{warning-solid}** | **Deprecated:** This was renamed. Please use `UPDATED_ASC`. Deprecated in 13.5. |
| `updated_desc` **{warning-solid}** | **Deprecated:** This was renamed. Please use `UPDATED_DESC`. Deprecated in 13.5. |
+### `TestCaseStatus`
+
+| Value | Description |
+| ----- | ----------- |
+| `error` | Test case that has a status of error. |
+| `failed` | Test case that has a status of failed. |
+| `skipped` | Test case that has a status of skipped. |
+| `success` | Test case that has a status of success. |
+
### `TestReportState`
State of a test report.
@@ -8967,6 +9069,10 @@ A `PackagesPackageID` is a global ID. It is encoded as a string.
An example `PackagesPackageID` is: `"gid://gitlab/Packages::Package/1"`.
+### `PayloadAlertFieldPathSegment`
+
+String or integer.
+
### `ProjectID`
A `ProjectID` is a global ID. It is encoded as a string.
diff --git a/doc/api/usage_data.md b/doc/api/usage_data.md
index 671e60be587..024caa96565 100644
--- a/doc/api/usage_data.md
+++ b/doc/api/usage_data.md
@@ -5,17 +5,61 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: reference, api
---
-# UsageData API **(FREE SELF)**
+# Usage Data API **(FREE SELF)**
-The UsageData API, associated with [Usage Ping](../development/usage_ping/index.md), is available only for
-the use of GitLab instance [Administrator](../user/permissions.md) users.
+The Usage Data API is associated with [Usage Ping](../development/usage_ping/index.md).
-## UsageDataQueries API
+## Export metric definitions as a single YAML file
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57270) in GitLab 13.11.
+
+Export all metric definitions as a single YAML file, similar to the [Metrics Dictionary](../development/usage_ping/dictionary.md), for easier importing.
+
+```plaintext
+GET /usage_data/metric_definitions
+```
+
+Example request:
+
+```shell
+curl "https://gitlab.example.com/api/v4/usage_data/metric_definitions"
+```
+
+Example response:
+
+```yaml
+---
+- key_path: redis_hll_counters.search.i_search_paid_monthly
+ description: Calculated unique users to perform a search with a paid license enabled
+ by month
+ product_section: enablement
+ product_stage: enablement
+ product_group: group::global search
+ product_category: global_search
+ value_type: number
+ status: data_available
+ time_frame: 28d
+ data_source: redis_hll
+ distribution:
+ - ee
+ tier:
+ - premium
+ - ultimate
+...
+```
+
+## Export Usage Ping SQL queries
+
+This action is available only for the GitLab instance [Administrator](../user/permissions.md) users.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57016) in GitLab 13.11.
> - [Deployed behind a feature flag](../user/feature_flags.md), disabled by default.
-Return all of the raw SQL queries used to compute usage ping.
+Return all of the raw SQL queries used to compute Usage Ping.
+
+```plaintext
+GET /usage_data/queries
+```
Example request:
@@ -23,7 +67,7 @@ Example request:
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/usage_data/queries"
```
-Sample response
+Example response:
```json
{
diff --git a/doc/development/usage_ping/index.md b/doc/development/usage_ping/index.md
index 4b584c6ecc9..a62b58d6095 100644
--- a/doc/development/usage_ping/index.md
+++ b/doc/development/usage_ping/index.md
@@ -1411,37 +1411,6 @@ bin/rake gitlab:usage_data:dump_sql_in_json
bin/rake gitlab:usage_data:dump_sql_in_yaml > ~/Desktop/usage-metrics-2020-09-02.yaml
```
-## Export metric definitions as a single YAML file
-
-Use this API endpoint to export all metric definitions as a single YAML file, similar to the [Metrics Dictionary](dictionary.md), for easier importing.
-
-```plaintext
-GET /usage_data/metric_definitions
-```
-
-Response
-
-```yaml
----
-- key_path: redis_hll_counters.search.i_search_paid_monthly
- description: Calculated unique users to perform a search with a paid license enabled
- by month
- product_section: enablement
- product_stage: enablement
- product_group: group::global search
- product_category: global_search
- value_type: number
- status: data_available
- time_frame: 28d
- data_source: redis_hll
- distribution:
- - ee
- tier:
- - premium
- - ultimate
-...
-```
-
## Generating and troubleshooting usage ping
This activity is to be done via a detached screen session on a remote server.
diff --git a/doc/integration/jira/connect-app.md b/doc/integration/jira/connect-app.md
index fa0df27585e..667fa9e6b19 100644
--- a/doc/integration/jira/connect-app.md
+++ b/doc/integration/jira/connect-app.md
@@ -11,6 +11,8 @@ You can integrate GitLab.com and Jira Cloud using the
app in the Atlassian Marketplace. The user configuring GitLab for Jira must have
[Maintainer](../../user/permissions.md) permissions in the GitLab namespace.
+This integration method supports [smart commits](dvcs.md#smart-commits).
+
This method is recommended when using GitLab.com and Jira Cloud because data is
synchronized in real-time. The DVCS connector updates data only once per hour.
If you are not using both of these environments, use the [Jira DVCS Connector](dvcs.md) method.
diff --git a/doc/integration/jira/dvcs.md b/doc/integration/jira/dvcs.md
index 1d67dab9a84..ce80d370627 100644
--- a/doc/integration/jira/dvcs.md
+++ b/doc/integration/jira/dvcs.md
@@ -18,6 +18,25 @@ are accessible.
- **Jira Cloud**: Your instance must be accessible through the internet.
- **Jira Server**: Your network must allow access to your instance.
+## Smart commits
+
+When connecting GitLab with Jira with DVCS, you can process your Jira issues using
+special commands, called
+[Smart Commits](https://support.atlassian.com/jira-software-cloud/docs/process-issues-with-smart-commits/),
+in your commit messages. With Smart Commits, you can:
+
+- Comment on issues.
+- Record time-tracking information against issues.
+- Transition issues to any status defined in the Jira project's workflow.
+
+Commands must be in the first line of the commit message. The
+[Jira Software documentation](https://support.atlassian.com/jira-software-cloud/docs/process-issues-with-smart-commits/)
+contains more information about how smart commits work, and what commands are available
+for your use.
+
+For smart commits to work, the committing user on GitLab must have a corresponding
+user on Jira with the same email address or username.
+
## Configure a GitLab application for DVCS
We recommend you create and use a `jira` user in GitLab, and use the account only
diff --git a/doc/user/clusters/agent/index.md b/doc/user/clusters/agent/index.md
index 0ad2972c5a8..31accfdd9e4 100644
--- a/doc/user/clusters/agent/index.md
+++ b/doc/user/clusters/agent/index.md
@@ -8,9 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223061) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.4.
> - [In GitLab 13.10](https://gitlab.com/gitlab-org/gitlab/-/issues/300960), KAS became available on GitLab.com under `wss://kas.gitlab.com` through an Early Adopter Program.
-
-WARNING:
-This feature might not be available to you. Check the **version history** note above for details.
+> - Introduced in GitLab 13.11, the GitLab Kubernetes Agent became available to every project on GitLab.com.
The [GitLab Kubernetes Agent](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent)
is an active in-cluster component for solving GitLab and Kubernetes integration
@@ -169,17 +167,21 @@ the Agent in subsequent steps. You can create an Agent record with GraphQL:
### Install the Agent into the cluster
-Next, install the in-cluster component of the Agent.
+To install the in-cluster component of the Agent, first you need to define a namespace. To create a new namespace,
+for example, `gitlab-kubernetes-agent`, run:
-NOTE:
-For GitLab.com users, the KAS is available at `wss://kas.gitlab.com`.
+```shell
+kubectl create namespace gitlab-kubernetes-agent
+```
-#### One-liner installation
+To perform a one-liner installation, run the command below. Make sure to replace:
-Replace the value of `agent-token` below with the token received from the previous step. Also, replace `kas-address` with the configured access of the Kubernetes Agent Server:
+- `your-agent-token` with the token received from the previous step.
+- `gitlab-kubernetes-agent` with the namespace you defined in the previous step.
+- `wss://kas.gitlab.example.com` with the configured access of the Kubernetes Agent Server (KAS). For GitLab.com users, the KAS is available under `wss://kas.gitlab.com`.
```shell
-docker run --pull=always --rm registry.gitlab.com/gitlab-org/cluster-integration/gitlab-agent/cli:stable generate --agent-token=your-agent-token --kas-address=wss://kas.gitlab.example.com --agent-version stable | kubectl apply -f -
+docker run --pull=always --rm registry.gitlab.com/gitlab-org/cluster-integration/gitlab-agent/cli:stable generate --agent-token=your-agent-token --kas-address=wss://kas.gitlab.example.com --agent-version stable --namespace gitlab-kubernetes-agent | kubectl apply -f -
```
Set `--agent-version` to the latest released patch version matching your
@@ -206,17 +208,11 @@ Otherwise, you can follow below for fully manual, detailed installation steps.
After generating the token, you must apply it to the Kubernetes cluster.
-1. If you haven't previously defined or created a namespace, run the following command:
+To create your Secret, run:
- ```shell
- kubectl create namespace <YOUR-DESIRED-NAMESPACE>
- ```
-
-1. Run the following command to create your Secret:
-
- ```shell
- kubectl create secret generic -n <YOUR-DESIRED-NAMESPACE> gitlab-agent-token --from-literal=token='YOUR_AGENT_TOKEN'
- ```
+```shell
+kubectl create secret generic -n <YOUR_NAMESPACE> gitlab-agent-token --from-literal=token='YOUR_AGENT_TOKEN'
+```
The following example file contains the
Kubernetes resources required for the Agent to be installed. You can modify this
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index dc63a32ed10..56a339e02d2 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -1311,7 +1311,6 @@ X-Gitlab-Event: Job Hook
"name": "User",
"email": "user@gitlab.com",
"avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon",
- "email": "admin@example.com"
},
"commit": {
"id": 2366,
diff --git a/doc/user/project/merge_requests/creating_merge_requests.md b/doc/user/project/merge_requests/creating_merge_requests.md
index 58e80504212..3a5a581198b 100644
--- a/doc/user/project/merge_requests/creating_merge_requests.md
+++ b/doc/user/project/merge_requests/creating_merge_requests.md
@@ -172,6 +172,9 @@ create a merge request from your fork to contribute back to the main project:
1. In the left menu, go to **Merge Requests**, and click **New Merge Request**.
1. In the **Source branch** drop-down list box, select your branch in your forked repository as the source branch.
1. In the **Target branch** drop-down list box, select the branch from the upstream repository as the target branch.
+ You can set a [default target project](#set-the-default-target-project) to
+ change the default target branch (which can be useful when working with a
+ forked project).
1. After entering the credentials, click **Compare branches and continue** to compare your local changes to the upstream repository.
1. Assign a user to review your changes, and click **Submit merge request**.
@@ -183,6 +186,24 @@ fork from its upstream project in the **Settings > Advanced Settings** section b
For further details, [see the forking workflow documentation](../repository/forking_workflow.md).
+## Set the default target project
+
+Merge requests have a source and a target project which are the same, unless
+forking is involved. Creating a fork of the project can cause either of these
+scenarios when you create a new merge request:
+
+- You target an upstream project (the project you forked, and the default
+ option).
+- You target your own fork.
+
+If you want to have merge requests from a fork by default target your own fork
+(instead of the upstream project), you can change the default by:
+
+1. In your project, go to **Settings > General > Merge requests**.
+1. In the **Target project** section, select the option you want to use for
+ your default target project.
+1. Select **Save changes**.
+
## New merge request by email **(FREE SELF)**
_This feature needs [incoming email](../../../administration/incoming_email.md)
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index bcfd7f5e5d9..6d37d26f6e8 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -214,6 +214,7 @@ Set up your project's merge request settings:
- Enable [merge only when all threads are resolved](../../discussions/index.md#only-allow-merge-requests-to-be-merged-if-all-threads-are-resolved).
- Enable [`delete source branch after merge` option by default](../merge_requests/getting_started.md#deleting-the-source-branch)
- Configure [suggested changes commit messages](../../discussions/index.md#configure-the-commit-message-for-applied-suggestions)
+- Configure [the default target project](../merge_requests/creating_merge_requests.md#set-the-default-target-project) for merge requests coming from forks.
### Service Desk
diff --git a/lib/gitlab/background_migration/migrate_pages_to_zip_storage.rb b/lib/gitlab/background_migration/migrate_pages_to_zip_storage.rb
new file mode 100644
index 00000000000..b7a912da060
--- /dev/null
+++ b/lib/gitlab/background_migration/migrate_pages_to_zip_storage.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # migrates pages from legacy storage to zip format
+ # we intentionally use application code here because
+ # it has a lot of dependencies including models, carrierwave uploaders and service objects
+ # and copying all or part of this code in the background migration doesn't add much value
+ # see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54578 for discussion
+ class MigratePagesToZipStorage
+ def perform(start_id, stop_id)
+ ::Pages::MigrateFromLegacyStorageService.new(Gitlab::AppLogger,
+ ignore_invalid_entries: false,
+ mark_projects_as_not_deployed: false)
+ .execute_for_batch(start_id..stop_id)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
index 46ecb10ea2b..c3c1728602c 100644
--- a/lib/gitlab/ci/pipeline/chain/command.rb
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -84,7 +84,7 @@ module Gitlab
end
def metrics
- @metrics ||= ::Gitlab::Ci::Pipeline::Metrics.new
+ @metrics ||= ::Gitlab::Ci::Pipeline::Metrics
end
def observe_creation_duration(duration)
@@ -97,6 +97,11 @@ module Gitlab
.observe({ source: pipeline.source.to_s }, pipeline.total_size)
end
+ def increment_pipeline_failure_reason_counter(reason)
+ metrics.pipeline_failure_reason_counter
+ .increment(reason: (reason || :unknown_failure).to_s)
+ end
+
def dangling_build?
%i[ondemand_dast_scan webide].include?(source)
end
diff --git a/lib/gitlab/ci/pipeline/chain/helpers.rb b/lib/gitlab/ci/pipeline/chain/helpers.rb
index f995f62f87b..22a7dbe61aa 100644
--- a/lib/gitlab/ci/pipeline/chain/helpers.rb
+++ b/lib/gitlab/ci/pipeline/chain/helpers.rb
@@ -13,16 +13,7 @@ module Gitlab
pipeline.add_error_message(message)
- if drop_reason && persist_pipeline?
- if Feature.enabled?(:ci_pipeline_ensure_iid_on_drop, pipeline.project, default_enabled: :yaml)
- # Project iid must be called outside a transaction, so we ensure it is set here
- # otherwise it may be set within the state transition transaction of the drop! call
- # which it will lock the InternalId row for the whole transaction
- pipeline.ensure_project_iid!
- end
-
- pipeline.drop!(drop_reason)
- end
+ drop_pipeline!(drop_reason)
# TODO: consider not to rely on AR errors directly as they can be
# polluted with other unrelated errors (e.g. state machine)
@@ -34,8 +25,23 @@ module Gitlab
pipeline.add_warning_message(message)
end
- def persist_pipeline?
- command.save_incompleted && !pipeline.readonly?
+ private
+
+ def drop_pipeline!(drop_reason)
+ return if pipeline.readonly?
+
+ if drop_reason && command.save_incompleted
+ if Feature.enabled?(:ci_pipeline_ensure_iid_on_drop, pipeline.project, default_enabled: :yaml)
+ # Project iid must be called outside a transaction, so we ensure it is set here
+ # otherwise it may be set within the state transition transaction of the drop! call
+ # which it will lock the InternalId row for the whole transaction
+ pipeline.ensure_project_iid!
+ end
+
+ pipeline.drop!(drop_reason)
+ else
+ command.increment_pipeline_failure_reason_counter(drop_reason)
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/metrics.rb b/lib/gitlab/ci/pipeline/chain/metrics.rb
index 0d7449813b4..b17ae77d445 100644
--- a/lib/gitlab/ci/pipeline/chain/metrics.rb
+++ b/lib/gitlab/ci/pipeline/chain/metrics.rb
@@ -14,7 +14,7 @@ module Gitlab
end
def counter
- ::Gitlab::Ci::Pipeline::Metrics.new.pipelines_created_counter
+ ::Gitlab::Ci::Pipeline::Metrics.pipelines_created_counter
end
end
end
diff --git a/lib/gitlab/ci/pipeline/metrics.rb b/lib/gitlab/ci/pipeline/metrics.rb
index c77f4dcca5a..6cb6fd3920d 100644
--- a/lib/gitlab/ci/pipeline/metrics.rb
+++ b/lib/gitlab/ci/pipeline/metrics.rb
@@ -4,55 +4,57 @@ module Gitlab
module Ci
module Pipeline
class Metrics
- include Gitlab::Utils::StrongMemoize
+ def self.pipeline_creation_duration_histogram
+ name = :gitlab_ci_pipeline_creation_duration_seconds
+ comment = 'Pipeline creation duration'
+ labels = {}
+ buckets = [0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0, 20.0, 50.0, 240.0]
- def pipeline_creation_duration_histogram
- strong_memoize(:pipeline_creation_duration_histogram) do
- name = :gitlab_ci_pipeline_creation_duration_seconds
- comment = 'Pipeline creation duration'
- labels = {}
- buckets = [0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0, 20.0, 50.0, 240.0]
+ ::Gitlab::Metrics.histogram(name, comment, labels, buckets)
+ end
+
+ def self.pipeline_size_histogram
+ name = :gitlab_ci_pipeline_size_builds
+ comment = 'Pipeline size'
+ labels = { source: nil }
+ buckets = [0, 1, 5, 10, 20, 50, 100, 200, 500, 1000]
+
+ ::Gitlab::Metrics.histogram(name, comment, labels, buckets)
+ end
+
+ def self.pipeline_processing_events_counter
+ name = :gitlab_ci_pipeline_processing_events_total
+ comment = 'Total amount of pipeline processing events'
- ::Gitlab::Metrics.histogram(name, comment, labels, buckets)
- end
+ Gitlab::Metrics.counter(name, comment)
end
- def pipeline_size_histogram
- strong_memoize(:pipeline_size_histogram) do
- name = :gitlab_ci_pipeline_size_builds
- comment = 'Pipeline size'
- labels = { source: nil }
- buckets = [0, 1, 5, 10, 20, 50, 100, 200, 500, 1000]
+ def self.pipelines_created_counter
+ name = :pipelines_created_total
+ comment = 'Counter of pipelines created'
- ::Gitlab::Metrics.histogram(name, comment, labels, buckets)
- end
+ Gitlab::Metrics.counter(name, comment)
end
- def pipeline_processing_events_counter
- strong_memoize(:pipeline_processing_events_counter) do
- name = :gitlab_ci_pipeline_processing_events_total
- comment = 'Total amount of pipeline processing events'
+ def self.legacy_update_jobs_counter
+ name = :ci_legacy_update_jobs_as_retried_total
+ comment = 'Counter of occurrences when jobs were not being set as retried before update_retried'
- Gitlab::Metrics.counter(name, comment)
- end
+ Gitlab::Metrics.counter(name, comment)
end
- def pipelines_created_counter
- strong_memoize(:pipelines_created_count) do
- name = :pipelines_created_total
- comment = 'Counter of pipelines created'
+ def self.pipeline_failure_reason_counter
+ name = :gitlab_ci_pipeline_failure_reasons
+ comment = 'Counter of pipeline failure reasons'
- Gitlab::Metrics.counter(name, comment)
- end
+ Gitlab::Metrics.counter(name, comment)
end
- def legacy_update_jobs_counter
- strong_memoize(:legacy_update_jobs_counter) do
- name = :ci_legacy_update_jobs_as_retried_total
- comment = 'Counter of occurrences when jobs were not being set as retried before update_retried'
+ def self.job_failure_reason_counter
+ name = :gitlab_ci_job_failure_reasons
+ comment = 'Counter of job failure reasons'
- Gitlab::Metrics.counter(name, comment)
- end
+ Gitlab::Metrics.counter(name, comment)
end
end
end
diff --git a/lib/gitlab/database/background_migration/batched_migration.rb b/lib/gitlab/database/background_migration/batched_migration.rb
index 324695adb1f..4aa33ed7946 100644
--- a/lib/gitlab/database/background_migration/batched_migration.rb
+++ b/lib/gitlab/database/background_migration/batched_migration.rb
@@ -57,6 +57,13 @@ module Gitlab
def batch_class_name=(class_name)
write_attribute(:batch_class_name, class_name.demodulize)
end
+
+ def prometheus_labels
+ @prometheus_labels ||= {
+ migration_id: id,
+ migration_identifier: "%s/%s.%s" % [job_class_name, table_name, column_name]
+ }
+ end
end
end
end
diff --git a/lib/gitlab/database/background_migration/batched_migration_wrapper.rb b/lib/gitlab/database/background_migration/batched_migration_wrapper.rb
index bbaa8040203..c276f8ce75b 100644
--- a/lib/gitlab/database/background_migration/batched_migration_wrapper.rb
+++ b/lib/gitlab/database/background_migration/batched_migration_wrapper.rb
@@ -4,6 +4,8 @@ module Gitlab
module Database
module BackgroundMigration
class BatchedMigrationWrapper
+ extend Gitlab::Utils::StrongMemoize
+
# Wraps the execution of a batched_background_migration.
#
# Updates the job's tracking records with the status of the migration
@@ -23,6 +25,7 @@ module Gitlab
raise e
ensure
finish_tracking_execution(batch_tracking_record)
+ track_prometheus_metrics(batch_tracking_record)
end
private
@@ -51,6 +54,65 @@ module Gitlab
tracking_record.finished_at = Time.current
tracking_record.save!
end
+
+ def track_prometheus_metrics(tracking_record)
+ migration = tracking_record.batched_migration
+ base_labels = migration.prometheus_labels
+
+ metric_for(:gauge_batch_size).set(base_labels, tracking_record.batch_size)
+ metric_for(:gauge_sub_batch_size).set(base_labels, tracking_record.sub_batch_size)
+ metric_for(:counter_updated_tuples).increment(base_labels, tracking_record.batch_size)
+
+ # Time efficiency: Ratio of duration to interval (ideal: less than, but close to 1)
+ efficiency = (tracking_record.finished_at - tracking_record.started_at).to_i / migration.interval.to_f
+ metric_for(:histogram_time_efficiency).observe(base_labels, efficiency)
+
+ if metrics = tracking_record.metrics
+ metrics['timings']&.each do |key, timings|
+ summary = metric_for(:histogram_timings)
+ labels = base_labels.merge(operation: key)
+
+ timings.each do |timing|
+ summary.observe(labels, timing)
+ end
+ end
+ end
+ end
+
+ def metric_for(name)
+ self.class.metrics[name]
+ end
+
+ def self.metrics
+ strong_memoize(:metrics) do
+ {
+ gauge_batch_size: Gitlab::Metrics.gauge(
+ :batched_migration_job_batch_size,
+ 'Batch size for a batched migration job'
+ ),
+ gauge_sub_batch_size: Gitlab::Metrics.gauge(
+ :batched_migration_job_sub_batch_size,
+ 'Sub-batch size for a batched migration job'
+ ),
+ counter_updated_tuples: Gitlab::Metrics.counter(
+ :batched_migration_job_updated_tuples_total,
+ 'Number of tuples updated by batched migration job'
+ ),
+ histogram_timings: Gitlab::Metrics.histogram(
+ :batched_migration_job_duration_seconds,
+ 'Timings for a batched migration job',
+ {},
+ [0.1, 0.25, 0.5, 1, 5].freeze
+ ),
+ histogram_time_efficiency: Gitlab::Metrics.histogram(
+ :batched_migration_job_time_efficiency,
+ 'Ratio of job duration to interval',
+ {},
+ [0.5, 0.9, 1, 1.5, 2].freeze
+ )
+ }
+ end
+ end
end
end
end
diff --git a/lib/gitlab/metrics/background_transaction.rb b/lib/gitlab/metrics/background_transaction.rb
index 3dda68bf93f..a1fabe75a97 100644
--- a/lib/gitlab/metrics/background_transaction.rb
+++ b/lib/gitlab/metrics/background_transaction.rb
@@ -34,8 +34,9 @@ module Gitlab
def labels
@labels ||= {
- endpoint_id: current_context&.get_attribute(:caller_id),
- feature_category: current_context&.get_attribute(:feature_category)
+ endpoint_id: endpoint_id,
+ feature_category: feature_category,
+ queue: queue
}
end
@@ -44,6 +45,21 @@ module Gitlab
def current_context
Labkit::Context.current
end
+
+ def feature_category
+ current_context&.get_attribute(:feature_category)
+ end
+
+ def endpoint_id
+ current_context&.get_attribute(:caller_id)
+ end
+
+ def queue
+ worker_class = endpoint_id.to_s.safe_constantize
+ return if worker_class.blank? || !worker_class.respond_to?(:queue)
+
+ worker_class.queue.to_s
+ end
end
end
end
diff --git a/lib/gitlab/pages/migration_helper.rb b/lib/gitlab/pages/migration_helper.rb
new file mode 100644
index 00000000000..8f8667fafd9
--- /dev/null
+++ b/lib/gitlab/pages/migration_helper.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Pages
+ class MigrationHelper
+ def initialize(logger = nil)
+ @logger = logger
+ end
+
+ def migrate_to_remote_storage
+ deployments = ::PagesDeployment.with_files_stored_locally
+ migrate(deployments, ObjectStorage::Store::REMOTE)
+ end
+
+ def migrate_to_local_storage
+ deployments = ::PagesDeployment.with_files_stored_remotely
+ migrate(deployments, ObjectStorage::Store::LOCAL)
+ end
+
+ private
+
+ def batch_size
+ ENV.fetch('MIGRATION_BATCH_SIZE', 10).to_i
+ end
+
+ def migrate(deployments, store)
+ deployments.find_each(batch_size: batch_size) do |deployment| # rubocop:disable CodeReuse/ActiveRecord
+ deployment.file.migrate!(store)
+
+ log_success(deployment, store)
+ rescue => e
+ log_error(e, deployment)
+ end
+ end
+
+ def log_success(deployment, store)
+ logger.info("Transferred deployment ID #{deployment.id} of type #{deployment.file_type} with size #{deployment.size} to #{storage_label(store)} storage")
+ end
+
+ def log_error(err, deployment)
+ logger.warn("Failed to transfer deployment of type #{deployment.file_type} and ID #{deployment.id} with error: #{err.message}")
+ end
+
+ def storage_label(store)
+ if store == ObjectStorage::Store::LOCAL
+ 'local'
+ else
+ 'object'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage_data_non_sql_metrics.rb b/lib/gitlab/usage_data_non_sql_metrics.rb
index 6bd74a2a993..1f72bf4ce26 100644
--- a/lib/gitlab/usage_data_non_sql_metrics.rb
+++ b/lib/gitlab/usage_data_non_sql_metrics.rb
@@ -24,6 +24,12 @@ module Gitlab
def histogram(relation, column, buckets:, bucket_size: buckets.size)
SQL_METRIC_DEFAULT
end
+
+ def maximum_id(model)
+ end
+
+ def minimum_id(model)
+ end
end
end
end
diff --git a/lib/tasks/gitlab/pages.rake b/lib/tasks/gitlab/pages.rake
index 606d1369e18..ee2931f0c4f 100644
--- a/lib/tasks/gitlab/pages.rake
+++ b/lib/tasks/gitlab/pages.rake
@@ -9,10 +9,9 @@ namespace :gitlab do
logger.info('Starting to migrate legacy pages storage to zip deployments')
result = ::Pages::MigrateFromLegacyStorageService.new(logger,
- migration_threads: migration_threads,
- batch_size: batch_size,
ignore_invalid_entries: ignore_invalid_entries,
- mark_projects_as_not_deployed: mark_projects_as_not_deployed).execute
+ mark_projects_as_not_deployed: mark_projects_as_not_deployed)
+ .execute_with_threads(threads: migration_threads, batch_size: batch_size)
logger.info("A total of #{result[:migrated] + result[:errored]} projects were processed.")
logger.info("- The #{result[:migrated]} projects migrated successfully")
@@ -58,5 +57,33 @@ namespace :gitlab do
ENV.fetch('PAGES_MIGRATION_MARK_PROJECTS_AS_NOT_DEPLOYED', 'false')
)
end
+
+ namespace :deployments do
+ task migrate_to_object_storage: :gitlab_environment do
+ logger = Logger.new(STDOUT)
+ logger.info('Starting transfer of pages deployments to remote storage')
+
+ helper = Gitlab::Pages::MigrationHelper.new(logger)
+
+ begin
+ helper.migrate_to_remote_storage
+ rescue => e
+ logger.error(e.message)
+ end
+ end
+
+ task migrate_to_local: :gitlab_environment do
+ logger = Logger.new(STDOUT)
+ logger.info('Starting transfer of Pages deployments to local storage')
+
+ helper = Gitlab::Pages::MigrationHelper.new(logger)
+
+ begin
+ helper.migrate_to_local_storage
+ rescue => e
+ logger.error(e.message)
+ end
+ end
+ end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 89890deb77a..58ca3603ff0 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5319,9 +5319,6 @@ msgstr ""
msgid "Branches|Show stale branches"
msgstr ""
-msgid "Branches|Sort by"
-msgstr ""
-
msgid "Branches|Stale"
msgstr ""
@@ -6656,9 +6653,6 @@ msgstr ""
msgid "CloudLicense|Paste your activation code"
msgstr ""
-msgid "CloudLicense|This instance is currently using the %{planName} plan."
-msgstr ""
-
msgid "CloudLicense|This is the highest peak of users on your installation since the license started."
msgstr ""
@@ -6677,9 +6671,6 @@ msgstr ""
msgid "CloudLicense|You'll be charged for %{trueUpLinkStart}users over license%{trueUpLinkEnd} on a quarterly or annual basis, depending on the terms of your agreement."
msgstr ""
-msgid "CloudLicense|Your subscription"
-msgstr ""
-
msgid "Cluster"
msgstr ""
@@ -30378,6 +30369,12 @@ msgstr ""
msgid "SuperSonics|Valid From"
msgstr ""
+msgid "SuperSonics|You do not have an active subscription"
+msgstr ""
+
+msgid "SuperSonics|Your subscription"
+msgstr ""
+
msgid "Support"
msgstr ""
diff --git a/qa/qa/page/project/menu.rb b/qa/qa/page/project/menu.rb
index d9fae3db23f..cb7323ac62d 100644
--- a/qa/qa/page/project/menu.rb
+++ b/qa/qa/page/project/menu.rb
@@ -14,7 +14,6 @@ module QA
include SubMenus::Packages
view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
- element :activity_link
element :merge_requests_link
element :snippets_link
element :members_link
@@ -24,6 +23,10 @@ module QA
element :wiki_link
end
+ view 'app/views/shared/nav/_sidebar_menu_item.html.haml' do
+ element :sidebar_menu_item_link
+ end
+
def click_merge_requests
within_sidebar do
click_element(:merge_requests_link)
@@ -38,7 +41,7 @@ module QA
def click_activity
within_sidebar do
- click_element(:activity_link)
+ click_element(:sidebar_menu_item_link, menu_item: 'Activity')
end
end
diff --git a/qa/qa/page/project/sub_menus/project.rb b/qa/qa/page/project/sub_menus/project.rb
index 8a648279919..ecb3148b486 100644
--- a/qa/qa/page/project/sub_menus/project.rb
+++ b/qa/qa/page/project/sub_menus/project.rb
@@ -13,8 +13,8 @@ module QA
base.class_eval do
include QA::Page::Project::SubMenus::Common
- view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
- element :project_link
+ view 'app/views/shared/nav/_sidebar_menu.html.haml' do
+ element :sidebar_menu_link
end
end
end
@@ -22,7 +22,7 @@ module QA
def click_project
retry_on_exception do
within_sidebar do
- click_element(:project_link)
+ click_element(:sidebar_menu_link, menu_item: 'Project overview')
end
end
end
diff --git a/spec/features/projects/branches/user_deletes_branch_spec.rb b/spec/features/projects/branches/user_deletes_branch_spec.rb
index 3da72bc1f24..bebb4bb679b 100644
--- a/spec/features/projects/branches/user_deletes_branch_spec.rb
+++ b/spec/features/projects/branches/user_deletes_branch_spec.rb
@@ -12,10 +12,12 @@ RSpec.describe "User deletes branch", :js do
end
it "deletes branch" do
- stub_feature_flags(gldropdown_branches: false)
visit(project_branches_path(project))
- fill_in("branch-search", with: "improve/awesome").native.send_keys(:enter)
+ branch_search = find('input[data-testid="branch-search"]')
+
+ branch_search.set('improve/awesome')
+ branch_search.native.send_keys(:enter)
page.within(".js-branch-improve\\/awesome") do
accept_alert { find(".btn-danger").click }
@@ -25,23 +27,4 @@ RSpec.describe "User deletes branch", :js do
expect(page).to have_css(".js-branch-improve\\/awesome", visible: :hidden)
end
-
- context 'with gldropdown_branches enabled' do
- it "deletes branch" do
- visit(project_branches_path(project))
-
- branch_search = find('input[data-testid="branch-search"]')
-
- branch_search.set('improve/awesome')
- branch_search.native.send_keys(:enter)
-
- page.within(".js-branch-improve\\/awesome") do
- accept_alert { find(".btn-danger").click }
- end
-
- wait_for_requests
-
- expect(page).to have_css(".js-branch-improve\\/awesome", visible: :hidden)
- end
- end
end
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index 98c125ec5db..f805416b03d 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -86,29 +86,16 @@ RSpec.describe 'Branches' do
describe 'Find branches' do
it 'shows filtered branches', :js do
- stub_feature_flags(gldropdown_branches: false)
visit project_branches_path(project)
- fill_in 'branch-search', with: 'fix'
- find('#branch-search').native.send_keys(:enter)
+ branch_search = find('input[data-testid="branch-search"]')
+
+ branch_search.set('fix')
+ branch_search.native.send_keys(:enter)
expect(page).to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 1)
end
-
- context 'with gldropdown_branches enabled' do
- it 'shows filtered branches', :js do
- visit project_branches_path(project)
-
- branch_search = find('input[data-testid="branch-search"]')
-
- branch_search.set('fix')
- branch_search.native.send_keys(:enter)
-
- expect(page).to have_content('fix')
- expect(find('.all-branches')).to have_selector('li', count: 1)
- end
- end
end
describe 'Delete unprotected branch on Overview' do
@@ -129,52 +116,28 @@ RSpec.describe 'Branches' do
expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :updated_desc))
end
- it 'sorts the branches by name' do
- stub_feature_flags(gldropdown_branches: false)
+ it 'sorts the branches by name', :js do
visit project_branches_filtered_path(project, state: 'all')
click_button "Last updated" # Open sorting dropdown
- click_link "Name"
+ within '[data-testid="branches-dropdown"]' do
+ find('p', text: 'Name').click
+ end
expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :name))
end
- context 'with gldropdown_branches enabled' do
- it 'sorts the branches by name', :js do
- visit project_branches_filtered_path(project, state: 'all')
-
- click_button "Last updated" # Open sorting dropdown
- within '[data-testid="branches-dropdown"]' do
- find('p', text: 'Name').click
- end
-
- expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :name))
- end
- end
-
- it 'sorts the branches by oldest updated' do
- stub_feature_flags(gldropdown_branches: false)
+ it 'sorts the branches by oldest updated', :js do
visit project_branches_filtered_path(project, state: 'all')
click_button "Last updated" # Open sorting dropdown
- click_link "Oldest updated"
+ within '[data-testid="branches-dropdown"]' do
+ find('p', text: 'Oldest updated').click
+ end
expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :updated_asc))
end
- context 'with gldropdown_branches enabled' do
- it 'sorts the branches by oldest updated', :js do
- visit project_branches_filtered_path(project, state: 'all')
-
- click_button "Last updated" # Open sorting dropdown
- within '[data-testid="branches-dropdown"]' do
- find('p', text: 'Oldest updated').click
- end
-
- expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :updated_asc))
- end
- end
-
it 'avoids a N+1 query in branches index' do
control_count = ActiveRecord::QueryRecorder.new { visit project_branches_path(project) }.count
@@ -186,39 +149,26 @@ RSpec.describe 'Branches' do
describe 'Find branches on All branches' do
it 'shows filtered branches', :js do
- stub_feature_flags(gldropdown_branches: false)
visit project_branches_filtered_path(project, state: 'all')
- fill_in 'branch-search', with: 'fix'
- find('#branch-search').native.send_keys(:enter)
+ branch_search = find('input[data-testid="branch-search"]')
+
+ branch_search.set('fix')
+ branch_search.native.send_keys(:enter)
expect(page).to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 1)
end
-
- context 'with gldropdown_branches enabled' do
- it 'shows filtered branches', :js do
- visit project_branches_filtered_path(project, state: 'all')
-
- branch_search = find('input[data-testid="branch-search"]')
-
- branch_search.set('fix')
- branch_search.native.send_keys(:enter)
-
- expect(page).to have_content('fix')
- expect(find('.all-branches')).to have_selector('li', count: 1)
- end
- end
end
describe 'Delete unprotected branch on All branches' do
it 'removes branch after confirmation', :js do
- stub_feature_flags(gldropdown_branches: false)
visit project_branches_filtered_path(project, state: 'all')
- fill_in 'branch-search', with: 'fix'
+ branch_search = find('input[data-testid="branch-search"]')
- find('#branch-search').native.send_keys(:enter)
+ branch_search.set('fix')
+ branch_search.native.send_keys(:enter)
expect(page).to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 1)
@@ -227,24 +177,6 @@ RSpec.describe 'Branches' do
expect(page).not_to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 0)
end
-
- context 'with gldropdown_branches enabled' do
- it 'removes branch after confirmation', :js do
- visit project_branches_filtered_path(project, state: 'all')
-
- branch_search = find('input[data-testid="branch-search"]')
-
- branch_search.set('fix')
- branch_search.native.send_keys(:enter)
-
- expect(page).to have_content('fix')
- expect(find('.all-branches')).to have_selector('li', count: 1)
- accept_confirm { find('.js-branch-fix .btn-danger').click }
-
- expect(page).not_to have_content('fix')
- expect(find('.all-branches')).to have_selector('li', count: 0)
- end
- end
end
context 'on project with 0 branch' do
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index a88dc9f8655..207b74c990a 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -22,25 +22,13 @@ RSpec.describe 'Protected Branches', :js do
end
it 'does not allow developer to removes protected branch' do
- stub_feature_flags(gldropdown_branches: false)
visit project_branches_path(project)
- fill_in 'branch-search', with: 'fix'
- find('#branch-search').native.send_keys(:enter)
+ find('input[data-testid="branch-search"]').set('fix')
+ find('input[data-testid="branch-search"]').native.send_keys(:enter)
expect(page).to have_css('.btn-danger.disabled')
end
-
- context 'with gldropdown_branches enabled' do
- it 'does not allow developer to removes protected branch' do
- visit project_branches_path(project)
-
- find('input[data-testid="branch-search"]').set('fix')
- find('input[data-testid="branch-search"]').native.send_keys(:enter)
-
- expect(page).to have_css('.btn-danger.disabled')
- end
- end
end
end
@@ -57,11 +45,10 @@ RSpec.describe 'Protected Branches', :js do
end
it 'removes branch after modal confirmation' do
- stub_feature_flags(gldropdown_branches: false)
visit project_branches_path(project)
- fill_in 'branch-search', with: 'fix'
- find('#branch-search').native.send_keys(:enter)
+ find('input[data-testid="branch-search"]').set('fix')
+ find('input[data-testid="branch-search"]').native.send_keys(:enter)
expect(page).to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 1)
@@ -71,33 +58,11 @@ RSpec.describe 'Protected Branches', :js do
fill_in 'delete_branch_input', with: 'fix'
click_link 'Delete protected branch'
- fill_in 'branch-search', with: 'fix'
- find('#branch-search').native.send_keys(:enter)
+ find('input[data-testid="branch-search"]').set('fix')
+ find('input[data-testid="branch-search"]').native.send_keys(:enter)
expect(page).to have_content('No branches to show')
end
-
- context 'with gldropdown_branches enabled' do
- it 'removes branch after modal confirmation' do
- visit project_branches_path(project)
-
- find('input[data-testid="branch-search"]').set('fix')
- find('input[data-testid="branch-search"]').native.send_keys(:enter)
-
- expect(page).to have_content('fix')
- expect(find('.all-branches')).to have_selector('li', count: 1)
- page.find('[data-target="#modal-delete-branch"]').click
-
- expect(page).to have_css('.js-delete-branch[disabled]')
- fill_in 'delete_branch_input', with: 'fix'
- click_link 'Delete protected branch'
-
- find('input[data-testid="branch-search"]').set('fix')
- find('input[data-testid="branch-search"]').native.send_keys(:enter)
-
- expect(page).to have_content('No branches to show')
- end
- end
end
end
diff --git a/spec/features/whats_new_spec.rb b/spec/features/whats_new_spec.rb
index 7c5625486f5..55b96361f03 100644
--- a/spec/features/whats_new_spec.rb
+++ b/spec/features/whats_new_spec.rb
@@ -2,34 +2,60 @@
require "spec_helper"
-RSpec.describe "renders a `whats new` dropdown item", :js do
+RSpec.describe "renders a `whats new` dropdown item" do
let_it_be(:user) { create(:user) }
- before do
- sign_in(user)
- end
+ context 'when not logged in' do
+ it 'and on .com it renders' do
+ allow(Gitlab).to receive(:com?).and_return(true)
- it 'shows notification dot and count and removes it once viewed' do
- visit root_dashboard_path
+ visit user_path(user)
- page.within '.header-help' do
- expect(page).to have_selector('.notification-dot', visible: true)
+ page.within '.header-help' do
+ find('.header-help-dropdown-toggle').click
- find('.header-help-dropdown-toggle').click
+ expect(page).to have_button(text: "What's new")
+ end
+ end
+
+ it "doesn't render what's new" do
+ visit user_path(user)
- expect(page).to have_button(text: "What's new")
- expect(page).to have_selector('.js-whats-new-notification-count')
+ page.within '.header-help' do
+ find('.header-help-dropdown-toggle').click
+
+ expect(page).not_to have_button(text: "What's new")
+ end
+ end
+ end
- find('button', text: "What's new").click
+ context 'when logged in', :js do
+ before do
+ sign_in(user)
end
- find('.whats-new-drawer .gl-drawer-close-button').click
- find('.header-help-dropdown-toggle').click
+ it 'shows notification dot and count and removes it once viewed' do
+ visit root_dashboard_path
+
+ page.within '.header-help' do
+ expect(page).to have_selector('.notification-dot', visible: true)
+
+ find('.header-help-dropdown-toggle').click
+
+ expect(page).to have_button(text: "What's new")
+ expect(page).to have_selector('.js-whats-new-notification-count')
+
+ find('button', text: "What's new").click
+ end
+
+ find('.whats-new-drawer .gl-drawer-close-button').click
+ find('.header-help-dropdown-toggle').click
- page.within '.header-help' do
- expect(page).not_to have_selector('.notification-dot', visible: true)
- expect(page).to have_button(text: "What's new")
- expect(page).not_to have_selector('.js-whats-new-notification-count')
+ page.within '.header-help' do
+ expect(page).not_to have_selector('.notification-dot', visible: true)
+ expect(page).to have_button(text: "What's new")
+ expect(page).not_to have_selector('.js-whats-new-notification-count')
+ end
end
end
end
diff --git a/spec/graphql/resolvers/ci/test_suite_resolver_spec.rb b/spec/graphql/resolvers/ci/test_suite_resolver_spec.rb
new file mode 100644
index 00000000000..606c6eb03a3
--- /dev/null
+++ b/spec/graphql/resolvers/ci/test_suite_resolver_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Ci::TestSuiteResolver do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+
+ describe '#resolve' do
+ subject(:test_suite) { resolve(described_class, obj: pipeline, args: { build_ids: build_ids }) }
+
+ context 'when pipeline has builds with test reports' do
+ let_it_be(:main_pipeline) { create(:ci_pipeline, :with_test_reports_with_three_failures, project: project) }
+ let_it_be(:pipeline) { create(:ci_pipeline, :with_test_reports_with_three_failures, project: project, ref: 'new-feature') }
+
+ let(:suite_name) { 'test' }
+ let(:build_ids) { pipeline.latest_builds.pluck(:id) }
+
+ before do
+ build = main_pipeline.builds.last
+ build.update_column(:finished_at, 1.day.ago) # Just to be sure we are included in the report window
+
+ # The JUnit fixture for the given build has 3 failures.
+ # This service will create 1 test case failure record for each.
+ Ci::TestFailureHistoryService.new(main_pipeline).execute
+ end
+
+ it 'renders test suite data' do
+ expect(test_suite[:name]).to eq('test')
+
+ # Each test failure in this pipeline has a matching failure in the default branch
+ recent_failures = test_suite[:test_cases].map { |tc| tc[:recent_failures] }
+ expect(recent_failures).to eq([
+ { count: 1, base_branch: 'master' },
+ { count: 1, base_branch: 'master' },
+ { count: 1, base_branch: 'master' }
+ ])
+ end
+ end
+
+ context 'when pipeline has no builds that matches the given build_ids' do
+ let_it_be(:pipeline) { create(:ci_empty_pipeline) }
+
+ let(:suite_name) { 'test' }
+ let(:build_ids) { [non_existing_record_id] }
+
+ it 'returns nil' do
+ expect(test_suite).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/graphql/types/ci/pipeline_type_spec.rb b/spec/graphql/types/ci/pipeline_type_spec.rb
index 56bd1294d5c..c7d2cbdb765 100644
--- a/spec/graphql/types/ci/pipeline_type_spec.rb
+++ b/spec/graphql/types/ci/pipeline_type_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Types::Ci::PipelineType do
coverage created_at updated_at started_at finished_at committed_at
stages user retryable cancelable jobs source_job job downstream
upstream path project active user_permissions warnings commit_path uses_needs
- test_report_summary
+ test_report_summary test_suite
]
if Gitlab.ee?
diff --git a/spec/graphql/types/ci/recent_failures_type_spec.rb b/spec/graphql/types/ci/recent_failures_type_spec.rb
new file mode 100644
index 00000000000..38369da46bf
--- /dev/null
+++ b/spec/graphql/types/ci/recent_failures_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::RecentFailuresType do
+ specify { expect(described_class.graphql_name).to eq('RecentFailures') }
+
+ it 'contains attributes related to a recent failure history for a test case' do
+ expected_fields = %w[
+ count base_branch
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/ci/test_case_status_enum_spec.rb b/spec/graphql/types/ci/test_case_status_enum_spec.rb
new file mode 100644
index 00000000000..ba2d1aefb20
--- /dev/null
+++ b/spec/graphql/types/ci/test_case_status_enum_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::TestCaseStatusEnum do
+ specify { expect(described_class.graphql_name).to eq('TestCaseStatus') }
+
+ it 'exposes all test case status types' do
+ expect(described_class.values.keys).to eq(
+ ::Gitlab::Ci::Reports::TestCase::STATUS_TYPES
+ )
+ end
+end
diff --git a/spec/graphql/types/ci/test_case_type_spec.rb b/spec/graphql/types/ci/test_case_type_spec.rb
new file mode 100644
index 00000000000..e6cd70c287e
--- /dev/null
+++ b/spec/graphql/types/ci/test_case_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::TestCaseType do
+ specify { expect(described_class.graphql_name).to eq('TestCase') }
+
+ it 'contains attributes related to a pipeline test case' do
+ expected_fields = %w[
+ name status classname file attachment_url execution_time stack_trace system_output recent_failures
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/ci/test_suite_type_spec.rb b/spec/graphql/types/ci/test_suite_type_spec.rb
new file mode 100644
index 00000000000..d9caca3e2c3
--- /dev/null
+++ b/spec/graphql/types/ci/test_suite_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::TestSuiteType do
+ specify { expect(described_class.graphql_name).to eq('TestSuite') }
+
+ it 'contains attributes related to a pipeline test suite' do
+ expected_fields = %w[
+ name total_time total_count success_count failed_count skipped_count error_count suite_error test_cases
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/repository/blob_type_spec.rb b/spec/graphql/types/repository/blob_type_spec.rb
new file mode 100644
index 00000000000..f8647e4e964
--- /dev/null
+++ b/spec/graphql/types/repository/blob_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Repository::BlobType do
+ specify { expect(described_class.graphql_name).to eq('RepositoryBlob') }
+
+ specify { expect(described_class).to have_graphql_fields(:id, :oid, :name, :path, :web_path, :lfs_oid, :mode) }
+end
diff --git a/spec/helpers/branches_helper_spec.rb b/spec/helpers/branches_helper_spec.rb
index 79f98550b3d..2ad15adff59 100644
--- a/spec/helpers/branches_helper_spec.rb
+++ b/spec/helpers/branches_helper_spec.rb
@@ -47,19 +47,4 @@ RSpec.describe BranchesHelper do
end
end
end
-
- describe '#gl_dropdown_branches_enabled?' do
- context 'when the feature is enabled' do
- it 'returns true' do
- expect(helper.gldropdrown_branches_enabled?).to be_truthy
- end
- end
-
- context 'when the feature is disabled' do
- it 'returns false' do
- stub_feature_flags(gldropdown_branches: false)
- expect(helper.gldropdrown_branches_enabled?).to be_falsy
- end
- end
- end
end
diff --git a/spec/helpers/whats_new_helper_spec.rb b/spec/helpers/whats_new_helper_spec.rb
index f7f0d19db30..0e4b4621560 100644
--- a/spec/helpers/whats_new_helper_spec.rb
+++ b/spec/helpers/whats_new_helper_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe WhatsNewHelper do
+ include Devise::Test::ControllerHelpers
+
describe '#whats_new_version_digest' do
let(:digest) { 'digest' }
@@ -32,4 +34,30 @@ RSpec.describe WhatsNewHelper do
end
end
end
+
+ describe '#display_whats_new?' do
+ subject { helper.display_whats_new? }
+
+ it 'returns true when gitlab.com' do
+ allow(Gitlab).to receive(:dev_env_org_or_com?).and_return(true)
+
+ expect(subject).to be true
+ end
+
+ context 'when self-managed' do
+ before do
+ allow(Gitlab).to receive(:dev_env_org_or_com?).and_return(false)
+ end
+
+ it 'returns true if user is signed in' do
+ sign_in(create(:user))
+
+ expect(subject).to be true
+ end
+
+ it "returns false if user isn't signed in" do
+ expect(subject).to be false
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/background_migration/migrate_pages_to_zip_storage_spec.rb b/spec/lib/gitlab/background_migration/migrate_pages_to_zip_storage_spec.rb
new file mode 100644
index 00000000000..557dd8ddee6
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/migrate_pages_to_zip_storage_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::MigratePagesToZipStorage do
+ let(:namespace) { create(:group) } # rubocop: disable RSpec/FactoriesInMigrationSpecs
+ let(:migration) { described_class.new }
+
+ describe '#perform' do
+ context 'when there is project to migrate' do
+ let!(:project) { create_project('project') }
+
+ after do
+ FileUtils.rm_rf(project.pages_path)
+ end
+
+ it 'migrates project to zip storage' do
+ expect_next_instance_of(::Pages::MigrateFromLegacyStorageService,
+ anything,
+ ignore_invalid_entries: false,
+ mark_projects_as_not_deployed: false) do |service|
+ expect(service).to receive(:execute_for_batch).with(project.id..project.id).and_call_original
+ end
+
+ migration.perform(project.id, project.id)
+
+ expect(project.reload.pages_metadatum.pages_deployment.file.filename).to eq("_migrated.zip")
+ end
+ end
+ end
+
+ def create_project(path)
+ project = create(:project) # rubocop: disable RSpec/FactoriesInMigrationSpecs
+ project.mark_pages_as_deployed
+
+ FileUtils.mkdir_p File.join(project.pages_path, "public")
+ File.open(File.join(project.pages_path, "public/index.html"), "w") do |f|
+ f.write("Hello!")
+ end
+
+ project
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
index 9ca5aeeea58..900dfec38e2 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
@@ -321,4 +321,25 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do
it { is_expected.to be_falsey }
end
end
+
+ describe '#increment_pipeline_failure_reason_counter' do
+ let(:command) { described_class.new }
+ let(:reason) { :size_limit_exceeded }
+
+ subject { command.increment_pipeline_failure_reason_counter(reason) }
+
+ it 'increments the error metric' do
+ counter = Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc')
+ expect { subject }.to change { counter.get(reason: reason.to_s) }.by(1)
+ end
+
+ context 'when the reason is nil' do
+ let(:reason) { nil }
+
+ it 'increments the error metric with unknown_failure' do
+ counter = Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc')
+ expect { subject }.to change { counter.get(reason: 'unknown_failure') }.by(1)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb
index 78363be7f36..23cdec61bb3 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::Deployments do
let(:save_incompleted) { false }
let(:command) do
- double(:command,
+ Gitlab::Ci::Pipeline::Chain::Command.new(
project: project,
pipeline_seed: pipeline_seed,
save_incompleted: save_incompleted
@@ -49,6 +49,11 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::Deployments do
expect(pipeline.deployments_limit_exceeded?).to be true
end
+
+ it 'calls increment_pipeline_failure_reason_counter' do
+ counter = Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc')
+ expect { perform }.to change { counter.get(reason: 'deployments_limit_exceeded') }.by(1)
+ end
end
context 'when not saving incomplete pipelines' do
@@ -71,6 +76,12 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::Deployments do
expect(pipeline.errors.messages).to include(base: ['Pipeline has too many deployments! Requested 2, but the limit is 1.'])
end
+
+ it 'increments the error metric' do
+ expect(command).to receive(:increment_pipeline_failure_reason_counter).with(:deployments_limit_exceeded)
+
+ perform
+ end
end
it 'logs the error' do
diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
index 53d8ca740b5..62de4d2e96d 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
@@ -96,6 +96,11 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Populate do
it 'wastes pipeline iid' do
expect(InternalId.ci_pipelines.where(project_id: project.id).last.last_value).to be > 0
end
+
+ it 'increments the error metric' do
+ counter = Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc')
+ expect { run_chain }.to change { counter.get(reason: 'unknown_failure') }.by(1)
+ end
end
describe 'pipeline protect' do
diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
index a7f29da7962..261e23d0745 100644
--- a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
@@ -195,4 +195,17 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
describe '#batch_class_name=' do
it_behaves_like 'an attr_writer that demodulizes assigned class names', :batch_class_name
end
+
+ describe '#prometheus_labels' do
+ let(:batched_migration) { create(:batched_background_migration, job_class_name: 'TestMigration', table_name: 'foo', column_name: 'bar') }
+
+ it 'returns a hash with labels for the migration' do
+ labels = {
+ migration_id: batched_migration.id,
+ migration_identifier: 'TestMigration/foo.bar'
+ }
+
+ expect(batched_migration.prometheus_labels).to eq(labels)
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
index e60f33adec8..00d13f23d36 100644
--- a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '#perform' do
- let(:migration_wrapper) { described_class.new }
+ subject { described_class.new.perform(job_record) }
+
let(:job_class) { Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJob }
let_it_be(:active_migration) { create(:batched_background_migration, :active, job_arguments: [:id, :other_id]) }
@@ -18,7 +19,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
it 'runs the migration job' do
expect(job_instance).to receive(:perform).with(1, 10, 'events', 'id', 1, 'id', 'other_id')
- migration_wrapper.perform(job_record)
+ subject
end
it 'updates the tracking record in the database' do
@@ -30,7 +31,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
expect(job_record).to receive(:update!).with(hash_including(attempts: 1, status: :running)).and_call_original
freeze_time do
- migration_wrapper.perform(job_record)
+ subject
reloaded_job_record = job_record.reload
@@ -41,12 +42,66 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
end
end
+ context 'reporting prometheus metrics' do
+ let(:labels) { job_record.batched_migration.prometheus_labels }
+
+ before do
+ allow(job_instance).to receive(:perform)
+ end
+
+ it 'reports batch_size' do
+ expect(described_class.metrics[:gauge_batch_size]).to receive(:set).with(labels, job_record.batch_size)
+
+ subject
+ end
+
+ it 'reports sub_batch_size' do
+ expect(described_class.metrics[:gauge_sub_batch_size]).to receive(:set).with(labels, job_record.sub_batch_size)
+
+ subject
+ end
+
+ it 'reports updated tuples (currently based on batch_size)' do
+ expect(described_class.metrics[:counter_updated_tuples]).to receive(:increment).with(labels, job_record.batch_size)
+
+ subject
+ end
+
+ it 'reports summary of query timings' do
+ metrics = { 'timings' => { 'update_all' => [1, 2, 3, 4, 5] } }
+
+ expect(job_instance).to receive(:batch_metrics).and_return(metrics)
+
+ metrics['timings'].each do |key, timings|
+ summary_labels = labels.merge(operation: key)
+ timings.each do |timing|
+ expect(described_class.metrics[:histogram_timings]).to receive(:observe).with(summary_labels, timing)
+ end
+ end
+
+ subject
+ end
+
+ it 'reports time efficiency' do
+ freeze_time do
+ expect(Time).to receive(:current).and_return(Time.zone.now - 5.seconds).ordered
+ expect(Time).to receive(:current).and_return(Time.zone.now).ordered
+
+ ratio = 5 / job_record.batched_migration.interval.to_f
+
+ expect(described_class.metrics[:histogram_time_efficiency]).to receive(:observe).with(labels, ratio)
+
+ subject
+ end
+ end
+ end
+
context 'when the migration job does not raise an error' do
it 'marks the tracking record as succeeded' do
expect(job_instance).to receive(:perform).with(1, 10, 'events', 'id', 1, 'id', 'other_id')
freeze_time do
- migration_wrapper.perform(job_record)
+ subject
reloaded_job_record = job_record.reload
@@ -63,7 +118,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
.and_raise(RuntimeError, 'Something broke!')
freeze_time do
- expect { migration_wrapper.perform(job_record) }.to raise_error(RuntimeError, 'Something broke!')
+ expect { subject }.to raise_error(RuntimeError, 'Something broke!')
reloaded_job_record = job_record.reload
diff --git a/spec/lib/gitlab/metrics/background_transaction_spec.rb b/spec/lib/gitlab/metrics/background_transaction_spec.rb
index 15e52f7b6c8..d36ee24fc50 100644
--- a/spec/lib/gitlab/metrics/background_transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/background_transaction_spec.rb
@@ -29,17 +29,60 @@ RSpec.describe Gitlab::Metrics::BackgroundTransaction do
end
describe '#labels' do
- it 'provides labels with endpoint_id and feature_category' do
- Gitlab::ApplicationContext.with_raw_context(feature_category: 'projects', caller_id: 'TestWorker') do
- expect(transaction.labels).to eq({ endpoint_id: 'TestWorker', feature_category: 'projects' })
+ context 'when the worker queue is accessible' do
+ before do
+ test_worker_class = Class.new do
+ def self.queue
+ 'test_worker'
+ end
+ end
+ stub_const('TestWorker', test_worker_class)
+ end
+
+ it 'provides labels with endpoint_id, feature_category and queue' do
+ Gitlab::ApplicationContext.with_raw_context(feature_category: 'projects', caller_id: 'TestWorker') do
+ expect(transaction.labels).to eq({ endpoint_id: 'TestWorker', feature_category: 'projects', queue: 'test_worker' })
+ end
+ end
+ end
+
+ context 'when the worker name does not exist' do
+ it 'provides labels with endpoint_id and feature_category' do
+ # 123TestWorker is an invalid constant
+ Gitlab::ApplicationContext.with_raw_context(feature_category: 'projects', caller_id: '123TestWorker') do
+ expect(transaction.labels).to eq({ endpoint_id: '123TestWorker', feature_category: 'projects', queue: nil })
+ end
+ end
+ end
+
+ context 'when the worker queue is not accessible' do
+ before do
+ stub_const('TestWorker', Class.new)
+ end
+
+ it 'provides labels with endpoint_id and feature_category' do
+ Gitlab::ApplicationContext.with_raw_context(feature_category: 'projects', caller_id: 'TestWorker') do
+ expect(transaction.labels).to eq({ endpoint_id: 'TestWorker', feature_category: 'projects', queue: nil })
+ end
end
end
end
RSpec.shared_examples 'metric with labels' do |metric_method|
+ before do
+ test_worker_class = Class.new do
+ def self.queue
+ 'test_worker'
+ end
+ end
+ stub_const('TestWorker', test_worker_class)
+ end
+
it 'measures with correct labels and value' do
value = 1
- expect(prometheus_metric).to receive(metric_method).with({ endpoint_id: 'TestWorker', feature_category: 'projects' }, value)
+ expect(prometheus_metric).to receive(metric_method).with({
+ endpoint_id: 'TestWorker', feature_category: 'projects', queue: 'test_worker'
+ }, value)
Gitlab::ApplicationContext.with_raw_context(feature_category: 'projects', caller_id: 'TestWorker') do
transaction.send(metric_method, :test_metric, value)
diff --git a/spec/lib/gitlab/usage_data_non_sql_metrics_spec.rb b/spec/lib/gitlab/usage_data_non_sql_metrics_spec.rb
index 0b1424133b1..32d1288c59c 100644
--- a/spec/lib/gitlab/usage_data_non_sql_metrics_spec.rb
+++ b/spec/lib/gitlab/usage_data_non_sql_metrics_spec.rb
@@ -34,4 +34,22 @@ RSpec.describe Gitlab::UsageDataNonSqlMetrics do
expect(described_class.histogram(JiraImportState.finished, :imported_issues_count, buckets: [], bucket_size: 0)).to eq(default_count)
end
end
+
+ describe 'min/max methods' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:model, :result) do
+ User | nil
+ Issue | nil
+ Deployment | nil
+ Project | nil
+ end
+
+ with_them do
+ it 'returns nil' do
+ expect(described_class.minimum_id(model)).to eq(result)
+ expect(described_class.maximum_id(model)).to eq(result)
+ end
+ end
+ end
end
diff --git a/spec/migrations/schedule_migrate_pages_to_zip_storage_spec.rb b/spec/migrations/schedule_migrate_pages_to_zip_storage_spec.rb
new file mode 100644
index 00000000000..1d35da528e4
--- /dev/null
+++ b/spec/migrations/schedule_migrate_pages_to_zip_storage_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20210302150310_schedule_migrate_pages_to_zip_storage.rb')
+
+RSpec.describe ScheduleMigratePagesToZipStorage, :sidekiq_might_not_need_inline, schema: 20201231133921 do
+ let(:migration_class) { described_class::MIGRATION }
+ let(:migration_name) { migration_class.to_s.demodulize }
+
+ let(:namespaces_table) { table(:namespaces) }
+ let(:projects_table) { table(:projects) }
+ let(:metadata_table) { table(:project_pages_metadata) }
+ let(:deployments_table) { table(:pages_deployments) }
+
+ let(:namespace) { namespaces_table.create!(path: "group", name: "group") }
+
+ def create_project_metadata(path, deployed, with_deployment)
+ project = projects_table.create!(path: path, namespace_id: namespace.id)
+
+ deployment_id = nil
+
+ if with_deployment
+ deployment_id = deployments_table.create!(project_id: project.id, file_store: 1, file: '1', file_count: 1, file_sha256: '123', size: 1).id
+ end
+
+ metadata_table.create!(project_id: project.id, deployed: deployed, pages_deployment_id: deployment_id)
+ end
+
+ it 'correctly schedules background migrations' do
+ Sidekiq::Testing.fake! do
+ freeze_time do
+ create_project_metadata("not-deployed-project", false, false)
+
+ first_id = create_project_metadata("project1", true, false).id
+ last_id = create_project_metadata("project2", true, false).id
+
+ create_project_metadata("project-with-deployment", true, true)
+
+ migrate!
+
+ expect(migration_name).to be_scheduled_delayed_migration(5.minutes, first_id, last_id)
+ expect(BackgroundMigrationWorker.jobs.size).to eq(1)
+ end
+ end
+ end
+end
diff --git a/spec/models/bulk_imports/stage_spec.rb b/spec/models/bulk_imports/stage_spec.rb
index f14e9425a06..7765fd4c5c4 100644
--- a/spec/models/bulk_imports/stage_spec.rb
+++ b/spec/models/bulk_imports/stage_spec.rb
@@ -12,10 +12,10 @@ RSpec.describe BulkImports::Stage do
[1, BulkImports::Groups::Pipelines::LabelsPipeline],
[1, BulkImports::Groups::Pipelines::MilestonesPipeline],
[1, BulkImports::Groups::Pipelines::BadgesPipeline],
- [1, 'EE::BulkImports::Groups::Pipelines::IterationsPipeline'.constantize],
- [2, 'EE::BulkImports::Groups::Pipelines::EpicsPipeline'.constantize],
- [3, 'EE::BulkImports::Groups::Pipelines::EpicAwardEmojiPipeline'.constantize],
- [3, 'EE::BulkImports::Groups::Pipelines::EpicEventsPipeline'.constantize],
+ [1, 'BulkImports::Groups::Pipelines::IterationsPipeline'.constantize],
+ [2, 'BulkImports::Groups::Pipelines::EpicsPipeline'.constantize],
+ [3, 'BulkImports::Groups::Pipelines::EpicAwardEmojiPipeline'.constantize],
+ [3, 'BulkImports::Groups::Pipelines::EpicEventsPipeline'.constantize],
[4, BulkImports::Groups::Pipelines::EntityFinisher]
]
else
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 958fe8d9455..b7f5811e945 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -3902,6 +3902,16 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
pipeline.drop
end
end
+
+ context 'with failure_reason' do
+ let(:pipeline) { create(:ci_pipeline, :running) }
+ let(:failure_reason) { 'config_error' }
+ let(:counter) { Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc') }
+
+ it 'increments the counter with the failure_reason' do
+ expect { pipeline.drop!(failure_reason) }.to change { counter.get(reason: failure_reason) }.by(1)
+ end
+ end
end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 1e0a875de73..e64dee2d26f 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -629,30 +629,45 @@ RSpec.describe CommitStatus do
end
end
- describe 'set failure_reason when drop' do
+ describe '#drop' do
let(:commit_status) { create(:commit_status, :created) }
+ let(:counter) { Gitlab::Metrics.counter(:gitlab_ci_job_failure_reasons, 'desc') }
+ let(:failure_reason) { reason.to_s }
subject do
commit_status.drop!(reason)
commit_status
end
+ shared_examples 'incrementing failure reason counter' do
+ it 'increments the counter with the failure_reason' do
+ expect { subject }.to change { counter.get(reason: failure_reason) }.by(1)
+ end
+ end
+
context 'when failure_reason is nil' do
let(:reason) { }
+ let(:failure_reason) { 'unknown_failure' }
it { is_expected.to be_unknown_failure }
+
+ it_behaves_like 'incrementing failure reason counter'
end
context 'when failure_reason is script_failure' do
let(:reason) { :script_failure }
it { is_expected.to be_script_failure }
+
+ it_behaves_like 'incrementing failure reason counter'
end
context 'when failure_reason is unmet_prerequisites' do
let(:reason) { :unmet_prerequisites }
it { is_expected.to be_unmet_prerequisites }
+
+ it_behaves_like 'incrementing failure reason counter'
end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 1df70f38707..14db9b530db 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -65,6 +65,23 @@ RSpec.describe Issuable do
it { expect(issuable_class).to respond_to(:opened) }
it { expect(issuable_class).to respond_to(:closed) }
it { expect(issuable_class).to respond_to(:assigned) }
+
+ describe '.includes_for_bulk_update' do
+ before do
+ stub_const('Example', Class.new(ActiveRecord::Base))
+
+ Example.class_eval do
+ include Issuable # adds :labels and :metrics, among others
+
+ belongs_to :author
+ has_many :assignees
+ end
+ end
+
+ it 'includes available associations' do
+ expect(Example.includes_for_bulk_update.includes_values).to eq([:author, :assignees, :labels, :metrics])
+ end
+ end
end
describe 'author_name' do
diff --git a/spec/models/concerns/milestoneable_spec.rb b/spec/models/concerns/milestoneable_spec.rb
index 5fb3b39f734..961eac4710d 100644
--- a/spec/models/concerns/milestoneable_spec.rb
+++ b/spec/models/concerns/milestoneable_spec.rb
@@ -50,13 +50,13 @@ RSpec.describe Milestoneable do
it 'returns true with a milestone from the issue project' do
milestone = create(:milestone, project: project)
- expect(build_milestoneable(milestone.id).milestone_available?).to be_truthy
+ expect(build_milestoneable(milestone.id).milestone_available?).to be(true)
end
it 'returns true with a milestone from the issue project group' do
milestone = create(:milestone, group: group)
- expect(build_milestoneable(milestone.id).milestone_available?).to be_truthy
+ expect(build_milestoneable(milestone.id).milestone_available?).to be(true)
end
it 'returns true with a milestone from the the parent of the issue project group' do
@@ -64,19 +64,23 @@ RSpec.describe Milestoneable do
group.update!(parent: parent)
milestone = create(:milestone, group: parent)
- expect(build_milestoneable(milestone.id).milestone_available?).to be_truthy
+ expect(build_milestoneable(milestone.id).milestone_available?).to be(true)
+ end
+
+ it 'returns true with a blank milestone' do
+ expect(build_milestoneable('').milestone_available?).to be(true)
end
it 'returns false with a milestone from another project' do
milestone = create(:milestone)
- expect(build_milestoneable(milestone.id).milestone_available?).to be_falsey
+ expect(build_milestoneable(milestone.id).milestone_available?).to be(false)
end
it 'returns false with a milestone from another group' do
milestone = create(:milestone, group: create(:group))
- expect(build_milestoneable(milestone.id).milestone_available?).to be_falsey
+ expect(build_milestoneable(milestone.id).milestone_available?).to be(false)
end
end
end
diff --git a/spec/models/pages_deployment_spec.rb b/spec/models/pages_deployment_spec.rb
index 029eb8e513a..a27d836e2c2 100644
--- a/spec/models/pages_deployment_spec.rb
+++ b/spec/models/pages_deployment_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe PagesDeployment do
+ let_it_be(:project) { create(:project) }
+
describe 'associations' do
it { is_expected.to belong_to(:project).required }
it { is_expected.to belong_to(:ci_build).optional }
@@ -28,7 +30,6 @@ RSpec.describe PagesDeployment do
describe '.migrated_from_legacy_storage' do
it 'only returns migrated deployments' do
- project = create(:project)
migrated_deployment = create_migrated_deployment(project)
# create one other deployment
create(:pages_deployment, project: project)
@@ -37,6 +38,27 @@ RSpec.describe PagesDeployment do
end
end
+ context 'with deployments stored locally and remotely' do
+ before do
+ stub_pages_object_storage(::Pages::DeploymentUploader)
+ end
+
+ let!(:remote_deployment) { create(:pages_deployment, project: project, file_store: ::ObjectStorage::Store::REMOTE) }
+ let!(:local_deployment) { create(:pages_deployment, project: project, file_store: ::ObjectStorage::Store::LOCAL) }
+
+ describe '.with_files_stored_locally' do
+ it 'only returns deployments with files stored locally' do
+ expect(described_class.with_files_stored_locally).to contain_exactly(local_deployment)
+ end
+ end
+
+ describe '.with_files_stored_remotely' do
+ it 'only returns deployments with files stored remotely' do
+ expect(described_class.with_files_stored_remotely).to contain_exactly(remote_deployment)
+ end
+ end
+ end
+
describe '#migrated?' do
it 'returns false for normal deployment' do
deployment = create(:pages_deployment)
@@ -45,7 +67,6 @@ RSpec.describe PagesDeployment do
end
it 'returns true for migrated deployment' do
- project = create(:project)
deployment = create_migrated_deployment(project)
expect(deployment.migrated?).to eq(true)
@@ -67,7 +88,6 @@ RSpec.describe PagesDeployment do
end
describe 'default for file_store' do
- let(:project) { create(:project) }
let(:deployment) do
filepath = Rails.root.join("spec/fixtures/pages.zip")
diff --git a/spec/models/sidebars/projects/menus/project_overview/menu_items/releases_spec.rb b/spec/models/sidebars/projects/menus/project_overview/menu_items/releases_spec.rb
new file mode 100644
index 00000000000..db124c2252e
--- /dev/null
+++ b/spec/models/sidebars/projects/menus/project_overview/menu_items/releases_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Projects::Menus::ProjectOverview::MenuItems::Releases do
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:user) { project.owner }
+ let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
+
+ subject { described_class.new(context) }
+
+ describe '#render?' do
+ context 'when project repository is empty' do
+ it 'returns false' do
+ allow(project).to receive(:empty_repo?).and_return(true)
+
+ expect(subject.render?).to eq false
+ end
+ end
+
+ context 'when project repository is not empty' do
+ context 'when user can read releases' do
+ it 'returns true' do
+ expect(subject.render?).to eq true
+ end
+ end
+
+ context 'when user cannot read releases' do
+ let(:user) { nil }
+
+ it 'returns false' do
+ expect(subject.render?).to eq false
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/sidebars/projects/menus/project_overview/menu_spec.rb b/spec/models/sidebars/projects/menus/project_overview/menu_spec.rb
new file mode 100644
index 00000000000..105a28ce953
--- /dev/null
+++ b/spec/models/sidebars/projects/menus/project_overview/menu_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Projects::Menus::ProjectOverview::Menu do
+ let(:project) { build(:project) }
+ let(:context) { Sidebars::Projects::Context.new(current_user: nil, container: project) }
+
+ subject { described_class.new(context) }
+
+ it 'has the required items' do
+ items = subject.instance_variable_get(:@items)
+
+ expect(items[0]).to be_a(Sidebars::Projects::Menus::ProjectOverview::MenuItems::Details)
+ expect(items[1]).to be_a(Sidebars::Projects::Menus::ProjectOverview::MenuItems::Activity)
+ expect(items[2]).to be_a(Sidebars::Projects::Menus::ProjectOverview::MenuItems::Releases)
+ end
+end
diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb
index 6436fe1e9ef..0a5bcc7a965 100644
--- a/spec/requests/api/graphql/project/pipeline_spec.rb
+++ b/spec/requests/api/graphql/project/pipeline_spec.rb
@@ -235,4 +235,51 @@ RSpec.describe 'getting pipeline information nested in a project' do
end
end
end
+
+ context 'when requesting a specific test suite' do
+ let_it_be(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
+ let(:suite_name) { 'test' }
+ let_it_be(:build_ids) { pipeline.latest_builds.pluck(:id) }
+
+ let(:variables) do
+ {
+ path: project.full_path,
+ pipelineIID: pipeline.iid.to_s
+ }
+ end
+
+ let(:query) do
+ <<~GQL
+ query($path: ID!, $pipelineIID: ID!, $buildIds: [ID!]!) {
+ project(fullPath: $path) {
+ pipeline(iid: $pipelineIID) {
+ testSuite(buildIds: $buildIds) {
+ name
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ it 'can request a test suite by an array of build_ids' do
+ vars = variables.merge(buildIds: build_ids)
+
+ post_graphql(query, current_user: current_user, variables: vars)
+
+ expect(graphql_data_at(:project, :pipeline, :testSuite, :name)).to eq(suite_name)
+ end
+
+ context 'when pipeline has no builds that matches the given build_ids' do
+ let_it_be(:build_ids) { [non_existing_record_id] }
+
+ it 'returns nil' do
+ vars = variables.merge(buildIds: build_ids)
+
+ post_graphql(query, current_user: current_user, variables: vars)
+
+ expect(graphql_data_at(*path, :test_suite)).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/project/repository/blobs_spec.rb b/spec/requests/api/graphql/project/repository/blobs_spec.rb
new file mode 100644
index 00000000000..12f6fbd793e
--- /dev/null
+++ b/spec/requests/api/graphql/project/repository/blobs_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'getting blobs in a project repository' do
+ include GraphqlHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:current_user) { project.owner }
+ let(:paths) { ["CONTRIBUTING.md", "README.md"] }
+ let(:ref) { project.default_branch }
+ let(:fields) do
+ <<~QUERY
+ blobs(paths:#{paths.inspect}, ref:#{ref.inspect}) {
+ nodes {
+ #{all_graphql_fields_for('repository_blob'.classify)}
+ }
+ }
+ QUERY
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('repository', {}, fields)
+ )
+ end
+
+ subject(:blobs) { graphql_data_at(:project, :repository, :blobs, :nodes) }
+
+ it 'returns the blob' do
+ post_graphql(query, current_user: current_user)
+
+ expect(blobs).to match_array(paths.map { |path| a_hash_including('path' => path) })
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 35f4b97df0a..98c85234fe7 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -71,19 +71,21 @@ RSpec.describe Ci::CreatePipelineService do
end
it 'increments the prometheus counter' do
- expect(Gitlab::Metrics).to receive(:counter)
- .with(:pipelines_created_total, "Counter of pipelines created")
- .and_call_original
- allow(Gitlab::Metrics).to receive(:counter).and_call_original # allow other counters
+ counter = spy('pipeline created counter')
+
+ allow(Gitlab::Ci::Pipeline::Metrics)
+ .to receive(:pipelines_created_counter).and_return(counter)
pipeline
+
+ expect(counter).to have_received(:increment)
end
it 'records pipeline size in a prometheus histogram' do
histogram = spy('pipeline size histogram')
allow(Gitlab::Ci::Pipeline::Metrics)
- .to receive(:new).and_return(histogram)
+ .to receive(:pipeline_size_histogram).and_return(histogram)
execute_service
@@ -580,6 +582,13 @@ RSpec.describe Ci::CreatePipelineService do
it_behaves_like 'a failed pipeline'
+ it 'increments the error metric' do
+ stub_ci_pipeline_yaml_file(ci_yaml)
+
+ counter = Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc')
+ expect { execute_service }.to change { counter.get(reason: 'config_error') }.by(1)
+ end
+
context 'when receive git commit' do
before do
allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index e02536fd07f..254bd19c808 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -10,6 +10,14 @@ RSpec.describe Ci::ProcessPipelineService do
create(:ci_empty_pipeline, ref: 'master', project: project)
end
+ let(:pipeline_processing_events_counter) { double(increment: true) }
+ let(:legacy_update_jobs_counter) { double(increment: true) }
+
+ let(:metrics) do
+ double(pipeline_processing_events_counter: pipeline_processing_events_counter,
+ legacy_update_jobs_counter: legacy_update_jobs_counter)
+ end
+
subject { described_class.new(pipeline) }
before do
@@ -17,22 +25,13 @@ RSpec.describe Ci::ProcessPipelineService do
stub_not_protect_default_branch
project.add_developer(user)
+
+ allow(subject).to receive(:metrics).and_return(metrics)
end
describe 'processing events counter' do
- let(:metrics) { double('pipeline metrics') }
- let(:counter) { double('events counter') }
-
- before do
- allow(subject)
- .to receive(:metrics).and_return(metrics)
- allow(metrics)
- .to receive(:pipeline_processing_events_counter)
- .and_return(counter)
- end
-
it 'increments processing events counter' do
- expect(counter).to receive(:increment)
+ expect(pipeline_processing_events_counter).to receive(:increment)
subject.execute
end
@@ -64,33 +63,22 @@ RSpec.describe Ci::ProcessPipelineService do
expect(all_builds.retried).to contain_exactly(build_retried)
end
- context 'counter ci_legacy_update_jobs_as_retried_total' do
- let(:counter) { double(increment: true) }
+ it 'increments the counter' do
+ expect(legacy_update_jobs_counter).to receive(:increment)
+ subject.execute
+ end
+
+ context 'when the previous build has already retried column true' do
before do
- allow(Gitlab::Metrics).to receive(:counter).and_call_original
- allow(Gitlab::Metrics).to receive(:counter)
- .with(:ci_legacy_update_jobs_as_retried_total, anything)
- .and_return(counter)
+ build_retried.update_columns(retried: true)
end
- it 'increments the counter' do
- expect(counter).to receive(:increment)
+ it 'does not increment the counter' do
+ expect(legacy_update_jobs_counter).not_to receive(:increment)
subject.execute
end
-
- context 'when the previous build has already retried column true' do
- before do
- build_retried.update_columns(retried: true)
- end
-
- it 'does not increment the counter' do
- expect(counter).not_to receive(:increment)
-
- subject.execute
- end
- end
end
end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 9346c92f98b..8c010855eb2 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -955,6 +955,40 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
end
context 'updating asssignee_ids' do
+ context ':use_specialized_service' do
+ context 'when true' do
+ it 'passes the update action to ::MergeRequests::UpdateAssigneesService' do
+ expect(::MergeRequests::UpdateAssigneesService)
+ .to receive(:new).and_call_original
+
+ update_merge_request({
+ assignee_ids: [user2.id],
+ use_specialized_service: true
+ })
+ end
+ end
+
+ context 'when false or nil' do
+ before do
+ expect(::MergeRequests::UpdateAssigneesService).not_to receive(:new)
+ end
+
+ it 'does not pass the update action to ::MergeRequests::UpdateAssigneesService when false' do
+ update_merge_request({
+ assignee_ids: [user2.id],
+ use_specialized_service: false
+ })
+ end
+
+ it 'does not pass the update action to ::MergeRequests::UpdateAssigneesService when nil' do
+ update_merge_request({
+ assignee_ids: [user2.id],
+ use_specialized_service: nil
+ })
+ end
+ end
+ end
+
it 'does not update assignee when assignee_id is invalid' do
merge_request.update!(assignee_ids: [user.id])
diff --git a/spec/services/namespaces/in_product_marketing_emails_service_spec.rb b/spec/services/namespaces/in_product_marketing_emails_service_spec.rb
index 43c41b5c99d..3094f574184 100644
--- a/spec/services/namespaces/in_product_marketing_emails_service_spec.rb
+++ b/spec/services/namespaces/in_product_marketing_emails_service_spec.rb
@@ -85,26 +85,46 @@ RSpec.describe Namespaces::InProductMarketingEmailsService, '#execute' do
end
describe 'experimentation' do
- context 'when the experiment is enabled' do
- it 'adds the group as an experiment subject in the experimental group' do
- expect(Experiment).to receive(:add_group)
- .with(:in_product_marketing_emails, variant: :experimental, group: group)
+ context 'when on dotcom' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(true)
+ end
- execute_service
+ context 'when the experiment is enabled' do
+ it 'adds the group as an experiment subject in the experimental group' do
+ expect(Experiment).to receive(:add_group)
+ .with(:in_product_marketing_emails, variant: :experimental, group: group)
+
+ execute_service
+ end
end
- end
- context 'when the experiment is disabled' do
- let(:experiment_enabled) { false }
+ context 'when the experiment is disabled' do
+ let(:experiment_enabled) { false }
+
+ it 'adds the group as an experiment subject in the control group' do
+ expect(Experiment).to receive(:add_group)
+ .with(:in_product_marketing_emails, variant: :control, group: group)
- it 'adds the group as an experiment subject in the control group' do
- expect(Experiment).to receive(:add_group)
- .with(:in_product_marketing_emails, variant: :control, group: group)
+ execute_service
+ end
- execute_service
+ it { is_expected.not_to send_in_product_marketing_email }
end
- it { is_expected.not_to send_in_product_marketing_email }
+ context 'when not on dotcom' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(false)
+ end
+
+ it 'does not add the group as an experiment subject' do
+ expect(Experiment).not_to receive(:add_group)
+
+ execute_service
+ end
+
+ it { is_expected.to send_in_product_marketing_email(user.id, group.id, :create, 0) }
+ end
end
end
diff --git a/spec/services/pages/migrate_from_legacy_storage_service_spec.rb b/spec/services/pages/migrate_from_legacy_storage_service_spec.rb
index 2a275bab4cc..d058324f3bb 100644
--- a/spec/services/pages/migrate_from_legacy_storage_service_spec.rb
+++ b/spec/services/pages/migrate_from_legacy_storage_service_spec.rb
@@ -5,104 +5,133 @@ require 'spec_helper'
RSpec.describe Pages::MigrateFromLegacyStorageService do
let(:batch_size) { 10 }
let(:mark_projects_as_not_deployed) { false }
- let(:service) { described_class.new(Rails.logger, migration_threads: 3, batch_size: batch_size, ignore_invalid_entries: false, mark_projects_as_not_deployed: mark_projects_as_not_deployed) }
+ let(:service) { described_class.new(Rails.logger, ignore_invalid_entries: false, mark_projects_as_not_deployed: mark_projects_as_not_deployed) }
- it 'does not try to migrate pages if pages are not deployed' do
- expect(::Pages::MigrateLegacyStorageToDeploymentService).not_to receive(:new)
+ shared_examples "migrates projects properly" do
+ it 'does not try to migrate pages if pages are not deployed' do
+ expect(::Pages::MigrateLegacyStorageToDeploymentService).not_to receive(:new)
- expect(service.execute).to eq(migrated: 0, errored: 0)
- end
+ is_expected.to eq(migrated: 0, errored: 0)
+ end
- context 'when there is work for multiple threads' do
- let(:batch_size) { 2 } # override to force usage of multiple threads
+ context 'when pages are marked as deployed' do
+ let(:project) { create(:project) }
- it 'uses multiple threads' do
- projects = create_list(:project, 20)
- projects.each do |project|
+ before do
project.mark_pages_as_deployed
+ end
+
+ context 'when pages directory does not exist' do
+ context 'when mark_projects_as_not_deployed is set' do
+ let(:mark_projects_as_not_deployed) { true }
+
+ it 'counts project as migrated' do
+ expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project, ignore_invalid_entries: false, mark_projects_as_not_deployed: true) do |service|
+ expect(service).to receive(:execute).and_call_original
+ end
+
+ is_expected.to eq(migrated: 1, errored: 0)
+ end
+ end
+
+ it 'counts project as errored' do
+ expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project, ignore_invalid_entries: false, mark_projects_as_not_deployed: false) do |service|
+ expect(service).to receive(:execute).and_call_original
+ end
- FileUtils.mkdir_p File.join(project.pages_path, "public")
- File.open(File.join(project.pages_path, "public/index.html"), "w") do |f|
- f.write("Hello!")
+ is_expected.to eq(migrated: 0, errored: 1)
end
end
- threads = Concurrent::Set.new
+ context 'when pages directory exists on disk' do
+ before do
+ FileUtils.mkdir_p File.join(project.pages_path, "public")
+ File.open(File.join(project.pages_path, "public/index.html"), "w") do |f|
+ f.write("Hello!")
+ end
+ end
+
+ it 'migrates pages projects without deployments' do
+ expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project, ignore_invalid_entries: false, mark_projects_as_not_deployed: false) do |service|
+ expect(service).to receive(:execute).and_call_original
+ end
- expect(service).to receive(:migrate_project).exactly(20).times.and_wrap_original do |m, *args|
- threads.add(Thread.current)
+ expect(project.pages_metadatum.reload.pages_deployment).to eq(nil)
+ expect(subject).to eq(migrated: 1, errored: 0)
+ expect(project.pages_metadatum.reload.pages_deployment).to be
+ end
- # sleep to be 100% certain that once thread can't consume all the queue
- # it works without it, but I want to avoid making this test flaky
- sleep(0.01)
+ context 'when deployed already exists for the project' do
+ before do
+ deployment = create(:pages_deployment, project: project)
+ project.set_first_pages_deployment!(deployment)
+ end
- m.call(*args)
- end
+ it 'does not try to migrate project' do
+ expect(::Pages::MigrateLegacyStorageToDeploymentService).not_to receive(:new)
- expect(service.execute).to eq(migrated: 20, errored: 0)
- expect(threads.length).to eq(3)
+ is_expected.to eq(migrated: 0, errored: 0)
+ end
+ end
+ end
end
end
- context 'when pages are marked as deployed' do
- let(:project) { create(:project) }
+ describe '#execute_with_threads' do
+ subject { service.execute_with_threads(threads: 3, batch_size: batch_size) }
- before do
- project.mark_pages_as_deployed
- end
+ include_examples "migrates projects properly"
- context 'when pages directory does not exist' do
- context 'when mark_projects_as_not_deployed is set' do
- let(:mark_projects_as_not_deployed) { true }
+ context 'when there is work for multiple threads' do
+ let(:batch_size) { 2 } # override to force usage of multiple threads
- it 'counts project as migrated' do
- expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project, ignore_invalid_entries: false, mark_projects_as_not_deployed: true) do |service|
- expect(service).to receive(:execute).and_call_original
- end
+ it 'uses multiple threads' do
+ projects = create_list(:project, 20)
+ projects.each do |project|
+ project.mark_pages_as_deployed
- expect(service.execute).to eq(migrated: 1, errored: 0)
+ FileUtils.mkdir_p File.join(project.pages_path, "public")
+ File.open(File.join(project.pages_path, "public/index.html"), "w") do |f|
+ f.write("Hello!")
+ end
end
- end
- it 'counts project as errored' do
- expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project, ignore_invalid_entries: false, mark_projects_as_not_deployed: false) do |service|
- expect(service).to receive(:execute).and_call_original
- end
+ threads = Concurrent::Set.new
- expect(service.execute).to eq(migrated: 0, errored: 1)
- end
- end
+ expect(service).to receive(:migrate_project).exactly(20).times.and_wrap_original do |m, *args|
+ threads.add(Thread.current)
- context 'when pages directory exists on disk' do
- before do
- FileUtils.mkdir_p File.join(project.pages_path, "public")
- File.open(File.join(project.pages_path, "public/index.html"), "w") do |f|
- f.write("Hello!")
- end
- end
+ # sleep to be 100% certain that once thread can't consume all the queue
+ # it works without it, but I want to avoid making this test flaky
+ sleep(0.01)
- it 'migrates pages projects without deployments' do
- expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project, ignore_invalid_entries: false, mark_projects_as_not_deployed: false) do |service|
- expect(service).to receive(:execute).and_call_original
+ m.call(*args)
end
- expect do
- expect(service.execute).to eq(migrated: 1, errored: 0)
- end.to change { project.pages_metadatum.reload.pages_deployment }.from(nil)
+ is_expected.to eq(migrated: 20, errored: 0)
+ expect(threads.length).to eq(3)
end
+ end
+ end
- context 'when deployed already exists for the project' do
- before do
- deployment = create(:pages_deployment, project: project)
- project.set_first_pages_deployment!(deployment)
- end
+ describe "#execute_for_batch" do
+ subject { service.execute_for_batch(Project.ids) }
+
+ include_examples "migrates projects properly"
+
+ it 'only tries to migrate projects with passed ids' do
+ projects = create_list(:project, 5)
- it 'does not try to migrate project' do
- expect(::Pages::MigrateLegacyStorageToDeploymentService).not_to receive(:new)
+ projects.each(&:mark_pages_as_deployed)
+ projects_to_migrate = projects.first(3)
- expect(service.execute).to eq(migrated: 0, errored: 0)
+ projects_to_migrate.each do |project|
+ expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project, ignore_invalid_entries: false, mark_projects_as_not_deployed: false) do |service|
+ expect(service).to receive(:execute).and_call_original
end
end
+
+ expect(service.execute_for_batch(projects_to_migrate.pluck(:id))).to eq(migrated: 0, errored: 3)
end
end
end
diff --git a/spec/tasks/gitlab/pages_rake_spec.rb b/spec/tasks/gitlab/pages_rake_spec.rb
index 1c5a803441d..664899c361b 100644
--- a/spec/tasks/gitlab/pages_rake_spec.rb
+++ b/spec/tasks/gitlab/pages_rake_spec.rb
@@ -12,11 +12,9 @@ RSpec.describe 'gitlab:pages' do
it 'calls migration service' do
expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything,
- migration_threads: 3,
- batch_size: 10,
ignore_invalid_entries: false,
mark_projects_as_not_deployed: false) do |service|
- expect(service).to receive(:execute).and_call_original
+ expect(service).to receive(:execute_with_threads).with(threads: 3, batch_size: 10).and_call_original
end
subject
@@ -26,11 +24,9 @@ RSpec.describe 'gitlab:pages' do
stub_env('PAGES_MIGRATION_THREADS', '5')
expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything,
- migration_threads: 5,
- batch_size: 10,
ignore_invalid_entries: false,
mark_projects_as_not_deployed: false) do |service|
- expect(service).to receive(:execute).and_call_original
+ expect(service).to receive(:execute_with_threads).with(threads: 5, batch_size: 10).and_call_original
end
subject
@@ -40,11 +36,9 @@ RSpec.describe 'gitlab:pages' do
stub_env('PAGES_MIGRATION_BATCH_SIZE', '100')
expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything,
- migration_threads: 3,
- batch_size: 100,
ignore_invalid_entries: false,
mark_projects_as_not_deployed: false) do |service|
- expect(service).to receive(:execute).and_call_original
+ expect(service).to receive(:execute_with_threads).with(threads: 3, batch_size: 100).and_call_original
end
subject
@@ -54,11 +48,9 @@ RSpec.describe 'gitlab:pages' do
stub_env('PAGES_MIGRATION_IGNORE_INVALID_ENTRIES', 'true')
expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything,
- migration_threads: 3,
- batch_size: 10,
ignore_invalid_entries: true,
mark_projects_as_not_deployed: false) do |service|
- expect(service).to receive(:execute).and_call_original
+ expect(service).to receive(:execute_with_threads).with(threads: 3, batch_size: 10).and_call_original
end
subject
@@ -68,11 +60,9 @@ RSpec.describe 'gitlab:pages' do
stub_env('PAGES_MIGRATION_MARK_PROJECTS_AS_NOT_DEPLOYED', 'true')
expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything,
- migration_threads: 3,
- batch_size: 10,
ignore_invalid_entries: false,
mark_projects_as_not_deployed: true) do |service|
- expect(service).to receive(:execute).and_call_original
+ expect(service).to receive(:execute_with_threads).with(threads: 3, batch_size: 10).and_call_original
end
subject
@@ -96,4 +86,80 @@ RSpec.describe 'gitlab:pages' do
expect(PagesDeployment.find_by_id(migrated_deployment.id)).to be_nil
end
end
+
+ describe 'gitlab:pages:deployments:migrate_to_object_storage' do
+ subject { run_rake_task('gitlab:pages:deployments:migrate_to_object_storage') }
+
+ before do
+ stub_pages_object_storage(::Pages::DeploymentUploader, enabled: object_storage_enabled)
+ end
+
+ let!(:deployment) { create(:pages_deployment, file_store: store) }
+ let(:object_storage_enabled) { true }
+
+ context 'when local storage is used' do
+ let(:store) { ObjectStorage::Store::LOCAL }
+
+ context 'and remote storage is defined' do
+ it 'migrates file to remote storage' do
+ subject
+
+ expect(deployment.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+
+ context 'and remote storage is not defined' do
+ let(:object_storage_enabled) { false }
+
+ it 'fails to migrate to remote storage' do
+ subject
+
+ expect(deployment.reload.file_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+ end
+ end
+
+ context 'when remote storage is used' do
+ let(:store) { ObjectStorage::Store::REMOTE }
+
+ it 'file stays on remote storage' do
+ subject
+
+ expect(deployment.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+ end
+
+ describe 'gitlab:pages:deployments:migrate_to_local' do
+ subject { run_rake_task('gitlab:pages:deployments:migrate_to_local') }
+
+ before do
+ stub_pages_object_storage(::Pages::DeploymentUploader, enabled: object_storage_enabled)
+ end
+
+ let!(:deployment) { create(:pages_deployment, file_store: store) }
+ let(:object_storage_enabled) { true }
+
+ context 'when remote storage is used' do
+ let(:store) { ObjectStorage::Store::REMOTE }
+
+ context 'and job has remote file store defined' do
+ it 'migrates file to local storage' do
+ subject
+
+ expect(deployment.reload.file_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+ end
+ end
+
+ context 'when local storage is used' do
+ let(:store) { ObjectStorage::Store::LOCAL }
+
+ it 'file stays on local storage' do
+ subject
+
+ expect(deployment.reload.file_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+ end
+ end
end
diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
index 18467f95380..b6aff316b17 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -5,16 +5,51 @@ require 'spec_helper'
RSpec.describe 'layouts/nav/sidebar/_project' do
let_it_be_with_reload(:project) { create(:project, :repository) }
+ let(:user) { project.owner }
+
before do
assign(:project, project)
assign(:repository, project.repository)
- allow(view).to receive(:current_ref).and_return('master')
+ allow(view).to receive(:current_ref).and_return('master')
allow(view).to receive(:can?).and_return(true)
+ allow(view).to receive(:current_user).and_return(user)
end
it_behaves_like 'has nav sidebar'
+ describe 'Project Overview' do
+ it 'has a link to the project path' do
+ render
+
+ expect(rendered).to have_link('Project overview', href: project_path(project), class: %w(shortcuts-project rspec-project-link))
+ end
+
+ describe 'Details' do
+ it 'has a link to the projects path' do
+ render
+
+ expect(rendered).to have_link('Details', href: project_path(project), class: 'shortcuts-project')
+ end
+ end
+
+ describe 'Activity' do
+ it 'has a link to the project activity path' do
+ render
+
+ expect(rendered).to have_link('Activity', href: activity_project_path(project), class: 'shortcuts-project-activity')
+ end
+ end
+
+ describe 'Releases' do
+ it 'has a link to the project releases path' do
+ render
+
+ expect(rendered).to have_link('Releases', href: project_releases_path(project), class: 'shortcuts-project-releases')
+ end
+ end
+ end
+
describe 'issue boards' do
it 'has board tab' do
render
@@ -99,19 +134,11 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
end
- describe 'releases entry' do
- it 'renders releases link' do
- render
-
- expect(rendered).to have_link('Releases', href: project_releases_path(project))
- end
- end
-
describe 'wiki entry tab' do
let(:can_read_wiki) { true }
before do
- allow(view).to receive(:can?).with(nil, :read_wiki, project).and_return(can_read_wiki)
+ allow(view).to receive(:can?).with(user, :read_wiki, project).and_return(can_read_wiki)
end
describe 'when wiki is enabled' do
@@ -299,7 +326,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
let(:read_cycle_analytics) { true }
before do
- allow(view).to receive(:can?).with(nil, :read_cycle_analytics, project).and_return(read_cycle_analytics)
+ allow(view).to receive(:can?).with(user, :read_cycle_analytics, project).and_return(read_cycle_analytics)
end
describe 'when value stream analytics is enabled' do