Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
427dbb30f0
commit
7c0e5472c8
|
@ -167,6 +167,7 @@ setup-test-env:
|
|||
variables:
|
||||
SETUP_DB: "false"
|
||||
script:
|
||||
- echo $CI_MERGE_REQUEST_APPROVED
|
||||
- source scripts/gitlab_workhorse_component_helpers.sh
|
||||
- run_timed_command "download_and_extract_gitlab_workhorse_package" || true
|
||||
- run_timed_command "scripts/setup-test-env"
|
||||
|
|
|
@ -538,6 +538,9 @@
|
|||
rules:
|
||||
- <<: *if-merge-request-approved
|
||||
when: never
|
||||
# Temporarily disabled minimal rspec jobs before and after approval because of https://gitlab.com/gitlab-org/gitlab/-/issues/373064.
|
||||
- <<: *if-merge-request-not-approved
|
||||
when: never
|
||||
- <<: *if-automated-merge-request
|
||||
when: never
|
||||
- <<: *if-security-merge-request
|
||||
|
@ -559,8 +562,6 @@
|
|||
changes: *backend-patterns
|
||||
- <<: *if-security-merge-request
|
||||
changes: *backend-patterns
|
||||
- <<: *if-merge-request-not-approved
|
||||
when: never
|
||||
|
||||
.rails:rules:as-if-foss-migration-unit-integration:minimal-default-rules:
|
||||
rules:
|
||||
|
@ -590,8 +591,6 @@
|
|||
changes: *code-backstage-patterns
|
||||
- <<: *if-security-merge-request
|
||||
changes: *code-backstage-patterns
|
||||
- <<: *if-merge-request-not-approved
|
||||
when: never
|
||||
|
||||
.rails:rules:system:minimal-default-rules:
|
||||
rules:
|
||||
|
@ -725,6 +724,16 @@
|
|||
##################
|
||||
# Frontend rules #
|
||||
##################
|
||||
|
||||
.frontend:rules:minimal-default-rules:
|
||||
rules:
|
||||
- <<: *if-merge-request-approved
|
||||
when: never
|
||||
- <<: *if-automated-merge-request
|
||||
when: never
|
||||
- <<: *if-security-merge-request
|
||||
when: never
|
||||
|
||||
.frontend:rules:compile-production-assets:
|
||||
rules:
|
||||
- <<: *if-not-canonical-namespace
|
||||
|
@ -798,7 +807,7 @@
|
|||
rules:
|
||||
- <<: *if-fork-merge-request
|
||||
changes: *code-backstage-patterns
|
||||
- !reference [".rails:rules:minimal-default-rules", rules]
|
||||
- !reference [".frontend:rules:minimal-default-rules", rules]
|
||||
- <<: *if-merge-request-labels-run-all-jest
|
||||
when: never
|
||||
- changes: *core-frontend-patterns
|
||||
|
@ -824,7 +833,7 @@
|
|||
.frontend:rules:jest:minimal:as-if-foss:
|
||||
rules:
|
||||
- !reference [".strict-ee-only-rules", rules]
|
||||
- !reference [".rails:rules:minimal-default-rules", rules]
|
||||
- !reference [".frontend:rules:minimal-default-rules", rules]
|
||||
- <<: *if-merge-request-labels-run-all-jest
|
||||
when: never
|
||||
- <<: *if-fork-merge-request
|
||||
|
@ -1022,8 +1031,6 @@
|
|||
changes: *db-patterns
|
||||
- <<: *if-security-merge-request
|
||||
changes: *db-patterns
|
||||
- <<: *if-merge-request-not-approved
|
||||
when: never
|
||||
- changes: *db-patterns
|
||||
|
||||
.rails:rules:ee-and-foss-migration:minimal:
|
||||
|
@ -1127,8 +1134,6 @@
|
|||
changes: *db-patterns
|
||||
- <<: *if-security-merge-request
|
||||
changes: *db-patterns
|
||||
- <<: *if-merge-request-not-approved
|
||||
when: never
|
||||
- changes: *db-patterns
|
||||
|
||||
.rails:rules:ee-only-migration:minimal:
|
||||
|
@ -1218,8 +1223,6 @@
|
|||
changes: *db-patterns
|
||||
- <<: *if-security-merge-request
|
||||
changes: *db-patterns
|
||||
- <<: *if-merge-request-not-approved
|
||||
when: never
|
||||
|
||||
.rails:rules:as-if-foss-migration:minimal:
|
||||
rules:
|
||||
|
|
|
@ -90,7 +90,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="showBlobControls">
|
||||
<div v-if="showBlobControls" class="gl-display-flex gl-gap-3">
|
||||
<gl-button data-testid="find" :href="blobInfo.findFilePath" :class="$options.buttonClassList">
|
||||
{{ $options.i18n.findFile }}
|
||||
</gl-button>
|
||||
|
|
|
@ -50,7 +50,11 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
|
|||
result = DraftNotes::PublishService.new(merge_request, current_user).execute(draft_note(allow_nil: true))
|
||||
|
||||
if Feature.enabled?(:mr_review_submit_comment, @project)
|
||||
::Notes::CreateService.new(@project, current_user, create_note_params).execute if create_note_params[:note]
|
||||
if create_note_params[:note]
|
||||
::Notes::CreateService.new(@project, current_user, create_note_params).execute
|
||||
|
||||
merge_request_activity_counter.track_submit_review_comment(user: current_user)
|
||||
end
|
||||
|
||||
if Gitlab::Utils.to_boolean(approve_params[:approve])
|
||||
success = ::MergeRequests::ApprovalService.new(project: @project, current_user: current_user, params: approve_params).execute(merge_request)
|
||||
|
@ -58,6 +62,8 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
|
|||
unless success
|
||||
return render json: { message: _('An error occurred while approving, please try again.') }, status: :internal_server_error
|
||||
end
|
||||
|
||||
merge_request_activity_counter.track_submit_review_approve(user: current_user)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -159,6 +165,10 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
|
|||
def authorize_create_note!
|
||||
access_denied! unless can?(current_user, :create_note, merge_request)
|
||||
end
|
||||
|
||||
def merge_request_activity_counter
|
||||
Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter
|
||||
end
|
||||
end
|
||||
|
||||
Projects::MergeRequests::DraftsController.prepend_mod
|
||||
|
|
|
@ -11,7 +11,7 @@ module Projects
|
|||
before_action :integration, only: [:edit, :update, :test]
|
||||
before_action :default_integration, only: [:edit, :update]
|
||||
before_action :web_hook_logs, only: [:edit, :update]
|
||||
before_action -> { check_rate_limit!(:project_testing_integration, scope: [@project, current_user]) }, only: :test
|
||||
before_action -> { check_test_rate_limit! }, only: :test
|
||||
|
||||
respond_to :html
|
||||
|
||||
|
@ -140,6 +140,15 @@ module Projects
|
|||
def use_inherited_settings?(attributes)
|
||||
default_integration && attributes[:inherit_from_id] == default_integration.id.to_s
|
||||
end
|
||||
|
||||
def check_test_rate_limit!
|
||||
check_rate_limit!(:project_testing_integration, scope: [@project, current_user]) do
|
||||
render json: {
|
||||
error: true,
|
||||
message: _('This endpoint has been requested too many times. Try again later.')
|
||||
}, status: :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module BranchRules
|
||||
class BranchProtectionType < BaseObject
|
||||
graphql_name 'BranchProtection'
|
||||
description 'Branch protection details for a branch rule.'
|
||||
accepts ::ProtectedBranch
|
||||
authorize :read_protected_branch
|
||||
|
||||
field :allow_force_push,
|
||||
type: GraphQL::Types::Boolean,
|
||||
null: false,
|
||||
description: 'Toggle force push to the branch for users with write access.'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -13,6 +13,12 @@ module Types
|
|||
null: false,
|
||||
description: 'Branch name, with wildcards, for the branch rules.'
|
||||
|
||||
field :branch_protection,
|
||||
type: Types::BranchRules::BranchProtectionType,
|
||||
null: false,
|
||||
description: 'Branch protections configured for this branch rule.',
|
||||
method: :itself
|
||||
|
||||
field :created_at,
|
||||
Types::TimeType,
|
||||
null: false,
|
||||
|
|
|
@ -38,6 +38,7 @@ class NotificationRecipient
|
|||
return !unsubscribed? if @type == :subscription
|
||||
|
||||
return false unless suitable_notification_level?
|
||||
return false if email_blocked?
|
||||
|
||||
# check this last because it's expensive
|
||||
# nobody should receive notifications if they've specifically unsubscribed
|
||||
|
@ -95,6 +96,15 @@ class NotificationRecipient
|
|||
end
|
||||
end
|
||||
|
||||
def email_blocked?
|
||||
return false if Feature.disabled?(:block_emails_with_failures)
|
||||
|
||||
recipient_email = user.notification_email_for(@group)
|
||||
|
||||
Gitlab::ApplicationRateLimiter.peek(:permanent_email_failure, scope: recipient_email) ||
|
||||
Gitlab::ApplicationRateLimiter.peek(:temporary_email_failure, scope: recipient_email)
|
||||
end
|
||||
|
||||
def has_access?
|
||||
DeclarativePolicy.subject_scope do
|
||||
break false unless user.can?(:receive_notifications)
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module MergeRequests
|
||||
module Mergeability
|
||||
class Logger
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def initialize(merge_request:, destination: Gitlab::AppJsonLogger)
|
||||
@destination = destination
|
||||
@merge_request = merge_request
|
||||
end
|
||||
|
||||
def commit
|
||||
return unless enabled?
|
||||
|
||||
commit_logs
|
||||
end
|
||||
|
||||
def instrument(mergeability_name:)
|
||||
raise ArgumentError, 'block not given' unless block_given?
|
||||
|
||||
return yield unless enabled?
|
||||
|
||||
op_start_db_counters = current_db_counter_payload
|
||||
op_started_at = current_monotonic_time
|
||||
|
||||
result = yield
|
||||
|
||||
observe("mergeability.#{mergeability_name}.duration_s", current_monotonic_time - op_started_at)
|
||||
|
||||
observe_sql_counters(mergeability_name, op_start_db_counters, current_db_counter_payload)
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :destination, :merge_request
|
||||
|
||||
def observe(name, value)
|
||||
return unless enabled?
|
||||
|
||||
observations[name.to_s].push(value)
|
||||
end
|
||||
|
||||
def commit_logs
|
||||
attributes = Gitlab::ApplicationContext.current.merge({
|
||||
mergeability_project_id: merge_request.project.id
|
||||
})
|
||||
|
||||
attributes[:mergeability_merge_request_id] = merge_request.id
|
||||
attributes.merge!(observations_hash)
|
||||
attributes.compact!
|
||||
attributes.stringify_keys!
|
||||
|
||||
destination.info(attributes)
|
||||
end
|
||||
|
||||
def observations_hash
|
||||
transformed = observations.transform_values do |values|
|
||||
next if values.empty?
|
||||
|
||||
{
|
||||
'values' => values
|
||||
}
|
||||
end.compact
|
||||
|
||||
transformed.each_with_object({}) do |key, hash|
|
||||
key[1].each { |k, v| hash["#{key[0]}.#{k}"] = v }
|
||||
end
|
||||
end
|
||||
|
||||
def observations
|
||||
strong_memoize(:observations) do
|
||||
Hash.new { |hash, key| hash[key] = [] }
|
||||
end
|
||||
end
|
||||
|
||||
def observe_sql_counters(name, start_db_counters, end_db_counters)
|
||||
end_db_counters.each do |key, value|
|
||||
result = value - start_db_counters.fetch(key, 0)
|
||||
next if result == 0
|
||||
|
||||
observe("mergeability.#{name}.#{key}", result)
|
||||
end
|
||||
end
|
||||
|
||||
def current_db_counter_payload
|
||||
::Gitlab::Metrics::Subscribers::ActiveRecord.db_counter_payload
|
||||
end
|
||||
|
||||
def enabled?
|
||||
strong_memoize(:enabled) do
|
||||
::Feature.enabled?(:mergeability_checks_logger, merge_request.project)
|
||||
end
|
||||
end
|
||||
|
||||
def current_monotonic_time
|
||||
::Gitlab::Metrics::System.monotonic_time
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -15,12 +15,17 @@ module MergeRequests
|
|||
|
||||
next if check.skip?
|
||||
|
||||
check_result = run_check(check)
|
||||
check_result = logger.instrument(mergeability_name: check_class.to_s.demodulize.underscore) do
|
||||
run_check(check)
|
||||
end
|
||||
|
||||
result_hash << check_result
|
||||
|
||||
break result_hash if check_result.failed?
|
||||
end
|
||||
|
||||
logger.commit
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
|
@ -57,6 +62,12 @@ module MergeRequests
|
|||
Gitlab::MergeRequests::Mergeability::ResultsStore.new(merge_request: merge_request)
|
||||
end
|
||||
end
|
||||
|
||||
def logger
|
||||
strong_memoize(:logger) do
|
||||
MergeRequests::Mergeability::Logger.new(merge_request: merge_request)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
# rubocop:disable Naming/FileName
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'cdn/google_cdn'
|
||||
|
||||
module ObjectStorage
|
||||
module CDN
|
||||
module Concern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def use_cdn?(request_ip)
|
||||
return false unless cdn_options.is_a?(Hash) && cdn_options['provider']
|
||||
return false unless cdn_provider
|
||||
|
||||
cdn_provider.use_cdn?(request_ip)
|
||||
end
|
||||
|
||||
def cdn_signed_url
|
||||
cdn_provider&.signed_url(path)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cdn_provider
|
||||
strong_memoize(:cdn_provider) do
|
||||
provider = cdn_options['provider']&.downcase
|
||||
|
||||
next unless provider
|
||||
next GoogleCDN.new(cdn_options) if provider == 'google'
|
||||
|
||||
raise "Unknown CDN provider: #{provider}"
|
||||
end
|
||||
end
|
||||
|
||||
def cdn_options
|
||||
return {} unless options.object_store.key?('cdn')
|
||||
|
||||
options.object_store.cdn
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:enable Naming/FileName
|
|
@ -0,0 +1,141 @@
|
|||
# rubocop:disable Naming/FileName
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ObjectStorage
|
||||
module CDN
|
||||
class GoogleCDN
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
IpListNotRetrievedError = Class.new(StandardError)
|
||||
|
||||
GOOGLE_CDN_LIST_KEY = 'google_cdn_ip_list'
|
||||
GOOGLE_IP_RANGES_URL = 'https://www.gstatic.com/ipranges/cloud.json'
|
||||
EXPECTED_CONTENT_TYPE = 'application/json'
|
||||
RESPONSE_BODY_LIMIT = 1.megabyte
|
||||
CACHE_EXPIRATION_TIME = 1.day
|
||||
|
||||
attr_reader :options
|
||||
|
||||
def initialize(options)
|
||||
@options = HashWithIndifferentAccess.new(options.to_h)
|
||||
end
|
||||
|
||||
def use_cdn?(request_ip)
|
||||
return false unless config_valid?
|
||||
|
||||
ip = IPAddr.new(request_ip)
|
||||
|
||||
return false if ip.private?
|
||||
return false unless google_ip_ranges.present?
|
||||
|
||||
!google_ip?(request_ip)
|
||||
end
|
||||
|
||||
def signed_url(path, expiry: 10.minutes)
|
||||
expiration = (Time.current + expiry).utc.to_i
|
||||
|
||||
uri = Addressable::URI.parse(cdn_url)
|
||||
uri.path = path
|
||||
uri.query = "Expires=#{expiration}&KeyName=#{key_name}"
|
||||
|
||||
signature = OpenSSL::HMAC.digest('SHA1', decoded_key, uri.to_s)
|
||||
encoded_signature = Base64.urlsafe_encode64(signature)
|
||||
|
||||
uri.query += "&Signature=#{encoded_signature}"
|
||||
uri.to_s
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def config_valid?
|
||||
[key_name, decoded_key, cdn_url].all?(&:present?)
|
||||
end
|
||||
|
||||
def key_name
|
||||
strong_memoize(:key_name) do
|
||||
options['key_name']
|
||||
end
|
||||
end
|
||||
|
||||
def decoded_key
|
||||
strong_memoize(:decoded_key) do
|
||||
Base64.urlsafe_decode64(options['key']) if options['key']
|
||||
rescue ArgumentError
|
||||
Gitlab::ErrorTracking.log_exception(ArgumentError.new("Google CDN key is not base64-encoded"))
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def cdn_url
|
||||
strong_memoize(:cdn_url) do
|
||||
options['url']
|
||||
end
|
||||
end
|
||||
|
||||
def google_ip?(request_ip)
|
||||
google_ip_ranges.any? { |range| range.include?(request_ip) }
|
||||
end
|
||||
|
||||
def google_ip_ranges
|
||||
strong_memoize(:google_ip_ranges) do
|
||||
cache_value(GOOGLE_CDN_LIST_KEY) { fetch_google_ip_list }
|
||||
end
|
||||
rescue IpListNotRetrievedError => err
|
||||
Gitlab::ErrorTracking.log_exception(err)
|
||||
[]
|
||||
end
|
||||
|
||||
def cache_value(key, expires_in: CACHE_EXPIRATION_TIME, &block)
|
||||
l1_cache.fetch(key, expires_in: expires_in) do
|
||||
l2_cache.fetch(key, expires_in: expires_in) { yield }
|
||||
end
|
||||
end
|
||||
|
||||
def l1_cache
|
||||
Gitlab::ProcessMemoryCache.cache_backend
|
||||
end
|
||||
|
||||
def l2_cache
|
||||
Rails.cache
|
||||
end
|
||||
|
||||
def fetch_google_ip_list
|
||||
response = Gitlab::HTTP.get(GOOGLE_IP_RANGES_URL)
|
||||
|
||||
raise IpListNotRetrievedError, "response was #{response.code}" unless response.code == 200
|
||||
|
||||
if response.body&.bytesize.to_i > RESPONSE_BODY_LIMIT
|
||||
raise IpListNotRetrievedError, "response was too large: #{response.body.bytesize}"
|
||||
end
|
||||
|
||||
parsed_response = response.parsed_response
|
||||
|
||||
unless response.content_type == EXPECTED_CONTENT_TYPE && parsed_response.is_a?(Hash)
|
||||
raise IpListNotRetrievedError, "response was not JSON"
|
||||
end
|
||||
|
||||
parse_google_prefixes(parsed_response)
|
||||
end
|
||||
|
||||
def parse_google_prefixes(parsed_response)
|
||||
prefixes = parsed_response['prefixes']
|
||||
|
||||
raise IpListNotRetrievedError, "JSON was type #{prefixes.class}, expected Array" unless prefixes.is_a?(Array)
|
||||
|
||||
ranges = prefixes.map do |prefix|
|
||||
ip_range = prefix['ipv4Prefix'] || prefix['ipv6Prefix']
|
||||
|
||||
next unless ip_range
|
||||
|
||||
IPAddr.new(ip_range)
|
||||
end.compact
|
||||
|
||||
raise IpListNotRetrievedError, "#{GOOGLE_IP_RANGES_URL} did not return any IP ranges" if ranges.empty?
|
||||
|
||||
ranges
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:enable Naming/FileName
|
|
@ -46,18 +46,18 @@
|
|||
= f.text_field :user_default_internal_regex, placeholder: _('Regex pattern'), class: 'form-control gl-form-input gl-mt-2'
|
||||
.help-block
|
||||
= _('Specify an email address regex pattern to identify default internal users.')
|
||||
= link_to _('Learn more'), help_page_path('user/permissions', anchor: 'setting-new-users-to-external'), target: '_blank', rel: 'noopener noreferrer'
|
||||
= link_to _('Learn more.'), help_page_path('user/permissions', anchor: 'setting-new-users-to-external'), target: '_blank', rel: 'noopener noreferrer'
|
||||
- unless Gitlab.com?
|
||||
.form-group
|
||||
= f.label :deactivate_dormant_users, _('Dormant users'), class: 'label-bold'
|
||||
- dormant_users_help_link = help_page_path('user/admin_area/moderate_users', anchor: 'automatically-deactivate-dormant-users')
|
||||
- dormant_users_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: dormant_users_help_link }
|
||||
= f.gitlab_ui_checkbox_component :deactivate_dormant_users, _('Deactivate dormant users after a period of inactivity'), help_text: _('Users can reactivate their account by signing in. %{link_start}Learn more%{link_end}').html_safe % { link_start: dormant_users_help_link_start, link_end: '</a>'.html_safe }
|
||||
= f.gitlab_ui_checkbox_component :deactivate_dormant_users, _('Deactivate dormant users after a period of inactivity'), help_text: _('Users can reactivate their account by signing in. %{link_start}Learn more.%{link_end}').html_safe % { link_start: dormant_users_help_link_start, link_end: '</a>'.html_safe }
|
||||
.form-group
|
||||
= f.label :deactivate_dormant_users_period, _('Period of inactivity (days)'), class: 'label-light'
|
||||
= f.number_field :deactivate_dormant_users_period, class: 'form-control gl-form-input', min: '1'
|
||||
.form-text.text-muted
|
||||
= _('Period of inactivity before deactivation')
|
||||
= _('Period of inactivity before deactivation.')
|
||||
|
||||
.form-group
|
||||
= f.label :personal_access_token_prefix, _('Personal Access Token prefix'), class: 'label-light'
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: block_emails_with_failures
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96902
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/373159
|
||||
milestone: '15.4'
|
||||
type: development
|
||||
group: group::project management
|
||||
default_enabled: false
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: mergeability_checks_logger
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96128
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/371717
|
||||
milestone: '15.4'
|
||||
type: development
|
||||
group: group::code review
|
||||
default_enabled: false
|
|
@ -37,6 +37,7 @@ ActiveSupport::Inflector.inflections do |inflect|
|
|||
vulnerabilities_feedback
|
||||
vulnerability_feedback
|
||||
)
|
||||
inflect.acronym 'CDN'
|
||||
inflect.acronym 'EE'
|
||||
inflect.acronym 'JH'
|
||||
inflect.acronym 'CSP'
|
||||
|
|
|
@ -116,6 +116,8 @@
|
|||
- 'i_code_review_merge_request_widget_status_checks_expand_success'
|
||||
- 'i_code_review_merge_request_widget_status_checks_expand_warning'
|
||||
- 'i_code_review_merge_request_widget_status_checks_expand_failed'
|
||||
- 'i_code_review_submit_review_approve'
|
||||
- 'i_code_review_submit_review_comment'
|
||||
- name: code_review_category_monthly_active_users
|
||||
operator: OR
|
||||
source: redis
|
||||
|
@ -220,6 +222,8 @@
|
|||
- 'i_code_review_merge_request_widget_status_checks_expand_success'
|
||||
- 'i_code_review_merge_request_widget_status_checks_expand_warning'
|
||||
- 'i_code_review_merge_request_widget_status_checks_expand_failed'
|
||||
- 'i_code_review_submit_review_approve'
|
||||
- 'i_code_review_submit_review_comment'
|
||||
- name: code_review_extension_category_monthly_active_users
|
||||
operator: OR
|
||||
source: redis
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
key_path: redis_hll_counters.code_review.i_code_review_submit_review_approve_monthly
|
||||
description: Count of unique users per month who submit a review and approve
|
||||
product_stage: create
|
||||
product_group: code_review
|
||||
product_category: code_review
|
||||
product_section: dev
|
||||
value_type: number
|
||||
status: active
|
||||
milestone: '15.4'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91073
|
||||
time_frame: 28d
|
||||
data_source: redis_hll
|
||||
data_category: optional
|
||||
instrumentation_class: RedisHLLMetric
|
||||
options:
|
||||
events:
|
||||
- i_code_review_submit_review_approve
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
key_path: redis_hll_counters.code_review.i_code_review_submit_review_comment_monthly
|
||||
description: Count of unique users per month who submit a review with a comment
|
||||
product_stage: create
|
||||
product_group: code_review
|
||||
product_category: code_review
|
||||
product_section: dev
|
||||
value_type: number
|
||||
status: active
|
||||
milestone: '15.4'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91073
|
||||
time_frame: 28d
|
||||
data_source: redis_hll
|
||||
data_category: optional
|
||||
instrumentation_class: RedisHLLMetric
|
||||
options:
|
||||
events:
|
||||
- i_code_review_submit_review_comment
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
key_path: redis_hll_counters.code_review.i_code_review_submit_review_approve_weekly
|
||||
description: Count of unique users per week who submit a review and approve
|
||||
product_stage: create
|
||||
product_group: code_review
|
||||
product_category: code_review
|
||||
product_section: dev
|
||||
value_type: number
|
||||
status: active
|
||||
milestone: '15.4'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91073
|
||||
time_frame: 7d
|
||||
data_source: redis_hll
|
||||
data_category: optional
|
||||
instrumentation_class: RedisHLLMetric
|
||||
options:
|
||||
events:
|
||||
- i_code_review_submit_review_approve
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
key_path: redis_hll_counters.code_review.i_code_review_submit_review_comment_weekly
|
||||
description: Count of unique users per week who submit a review with a comment
|
||||
product_stage: create
|
||||
product_group: code_review
|
||||
product_category: code_review
|
||||
product_section: dev
|
||||
value_type: number
|
||||
status: active
|
||||
milestone: '15.4'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91073
|
||||
time_frame: 7d
|
||||
data_source: redis_hll
|
||||
data_category: optional
|
||||
instrumentation_class: RedisHLLMetric
|
||||
options:
|
||||
events:
|
||||
- i_code_review_submit_review_comment
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
|
@ -10074,6 +10074,16 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
|||
| <a id="branchcommit"></a>`commit` | [`Commit`](#commit) | Commit for the branch. |
|
||||
| <a id="branchname"></a>`name` | [`String!`](#string) | Name of the branch. |
|
||||
|
||||
### `BranchProtection`
|
||||
|
||||
Branch protection details for a branch rule.
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="branchprotectionallowforcepush"></a>`allowForcePush` | [`Boolean!`](#boolean) | Toggle force push to the branch for users with write access. |
|
||||
|
||||
### `BranchRule`
|
||||
|
||||
List of branch rules for a project, grouped by branch name.
|
||||
|
@ -10082,6 +10092,7 @@ List of branch rules for a project, grouped by branch name.
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="branchrulebranchprotection"></a>`branchProtection` | [`BranchProtection!`](#branchprotection) | Branch protections configured for this branch rule. |
|
||||
| <a id="branchrulecreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of when the branch rule was created. |
|
||||
| <a id="branchrulename"></a>`name` | [`String!`](#string) | Branch name, with wildcards, for the branch rules. |
|
||||
| <a id="branchruleupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the branch rule was last updated. |
|
||||
|
|
|
@ -36,7 +36,8 @@ Sign in to BrowserStack with the credentials saved in the **Engineering** vault
|
|||
|
||||
## Initiatives
|
||||
|
||||
Current high-level frontend goals are listed on [Frontend Epics](https://gitlab.com/groups/gitlab-org/-/epics?label_name%5B%5D=frontend).
|
||||
You can find current frontend initiatives with a cross-functional impact on epics
|
||||
with the label [frontend-initiative](https://gitlab.com/groups/gitlab-org/-/epics?state=opened&page=1&sort=UPDATED_AT_DESC&label_name[]=frontend-initiative).
|
||||
|
||||
## Principles
|
||||
|
||||
|
|
|
@ -383,7 +383,7 @@ The following table lists group permissions available for each role:
|
|||
| Pull a container image using the dependency proxy | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| View Contribution analytics | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| View group [epic](group/epics/index.md) | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| View [group wiki](project/wiki/group.md) pages | ✓ (6) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View [group wiki](project/wiki/group.md) pages | ✓ (5) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View [Insights](project/insights/index.md) | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| View [Insights](project/insights/index.md) charts | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| View [Issue analytics](analytics/issue_analytics.md) | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
|
@ -395,13 +395,13 @@ The following table lists group permissions available for each role:
|
|||
| Pull [packages](packages/index.md) | | ✓ | ✓ | ✓ | ✓ |
|
||||
| Delete [packages](packages/index.md) | | | | ✓ | ✓ |
|
||||
| Create/edit/delete [Maven and generic package duplicate settings](packages/generic_packages/index.md#do-not-allow-duplicate-generic-packages) | | | | ✓ | ✓ |
|
||||
| Pull a Container Registry image | ✓ (7) | ✓ | ✓ | ✓ | ✓ |
|
||||
| Pull a Container Registry image | ✓ (6) | ✓ | ✓ | ✓ | ✓ |
|
||||
| Remove a Container Registry image | | | ✓ | ✓ | ✓ |
|
||||
| View [Group DevOps Adoption](group/devops_adoption/index.md) | | ✓ | ✓ | ✓ | ✓ |
|
||||
| View metrics dashboard annotations | | ✓ | ✓ | ✓ | ✓ |
|
||||
| View [Productivity analytics](analytics/productivity_analytics.md) | | ✓ | ✓ | ✓ | ✓ |
|
||||
| Create and edit [group wiki](project/wiki/group.md) pages | | | ✓ | ✓ | ✓ |
|
||||
| Create project in group | | | ✓ (3)(5) | ✓ (3) | ✓ (3) |
|
||||
| Create project in group | | | ✓ (2)(4) | ✓ (2) | ✓ (2) |
|
||||
| Create/edit/delete group milestones | | ✓ | ✓ | ✓ | ✓ |
|
||||
| Create/edit/delete iterations | | ✓ | ✓ | ✓ | ✓ |
|
||||
| Create/edit/delete metrics dashboard annotations | | | ✓ | ✓ | ✓ |
|
||||
|
@ -409,10 +409,10 @@ The following table lists group permissions available for each role:
|
|||
| Purge the dependency proxy for a group | | | | | ✓ |
|
||||
| Create/edit/delete dependency proxy [cleanup policies](packages/dependency_proxy/reduce_dependency_proxy_storage.md#cleanup-policies) | | | | ✓ | ✓ |
|
||||
| Use [security dashboard](application_security/security_dashboard/index.md) | | | ✓ | ✓ | ✓ |
|
||||
| View group Audit Events | | | ✓ (7) | ✓ (7) | ✓ |
|
||||
| View group Audit Events | | | ✓ (6) | ✓ (6) | ✓ |
|
||||
| Create subgroup | | | | ✓ (1) | ✓ |
|
||||
| Delete [group wiki](project/wiki/group.md) pages | | | ✓ | ✓ | ✓ |
|
||||
| Edit [epic](group/epics/index.md) comments (posted by any user) | | | | ✓ (2) | ✓ (2) |
|
||||
| Edit [epic](group/epics/index.md) comments (posted by any user) | | | | ✓ | ✓ |
|
||||
| List group deploy tokens | | | | ✓ | ✓ |
|
||||
| Manage [group push rules](group/access_and_permissions.md#group-push-rules) | | | | ✓ | ✓ |
|
||||
| View/manage group-level Kubernetes cluster | | | | ✓ | ✓ |
|
||||
|
@ -423,14 +423,14 @@ The following table lists group permissions available for each role:
|
|||
| Delete group [epic](group/epics/index.md) | | | | | ✓ |
|
||||
| Disable notification emails | | | | | ✓ |
|
||||
| Edit group settings | | | | | ✓ |
|
||||
| Edit [SAML SSO](group/saml_sso/index.md) | | | | | ✓ (4) |
|
||||
| Edit [SAML SSO](group/saml_sso/index.md) | | | | | ✓ (3) |
|
||||
| Filter members by 2FA status | | | | | ✓ |
|
||||
| Manage group level CI/CD variables | | | | | ✓ |
|
||||
| Manage group members | | | | | ✓ |
|
||||
| Share (invite) groups with groups | | | | | ✓ |
|
||||
| View 2FA status of members | | | | | ✓ |
|
||||
| View [Billing](../subscriptions/gitlab_com/index.md#view-your-gitlab-saas-subscription) | | | | | ✓ (4) |
|
||||
| View group [Usage Quotas](usage_quotas.md) page | | | | | ✓ (4) |
|
||||
| View [Billing](../subscriptions/gitlab_com/index.md#view-your-gitlab-saas-subscription) | | | | | ✓ (3) |
|
||||
| View group [Usage Quotas](usage_quotas.md) page | | | | | ✓ (3) |
|
||||
| Manage group runners | | | | | ✓ |
|
||||
| [Migrate groups](group/import/index.md) | | | | | ✓ |
|
||||
| Manage [subscriptions, and purchase CI/CD minutes and storage](../subscriptions/gitlab_com/index.md) | | | | | ✓ |
|
||||
|
@ -438,14 +438,13 @@ The following table lists group permissions available for each role:
|
|||
<!-- markdownlint-disable MD029 -->
|
||||
|
||||
1. Groups can be set to allow either Owners, or Owners and users with the Maintainer role, to [create subgroups](group/subgroups/index.md#create-a-subgroup).
|
||||
2. Introduced in GitLab 12.2.
|
||||
3. Default project creation role can be changed at:
|
||||
2. Default project creation role can be changed at:
|
||||
- The [instance level](admin_area/settings/visibility_and_access_controls.md#define-which-roles-can-create-projects).
|
||||
- The [group level](group/manage.md#specify-who-can-add-projects-to-a-group).
|
||||
4. Does not apply to subgroups.
|
||||
5. Developers can push commits to the default branch of a new project only if the [default branch protection](group/manage.md#change-the-default-branch-protection-of-a-group) is set to "Partially protected" or "Not protected".
|
||||
6. In addition, if your group is public or internal, all users who can see the group can also see group wiki pages.
|
||||
7. Users can only view events based on their individual actions.
|
||||
3. Does not apply to subgroups.
|
||||
4. Developers can push commits to the default branch of a new project only if the [default branch protection](group/manage.md#change-the-default-branch-protection-of-a-group) is set to "Partially protected" or "Not protected".
|
||||
5. In addition, if your group is public or internal, all users who can see the group can also see group wiki pages.
|
||||
6. Users can only view events based on their individual actions.
|
||||
|
||||
<!-- markdownlint-enable MD029 -->
|
||||
|
||||
|
|
|
@ -10,9 +10,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
> - [Creating, editing, and deleting tasks](https://gitlab.com/groups/gitlab-org/-/epics/7169) introduced in GitLab 15.0.
|
||||
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/334812) in GitLab 15.3.
|
||||
|
||||
WARNING:
|
||||
Tasks are in [**Alpha**](../policy/alpha-beta-support.md#alpha-features).
|
||||
|
||||
Known limitation:
|
||||
|
||||
- [Tasks currently cannot be accessed via REST API.](https://gitlab.com/gitlab-org/gitlab/-/issues/368055)
|
||||
|
|
|
@ -55,7 +55,10 @@ you must purchase additional storage. For more details, see [Excess storage usag
|
|||
|
||||
## View storage usage
|
||||
|
||||
You can view storage usage for your project or [namespace](../user/namespace/index.md).
|
||||
Prerequisites:
|
||||
|
||||
- To view storage usage for a project, you must be a project Maintainer or namespace Owner.
|
||||
- To view storage usage for a namespace, you must be the namespace Owner.
|
||||
|
||||
1. Go to your project or namespace:
|
||||
- For a project, on the top bar, select **Menu > Projects** and find your project.
|
||||
|
@ -84,16 +87,16 @@ The following storage usage statistics are available to a maintainer:
|
|||
|
||||
## Manage your storage usage
|
||||
|
||||
You can use several methods to manage and reduce your usage for some storage types.
|
||||
To manage your storage, if you are a namespace Owner you can [purchase more storage for the namespace](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer).
|
||||
|
||||
For more information, see the following pages:
|
||||
Depending on your role, you can also use the following methods to manage or reduce your storage:
|
||||
|
||||
- [Reduce package registry storage](packages/package_registry/reduce_package_registry_storage.md)
|
||||
- [Reduce dependency proxy storage](packages/dependency_proxy/reduce_dependency_proxy_storage.md)
|
||||
- [Reduce repository size](project/repository/reducing_the_repo_size_using_git.md)
|
||||
- [Reduce container registry storage](packages/container_registry/reduce_container_registry_storage.md)
|
||||
- [Reduce container registry data transfers](packages/container_registry/reduce_container_registry_data_transfer.md)
|
||||
- [Reduce wiki repository size](../administration/wikis/index.md#reduce-wiki-repository-size)
|
||||
- [Reduce package registry storage](packages/package_registry/reduce_package_registry_storage.md).
|
||||
- [Reduce dependency proxy storage](packages/dependency_proxy/reduce_dependency_proxy_storage.md).
|
||||
- [Reduce repository size](project/repository/reducing_the_repo_size_using_git.md).
|
||||
- [Reduce container registry storage](packages/container_registry/reduce_container_registry_storage.md).
|
||||
- [Reduce container registry data transfers](packages/container_registry/reduce_container_registry_data_transfer.md).
|
||||
- [Reduce wiki repository size](../administration/wikis/index.md#reduce-wiki-repository-size).
|
||||
|
||||
## Excess storage usage
|
||||
|
||||
|
|
|
@ -46,7 +46,8 @@ module Gitlab
|
|||
search_rate_limit_unauthenticated: { threshold: -> { application_settings.search_rate_limit_unauthenticated }, interval: 1.minute },
|
||||
gitlab_shell_operation: { threshold: 600, interval: 1.minute },
|
||||
pipelines_create: { threshold: -> { application_settings.pipeline_limit_per_project_user_sha }, interval: 1.minute },
|
||||
temporary_email_failure: { threshold: 50, interval: 1.day },
|
||||
temporary_email_failure: { threshold: 300, interval: 1.day },
|
||||
permanent_email_failure: { threshold: 5, interval: 1.day },
|
||||
project_testing_integration: { threshold: 5, interval: 1.minute },
|
||||
email_verification: { threshold: 10, interval: 10.minutes },
|
||||
email_verification_code_send: { threshold: 10, interval: 1.hour },
|
||||
|
|
|
@ -7,7 +7,8 @@ module Gitlab
|
|||
|
||||
TAGS_SORT_KEY = {
|
||||
'name' => Gitaly::FindAllTagsRequest::SortBy::Key::REFNAME,
|
||||
'updated' => Gitaly::FindAllTagsRequest::SortBy::Key::CREATORDATE
|
||||
'updated' => Gitaly::FindAllTagsRequest::SortBy::Key::CREATORDATE,
|
||||
'version' => Gitaly::FindAllTagsRequest::SortBy::Key::VERSION_REFNAME
|
||||
}.freeze
|
||||
|
||||
TAGS_SORT_DIRECTION = {
|
||||
|
@ -258,7 +259,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def sort_tags_by_param(sort_by)
|
||||
match = sort_by.match(/^(?<key>name|updated)_(?<direction>asc|desc)$/)
|
||||
match = sort_by.match(/^(?<key>name|updated|version)_(?<direction>asc|desc)$/)
|
||||
|
||||
return unless match
|
||||
|
||||
|
|
|
@ -5,11 +5,12 @@ module Gitlab
|
|||
module WebhookProcessors
|
||||
class FailureLogger < Base
|
||||
def execute
|
||||
log_failure if permanent_failure? || temporary_failure_over_threshold?
|
||||
log_failure if permanent_failure_over_threshold? || temporary_failure_over_threshold?
|
||||
end
|
||||
|
||||
def permanent_failure?
|
||||
payload['event'] == 'failed' && payload['severity'] == 'permanent'
|
||||
def permanent_failure_over_threshold?
|
||||
payload['event'] == 'failed' && payload['severity'] == 'permanent' &&
|
||||
Gitlab::ApplicationRateLimiter.throttled?(:permanent_email_failure, scope: payload['recipient'])
|
||||
end
|
||||
|
||||
def temporary_failure_over_threshold?
|
||||
|
|
|
@ -21,7 +21,6 @@ module Gitlab
|
|||
CATEGORIES_FOR_TOTALS = %w[
|
||||
analytics
|
||||
compliance
|
||||
epic_boards_usage
|
||||
epics_usage
|
||||
error_tracking
|
||||
ide_edit
|
||||
|
|
|
@ -396,3 +396,11 @@
|
|||
redis_slot: code_review
|
||||
category: code_review
|
||||
aggregation: weekly
|
||||
- name: i_code_review_submit_review_approve
|
||||
redis_slot: code_review
|
||||
category: code_review
|
||||
aggregation: weekly
|
||||
- name: i_code_review_submit_review_comment
|
||||
redis_slot: code_review
|
||||
category: code_review
|
||||
aggregation: weekly
|
||||
|
|
|
@ -337,11 +337,6 @@
|
|||
redis_slot: testing
|
||||
category: testing
|
||||
aggregation: weekly
|
||||
# Container Security - Network Policies
|
||||
- name: clusters_using_network_policies_ui
|
||||
redis_slot: network_policies
|
||||
category: network_policies
|
||||
aggregation: weekly
|
||||
# Geo group
|
||||
- name: g_geo_proxied_requests
|
||||
category: geo
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
# Epic board events
|
||||
#
|
||||
# We are using the same slot of issue events 'project_management' for
|
||||
# epic events to allow data aggregation.
|
||||
# More information in: https://gitlab.com/gitlab-org/gitlab/-/issues/322405
|
||||
- name: g_project_management_users_creating_epic_boards
|
||||
category: epic_boards_usage
|
||||
redis_slot: project_management
|
||||
aggregation: daily
|
||||
|
||||
- name: g_project_management_users_viewing_epic_boards
|
||||
category: epic_boards_usage
|
||||
redis_slot: project_management
|
||||
aggregation: daily
|
||||
|
||||
- name: g_project_management_users_updating_epic_board_names
|
||||
category: epic_boards_usage
|
||||
redis_slot: project_management
|
||||
aggregation: daily
|
|
@ -49,6 +49,8 @@ module Gitlab
|
|||
MR_LOAD_CONFLICT_UI_ACTION = 'i_code_review_user_load_conflict_ui'
|
||||
MR_RESOLVE_CONFLICT_ACTION = 'i_code_review_user_resolve_conflict'
|
||||
MR_RESOLVE_THREAD_IN_ISSUE_ACTION = 'i_code_review_user_resolve_thread_in_issue'
|
||||
MR_SUBMIT_REVIEW_APPROVE = 'i_code_review_submit_review_approve'
|
||||
MR_SUBMIT_REVIEW_COMMENT = 'i_code_review_submit_review_comment'
|
||||
|
||||
class << self
|
||||
def track_mr_diffs_action(merge_request:)
|
||||
|
@ -230,6 +232,14 @@ module Gitlab
|
|||
track_unique_action_by_user(MR_RESOLVE_THREAD_IN_ISSUE_ACTION, user)
|
||||
end
|
||||
|
||||
def track_submit_review_approve(user:)
|
||||
track_unique_action_by_user(MR_SUBMIT_REVIEW_APPROVE, user)
|
||||
end
|
||||
|
||||
def track_submit_review_comment(user:)
|
||||
track_unique_action_by_user(MR_SUBMIT_REVIEW_COMMENT, user)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def track_unique_action_by_merge_request(action, merge_request)
|
||||
|
|
|
@ -21005,7 +21005,7 @@ msgstr ""
|
|||
msgid "Include the username in the URL if required: %{code_open}https://username@gitlab.company.com/group/project.git%{code_close}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Includes LFS objects. It can be overridden per group, or per project. 0 for unlimited."
|
||||
msgid "Includes LFS objects. It can be overridden per group, or per project. Set to 0 for no limit."
|
||||
msgstr ""
|
||||
|
||||
msgid "Includes an MVC structure to help you get started"
|
||||
|
@ -24385,7 +24385,7 @@ msgstr ""
|
|||
msgid "Maximum allowable lifetime for access token (days)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Maximum allowed lifetime for SSH keys (in days)"
|
||||
msgid "Maximum allowed lifetime for SSH keys (days)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Maximum artifacts size"
|
||||
|
@ -28677,7 +28677,7 @@ msgstr ""
|
|||
msgid "Period of inactivity (days)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Period of inactivity before deactivation"
|
||||
msgid "Period of inactivity before deactivation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Permalink"
|
||||
|
@ -43092,7 +43092,7 @@ msgstr ""
|
|||
msgid "Users can launch a development environment from a GitLab browser tab when the %{linkStart}Gitpod%{linkEnd} integration is enabled."
|
||||
msgstr ""
|
||||
|
||||
msgid "Users can reactivate their account by signing in. %{link_start}Learn more%{link_end}"
|
||||
msgid "Users can reactivate their account by signing in. %{link_start}Learn more.%{link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Users can render diagrams in AsciiDoc, Markdown, reStructuredText, and Textile documents using Kroki."
|
||||
|
|
|
@ -12,6 +12,8 @@ module QA
|
|||
|
||||
NoValueError = Class.new(RuntimeError)
|
||||
|
||||
attr_reader :retrieved_from_cache
|
||||
|
||||
class << self
|
||||
# Initialize new instance of class without fabrication
|
||||
#
|
||||
|
@ -81,7 +83,7 @@ module QA
|
|||
Support::FabricationTracker.start_fabrication
|
||||
result = yield.tap do
|
||||
fabrication_time = Time.now - start
|
||||
fabrication_http_method = if resource.api_fabrication_http_method == :get
|
||||
fabrication_http_method = if resource.api_fabrication_http_method == :get || resource.retrieved_from_cache
|
||||
if include?(Reusable)
|
||||
"Retrieved for reuse"
|
||||
else
|
||||
|
@ -92,24 +94,28 @@ module QA
|
|||
end
|
||||
|
||||
Support::FabricationTracker.save_fabrication(:"#{fabrication_method}_fabrication", fabrication_time)
|
||||
Tools::TestResourceDataProcessor.collect(
|
||||
resource: resource,
|
||||
info: resource.identifier,
|
||||
fabrication_method: fabrication_method,
|
||||
fabrication_time: fabrication_time
|
||||
)
|
||||
|
||||
unless resource.retrieved_from_cache
|
||||
Tools::TestResourceDataProcessor.collect(
|
||||
resource: resource,
|
||||
info: resource.identifier,
|
||||
fabrication_method: fabrication_method,
|
||||
fabrication_time: fabrication_time
|
||||
)
|
||||
end
|
||||
|
||||
Runtime::Logger.info do
|
||||
msg = ["==#{'=' * parents.size}>"]
|
||||
msg << "#{fabrication_http_method} a #{Rainbow(name).black.bg(:white)}"
|
||||
msg << resource.identifier
|
||||
msg << "as a dependency of #{parents.last}" if parents.any?
|
||||
msg << "via #{fabrication_method}"
|
||||
msg << "via #{resource.retrieved_from_cache ? 'cache' : fabrication_method}"
|
||||
msg << "in #{fabrication_time.round(2)} seconds"
|
||||
|
||||
msg.compact.join(' ')
|
||||
end
|
||||
end
|
||||
|
||||
Support::FabricationTracker.finish_fabrication
|
||||
|
||||
result
|
||||
|
|
|
@ -46,8 +46,15 @@ module QA
|
|||
rescue NoValueError
|
||||
user_id = user.respond_to?(:id) ? user.id : Resource::User.build(user).reload!.id
|
||||
|
||||
token = auto_paginated_response(request_url("/personal_access_tokens?user_id=#{user_id}", per_page: '100'))
|
||||
.find { |t| t[:name] == name }
|
||||
api_client = Runtime::API::Client.new(:gitlab,
|
||||
is_new_session: false,
|
||||
user: user,
|
||||
personal_access_token: self.token)
|
||||
request_url = Runtime::API::Request.new(api_client,
|
||||
"/personal_access_tokens?user_id=#{user_id}",
|
||||
per_page: '100').url
|
||||
|
||||
token = auto_paginated_response(request_url).find { |t| t[:name] == name }
|
||||
|
||||
raise ResourceNotFoundError unless token
|
||||
|
||||
|
@ -56,7 +63,7 @@ module QA
|
|||
end
|
||||
|
||||
def name
|
||||
@name ||= "api-personal-access-token-#{Faker::Alphanumeric.alphanumeric(number: 8)}"
|
||||
@name ||= "api-pat-#{user.username}-#{Faker::Alphanumeric.alphanumeric(number: 8)}"
|
||||
end
|
||||
|
||||
def api_post_body
|
||||
|
@ -75,6 +82,9 @@ module QA
|
|||
|
||||
def find_and_set_value
|
||||
@token ||= QA::Resource::PersonalAccessTokenCache.get_token_for_username(user.username)
|
||||
@retrieved_from_cache = true if @token
|
||||
|
||||
@token
|
||||
end
|
||||
|
||||
def cache_token
|
||||
|
|
|
@ -2,11 +2,7 @@
|
|||
|
||||
module QA
|
||||
RSpec.describe 'Manage' do
|
||||
describe 'Project owner permissions', :reliable, quarantine: {
|
||||
only: { pipeline: %i[staging staging-canary production canary] },
|
||||
type: :investigating,
|
||||
issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/373038'
|
||||
} do
|
||||
describe 'Project owner permissions', :reliable do
|
||||
let!(:owner) do
|
||||
Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1)
|
||||
end
|
||||
|
|
|
@ -404,6 +404,11 @@ RSpec.describe Projects::MergeRequests::DraftsController do
|
|||
end
|
||||
|
||||
context 'when feature flag is enabled' do
|
||||
before do
|
||||
allow(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
|
||||
.to receive(:track_submit_review_comment)
|
||||
end
|
||||
|
||||
it 'creates note' do
|
||||
post :publish, params: params.merge!(note: 'Hello world')
|
||||
|
||||
|
@ -415,6 +420,13 @@ RSpec.describe Projects::MergeRequests::DraftsController do
|
|||
|
||||
expect(merge_request.notes.reload.size).to be(1)
|
||||
end
|
||||
|
||||
it 'tracks merge request activity' do
|
||||
post :publish, params: params.merge!(note: 'Hello world')
|
||||
|
||||
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
|
||||
.to have_received(:track_submit_review_comment).with(user: user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -436,6 +448,11 @@ RSpec.describe Projects::MergeRequests::DraftsController do
|
|||
end
|
||||
|
||||
context 'when feature flag is enabled' do
|
||||
before do
|
||||
allow(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
|
||||
.to receive(:track_submit_review_approve)
|
||||
end
|
||||
|
||||
it 'approves merge request' do
|
||||
post :publish, params: params.merge!(approve: true)
|
||||
|
||||
|
@ -447,6 +464,13 @@ RSpec.describe Projects::MergeRequests::DraftsController do
|
|||
|
||||
expect(merge_request.approvals.reload.size).to be(0)
|
||||
end
|
||||
|
||||
it 'tracks merge request activity' do
|
||||
post :publish, params: params.merge!(approve: true)
|
||||
|
||||
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
|
||||
.to have_received(:track_submit_review_approve).with(user: user)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -50,7 +50,7 @@ RSpec.describe Projects::Settings::IntegrationsController do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when validations fail' do
|
||||
context 'when validations fail', :clean_gitlab_redis_rate_limiting do
|
||||
let(:integration_params) { { active: 'true', url: '' } }
|
||||
|
||||
it 'returns error messages in JSON response' do
|
||||
|
@ -62,7 +62,7 @@ RSpec.describe Projects::Settings::IntegrationsController do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when successful' do
|
||||
context 'when successful', :clean_gitlab_redis_rate_limiting do
|
||||
context 'with empty project' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
|
@ -200,8 +200,8 @@ RSpec.describe Projects::Settings::IntegrationsController do
|
|||
|
||||
2.times { post :test, params: project_params(service: integration_params) }
|
||||
|
||||
expect(response.body).to eq(_('This endpoint has been requested too many times. Try again later.'))
|
||||
expect(response).to have_gitlab_http_status(:too_many_requests)
|
||||
expect(response.body).to include(_('This endpoint has been requested too many times. Try again later.'))
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,6 +17,7 @@ RSpec.describe 'User uses header search field', :js do
|
|||
end
|
||||
|
||||
before do
|
||||
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).and_return(0)
|
||||
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1000)
|
||||
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit_unauthenticated).and_return(1000)
|
||||
sign_in(user)
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"syncToken": "1661533328840",
|
||||
"creationTime": "2022-08-26T10:02:08.840384",
|
||||
"prefixes": [{
|
||||
"ipv4Prefix": "34.80.0.0/15",
|
||||
"service": "Google Cloud",
|
||||
"scope": "asia-east1"
|
||||
}, {
|
||||
"ipv4Prefix": "34.137.0.0/16",
|
||||
"service": "Google Cloud",
|
||||
"scope": "asia-east1"
|
||||
}, {
|
||||
"ipv6Prefix": "2600:1900:4180::/44",
|
||||
"service": "Google Cloud",
|
||||
"scope": "us-west4"
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GitlabSchema.types['BranchProtection'] do
|
||||
subject { described_class }
|
||||
|
||||
let(:fields) { %i[allow_force_push] }
|
||||
|
||||
specify { is_expected.to require_graphql_authorizations(:read_protected_branch) }
|
||||
|
||||
specify { is_expected.to have_graphql_fields(fields) }
|
||||
end
|
|
@ -7,21 +7,16 @@ RSpec.describe GitlabSchema.types['BranchRule'] do
|
|||
|
||||
subject { described_class }
|
||||
|
||||
let(:fields) { %i[name created_at updated_at] }
|
||||
|
||||
specify { is_expected.to have_graphql_name('BranchRule') }
|
||||
let(:fields) do
|
||||
%i[
|
||||
name
|
||||
branch_protection
|
||||
created_at
|
||||
updated_at
|
||||
]
|
||||
end
|
||||
|
||||
specify { is_expected.to require_graphql_authorizations(:read_protected_branch) }
|
||||
|
||||
specify { is_expected.to have_graphql_description }
|
||||
|
||||
specify { is_expected.to have_graphql_fields(fields) }
|
||||
|
||||
describe 'graphql_fields' do
|
||||
subject do
|
||||
described_class.all_field_definitions
|
||||
end
|
||||
|
||||
specify { is_expected.to all(have_graphql_description) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -260,6 +260,22 @@ RSpec.describe Gitlab::GitalyClient::RefService do
|
|||
|
||||
client.tags(sort_by: 'name_asc')
|
||||
end
|
||||
|
||||
context 'with semantic version sorting' do
|
||||
it 'sends a correct find_all_tags message' do
|
||||
expected_sort_by = Gitaly::FindAllTagsRequest::SortBy.new(
|
||||
key: :VERSION_REFNAME,
|
||||
direction: :ASCENDING
|
||||
)
|
||||
|
||||
expect_any_instance_of(Gitaly::RefService::Stub)
|
||||
.to receive(:find_all_tags)
|
||||
.with(gitaly_request_with_params(sort_by: expected_sort_by), kind_of(Hash))
|
||||
.and_return([])
|
||||
|
||||
client.tags(sort_by: 'version_asc')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with pagination option' do
|
||||
|
|
|
@ -20,18 +20,43 @@ RSpec.describe Gitlab::Mailgun::WebhookProcessors::FailureLogger do
|
|||
context 'on permanent failure' do
|
||||
let(:processor) { described_class.new(base_payload.merge({ 'severity' => 'permanent' })) }
|
||||
|
||||
it 'logs the failure immediately' do
|
||||
expect(Gitlab::ErrorTracking::Logger).to receive(:error).with(
|
||||
event: 'email_delivery_failure',
|
||||
mailgun_event_id: base_payload['id'],
|
||||
recipient: base_payload['recipient'],
|
||||
failure_type: 'permanent',
|
||||
failure_reason: base_payload['reason'],
|
||||
failure_code: base_payload['delivery-status']['code'],
|
||||
failure_message: base_payload['delivery-status']['message']
|
||||
)
|
||||
before do
|
||||
allow(Gitlab::ApplicationRateLimiter).to receive(:rate_limits)
|
||||
.and_return(permanent_email_failure: { threshold: 1, interval: 1.minute })
|
||||
end
|
||||
|
||||
processor.execute
|
||||
context 'when threshold is not exceeded' do
|
||||
it 'increments counter but does not log the failure' do
|
||||
expect(Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(
|
||||
:permanent_email_failure, scope: 'recipient@gitlab.com'
|
||||
).and_call_original
|
||||
expect(Gitlab::ErrorTracking::Logger).not_to receive(:error)
|
||||
|
||||
processor.execute
|
||||
end
|
||||
end
|
||||
|
||||
context 'when threshold is exceeded' do
|
||||
before do
|
||||
processor.execute
|
||||
end
|
||||
|
||||
it 'increments counter and logs the failure' do
|
||||
expect(Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(
|
||||
:permanent_email_failure, scope: 'recipient@gitlab.com'
|
||||
).and_call_original
|
||||
expect(Gitlab::ErrorTracking::Logger).to receive(:error).with(
|
||||
event: 'email_delivery_failure',
|
||||
mailgun_event_id: base_payload['id'],
|
||||
recipient: base_payload['recipient'],
|
||||
failure_type: 'permanent',
|
||||
failure_reason: base_payload['reason'],
|
||||
failure_code: base_payload['delivery-status']['code'],
|
||||
failure_message: base_payload['delivery-status']['message']
|
||||
)
|
||||
|
||||
processor.execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -107,10 +107,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
|
|||
'quickactions',
|
||||
'pipeline_authoring',
|
||||
'epics_usage',
|
||||
'epic_boards_usage',
|
||||
'secure',
|
||||
'importer',
|
||||
'network_policies',
|
||||
'geo',
|
||||
'growth',
|
||||
'work_items',
|
||||
|
|
|
@ -130,18 +130,22 @@ RSpec.describe NamespaceSetting, type: :model do
|
|||
|
||||
describe '#show_diff_preview_in_email?' do
|
||||
context 'when not a subgroup' do
|
||||
it 'returns false' do
|
||||
settings = create(:namespace_settings, show_diff_preview_in_email: false)
|
||||
group = create(:group, namespace_settings: settings )
|
||||
context 'when :show_diff_preview_in_email is false' do
|
||||
it 'returns false' do
|
||||
settings = create(:namespace_settings, show_diff_preview_in_email: false)
|
||||
group = create(:group, namespace_settings: settings )
|
||||
|
||||
expect(group.show_diff_preview_in_email?).to be_falsey
|
||||
expect(group.show_diff_preview_in_email?).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
settings = create(:namespace_settings, show_diff_preview_in_email: true)
|
||||
group = create(:group, namespace_settings: settings )
|
||||
context 'when :show_diff_preview_in_email is true' do
|
||||
it 'returns true' do
|
||||
settings = create(:namespace_settings, show_diff_preview_in_email: true)
|
||||
group = create(:group, namespace_settings: settings )
|
||||
|
||||
expect(group.show_diff_preview_in_email?).to be_truthy
|
||||
expect(group.show_diff_preview_in_email?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not query the db when there is no parent group' do
|
||||
|
|
|
@ -39,6 +39,56 @@ RSpec.describe NotificationRecipient do
|
|||
expect(recipient.notifiable?).to eq true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when recipient email is blocked', :clean_gitlab_redis_rate_limiting do
|
||||
before do
|
||||
allow(Gitlab::ApplicationRateLimiter).to receive(:rate_limits)
|
||||
.and_return(
|
||||
temporary_email_failure: { threshold: 1, interval: 1.minute },
|
||||
permanent_email_failure: { threshold: 1, interval: 1.minute }
|
||||
)
|
||||
end
|
||||
|
||||
context 'with permanent failures' do
|
||||
before do
|
||||
2.times { Gitlab::ApplicationRateLimiter.throttled?(:permanent_email_failure, scope: user.email) }
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(recipient.notifiable?).to eq(false)
|
||||
end
|
||||
|
||||
context 'when block_emails_with_failures is disabled' do
|
||||
before do
|
||||
stub_feature_flags(block_emails_with_failures: false)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(recipient.notifiable?).to eq(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with temporary failures' do
|
||||
before do
|
||||
2.times { Gitlab::ApplicationRateLimiter.throttled?(:temporary_email_failure, scope: user.email) }
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(recipient.notifiable?).to eq(false)
|
||||
end
|
||||
|
||||
context 'when block_emails_with_failures is disabled' do
|
||||
before do
|
||||
stub_feature_flags(block_emails_with_failures: false)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(recipient.notifiable?).to eq(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#has_access?' do
|
||||
|
|
|
@ -161,6 +161,33 @@ RSpec.describe Repository do
|
|||
end
|
||||
end
|
||||
|
||||
context 'semantic versioning sort' do
|
||||
let(:version_two) { 'v2.0.0' }
|
||||
let(:version_ten) { 'v10.0.0' }
|
||||
|
||||
before do
|
||||
repository.add_tag(user, version_two, repository.commit.id)
|
||||
repository.add_tag(user, version_ten, repository.commit.id)
|
||||
end
|
||||
|
||||
after do
|
||||
repository.rm_tag(user, version_two)
|
||||
repository.rm_tag(user, version_ten)
|
||||
end
|
||||
|
||||
context 'desc' do
|
||||
subject { repository.tags_sorted_by('version_desc').map(&:name) & (tags_to_compare + [version_two, version_ten]) }
|
||||
|
||||
it { is_expected.to eq([version_ten, version_two, 'v1.1.0', 'v1.0.0']) }
|
||||
end
|
||||
|
||||
context 'asc' do
|
||||
subject { repository.tags_sorted_by('version_asc').map(&:name) & (tags_to_compare + [version_two, version_ten]) }
|
||||
|
||||
it { is_expected.to eq(['v1.0.0', 'v1.1.0', version_two, version_ten]) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'unknown option' do
|
||||
subject { repository.tags_sorted_by('unknown_desc').map(&:name) & tags_to_compare }
|
||||
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'getting branch protection for a branch rule' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:branch_rule) { create(:protected_branch) }
|
||||
let_it_be(:project) { branch_rule.project }
|
||||
|
||||
let(:branch_protection_data) do
|
||||
graphql_data_at('project', 'branchRules', 'nodes', 0, 'branchProtection')
|
||||
end
|
||||
|
||||
let(:variables) { { path: project.full_path } }
|
||||
|
||||
let(:fields) { all_graphql_fields_for('BranchProtection') }
|
||||
|
||||
let(:query) do
|
||||
<<~GQL
|
||||
query($path: ID!) {
|
||||
project(fullPath: $path) {
|
||||
branchRules(first: 1) {
|
||||
nodes {
|
||||
branchProtection {
|
||||
#{fields}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
GQL
|
||||
end
|
||||
|
||||
context 'when the user does not have read_protected_branch abilities' do
|
||||
before do
|
||||
project.add_guest(current_user)
|
||||
post_graphql(query, current_user: current_user, variables: variables)
|
||||
end
|
||||
|
||||
it_behaves_like 'a working graphql query'
|
||||
|
||||
it { expect(branch_protection_data).not_to be_present }
|
||||
end
|
||||
|
||||
context 'when the user does have read_protected_branch abilities' do
|
||||
before do
|
||||
project.add_maintainer(current_user)
|
||||
post_graphql(query, current_user: current_user, variables: variables)
|
||||
end
|
||||
|
||||
it_behaves_like 'a working graphql query'
|
||||
|
||||
it 'includes allow_force_push' do
|
||||
expect(branch_protection_data['allowForcePush']).to be_in([true, false])
|
||||
expect(branch_protection_data['allowForcePush']).to eq(branch_rule.allow_force_push)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -34,6 +34,7 @@ RSpec.describe API::GroupExport do
|
|||
before do
|
||||
allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy|
|
||||
allow(strategy).to receive(:increment).and_return(0)
|
||||
allow(strategy).to receive(:read).and_return(0)
|
||||
end
|
||||
|
||||
upload.export_file = fixture_file_upload('spec/fixtures/group_export.tar.gz', "`/tar.gz")
|
||||
|
|
|
@ -9,6 +9,7 @@ RSpec.describe API::Search do
|
|||
let_it_be(:repo_project) { create(:project, :public, :repository, group: group) }
|
||||
|
||||
before do
|
||||
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).and_return(0)
|
||||
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1000)
|
||||
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit_unauthenticated).and_return(1000)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe MergeRequests::Mergeability::Logger, :request_store do
|
||||
let_it_be(:merge_request) { create(:merge_request) }
|
||||
|
||||
subject(:logger) { described_class.new(merge_request: merge_request) }
|
||||
|
||||
let(:caller_id) { 'a' }
|
||||
|
||||
before do
|
||||
allow(Gitlab::ApplicationContext).to receive(:current_context_attribute).with(:caller_id).and_return(caller_id)
|
||||
end
|
||||
|
||||
def loggable_data(**extras)
|
||||
{
|
||||
'mergeability.expensive_operation.duration_s.values' => a_kind_of(Array),
|
||||
"mergeability_merge_request_id" => merge_request.id,
|
||||
"correlation_id" => a_kind_of(String),
|
||||
"mergeability_project_id" => merge_request.project.id
|
||||
}.merge(extras)
|
||||
end
|
||||
|
||||
describe '#instrument' do
|
||||
let(:operation_count) { 1 }
|
||||
|
||||
context 'when enabled' do
|
||||
it "returns the block's value" do
|
||||
expect(logger.instrument(mergeability_name: :expensive_operation) { 123 }).to eq(123)
|
||||
end
|
||||
|
||||
it 'records durations of instrumented operations' do
|
||||
expect_next_instance_of(Gitlab::AppJsonLogger) do |app_logger|
|
||||
expect(app_logger).to receive(:info).with(match(a_hash_including(loggable_data)))
|
||||
end
|
||||
|
||||
expect(logger.instrument(mergeability_name: :expensive_operation) { 123 }).to eq(123)
|
||||
|
||||
logger.commit
|
||||
end
|
||||
|
||||
context 'with multiple observations' do
|
||||
let(:operation_count) { 2 }
|
||||
|
||||
it 'records durations of instrumented operations' do
|
||||
expect_next_instance_of(Gitlab::AppJsonLogger) do |app_logger|
|
||||
expect(app_logger).to receive(:info).with(match(a_hash_including(loggable_data)))
|
||||
end
|
||||
|
||||
2.times do
|
||||
expect(logger.instrument(mergeability_name: :expensive_operation) { 123 }).to eq(123)
|
||||
end
|
||||
|
||||
logger.commit
|
||||
end
|
||||
end
|
||||
|
||||
context 'when its a query' do
|
||||
let(:extra_data) do
|
||||
{
|
||||
'mergeability.expensive_operation.db_count.values' => a_kind_of(Array),
|
||||
'mergeability.expensive_operation.db_main_count.values' => a_kind_of(Array),
|
||||
'mergeability.expensive_operation.db_main_duration_s.values' => a_kind_of(Array),
|
||||
'mergeability.expensive_operation.db_primary_count.values' => a_kind_of(Array),
|
||||
'mergeability.expensive_operation.db_primary_duration_s.values' => a_kind_of(Array)
|
||||
}
|
||||
end
|
||||
|
||||
context 'with a single query' do
|
||||
it 'includes SQL metrics' do
|
||||
expect_next_instance_of(Gitlab::AppJsonLogger) do |app_logger|
|
||||
expect(app_logger).to receive(:info).with(match(a_hash_including(loggable_data(**extra_data))))
|
||||
end
|
||||
|
||||
expect(logger.instrument(mergeability_name: :expensive_operation) { MergeRequest.count }).to eq(1)
|
||||
|
||||
logger.commit
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiple queries' do
|
||||
it 'includes SQL metrics' do
|
||||
expect_next_instance_of(Gitlab::AppJsonLogger) do |app_logger|
|
||||
expect(app_logger).to receive(:info).with(match(a_hash_including(loggable_data(**extra_data))))
|
||||
end
|
||||
|
||||
expect(logger.instrument(mergeability_name: :expensive_operation) { Project.count + MergeRequest.count })
|
||||
.to eq(2)
|
||||
|
||||
logger.commit
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when disabled' do
|
||||
before do
|
||||
stub_feature_flags(mergeability_checks_logger: false)
|
||||
end
|
||||
|
||||
it "returns the block's value" do
|
||||
expect(logger.instrument(mergeability_name: :expensive_operation) { 123 }).to eq(123)
|
||||
end
|
||||
|
||||
it 'does not call the logger' do
|
||||
expect(Gitlab::AppJsonLogger).not_to receive(:new)
|
||||
|
||||
expect(logger.instrument(mergeability_name: :expensive_operation) { Project.count + MergeRequest.count })
|
||||
.to eq(2)
|
||||
|
||||
logger.commit
|
||||
end
|
||||
end
|
||||
|
||||
it 'raises an error when block is not provided' do
|
||||
expect { logger.instrument(mergeability_name: :expensive_operation) }
|
||||
.to raise_error(ArgumentError, 'block not given')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -69,6 +69,11 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
|
|||
expect(service).to receive(:read).with(merge_check: merge_check).and_return(success_result)
|
||||
end
|
||||
|
||||
expect_next_instance_of(MergeRequests::Mergeability::Logger, merge_request: merge_request) do |logger|
|
||||
expect(logger).to receive(:instrument).with(mergeability_name: 'check_ci_status_service').and_call_original
|
||||
expect(logger).to receive(:commit)
|
||||
end
|
||||
|
||||
expect(execute.success?).to eq(true)
|
||||
end
|
||||
end
|
||||
|
@ -80,6 +85,11 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
|
|||
expect(service).to receive(:write).with(merge_check: merge_check, result_hash: success_result.to_hash).and_return(true)
|
||||
end
|
||||
|
||||
expect_next_instance_of(MergeRequests::Mergeability::Logger, merge_request: merge_request) do |logger|
|
||||
expect(logger).to receive(:instrument).with(mergeability_name: 'check_ci_status_service').and_call_original
|
||||
expect(logger).to receive(:commit)
|
||||
end
|
||||
|
||||
expect(execute.success?).to eq(true)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ObjectStorage::CDN::GoogleCDN, :use_clean_rails_memory_store_caching do
|
||||
include StubRequests
|
||||
|
||||
let(:key) { SecureRandom.hex }
|
||||
let(:key_name) { 'test-key' }
|
||||
let(:options) { { url: 'https://cdn.gitlab.example.com', key_name: key_name, key: Base64.urlsafe_encode64(key) } }
|
||||
let(:google_cloud_ips) { File.read(Rails.root.join('spec/fixtures/cdn/google_cloud.json')) }
|
||||
let(:headers) { { 'Content-Type' => 'application/json' } }
|
||||
let(:public_ip) { '18.245.0.42' }
|
||||
|
||||
subject { described_class.new(options) }
|
||||
|
||||
before do
|
||||
WebMock.stub_request(:get, described_class::GOOGLE_IP_RANGES_URL)
|
||||
.to_return(status: 200, body: google_cloud_ips, headers: headers)
|
||||
end
|
||||
|
||||
describe '#use_cdn?' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:ip_address, :expected) do
|
||||
'34.80.0.1' | false
|
||||
'18.245.0.42' | true
|
||||
'2500:1900:4180:0000:0000:0000:0000:0000' | true
|
||||
'2600:1900:4180:0000:0000:0000:0000:0000' | false
|
||||
'10.10.1.5' | false
|
||||
'fc00:0000:0000:0000:0000:0000:0000:0000' | false
|
||||
end
|
||||
|
||||
with_them do
|
||||
it { expect(subject.use_cdn?(ip_address)).to eq(expected) }
|
||||
end
|
||||
|
||||
it 'caches the value' do
|
||||
expect(subject.use_cdn?(public_ip)).to be true
|
||||
expect(Rails.cache.fetch(described_class::GOOGLE_CDN_LIST_KEY)).to be_present
|
||||
expect(Gitlab::ProcessMemoryCache.cache_backend.fetch(described_class::GOOGLE_CDN_LIST_KEY)).to be_present
|
||||
end
|
||||
|
||||
context 'when the key name is missing' do
|
||||
let(:options) { { url: 'https://cdn.gitlab.example.com', key: Base64.urlsafe_encode64(SecureRandom.hex) } }
|
||||
|
||||
it 'returns false' do
|
||||
expect(subject.use_cdn?(public_ip)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the key is missing' do
|
||||
let(:options) { { url: 'https://invalid.example.com' } }
|
||||
|
||||
it 'returns false' do
|
||||
expect(subject.use_cdn?(public_ip)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the key is invalid' do
|
||||
let(:options) { { key_name: key_name, key: '\0x1' } }
|
||||
|
||||
it 'returns false' do
|
||||
expect(Gitlab::ErrorTracking).to receive(:log_exception).and_call_original
|
||||
expect(subject.use_cdn?(public_ip)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the URL is missing' do
|
||||
let(:options) { { key: Base64.urlsafe_encode64(SecureRandom.hex) } }
|
||||
|
||||
it 'returns false' do
|
||||
expect(subject.use_cdn?(public_ip)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'IP range retrieval failure' do
|
||||
it 'does not cache the result and logs an error' do
|
||||
expect(Gitlab::ErrorTracking).to receive(:log_exception).and_call_original
|
||||
expect(subject.use_cdn?(public_ip)).to be false
|
||||
expect(Rails.cache.fetch(described_class::GOOGLE_CDN_LIST_KEY)).to be_nil
|
||||
expect(Gitlab::ProcessMemoryCache.cache_backend.fetch(described_class::GOOGLE_CDN_LIST_KEY)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the URL returns a 404' do
|
||||
before do
|
||||
WebMock.stub_request(:get, described_class::GOOGLE_IP_RANGES_URL).to_return(status: 404)
|
||||
end
|
||||
|
||||
it_behaves_like 'IP range retrieval failure'
|
||||
end
|
||||
|
||||
context 'when the URL returns too large of a payload' do
|
||||
before do
|
||||
stub_const("#{described_class}::RESPONSE_BODY_LIMIT", 300)
|
||||
end
|
||||
|
||||
it_behaves_like 'IP range retrieval failure'
|
||||
end
|
||||
|
||||
context 'when the URL returns HTML' do
|
||||
let(:headers) { { 'Content-Type' => 'text/html' } }
|
||||
|
||||
it_behaves_like 'IP range retrieval failure'
|
||||
end
|
||||
|
||||
context 'when the URL returns empty results' do
|
||||
let(:google_cloud_ips) { '{}' }
|
||||
|
||||
it_behaves_like 'IP range retrieval failure'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#signed_url' do
|
||||
let(:path) { '/path/to/file.txt' }
|
||||
|
||||
it 'returns a valid signed URL' do
|
||||
url = subject.signed_url(path)
|
||||
|
||||
expect(url).to start_with("#{options[:url]}#{path}")
|
||||
|
||||
uri = Addressable::URI.parse(url)
|
||||
parsed_query = Rack::Utils.parse_nested_query(uri.query)
|
||||
signature = parsed_query.delete('Signature')
|
||||
|
||||
signed_url = "#{options[:url]}#{path}?Expires=#{parsed_query['Expires']}&KeyName=#{key_name}"
|
||||
computed_signature = OpenSSL::HMAC.digest('SHA1', key, signed_url)
|
||||
|
||||
aggregate_failures do
|
||||
expect(parsed_query['Expires'].to_i).to be > 0
|
||||
expect(parsed_query['KeyName']).to eq(key_name)
|
||||
expect(signature).to eq(Base64.urlsafe_encode64(computed_signature))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,85 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ObjectStorage::CDN do
|
||||
let(:cdn_options) do
|
||||
{
|
||||
'object_store' => {
|
||||
'cdn' => {
|
||||
'provider' => 'google',
|
||||
'url' => 'https://gitlab.example.com',
|
||||
'key_name' => 'test-key',
|
||||
'key' => '12345'
|
||||
}
|
||||
}
|
||||
}.freeze
|
||||
end
|
||||
|
||||
let(:uploader_class) do
|
||||
Class.new(GitlabUploader) do
|
||||
include ObjectStorage::Concern
|
||||
include ObjectStorage::CDN::Concern
|
||||
|
||||
private
|
||||
|
||||
# user/:id
|
||||
def dynamic_segment
|
||||
File.join(model.class.underscore, model.id.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:object) { build_stubbed(:user) }
|
||||
|
||||
subject { uploader_class.new(object, :file) }
|
||||
|
||||
context 'with CDN config' do
|
||||
before do
|
||||
uploader_class.options = Settingslogic.new(Gitlab.config.uploads.deep_merge(cdn_options))
|
||||
end
|
||||
|
||||
describe '#use_cdn?' do
|
||||
it 'returns true' do
|
||||
expect_next_instance_of(ObjectStorage::CDN::GoogleCDN) do |cdn|
|
||||
expect(cdn).to receive(:use_cdn?).and_return(true)
|
||||
end
|
||||
|
||||
expect(subject.use_cdn?('18.245.0.1')).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#cdn_signed_url' do
|
||||
it 'returns a URL' do
|
||||
expect_next_instance_of(ObjectStorage::CDN::GoogleCDN) do |cdn|
|
||||
expect(cdn).to receive(:signed_url).and_return("https://cdn.example.com/path")
|
||||
end
|
||||
|
||||
expect(subject.cdn_signed_url).to eq("https://cdn.example.com/path")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without CDN config' do
|
||||
before do
|
||||
uploader_class.options = Gitlab.config.uploads
|
||||
end
|
||||
|
||||
describe '#use_cdn?' do
|
||||
it 'returns false' do
|
||||
expect(subject.use_cdn?('18.245.0.1')).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an unknown CDN provider' do
|
||||
before do
|
||||
cdn_options['object_store']['cdn']['provider'] = 'amazon'
|
||||
uploader_class.options = Settingslogic.new(Gitlab.config.uploads.deep_merge(cdn_options))
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject.use_cdn?('18.245.0.1') }.to raise_error("Unknown CDN provider: amazon")
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue