Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
69b0ff9002
commit
10130901f1
|
@ -1 +1 @@
|
|||
e505e98295e765f945719f289125a98e671a7e19
|
||||
6904387a86815c80988d87f23af9d3fe1e2d4c85
|
||||
|
|
|
@ -1 +1 @@
|
|||
1.37.0
|
||||
1.38.0
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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.',
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module VulnerabilityFindingHelpers
|
||||
extend ActiveSupport::Concern
|
||||
end
|
||||
|
||||
VulnerabilityFindingHelpers.prepend_if_ee('EE::VulnerabilityFindingHelpers')
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module VulnerabilityFindingSignatureHelpers
|
||||
extend ActiveSupport::Concern
|
||||
end
|
||||
|
||||
VulnerabilityFindingSignatureHelpers.prepend_if_ee('EE::VulnerabilityFindingSignatureHelpers')
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -19,7 +19,7 @@ module Ci
|
|||
end
|
||||
|
||||
def metrics
|
||||
@metrics ||= ::Gitlab::Ci::Pipeline::Metrics.new
|
||||
@metrics ||= ::Gitlab::Ci::Pipeline::Metrics
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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 }
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Optimize issuable updates
|
||||
merge_request: 59468
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix Jenkins integration for GitLab FOSS
|
||||
merge_request: 59476
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Automatically try to migrate gitlab pages to zip storage
|
||||
merge_request: 54578
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add rake tasks for Pages deployment migration
|
||||
merge_request: 57120
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add GraphQL endpoint for a specific test suite in pipelines
|
||||
merge_request: 58924
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove gldropdown_branches feature flag
|
||||
merge_request: 59179
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Hide What's New for unauthenticated users
|
||||
merge_request: 59330
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add framework for using specialized services to improve performance of MergeRequests::UpdateService
|
||||
merge_request: 58836
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add queue label to metrics dispatched by background transaction
|
||||
merge_request: 59344
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Upgrade GitLab Pages to 1.38.0
|
||||
merge_request: 59464
|
||||
author:
|
||||
type: added
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
7c562d43801c18af48dc526dc6574aebd11689b62bad864b107580d341ba64a1
|
|
@ -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`.
|
|
@ -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` |
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
NOTE:
|
||||
For GitLab.com users, the KAS is available at `wss://kas.gitlab.com`.
|
||||
|
||||
#### One-liner installation
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
```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 -
|
||||
kubectl create namespace gitlab-kubernetes-agent
|
||||
```
|
||||
|
||||
To perform a one-liner installation, run the command below. Make sure to replace:
|
||||
|
||||
- `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 --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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
::Gitlab::Metrics.histogram(name, comment, labels, buckets)
|
||||
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.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
|
||||
::Gitlab::Metrics.histogram(name, comment, labels, buckets)
|
||||
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.pipeline_processing_events_counter
|
||||
name = :gitlab_ci_pipeline_processing_events_total
|
||||
comment = 'Total amount of pipeline processing events'
|
||||
|
||||
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.pipelines_created_counter
|
||||
name = :pipelines_created_total
|
||||
comment = 'Counter of pipelines created'
|
||||
|
||||
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.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 self.pipeline_failure_reason_counter
|
||||
name = :gitlab_ci_pipeline_failure_reasons
|
||||
comment = 'Counter of pipeline failure reasons'
|
||||
|
||||
Gitlab::Metrics.counter(name, comment)
|
||||
end
|
||||
|
||||
def self.job_failure_reason_counter
|
||||
name = :gitlab_ci_job_failure_reasons
|
||||
comment = 'Counter of job failure reasons'
|
||||
|
||||
Gitlab::Metrics.counter(name, comment)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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")
|
||||
expect(page).to have_selector('.js-whats-new-notification-count')
|
||||
|
||||
find('button', text: "What's new").click
|
||||
expect(page).to have_button(text: "What's new")
|
||||
end
|
||||
end
|
||||
|
||||
find('.whats-new-drawer .gl-drawer-close-button').click
|
||||
find('.header-help-dropdown-toggle').click
|
||||
it "doesn't render what's new" do
|
||||
visit user_path(user)
|
||||
|
||||
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
|
||||
find('.header-help-dropdown-toggle').click
|
||||
|
||||
expect(page).not_to have_button(text: "What's new")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when logged in', :js do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
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')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -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?
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue