Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot
2025-08-01 21:22:49 +00:00
parent 391a8387a7
commit 968c4dc256
31 changed files with 954 additions and 36 deletions

View File

@ -3825,6 +3825,7 @@ Gitlab/BoundedContexts:
- 'lib/feature/actor_wrapper.rb'
- 'lib/feature/definition.rb'
- 'lib/feature/gitaly.rb'
- 'lib/feature/kas.rb'
- 'lib/feature/logger.rb'
- 'lib/feature/shared.rb'
- 'lib/file_size_validator.rb'

View File

@ -106,11 +106,6 @@ InternalAffairs/CopDescriptionWithExample:
- 'rubocop/cop/migration/versioned_migration_class.rb'
- 'rubocop/cop/migration/with_lock_retries_disallowed_method.rb'
- 'rubocop/cop/migration/with_lock_retries_with_change.rb'
- 'rubocop/cop/performance/active_record_subtransaction_methods.rb'
- 'rubocop/cop/performance/active_record_subtransactions.rb'
- 'rubocop/cop/performance/ar_count_each.rb'
- 'rubocop/cop/performance/ar_exists_and_present_blank.rb'
- 'rubocop/cop/performance/readlines_each.rb'
- 'rubocop/cop/project_path_helper.rb'
- 'rubocop/cop/put_group_routes_under_scope.rb'
- 'rubocop/cop/put_project_routes_under_scope.rb'

View File

@ -226,7 +226,7 @@ gem 'google-apis-container_v1', '~> 0.100.0', feature_category: :shared
gem 'google-apis-container_v1beta1', '~> 0.43.0', feature_category: :shared
gem 'google-apis-cloudbilling_v1', '~> 0.22.0', feature_category: :shared
gem 'google-apis-cloudresourcemanager_v1', '~> 0.31.0', feature_category: :shared
gem 'google-apis-iam_v1', '~> 0.72.0', feature_category: :shared
gem 'google-apis-iam_v1', '~> 0.73.0', feature_category: :shared
gem 'google-apis-serviceusage_v1', '~> 0.28.0', feature_category: :shared
gem 'google-apis-sqladmin_v1beta4', '~> 0.41.0', feature_category: :shared
gem 'google-apis-androidpublisher_v3', '~> 0.84.0', feature_category: :shared

View File

@ -254,7 +254,7 @@
{"name":"google-apis-container_v1beta1","version":"0.43.0","platform":"ruby","checksum":"68c48fcf88db926ceab16f56890c85890269e6366b272fcde958a9b5550313d0"},
{"name":"google-apis-core","version":"0.18.0","platform":"ruby","checksum":"96b057816feeeab448139ed5b5c78eab7fc2a9d8958f0fbc8217dedffad054ee"},
{"name":"google-apis-dns_v1","version":"0.36.0","platform":"ruby","checksum":"5dd273d78ab37d03d1bc07837186f79ad0399e9f2b8b1ec2629ed682ea347d47"},
{"name":"google-apis-iam_v1","version":"0.72.0","platform":"ruby","checksum":"0358e4187bbf94676a83f3b72b01045e9d0ca0cdad7cf4f06d297044c8feb4d4"},
{"name":"google-apis-iam_v1","version":"0.73.0","platform":"ruby","checksum":"6f181165f161dd4d53e98c412d345d262114b2e26ef790d57a754f1fcf436a49"},
{"name":"google-apis-iamcredentials_v1","version":"0.15.0","platform":"ruby","checksum":"e9a256a6d80fbfc77d44bd7e65bc94b9e1e9863a00e6d413edc0102d6cb5551b"},
{"name":"google-apis-monitoring_v3","version":"0.54.0","platform":"ruby","checksum":"677fe1dce5b4cc937813303b020962fffb86f50a1f61f6422516937b5ad46128"},
{"name":"google-apis-pubsub_v1","version":"0.45.0","platform":"ruby","checksum":"1dfe4614c781250a0d4491be43e134936d5c08adc75a843e27d4bb66ba3cb205"},

View File

@ -851,7 +851,7 @@ GEM
retriable (>= 2.0, < 4.a)
google-apis-dns_v1 (0.36.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-iam_v1 (0.72.0)
google-apis-iam_v1 (0.73.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-iamcredentials_v1 (0.15.0)
google-apis-core (>= 0.9.0, < 2.a)
@ -2192,7 +2192,7 @@ DEPENDENCIES
google-apis-container_v1 (~> 0.100.0)
google-apis-container_v1beta1 (~> 0.43.0)
google-apis-core (~> 0.18.0, >= 0.18.0)
google-apis-iam_v1 (~> 0.72.0)
google-apis-iam_v1 (~> 0.73.0)
google-apis-serviceusage_v1 (~> 0.28.0)
google-apis-sqladmin_v1beta4 (~> 0.41.0)
google-apis-storage_v1 (~> 0.29)

View File

@ -254,7 +254,7 @@
{"name":"google-apis-container_v1beta1","version":"0.43.0","platform":"ruby","checksum":"68c48fcf88db926ceab16f56890c85890269e6366b272fcde958a9b5550313d0"},
{"name":"google-apis-core","version":"0.18.0","platform":"ruby","checksum":"96b057816feeeab448139ed5b5c78eab7fc2a9d8958f0fbc8217dedffad054ee"},
{"name":"google-apis-dns_v1","version":"0.36.0","platform":"ruby","checksum":"5dd273d78ab37d03d1bc07837186f79ad0399e9f2b8b1ec2629ed682ea347d47"},
{"name":"google-apis-iam_v1","version":"0.72.0","platform":"ruby","checksum":"0358e4187bbf94676a83f3b72b01045e9d0ca0cdad7cf4f06d297044c8feb4d4"},
{"name":"google-apis-iam_v1","version":"0.73.0","platform":"ruby","checksum":"6f181165f161dd4d53e98c412d345d262114b2e26ef790d57a754f1fcf436a49"},
{"name":"google-apis-iamcredentials_v1","version":"0.15.0","platform":"ruby","checksum":"e9a256a6d80fbfc77d44bd7e65bc94b9e1e9863a00e6d413edc0102d6cb5551b"},
{"name":"google-apis-monitoring_v3","version":"0.54.0","platform":"ruby","checksum":"677fe1dce5b4cc937813303b020962fffb86f50a1f61f6422516937b5ad46128"},
{"name":"google-apis-pubsub_v1","version":"0.45.0","platform":"ruby","checksum":"1dfe4614c781250a0d4491be43e134936d5c08adc75a843e27d4bb66ba3cb205"},

View File

@ -845,7 +845,7 @@ GEM
retriable (>= 2.0, < 4.a)
google-apis-dns_v1 (0.36.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-iam_v1 (0.72.0)
google-apis-iam_v1 (0.73.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-iamcredentials_v1 (0.15.0)
google-apis-core (>= 0.9.0, < 2.a)
@ -2187,7 +2187,7 @@ DEPENDENCIES
google-apis-container_v1 (~> 0.100.0)
google-apis-container_v1beta1 (~> 0.43.0)
google-apis-core (~> 0.18.0, >= 0.18.0)
google-apis-iam_v1 (~> 0.72.0)
google-apis-iam_v1 (~> 0.73.0)
google-apis-serviceusage_v1 (~> 0.28.0)
google-apis-sqladmin_v1beta4 (~> 0.41.0)
google-apis-storage_v1 (~> 0.29)

View File

@ -257,7 +257,13 @@ module Ci
#
# TODO consider using callbacks and state machine to remove old data
#
unsafe_set_data!(current_data)
saved = unsafe_set_data!(current_data)
# We've seen save! fail silently so don't delete the redis data to allow
# a self-heal to happen automatically the next run, raise to re-try
if Feature.enabled?(:self_heal_build_trace_chunk_flushing, build.project) && !saved
raise FailedToPersistDataError, 'Check PG logs'
end
old_store_class.delete_data(self)
end

View File

@ -4,6 +4,10 @@ module WorkItems
class DatesSource < ApplicationRecord
include FromUnion
# ElasticSearch is limited to use dates within this range
MAX_DATE_LIMIT = Date.new(9999, 12, 31).freeze
MIN_DATE_LIMIT = Date.new(1000, 1, 1).freeze
self.table_name = 'work_item_dates_sources'
# namespace is required as the sharding key
@ -22,8 +26,29 @@ module WorkItems
scope :work_items_in, ->(work_items) { where(work_item: work_items) }
with_options(comparison: {
allow_nil: true,
less_than_or_equal_to: MAX_DATE_LIMIT,
greater_than_or_equal_to: MIN_DATE_LIMIT
}, if: :validate_dates?) do
validates :start_date
validates :start_date_fixed
validates :due_date
validates :due_date_fixed
end
private
# Validate for new records or when any date field has changed
def validate_dates?
new_record? || any_dates_changed?
end
def any_dates_changed?
(changed_attributes.keys & %w[start_date start_date_fixed due_date due_date_fixed])
.present?
end
def set_namespace
return if work_item.blank?
return if work_item.namespace == namespace

View File

@ -0,0 +1,10 @@
---
name: self_heal_build_trace_chunk_flushing
description:
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/555415
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/199328
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/555523
milestone: '18.3'
group: group::pipeline execution
type: gitlab_com_derisk
default_enabled: false

View File

@ -22,6 +22,12 @@ sections_with_no_tw_review = {
# One exception to the exceptions above: Technical Writing docs should get a TW review.
TW_DOCS_PATH = 'doc/development/documentation'
# Some docs require a longer pipeline, which cannot be avoided.
sections_with_tier3_review = {
'doc/_index.md' => [],
'doc/api/settings.md' => []
}
docs_paths_to_review.reject! do |doc|
section_with_no_tw_review = sections_with_no_tw_review.keys.find { |skip_path| doc.start_with?(skip_path) && !doc.start_with?(TW_DOCS_PATH) }
next unless section_with_no_tw_review
@ -31,6 +37,15 @@ docs_paths_to_review.reject! do |doc|
true
end
docs_paths_to_review.reject! do |doc|
section = sections_with_tier3_review.keys.find { |skip_path| doc.start_with?(skip_path) && !doc.start_with?(TW_DOCS_PATH) }
next unless section
sections_with_tier3_review[section] << doc
true
end
SOLUTIONS_LABELS = %w[Solutions].freeze
DEVELOPMENT_LABELS = ['development guidelines'].freeze
@ -50,6 +65,10 @@ LOCALIZATION_MESSAGE = <<~MSG
This MR contains files in the /doc-locale directory. These files are translations maintained through a separate process and should not be edited directly. If you are not part of the Localization team, please remove the changes to these files from your MR.
MSG
DOCS_LONG_PIPELINE_MESSAGE = <<~MSG
This merge request contains documentation files that require a tier-3 code pipeline before merge. After you complete all needed documentation reviews with short docs pipelines, see the [instructions for running a long pipeline](https://docs.gitlab.com/development/documentation/workflow/#pipelines-and-branch-naming) to this merge request.
MSG
# For regular pages, prompt for a TW review
DOCS_UPDATE_SHORT_MESSAGE = <<~MSG
This merge request adds or changes documentation files and requires Technical Writing review. The review should happen before merge, but can be post-merge if the merge request is time sensitive.
@ -98,6 +117,8 @@ end
message(LOCALIZATION_MESSAGE) if sections_with_no_tw_review["doc-locale"].any?
message(DOCS_LONG_PIPELINE_MESSAGE) if sections_with_tier3_review.values.flatten.any?
unless docs_paths_to_review.empty?
message(DOCS_UPDATE_SHORT_MESSAGE)
markdown(DOCS_UPDATE_LONG_MESSAGE)

View File

@ -1043,6 +1043,23 @@ This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#pagination-arguments):
`before: String`, `after: String`, `first: Int`, and `last: Int`.
### `Query.mavenVirtualRegistry`
{{< details >}}
**Introduced** in GitLab 18.3.
**Status**: Experiment.
{{< /details >}}
Find a Maven virtual registry. Returns null if the `maven_virtual_registry` feature flag is disabled.
Returns [`MavenVirtualRegistry`](#mavenvirtualregistry).
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="querymavenvirtualregistryid"></a>`id` | [`VirtualRegistriesPackagesMavenRegistryID!`](#virtualregistriespackagesmavenregistryid) | Global ID of the Maven virtual registry. |
### `Query.memberRole`
{{< details >}}

View File

@ -237,6 +237,8 @@ module API
end
resource :job do
helpers ::API::Helpers::KasHelpers
before do
# Use primary for both main and ci database as authenticating in the scope of runners will load
# Ci::Build model and other standard authn related models like License, Project and User.
@ -294,6 +296,8 @@ module API
pipeline.project
).execute
set_feature_flag_header(user: current_user, project: project)
# See https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/kubernetes_ci_access.md#apiv4joballowed_agents-api
{
allowed_agents: Entities::Clusters::Agents::Authorizations::CiAccess.represent(agent_authorizations),

View File

@ -3,6 +3,8 @@
module API
module Helpers
module KasHelpers
FEATURE_FLAG_HEADER_NAME = 'Gitlab-Feature-Flag'
def authenticate_gitlab_kas_request!
render_api_error!('KAS JWT authentication invalid', 401) unless Gitlab::Kas.verify_api_request(headers)
end
@ -14,6 +16,26 @@ module API
def gitaly_repository(project)
project.repository.gitaly_repository.to_h
end
def set_feature_flag_header(user: nil, project: nil, group: nil)
# set feature flag headers
feature_flags = ::Feature::Kas.server_feature_flags_for_http_response(
user: ::Feature::Kas.user_actor(user),
project: ::Feature::Kas.project_actor(project),
group: ::Feature::Kas.group_actor(group)
)
header FEATURE_FLAG_HEADER_NAME, feature_flags.map { |k, v| "#{k}=#{v}" }.join(', ')
end
def set_feature_flag_header_for_agent(user: nil, agent: nil)
# set feature flag headers
feature_flags = ::Feature::Kas.server_feature_flags_for_http_response(
user: ::Feature::Kas.user_actor(user),
project: ::Feature::Kas.project_actor(agent),
group: ::Feature::Kas.group_actor(agent)
)
header FEATURE_FLAG_HEADER_NAME, feature_flags.map { |k, v| "#{k}=#{v}" }.join(', ')
end
end
end
end

View File

@ -119,6 +119,8 @@ module API
service_response = ::Clusters::Agents::AuthorizeProxyUserService.new(user, agent).execute
render_api_error!(service_response[:message], service_response[:reason]) unless service_response.success?
set_feature_flag_header_for_agent(user: user, agent: agent)
service_response.payload
end
end

87
lib/feature/kas.rb Normal file
View File

@ -0,0 +1,87 @@
# frozen_string_literal: true
module Feature
class Kas
PREFIX = "kas_"
class << self
def enabled_for_any?(feature_flag, *actors)
return false unless Feature::FlipperFeature.table_exists?
actors = actors.compact
# rubocop:disable Gitlab/FeatureFlagKeyDynamic -- this is a helper function and we need a variable argument here
# rubocop:disable Gitlab/FeatureFlagWithoutActor -- we explicitly don't want an actor here
return Feature.enabled?(feature_flag, type: :undefined, default_enabled_if_undefined: false) if actors.empty?
# rubocop:enable Gitlab/FeatureFlagWithoutActor
actors.any? do |actor|
Feature.enabled?(feature_flag, actor, type: :undefined, default_enabled_if_undefined: false)
end
# rubocop:enable Gitlab/FeatureFlagKeyDynamic
rescue ActiveRecord::NoDatabaseError, PG::ConnectionBad
false
end
def server_feature_flags_for_grpc_request(user: nil, project: nil, group: nil)
server_feature_flags(
->(f) { "kas-feature-#{f.delete_prefix(PREFIX).tr('_', '-')}" },
user: user,
project: project,
group: group
)
end
def server_feature_flags_for_http_response(user: nil, project: nil, group: nil)
server_feature_flags(
->(f) { f.delete_prefix(PREFIX) },
user: user,
project: project,
group: group
)
end
def user_actor(user = nil)
return ::User.actor_from_id(user.id) if user.is_a?(::User)
user_id = Gitlab::ApplicationContext.current_context_attribute(:user_id)
::User.actor_from_id(user_id) if user_id
end
def project_actor(container)
return unless container
return ::Project.actor_from_id(container.project.id) if container.is_a?(::Clusters::Agent)
::Project.actor_from_id(container.id) if container.is_a?(::Project)
end
def group_actor(container)
return unless container
return ::Group.actor_from_id(container.id) if container.is_a?(::Group)
return ::Group.actor_from_id(container.project.namespace_id) if container.is_a?(::Clusters::Agent)
::Group.actor_from_id(container.namespace_id) if container.is_a?(::Project)
end
private
def server_feature_flags(key_transform, user: nil, project: nil, group: nil)
# We need to check that both the DB connection and table exists
return {} unless FlipperFeature.database.cached_table_exists?
# The order of actors here is significant.
# Percentage-based actor selection may not work as expected if this order changes.
actors = [user, project, group].compact
Feature.persisted_names
.select { |f| f.start_with?(PREFIX) }
.to_h do |f|
key = key_transform ? key_transform.call(f) : f
[key, enabled_for_any?(f, *actors).to_s]
end
end
end
end
end

View File

@ -51,7 +51,10 @@ module Gitlab
)
stub_for(:configuration_project)
.list_agent_config_files(request, metadata: metadata)
.list_agent_config_files(request, metadata: metadata(
project: ::Feature::Kas.project_actor(project),
group: ::Feature::Kas.group_actor(project)
))
.config_files
.to_a
end
@ -67,7 +70,10 @@ module Gitlab
)
stub_for(:notifications)
.git_push_event(request, metadata: metadata)
.git_push_event(request, metadata: metadata(
project: ::Feature::Kas.project_actor(project),
group: ::Feature::Kas.group_actor(project)
))
end
def send_autoflow_event(project:, type:, id:, data:)
@ -99,7 +105,10 @@ module Gitlab
)
stub_for(:autoflow)
.cloud_event(request, metadata: metadata)
.cloud_event(request, metadata: metadata(
project: ::Feature::Kas.project_actor(project),
group: ::Feature::Kas.group_actor(project)
))
end
def get_environment_template(agent:, template_name:)
@ -114,7 +123,10 @@ module Gitlab
)
stub_for(:managed_resources)
.get_environment_template(request, metadata: metadata)
.get_environment_template(request, metadata: metadata(
project: ::Feature::Kas.project_actor(project),
group: ::Feature::Kas.group_actor(project)
))
.template
end
@ -132,7 +144,10 @@ module Gitlab
data: template.data),
info: templating_info(environment:, build:))
stub_for(:managed_resources)
.render_environment_template(request, metadata: metadata)
.render_environment_template(request, metadata: metadata(
project: ::Feature::Kas.project_actor(environment.project),
group: ::Feature::Kas.group_actor(environment.project)
))
.template
end
@ -143,7 +158,10 @@ module Gitlab
data: template.data),
info: templating_info(environment:, build:))
stub_for(:managed_resources)
.ensure_environment(request, metadata: metadata)
.ensure_environment(request, metadata: metadata(
project: ::Feature::Kas.project_actor(environment.project),
group: ::Feature::Kas.group_actor(environment.project)
))
end
def delete_environment(managed_resource:)
@ -154,7 +172,10 @@ module Gitlab
objects: managed_resource.tracked_objects
)
stub_for(:managed_resources).delete_environment(request, metadata: metadata)
stub_for(:managed_resources).delete_environment(request, metadata: metadata(
project: ::Feature::Kas.project_actor(managed_resource.project),
group: ::Feature::Kas.group_actor(managed_resource.project)
))
end
private
@ -190,8 +211,11 @@ module Gitlab
end
end
def metadata
{ 'authorization' => "bearer #{token}" }
def metadata(**feature_flag_actors)
{
'authorization' => "bearer #{token}",
**::Feature::Kas.server_feature_flags_for_grpc_request(**feature_flag_actors)
}
end
def token

View File

@ -63,6 +63,8 @@ module RuboCop
if flag_arg_is_str_or_sym?(flag_arg)
if caller_is_feature_gitaly?(node)
save_used_feature_flag("gitaly_#{flag_value}")
elsif caller_is_feature_kas?(node)
save_used_feature_flag("kas_#{flag_value}")
else
save_used_feature_flag(flag_value)
end
@ -158,8 +160,12 @@ module RuboCop
class_caller(node) == "Feature::Gitaly"
end
def caller_is_feature_kas?(node)
class_caller(node) == "Feature::Kas"
end
def feature_method?(node)
FEATURE_METHODS.include?(method_name(node)) && (caller_is_feature?(node) || caller_is_feature_gitaly?(node))
FEATURE_METHODS.include?(method_name(node)) && (caller_is_feature?(node) || caller_is_feature_gitaly?(node) || caller_is_feature_kas?(node))
end
def worker_method?(node)

View File

@ -5,6 +5,29 @@ module RuboCop
module Performance
# Cop that disallows certain methods that rely on subtransactions in their implementation.
# Companion to Performance/ActiveRecordSubtransactions, which bans direct usage of subtransactions.
# @example
#
# # bad
# User.create_or_find_by(name: 'Alice')
#
# # good
# User.find_or_create_by(name: 'Alice')
#
# # Disallowed method `safe_find_or_create_by` or `safe_find_or_create_by!`
# # bad
# User.safe_find_or_create_by!(email: 'alice@example.com')
#
# # good
# User.find_or_create_by!(email: 'alice@example.com')
#
# # Disallowed method `with_fast_read_statement_timeout`
# # bad
# record.with_fast_read_statement_timeout do
# record.some_heavy_read_operation
# end
#
# # good
# record.some_heavy_read_operation
class ActiveRecordSubtransactionMethods < RuboCop::Cop::Base
MSG = 'Methods that rely on subtransactions should not be used. ' \
'For more information see: https://gitlab.com/gitlab-org/gitlab/-/issues/338346'

View File

@ -3,6 +3,17 @@
module RuboCop
module Cop
module Performance
# Cop that bans direct usage of subtransactions in active record.
# @example
# # bad
# ActiveRecord::Base.transaction(requires_new: true) do
# user.update!(active: true)
# end
#
# # good
# ActiveRecord::Base.transaction do
# user.update!(active: true)
# end
class ActiveRecordSubtransactions < RuboCop::Cop::Base
MSG = 'Subtransactions should not be used. ' \
'For more information see: https://gitlab.com/gitlab-org/gitlab/-/issues/338346'

View File

@ -3,6 +3,15 @@
module RuboCop
module Cop
module Performance
# Cop that encourages efficient use of counts on collection by substituting more than one queries with one query.
# @example
# # bad
# users.count
# users.each { |u| work(u) }
#
# # good
# users.load.size
# users.each { |u| work(u) }
class ARCountEach < RuboCop::Cop::Base
def message(ivar)
"If #{ivar} is AR relation, avoid `#{ivar}.count ...; #{ivar}.each... `, this will trigger two queries. " \

View File

@ -3,6 +3,14 @@
module RuboCop
module Cop
module Performance
# Cop that detects inefficient patterns using ActiveRecord's `exists?`, `present?`,
# or `blank?` in a suboptimal way.
# @example
# # bad
# users.present?
#
# # good
# users.any?
class ARExistsAndPresentBlank < RuboCop::Cop::Base
def message_present(ivar)
"Avoid `#{ivar}.present?`, because it will generate database query 'Select TABLE.*' which is expensive. "\

View File

@ -3,6 +3,17 @@
module RuboCop
module Cop
module Performance
# Cop that encourages streaming file reading instead of reading the entire file into memory.
# @example
# # bad
# File.readlines('some_file.txt').each do |line|
# process(line)
# end
#
# # good
# File.each_line('some_file.txt') do |line|
# process(line)
# end
class ReadlinesEach < RuboCop::Cop::Base
extend RuboCop::Cop::AutoCorrector

View File

@ -0,0 +1,363 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Feature::Kas, feature_category: :deployment_management do
let_it_be(:cluster) { create(:cluster_agent) }
let_it_be(:cluster_2) { create(:cluster_agent) }
let_it_be(:project) { cluster.project }
let_it_be(:project_2) { cluster_2.project }
before do
allow(Feature::Definition).to receive(:get).and_call_original
allow(Feature::Definition).to receive(:get).with(:flag).and_return(
Feature::Definition.new('flag.yml', name: :flag, type: :undefined)
)
end
describe ".enabled_for_any?" do
context 'when the flag is set globally' do
context 'when the gate is closed' do
before do
stub_feature_flags(kas_global_flag: false)
end
it 'returns false' do
expect(described_class.enabled_for_any?(:kas_global_flag)).to be(false)
end
end
context 'when the flag defaults to on' do
it 'returns true' do
expect(described_class.enabled_for_any?(:kas_global_flag)).to be(true)
end
end
end
context 'when the flag is enabled for a particular project' do
before do
stub_feature_flags(kas_project_flag: project)
end
it 'returns true for that project' do
expect(described_class.enabled_for_any?(:kas_project_flag, project)).to be(true)
end
it 'returns false for any other project' do
expect(described_class.enabled_for_any?(:kas_project_flag, project_2)).to be(false)
end
it 'returns false when no project is passed' do
expect(described_class.enabled_for_any?(:kas_project_flag)).to be(false)
end
end
context 'when the flag is checked with multiple input actors' do
before do
stub_feature_flags(kas_flag: project)
end
it 'returns true if any of the flag is enabled for any of the input actors' do
expect(described_class.enabled_for_any?(:kas_flag, project, project.group)).to be(true)
end
end
end
describe ".server_feature_flags_for_grpc_request" do
let(:group) { create(:group) }
let(:user) { create(:user) }
before do
stub_feature_flags(
kas_global_flag: true,
kas_project_flag: project,
kas_user_flag: user,
kas_group_flag: group,
non_kas_flag: false
)
end
it 'returns a hash of flags starting with the prefix, with dashes instead of underscores' do
expect(described_class.server_feature_flags_for_grpc_request).to eq(
'kas-feature-global-flag' => 'true',
'kas-feature-project-flag' => 'false',
'kas-feature-user-flag' => 'false',
'kas-feature-group-flag' => 'false'
)
end
context 'when a project is passed' do
it 'returns the value for the flag on the given project' do
expect(described_class.server_feature_flags_for_grpc_request(project: project)).to eq(
'kas-feature-global-flag' => 'true',
'kas-feature-project-flag' => 'true',
'kas-feature-user-flag' => 'false',
'kas-feature-group-flag' => 'false'
)
expect(described_class.server_feature_flags_for_grpc_request(project: project_2)).to eq(
'kas-feature-global-flag' => 'true',
'kas-feature-project-flag' => 'false',
'kas-feature-user-flag' => 'false',
'kas-feature-group-flag' => 'false'
)
end
end
context 'when a user is passed' do
it 'returns the value for the flag on the given user' do
expect(described_class.server_feature_flags_for_grpc_request(user: user)).to eq(
'kas-feature-global-flag' => 'true',
'kas-feature-project-flag' => 'false',
'kas-feature-user-flag' => 'true',
'kas-feature-group-flag' => 'false'
)
expect(described_class.server_feature_flags_for_grpc_request(user: create(:user))).to eq(
'kas-feature-global-flag' => 'true',
'kas-feature-project-flag' => 'false',
'kas-feature-user-flag' => 'false',
'kas-feature-group-flag' => 'false'
)
end
end
context 'when a group is passed' do
it 'returns the value for the flag on the given group' do
expect(described_class.server_feature_flags_for_grpc_request(group: group)).to eq(
'kas-feature-global-flag' => 'true',
'kas-feature-project-flag' => 'false',
'kas-feature-user-flag' => 'false',
'kas-feature-group-flag' => 'true'
)
expect(described_class.server_feature_flags_for_grpc_request(group: create(:group))).to eq(
'kas-feature-global-flag' => 'true',
'kas-feature-project-flag' => 'false',
'kas-feature-user-flag' => 'false',
'kas-feature-group-flag' => 'false'
)
end
end
context 'when multiple actors are passed' do
it 'returns the corresponding enablement status for actors' do
expect(described_class.server_feature_flags_for_grpc_request(project: project_2, group: group)).to eq(
'kas-feature-global-flag' => 'true',
'kas-feature-project-flag' => 'false',
'kas-feature-user-flag' => 'false',
'kas-feature-group-flag' => 'true'
)
expect(described_class.server_feature_flags_for_grpc_request(project: project, group: project_2.group)).to eq(
'kas-feature-global-flag' => 'true',
'kas-feature-project-flag' => 'true',
'kas-feature-user-flag' => 'false',
'kas-feature-group-flag' => 'false'
)
expect(
described_class.server_feature_flags_for_grpc_request(user: user, project: project, group: group)
).to eq(
'kas-feature-global-flag' => 'true',
'kas-feature-project-flag' => 'true',
'kas-feature-user-flag' => 'true',
'kas-feature-group-flag' => 'true'
)
end
end
context 'when table does not exist' do
before do
allow(Feature::FlipperFeature.database)
.to receive(:cached_table_exists?)
.and_return(false)
end
it 'returns an empty Hash' do
expect(described_class.server_feature_flags_for_grpc_request).to eq({})
end
end
end
describe ".server_feature_flags_for_http_response" do
let(:group) { create(:group) }
let(:user) { create(:user) }
before do
stub_feature_flags(
kas_global_flag: true,
kas_project_flag: project,
kas_user_flag: user,
kas_group_flag: group,
non_kas_flag: false
)
end
it 'returns a hash of flags' do
expect(described_class.server_feature_flags_for_http_response).to eq(
'global_flag' => 'true',
'project_flag' => 'false',
'user_flag' => 'false',
'group_flag' => 'false'
)
end
context 'when a project is passed' do
it 'returns the value for the flag on the given project' do
expect(described_class.server_feature_flags_for_http_response(project: project)).to eq(
'global_flag' => 'true',
'project_flag' => 'true',
'user_flag' => 'false',
'group_flag' => 'false'
)
expect(described_class.server_feature_flags_for_http_response(project: project_2)).to eq(
'global_flag' => 'true',
'project_flag' => 'false',
'user_flag' => 'false',
'group_flag' => 'false'
)
end
end
context 'when a user is passed' do
it 'returns the value for the flag on the given user' do
expect(described_class.server_feature_flags_for_http_response(user: user)).to eq(
'global_flag' => 'true',
'project_flag' => 'false',
'user_flag' => 'true',
'group_flag' => 'false'
)
expect(described_class.server_feature_flags_for_http_response(user: create(:user))).to eq(
'global_flag' => 'true',
'project_flag' => 'false',
'user_flag' => 'false',
'group_flag' => 'false'
)
end
end
context 'when a group is passed' do
it 'returns the value for the flag on the given group' do
expect(described_class.server_feature_flags_for_http_response(group: group)).to eq(
'global_flag' => 'true',
'project_flag' => 'false',
'user_flag' => 'false',
'group_flag' => 'true'
)
expect(described_class.server_feature_flags_for_http_response(group: create(:group))).to eq(
'global_flag' => 'true',
'project_flag' => 'false',
'user_flag' => 'false',
'group_flag' => 'false'
)
end
end
context 'when multiple actors are passed' do
it 'returns the corresponding enablement status for actors' do
expect(described_class.server_feature_flags_for_http_response(project: project_2, group: group)).to eq(
'global_flag' => 'true',
'project_flag' => 'false',
'user_flag' => 'false',
'group_flag' => 'true'
)
expect(described_class.server_feature_flags_for_http_response(project: project, group: project_2.group)).to eq(
'global_flag' => 'true',
'project_flag' => 'true',
'user_flag' => 'false',
'group_flag' => 'false'
)
expect(
described_class.server_feature_flags_for_http_response(user: user, project: project, group: group)
).to eq(
'global_flag' => 'true',
'project_flag' => 'true',
'user_flag' => 'true',
'group_flag' => 'true'
)
end
end
context 'when table does not exist' do
before do
allow(Feature::FlipperFeature.database)
.to receive(:cached_table_exists?)
.and_return(false)
end
it 'returns an empty Hash' do
expect(described_class.server_feature_flags_for_http_response).to eq({})
end
end
end
describe ".user_actor" do
let(:user) { create(:user) }
context 'when user is passed in' do
it 'returns a actor wrapper from user' do
expect(described_class.user_actor(user).flipper_id).to eql(user.flipper_id)
end
end
context 'when called without user and user_id is available in application context' do
it 'returns a actor wrapper from user_id' do
::Gitlab::ApplicationContext.with_context(user: user) do
expect(described_class.user_actor.flipper_id).to eql(user.flipper_id)
end
end
end
context 'when called without user and user_id is absent from application context' do
it 'returns nil' do
expect(described_class.user_actor).to be_nil
end
end
context 'when something else is passed' do
it 'returns nil' do
expect(described_class.user_actor(1234)).to be_nil
end
end
end
describe ".project_actor" do
let_it_be(:project) { create(:project) }
context 'when project is passed in' do
it 'returns a actor wrapper from project' do
expect(described_class.project_actor(project).flipper_id).to eql(project.flipper_id)
end
end
context 'when something else is passed in' do
it 'returns nil' do
expect(described_class.project_actor(1234)).to be_nil
end
end
end
describe ".group_actor" do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
context 'when project is passed in' do
it "returns a actor wrapper from project's group" do
expect(described_class.group_actor(project).flipper_id).to eql(group.flipper_id)
end
end
context 'when something else is passed in' do
it 'returns nil' do
expect(described_class.group_actor(1234)).to be_nil
end
end
end
end

View File

@ -34,6 +34,7 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
describe 'gRPC calls' do
let(:token) { instance_double(JSONWebToken::HMACToken, encoded: 'test-token') }
let(:kas_url) { 'grpc://example.kas.internal' }
let(:feature_flags) { { 'kas-feature-a' => 'true', 'kas-feature-b': 'false' } }
before do
allow(Gitlab::Kas).to receive(:enabled?).and_return(true)
@ -45,6 +46,8 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
allow(token).to receive(:issuer=).with(Settings.gitlab.host)
allow(token).to receive(:audience=).with(described_class::JWT_AUDIENCE)
allow(::Feature::Kas).to receive(:server_feature_flags_for_grpc_request).and_return(feature_flags)
end
describe '#get_server_info' do
@ -64,7 +67,7 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
.and_return(request)
expect(stub).to receive(:get_server_info)
.with(request, metadata: { 'authorization' => 'bearer test-token' })
.with(request, metadata: { 'authorization' => 'bearer test-token', **feature_flags })
.and_return(response)
end
@ -90,7 +93,7 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
.and_return(request)
expect(stub).to receive(:get_connected_agentks_by_agent_i_ds)
.with(request, metadata: { 'authorization' => 'bearer test-token' })
.with(request, metadata: { 'authorization' => 'bearer test-token', **feature_flags })
.and_return(response)
end
@ -129,7 +132,7 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
.and_return(request)
expect(stub).to receive(:list_agent_config_files)
.with(request, metadata: { 'authorization' => 'bearer test-token' })
.with(request, metadata: { 'authorization' => 'bearer test-token', **feature_flags })
.and_return(response)
end
@ -191,7 +194,7 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
.and_return(request)
expect(stub).to receive(:cloud_event)
.with(request, metadata: { 'authorization' => 'bearer test-token' })
.with(request, metadata: { 'authorization' => 'bearer test-token', **feature_flags })
.and_return(response)
end
@ -226,7 +229,7 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
.and_return(request)
expect(stub).to receive(:git_push_event)
.with(request, metadata: { 'authorization' => 'bearer test-token' })
.with(request, metadata: { 'authorization' => 'bearer test-token', **feature_flags })
.and_return(response)
end
@ -278,7 +281,7 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
.and_return(request)
expect(stub).to receive(:get_environment_template)
.with(request, metadata: { 'authorization' => 'bearer test-token' })
.with(request, metadata: { 'authorization' => 'bearer test-token', **feature_flags })
.and_return(response)
end
@ -302,7 +305,7 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
.and_return(request)
expect(stub).to receive(:get_default_environment_template)
.with(request, metadata: { 'authorization' => 'bearer test-token' })
.with(request, metadata: { 'authorization' => 'bearer test-token', **feature_flags })
.and_return(response)
end
@ -340,7 +343,7 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
.and_return(request)
expect(stub).to receive(:render_environment_template)
.with(request, metadata: { 'authorization' => 'bearer test-token' })
.with(request, metadata: { 'authorization' => 'bearer test-token', **feature_flags })
.and_return(response)
end
@ -378,7 +381,7 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
.and_return(request)
expect(stub).to receive(:ensure_environment)
.with(request, metadata: { 'authorization' => 'bearer test-token' })
.with(request, metadata: { 'authorization' => 'bearer test-token', **feature_flags })
.and_return(response)
end
@ -408,7 +411,7 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
).and_return(request)
expect(stub).to receive(:delete_environment)
.with(request, metadata: { 'authorization' => 'bearer test-token' })
.with(request, metadata: { 'authorization' => 'bearer test-token', **feature_flags })
.and_return(response)
end

View File

@ -782,6 +782,136 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_git
end
end
end
context 'with self_heal_build_trace_chunk_flushing feature flag' do
where(:data_store, :redis_class) do
[
[:redis, Ci::BuildTraceChunks::Redis],
[:redis_trace_chunks, Ci::BuildTraceChunks::RedisTraceChunks]
]
end
with_them do
let(:data) { 'a' * described_class::CHUNK_SIZE }
before do
build_trace_chunk.send(:unsafe_set_data!, data)
end
context 'when feature flag is enabled' do
before do
stub_feature_flags(self_heal_build_trace_chunk_flushing: true)
end
context 'when save operation succeeds' do
before do
allow(build_trace_chunk).to receive_messages(changed?: true, save!: true)
end
it 'deletes old data from redis store' do
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
subject
expect(redis_class.new.data(build_trace_chunk)).to be_nil
end
end
context 'when save operation fails' do
before do
allow(build_trace_chunk).to receive_messages(changed?: true, save!: false)
end
it 'preserves old data to allow self-healing' do
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
expect { subject }.to raise_error(described_class::FailedToPersistDataError)
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
end
end
context 'when save operation returns nil' do
before do
allow(build_trace_chunk).to receive_messages(changed?: true, save!: nil)
end
it 'preserves old data to allow self-healing' do
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
expect { subject }.to raise_error(described_class::FailedToPersistDataError)
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
end
end
context 'when save is not called due to no changes' do
before do
allow(build_trace_chunk).to receive(:changed?).and_return(false)
end
it 'preserves old data since save was not attempted' do
allow(build_trace_chunk).to receive(:changed?).and_return(false)
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
expect { subject }.to raise_error(described_class::FailedToPersistDataError)
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
end
end
context 'when save raises an exception' do
before do
allow(build_trace_chunk).to receive(:changed?).and_return(true)
allow(build_trace_chunk).to receive(:save!).and_raise(ActiveRecord::RecordInvalid)
end
it 'preserves old data and re-raises exception' do
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
expect { subject }.to raise_error(ActiveRecord::RecordInvalid)
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
end
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(self_heal_build_trace_chunk_flushing: false)
end
context 'when data is not saved' do
before do
allow(build_trace_chunk).to receive(:unsafe_set_data!).and_return(nil)
end
it 'deletes old data' do
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
subject
expect(redis_class.new.data(build_trace_chunk)).to be_nil
end
end
context 'when data is saved' do
before do
allow(build_trace_chunk).to receive(:unsafe_set_data!).and_return(true)
end
it 'deletes old data' do
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
subject
expect(redis_class.new.data(build_trace_chunk)).to be_nil
end
end
end
end
end
end
describe 'final?' do

View File

@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe WorkItems::DatesSource, feature_category: :portfolio_management do
let_it_be(:work_item) { create(:work_item) }
describe 'associations' do
it { is_expected.to belong_to(:namespace).inverse_of(:work_items_dates_source) }
it { is_expected.to belong_to(:work_item).with_foreign_key('issue_id').inverse_of(:dates_source) }
@ -13,7 +15,6 @@ RSpec.describe WorkItems::DatesSource, feature_category: :portfolio_management d
end
it 'ensures to use work_item namespace' do
work_item = create(:work_item)
date_source = described_class.new(work_item: work_item)
expect(date_source).to be_valid
@ -22,8 +23,6 @@ RSpec.describe WorkItems::DatesSource, feature_category: :portfolio_management d
end
describe 'before_save' do
let_it_be(:work_item) { create(:work_item) }
describe 'set_fixed_start_date' do
context 'when start date is fixed' do
it 'sets start_date to match fixed_start_date' do
@ -63,6 +62,44 @@ RSpec.describe WorkItems::DatesSource, feature_category: :portfolio_management d
end
end
describe 'validation' do
%i[start_date start_date_fixed due_date due_date_fixed].each do |field|
context 'for new records' do
it "validates #{field} minimum value" do
dates_source = build(:work_items_dates_source, field => WorkItems::DatesSource::MIN_DATE_LIMIT - 1.day)
expect(dates_source).not_to be_valid
expect(dates_source.errors[field]).to include('must be greater than or equal to 1000-01-01')
end
it "validates #{field} maximum value" do
dates_source = build(:work_items_dates_source, field => WorkItems::DatesSource::MAX_DATE_LIMIT + 1.day)
expect(dates_source).not_to be_valid
expect(dates_source.errors[field]).to include('must be less than or equal to 9999-12-31')
end
end
context 'for existing records' do
it "validates #{field} only if it was updated", :aggregate_failures do
dates_source = build(
:work_items_dates_source,
work_item: work_item,
namespace: work_item.namespace,
field => WorkItems::DatesSource::MAX_DATE_LIMIT + 1.day
)
dates_source.save!(validate: false)
expect(dates_source).to be_valid
dates_source[field] += 1.day
expect(dates_source).not_to be_valid
expect(dates_source.errors[field]).to include('must be less than or equal to 9999-12-31')
end
end
end
end
context 'on database triggers' do
let_it_be_with_reload(:work_item) { create(:work_item) }

View File

@ -283,6 +283,10 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
end
before do |example|
allow(::Feature::Kas).to receive(:server_feature_flags_for_http_response).and_return(
{ 'feature_flag_a' => 'true', 'feature_flag_b' => 'false' }
)
unless example.metadata[:skip_before_request]
subject
end
@ -314,6 +318,10 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
expect(json_response['allowed_agents']).to match_array expected_allowed_agents
end
it 'returns feature flags in response header' do
expect(response.headers.to_h).to include({ 'Gitlab-Feature-Flag' => 'feature_flag_a=true, feature_flag_b=false' })
end
end
shared_examples_for 'valid allowed_agents request for a job with environment' do
@ -331,6 +339,10 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
expect(json_response['allowed_agents']).to match_array(expected_allowed_agents)
end
it 'returns feature flags in response header' do
expect(response.headers.to_h).to include({ 'Gitlab-Feature-Flag' => 'feature_flag_a=true, feature_flag_b=false' })
end
end
it_behaves_like 'valid allowed_agents request'

View File

@ -832,5 +832,23 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :deployment_manageme
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'with feature flags' do
before do
allow(::Feature::Kas).to receive(:server_feature_flags_for_http_response).and_return(
{ 'feature_flag_a' => 'true', 'feature_flag_b' => 'false' }
)
end
it 'returns feature flags in response header' do
deployment_project.add_member(user, :developer)
token = new_token
public_id = stub_user_session(user, token)
access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id)
send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) })
expect(response.headers.to_h).to include({ 'Gitlab-Feature-Flag' => 'feature_flag_a=true, feature_flag_b=false' })
end
end
end
end

View File

@ -119,6 +119,41 @@ RSpec.describe RuboCop::Cop::Gitlab::MarkUsedFeatureFlags do
end
end
%w[
Feature::Kas.enabled?
Feature::Kas.disabled?
].each do |feature_flag_method|
context "#{feature_flag_method} method", feature_category: :deployment_management do
context 'a string feature flag' do
include_examples 'sets flag as used', %|#{feature_flag_method}("foo")|, 'kas_foo'
end
context 'a symbol feature flag' do
include_examples 'sets flag as used', %|#{feature_flag_method}(:foo)|, 'kas_foo'
end
context 'an interpolated string feature flag with a string prefix' do
include_examples 'sets flag as used', %|#{feature_flag_method}("foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated symbol feature flag with a string prefix' do
include_examples 'sets flag as used', %|#{feature_flag_method}(:"foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated string feature flag with a string prefix and suffix' do
include_examples 'does not set any flags as used', %|#{feature_flag_method}(:"foo_\#{bar}_baz")|
end
context 'a dynamic string feature flag as a variable' do
include_examples 'does not set any flags as used', %|#{feature_flag_method}(a_variable, an_arg)|
end
context 'an integer feature flag' do
include_examples 'does not set any flags as used', %|#{feature_flag_method}(123)|
end
end
end
context 'with the experiment method' do
context 'a string feature flag' do
include_examples 'sets flag as used', %q|experiment("baz")|, %w[baz]

View File

@ -27,5 +27,43 @@ RSpec.describe Ci::BuildTraceChunkFlushWorker, feature_category: :continuous_int
expect(chunk.reload).to be_migrated
end
end
# rubocop: disable RSpec/AnyInstanceOf -- next_instance_of will not work here
context 'when save operation fails and self-healing is enabled' do
before do
stub_feature_flags(self_heal_build_trace_chunk_flushing: true)
end
it 'preserves Redis data on first failure and completes migration on retry' do
expect(chunk).to be_live
allow_any_instance_of(Ci::BuildTraceChunk).to receive(:save!).and_return(false)
# First run save! fails so we should still have redis data
expect do
described_class.new.perform(chunk.id)
end.to raise_error(Ci::BuildTraceChunk::FailedToPersistDataError)
chunk.reload
expect(chunk).to be_live
expect(chunk).not_to be_migrated
redis_data = Ci::BuildTraceChunks::RedisTraceChunks.new.data(chunk)
expect(redis_data).to eq(data)
allow_any_instance_of(Ci::BuildTraceChunk).to receive(:save!).and_call_original
# Second run it recovers
described_class.new.perform(chunk.id)
chunk.reload
expect(chunk).to be_migrated
expect(chunk).not_to be_live
redis_data = Ci::BuildTraceChunks::RedisTraceChunks.new.data(chunk)
expect(redis_data).to be_nil
end
end
# rubocop: enable RSpec/AnyInstanceOf
end
end