From 155fb78b9a3017640b527bc41c9096642ca11aca Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 4 Aug 2021 21:09:04 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../strategies/flexible_rollout.vue | 8 +-- app/models/namespaces/traversal/linear.rb | 19 +++++- .../namespaces/traversal/linear_scopes.rb | 59 ----------------- app/models/namespaces/traversal/recursive.rb | 1 - .../namespaces/traversal/recursive_scopes.rb | 25 ------- .../operations/feature_flags/strategy.rb | 2 +- app/models/project_feature.rb | 2 +- ...e_flags_correct_flexible_rollout_values.rb | 29 ++++++++ db/schema_migrations/20210722150102 | 1 + package.json | 4 +- qa/qa/resource/issue.rb | 7 +- qa/qa/resource/merge_request.rb | 7 +- qa/qa/resource/project.rb | 29 ++++---- qa/qa/runtime/api/request.rb | 6 +- .../1_manage/import_large_github_repo_spec.rb | 20 +++--- qa/qa/support/api.rb | 24 +++++-- qa/qa/support/repeater.rb | 26 +++++--- qa/qa/support/retrier.rb | 20 +++--- qa/spec/support/retrier_spec.rb | 10 +-- .../projects/feature_flags_controller_spec.rb | 8 +-- spec/factories/projects.rb | 16 +---- .../strategies/flexible_rollout_spec.js | 5 +- spec/frontend/feature_flags/mock_data.js | 2 +- ...gs_correct_flexible_rollout_values_spec.rb | 66 +++++++++++++++++++ spec/models/namespace_spec.rb | 16 +---- .../operations/feature_flags/strategy_spec.rb | 22 +++---- spec/models/project_feature_spec.rb | 24 +++++++ spec/models/project_spec.rb | 3 +- spec/requests/api/feature_flags_spec.rb | 12 ++-- .../namespaces/linear_traversal_examples.rb | 23 +++++++ .../namespaces/traversal_scope_examples.rb | 51 -------------- yarn.lock | 24 +++---- 32 files changed, 304 insertions(+), 267 deletions(-) delete mode 100644 app/models/namespaces/traversal/linear_scopes.rb delete mode 100644 app/models/namespaces/traversal/recursive_scopes.rb create mode 100644 db/migrate/20210722150102_operations_feature_flags_correct_flexible_rollout_values.rb create mode 100644 db/schema_migrations/20210722150102 create mode 100644 spec/migrations/20210722150102_operations_feature_flags_correct_flexible_rollout_values_spec.rb create mode 100644 spec/support/shared_examples/namespaces/linear_traversal_examples.rb delete mode 100644 spec/support/shared_examples/namespaces/traversal_scope_examples.rb diff --git a/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue b/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue index 4daf8b4e6bf..858c30649bb 100644 --- a/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue +++ b/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue @@ -25,19 +25,19 @@ export default { }, stickinessOptions: [ { - value: 'DEFAULT', + value: 'default', text: __('Available ID'), }, { - value: 'USERID', + value: 'userId', text: __('User ID'), }, { - value: 'SESSIONID', + value: 'sessionId', text: __('Session ID'), }, { - value: 'RANDOM', + value: 'random', text: __('Random'), }, ], diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb index 79df466fd64..3d78f384634 100644 --- a/app/models/namespaces/traversal/linear.rb +++ b/app/models/namespaces/traversal/linear.rb @@ -37,7 +37,6 @@ module Namespaces module Traversal module Linear extend ActiveSupport::Concern - include LinearScopes UnboundedSearch = Class.new(StandardError) @@ -45,6 +44,14 @@ module Namespaces before_update :lock_both_roots, if: -> { sync_traversal_ids? && parent_id_changed? } after_create :sync_traversal_ids, if: -> { sync_traversal_ids? } after_update :sync_traversal_ids, if: -> { sync_traversal_ids? && saved_change_to_parent_id? } + + scope :traversal_ids_contains, ->(ids) { where("traversal_ids @> (?)", ids) } + # When filtering namespaces by the traversal_ids column to compile a + # list of namespace IDs, it's much faster to reference the ID in + # traversal_ids than the primary key ID column. + # WARNING This scope must be used behind a linear query feature flag + # such as `use_traversal_ids`. + scope :as_ids, -> { select('traversal_ids[array_length(traversal_ids, 1)] AS id') } end def sync_traversal_ids? @@ -157,14 +164,20 @@ module Namespaces Namespace.lock.select(:id).where(id: roots).order(id: :asc).load end + # Make sure we drop the STI `type = 'Group'` condition for better performance. + # Logically equivalent so long as hierarchies remain homogeneous. + def without_sti_condition + self.class.unscope(where: :type) + end + # Search this namespace's lineage. Bound inclusively by top node. def lineage(top: nil, bottom: nil, hierarchy_order: nil) raise UnboundedSearch, 'Must bound search by either top or bottom' unless top || bottom - skope = self.class.without_sti_condition + skope = without_sti_condition if top - skope = skope.where("traversal_ids @> ('{?}')", top.id) + skope = skope.traversal_ids_contains("{#{top.id}}") end if bottom diff --git a/app/models/namespaces/traversal/linear_scopes.rb b/app/models/namespaces/traversal/linear_scopes.rb deleted file mode 100644 index f352497e6de..00000000000 --- a/app/models/namespaces/traversal/linear_scopes.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -module Namespaces - module Traversal - module LinearScopes - extend ActiveSupport::Concern - - class_methods do - # When filtering namespaces by the traversal_ids column to compile a - # list of namespace IDs, it can be faster to reference the ID in - # traversal_ids than the primary key ID column. - def as_ids - return super unless use_traversal_ids? - - select('namespaces.traversal_ids[array_length(namespaces.traversal_ids, 1)] AS id') - end - - def self_and_descendants - return super unless use_traversal_ids? - - without_dups = self_and_descendants_with_duplicates - .select('DISTINCT on(namespaces.id) namespaces.*') - - # Wrap the `SELECT DISTINCT on(....)` with a normal query so we - # retain expected Rails behavior. Otherwise count and other - # aggregates won't work. - unscoped.without_sti_condition.from(without_dups, :namespaces) - end - - def self_and_descendant_ids - return super unless use_traversal_ids? - - self_and_descendants_with_duplicates.select('DISTINCT namespaces.id') - end - - # Make sure we drop the STI `type = 'Group'` condition for better performance. - # Logically equivalent so long as hierarchies remain homogeneous. - def without_sti_condition - unscope(where: :type) - end - - private - - def use_traversal_ids? - Feature.enabled?(:use_traversal_ids, default_enabled: :yaml) - end - - def self_and_descendants_with_duplicates - base_ids = select(:id) - - unscoped - .without_sti_condition - .from("namespaces, (#{base_ids.to_sql}) base") - .where('namespaces.traversal_ids @> ARRAY[base.id]') - end - end - end - end -end diff --git a/app/models/namespaces/traversal/recursive.rb b/app/models/namespaces/traversal/recursive.rb index c1ada715d6d..d9e8743aa50 100644 --- a/app/models/namespaces/traversal/recursive.rb +++ b/app/models/namespaces/traversal/recursive.rb @@ -4,7 +4,6 @@ module Namespaces module Traversal module Recursive extend ActiveSupport::Concern - include RecursiveScopes def root_ancestor return self if parent.nil? diff --git a/app/models/namespaces/traversal/recursive_scopes.rb b/app/models/namespaces/traversal/recursive_scopes.rb deleted file mode 100644 index 0dcb23b6567..00000000000 --- a/app/models/namespaces/traversal/recursive_scopes.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -module Namespaces - module Traversal - module RecursiveScopes - extend ActiveSupport::Concern - - class_methods do - def as_ids - select('id') - end - - def self_and_descendants - Gitlab::ObjectHierarchy.new(all).base_and_descendants - end - alias_method :recursive_self_and_descendants, :self_and_descendants - - def self_and_descendant_ids - self_and_descendants.as_ids - end - alias_method :recursive_self_and_descendant_ids, :self_and_descendant_ids - end - end - end -end diff --git a/app/models/operations/feature_flags/strategy.rb b/app/models/operations/feature_flags/strategy.rb index c70e10c72d5..ed9400dde8f 100644 --- a/app/models/operations/feature_flags/strategy.rb +++ b/app/models/operations/feature_flags/strategy.rb @@ -16,7 +16,7 @@ module Operations STRATEGY_USERWITHID => ['userIds'].freeze }.freeze USERID_MAX_LENGTH = 256 - STICKINESS_SETTINGS = %w[DEFAULT USERID SESSIONID RANDOM].freeze + STICKINESS_SETTINGS = %w[default userId sessionId random].freeze self.table_name = 'operations_strategies' diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 94db683267d..11dd10007e9 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -79,7 +79,7 @@ class ProjectFeature < ApplicationRecord end end - default_value_for(:container_registry_access_level, allows_nil: false) do |feature| + default_value_for(:container_registry_access_level) do |feature| if gitlab_config_features.container_registry ENABLED else diff --git a/db/migrate/20210722150102_operations_feature_flags_correct_flexible_rollout_values.rb b/db/migrate/20210722150102_operations_feature_flags_correct_flexible_rollout_values.rb new file mode 100644 index 00000000000..974559239d7 --- /dev/null +++ b/db/migrate/20210722150102_operations_feature_flags_correct_flexible_rollout_values.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class OperationsFeatureFlagsCorrectFlexibleRolloutValues < ActiveRecord::Migration[6.1] + STICKINESS = { "USERID" => "userId", "RANDOM" => "random", "SESSIONID" => "sessionId", "DEFAULT" => "default" }.freeze + + def up + STICKINESS.each do |before, after| + update_statement = <<-SQL + UPDATE operations_strategies + SET parameters = parameters || jsonb_build_object('stickiness', '#{quote_string(after)}') + WHERE name = 'flexibleRollout' AND parameters->>'stickiness' = '#{quote_string(before)}' + SQL + + execute(update_statement) + end + end + + def down + STICKINESS.each do |before, after| + update_statement = <<-SQL + UPDATE operations_strategies + SET parameters = parameters || jsonb_build_object('stickiness', '#{quote_string(before)}') + WHERE name = 'flexibleRollout' AND parameters->>'stickiness' = '#{quote_string(after)}' + SQL + + execute(update_statement) + end + end +end diff --git a/db/schema_migrations/20210722150102 b/db/schema_migrations/20210722150102 new file mode 100644 index 00000000000..42f6cfb3b7e --- /dev/null +++ b/db/schema_migrations/20210722150102 @@ -0,0 +1 @@ +cea8e51f6917be9ad43280fba9f8e7d9b9db1f508e249d9f5df792e43c0b8313 \ No newline at end of file diff --git a/package.json b/package.json index 0f00ac90862..e9119753584 100644 --- a/package.json +++ b/package.json @@ -212,7 +212,7 @@ "babel-plugin-istanbul": "^6.0.0", "chalk": "^2.4.1", "cheerio": "^1.0.0-rc.9", - "commander": "^2.18.0", + "commander": "^2.20.3", "custom-jquery-matchers": "^2.1.0", "docdash": "^1.0.2", "eslint": "7.31.0", @@ -264,7 +264,7 @@ "webpack-dev-server": "^3.11.2", "xhr-mock": "^2.5.1", "yarn-check-webpack-plugin": "^1.2.0", - "yarn-deduplicate": "^1.1.1" + "yarn-deduplicate": "^3.1.0" }, "blockedDependencies": { "bootstrap-vue": "https://docs.gitlab.com/ee/development/fe_guide/dependencies.html#bootstrapvue" diff --git a/qa/qa/resource/issue.rb b/qa/qa/resource/issue.rb index 2144e619c70..c45ab7593b6 100644 --- a/qa/qa/resource/issue.rb +++ b/qa/qa/resource/issue.rb @@ -84,10 +84,13 @@ module QA # Get issue comments # # @return [Array] - def comments(auto_paginate: false) + def comments(auto_paginate: false, attempts: 0) return parse_body(api_get_from(api_comments_path)) unless auto_paginate - auto_paginated_response(Runtime::API::Request.new(api_client, api_comments_path, per_page: '100').url) + auto_paginated_response( + Runtime::API::Request.new(api_client, api_comments_path, per_page: '100').url, + attempts: attempts + ) end end end diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb index 419893f0b11..e09a4b5860d 100644 --- a/qa/qa/resource/merge_request.rb +++ b/qa/qa/resource/merge_request.rb @@ -160,10 +160,13 @@ module QA # Get MR comments # # @return [Array] - def comments(auto_paginate: false) + def comments(auto_paginate: false, attempts: 0) return parse_body(api_get_from(api_comments_path)) unless auto_paginate - auto_paginated_response(Runtime::API::Request.new(api_client, api_comments_path, per_page: '100').url) + auto_paginated_response( + Runtime::API::Request.new(api_client, api_comments_path, per_page: '100').url, + attempts: attempts + ) end private diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index c9aa2a80187..16af37f0a29 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -264,21 +264,24 @@ module QA result = parse_body(response) - Runtime::Logger.error("Import failed: #{result[:import_error]}") if result[:import_status] == "failed" + if result[:import_status] == "failed" + Runtime::Logger.error("Import failed: #{result[:import_error]}") + Runtime::Logger.error("Failed relations: #{result[:failed_relations]}") + end result[:import_status] end - def commits(auto_paginate: false) + def commits(auto_paginate: false, attempts: 0) return parse_body(api_get_from(api_commits_path)) unless auto_paginate - auto_paginated_response(request_url(api_commits_path, per_page: '100')) + auto_paginated_response(request_url(api_commits_path, per_page: '100'), attempts: attempts) end - def merge_requests(auto_paginate: false) + def merge_requests(auto_paginate: false, attempts: 0) return parse_body(api_get_from(api_merge_requests_path)) unless auto_paginate - auto_paginated_response(request_url(api_merge_requests_path, per_page: '100')) + auto_paginated_response(request_url(api_merge_requests_path, per_page: '100'), attempts: attempts) end def merge_request_with_title(title) @@ -302,10 +305,10 @@ module QA parse_body(response) end - def repository_branches(auto_paginate: false) + def repository_branches(auto_paginate: false, attempts: 0) return parse_body(api_get_from(api_repository_branches_path)) unless auto_paginate - auto_paginated_response(request_url(api_repository_branches_path, per_page: '100')) + auto_paginated_response(request_url(api_repository_branches_path, per_page: '100'), attempts: attempts) end def repository_tags @@ -328,22 +331,22 @@ module QA parse_body(response) end - def issues(auto_paginate: false) + def issues(auto_paginate: false, attempts: 0) return parse_body(api_get_from(api_issues_path)) unless auto_paginate - auto_paginated_response(request_url(api_issues_path, per_page: '100')) + auto_paginated_response(request_url(api_issues_path, per_page: '100'), attempts: attempts) end - def labels(auto_paginate: false) + def labels(auto_paginate: false, attempts: 0) return parse_body(api_get_from(api_labels_path)) unless auto_paginate - auto_paginated_response(request_url(api_labels_path, per_page: '100')) + auto_paginated_response(request_url(api_labels_path, per_page: '100'), attempts: attempts) end - def milestones(auto_paginate: false) + def milestones(auto_paginate: false, attempts: 0) return parse_body(api_get_from(api_milestones_path)) unless auto_paginate - auto_paginated_response(request_url(api_milestones_path, per_page: '100')) + auto_paginated_response(request_url(api_milestones_path, per_page: '100'), attempts: attempts) end def wikis diff --git a/qa/qa/runtime/api/request.rb b/qa/qa/runtime/api/request.rb index 28bae541cb8..c1df5e84f6c 100644 --- a/qa/qa/runtime/api/request.rb +++ b/qa/qa/runtime/api/request.rb @@ -6,6 +6,10 @@ module QA class Request API_VERSION = 'v4' + def self.masked_url(url) + url.sub(/private_token=.*/, "private_token=[****]") + end + def initialize(api_client, path, **query_string) query_string[:private_token] ||= api_client.personal_access_token unless query_string[:oauth_access_token] request_path = request_path(path, **query_string) @@ -13,7 +17,7 @@ module QA end def mask_url - @session_address.address.sub(/private_token=.*/, "private_token=[****]") + QA::Runtime::API::Request.masked_url(url) end def url diff --git a/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb index 52051ddab02..bc1e67f93e0 100644 --- a/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb +++ b/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb @@ -135,10 +135,12 @@ module QA imported_project # import the project fetch_github_objects # fetch all objects right after import has started - expect { imported_project.reload!.import_status }.to eventually_eq('finished').within( - duration: 3600, - interval: 30 - ) + import_status = lambda do + imported_project.reload!.import_status.tap do |status| + raise "Import of '#{imported_project.name}' failed!" if status == 'failed' + end + end + expect(import_status).to eventually_eq('finished').within(duration: 3600, interval: 30) @import_time = Time.now - start aggregate_failures do @@ -264,7 +266,7 @@ module QA def gl_commits @gl_commits ||= begin logger.debug("= Fetching commits =") - imported_project.commits(auto_paginate: true).map { |c| c[:id] } + imported_project.commits(auto_paginate: true, attempts: 2).map { |c| c[:id] } end end @@ -294,7 +296,7 @@ module QA def mrs @mrs ||= begin logger.debug("= Fetching merge requests =") - imported_mrs = imported_project.merge_requests(auto_paginate: true) + imported_mrs = imported_project.merge_requests(auto_paginate: true, attempts: 2) logger.debug("= Transforming merge request objects for comparison =") imported_mrs.each_with_object({}) do |mr, hash| resource = Resource::MergeRequest.init do |resource| @@ -305,7 +307,7 @@ module QA hash[mr[:title]] = { body: mr[:description], - comments: resource.comments(auto_paginate: true) + comments: resource.comments(auto_paginate: true, attempts: 2) # remove system notes .reject { |c| c[:system] || c[:body].match?(/^(\*\*Review:\*\*)|(\*Merged by:).*/) } .map { |c| sanitize(c[:body]) } @@ -320,7 +322,7 @@ module QA def gl_issues @gl_issues ||= begin logger.debug("= Fetching issues =") - imported_issues = imported_project.issues(auto_paginate: true) + imported_issues = imported_project.issues(auto_paginate: true, attempts: 2) logger.debug("= Transforming issue objects for comparison =") imported_issues.each_with_object({}) do |issue, hash| resource = Resource::Issue.init do |issue_resource| @@ -331,7 +333,7 @@ module QA hash[issue[:title]] = { body: issue[:description], - comments: resource.comments(auto_paginate: true).map { |c| sanitize(c[:body]) } + comments: resource.comments(auto_paginate: true, attempts: 2).map { |c| sanitize(c[:body]) } } end end diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb index 1493feeeed7..579227b4f7a 100644 --- a/qa/qa/support/api.rb +++ b/qa/qa/support/api.rb @@ -79,16 +79,27 @@ module QA error.response end - def auto_paginated_response(url) + def auto_paginated_response(url, attempts: 0) pages = [] - with_paginated_response_body(url) { |response| pages << response } + with_paginated_response_body(url, attempts: attempts) { |response| pages << response } pages.flatten end - def with_paginated_response_body(url) + def with_paginated_response_body(url, attempts: 0) + not_ok_error = lambda do |resp| + raise "Failed to GET #{QA::Runtime::API::Request.masked_url(url)} - (#{resp.code}): `#{resp}`." + end + loop do - response = get(url) + response = if attempts > 0 + Retrier.retry_on_exception(max_attempts: attempts, log: false) do + get(url).tap { |resp| not_ok_error.call(resp) if resp.code != HTTP_STATUS_OK } + end + else + get(url).tap { |resp| not_ok_error.call(resp) if resp.code != HTTP_STATUS_OK } + end + page, pages = response.headers.values_at(:x_page, :x_total_pages) api_endpoint = url.match(%r{v4/(\S+)\?})[1] @@ -104,7 +115,10 @@ module QA end def pagination_links(response) - response.headers[:link].split(',').map do |link| + link = response.headers[:link] + return unless link + + link.split(',').map do |link| match = link.match(/<(?.*)>; rel="(?\w+)"/) break nil unless match diff --git a/qa/qa/support/repeater.rb b/qa/qa/support/repeater.rb index 6f8c4a59566..b3a2472d702 100644 --- a/qa/qa/support/repeater.rb +++ b/qa/qa/support/repeater.rb @@ -11,7 +11,15 @@ module QA RetriesExceededError = Class.new(RepeaterConditionExceededError) WaitExceededError = Class.new(RepeaterConditionExceededError) - def repeat_until(max_attempts: nil, max_duration: nil, reload_page: nil, sleep_interval: 0, raise_on_failure: true, retry_on_exception: false, log: true) + def repeat_until( + max_attempts: nil, + max_duration: nil, + reload_page: nil, + sleep_interval: 0, + raise_on_failure: true, + retry_on_exception: false, + log: true + ) attempts = 0 start = Time.now @@ -29,17 +37,19 @@ module QA raise unless retry_on_exception attempts += 1 - if remaining_attempts?(attempts, max_attempts) && remaining_time?(start, max_duration) - sleep_and_reload_if_needed(sleep_interval, reload_page) + raise unless remaining_attempts?(attempts, max_attempts) && remaining_time?(start, max_duration) - retry - else - raise - end + sleep_and_reload_if_needed(sleep_interval, reload_page) + retry end if raise_on_failure - raise RetriesExceededError, "Retry condition not met after #{max_attempts} #{'attempt'.pluralize(max_attempts)}" unless remaining_attempts?(attempts, max_attempts) + unless remaining_attempts?(attempts, max_attempts) + raise( + RetriesExceededError, + "Retry condition not met after #{max_attempts} #{'attempt'.pluralize(max_attempts)}" + ) + end raise WaitExceededError, "Wait condition not met after #{max_duration} #{'second'.pluralize(max_duration)}" end diff --git a/qa/qa/support/retrier.rb b/qa/qa/support/retrier.rb index 25dbb42cf6f..fde8ac263ca 100644 --- a/qa/qa/support/retrier.rb +++ b/qa/qa/support/retrier.rb @@ -7,21 +7,21 @@ module QA module_function - def retry_on_exception(max_attempts: 3, reload_page: nil, sleep_interval: 0.5) - QA::Runtime::Logger.debug( - <<~MSG.tr("\n", ' ') - with retry_on_exception: max_attempts: #{max_attempts}; - reload_page: #{reload_page}; - sleep_interval: #{sleep_interval} - MSG - ) + def retry_on_exception(max_attempts: 3, reload_page: nil, sleep_interval: 0.5, log: true) + if log + msg = ["with retry_on_exception: max_attempts: #{max_attempts}"] + msg << "reload_page: #{reload_page}" if reload_page + msg << "sleep_interval: #{sleep_interval}" + QA::Runtime::Logger.debug(msg.join('; ')) + end result = nil repeat_until( max_attempts: max_attempts, reload_page: reload_page, sleep_interval: sleep_interval, - retry_on_exception: true + retry_on_exception: true, + log: log ) do result = yield @@ -29,7 +29,7 @@ module QA # We set it to `true` so that it doesn't repeat if there's no exception true end - QA::Runtime::Logger.debug("ended retry_on_exception") + QA::Runtime::Logger.debug("ended retry_on_exception") if log result end diff --git a/qa/spec/support/retrier_spec.rb b/qa/spec/support/retrier_spec.rb index 6f052519516..4e27915553c 100644 --- a/qa/spec/support/retrier_spec.rb +++ b/qa/spec/support/retrier_spec.rb @@ -70,8 +70,9 @@ RSpec.describe QA::Support::Retrier do describe '.retry_on_exception' do context 'when the condition is true' do it 'logs max_attempts, reload_page, and sleep_interval parameters' do - expect { subject.retry_on_exception(max_attempts: 1, reload_page: nil, sleep_interval: 0) { true } } - .to output(/with retry_on_exception: max_attempts: 1; reload_page: ; sleep_interval: 0/).to_stdout_from_any_process + message = /with retry_on_exception: max_attempts: 1; reload_page: true; sleep_interval: 0/ + expect { subject.retry_on_exception(max_attempts: 1, reload_page: true, sleep_interval: 0) { true } } + .to output(message).to_stdout_from_any_process end it 'logs the end' do @@ -82,8 +83,9 @@ RSpec.describe QA::Support::Retrier do context 'when the condition is false' do it 'logs the start' do - expect { subject.retry_on_exception(max_attempts: 1, reload_page: nil, sleep_interval: 0) { false } } - .to output(/with retry_on_exception: max_attempts: 1; reload_page: ; sleep_interval: 0/).to_stdout_from_any_process + message = /with retry_on_exception: max_attempts: 1; reload_page: true; sleep_interval: 0/ + expect { subject.retry_on_exception(max_attempts: 1, reload_page: true, sleep_interval: 0) { false } } + .to output(message).to_stdout_from_any_process end it 'logs the end' do diff --git a/spec/controllers/projects/feature_flags_controller_spec.rb b/spec/controllers/projects/feature_flags_controller_spec.rb index f809dd31b3b..e038b247eff 100644 --- a/spec/controllers/projects/feature_flags_controller_spec.rb +++ b/spec/controllers/projects/feature_flags_controller_spec.rb @@ -652,7 +652,7 @@ RSpec.describe Projects::FeatureFlagsController do version: 'new_version_flag', strategies_attributes: [{ name: 'flexibleRollout', - parameters: { groupId: 'default', rollout: '15', stickiness: 'DEFAULT' }, + parameters: { groupId: 'default', rollout: '15', stickiness: 'default' }, scopes_attributes: [{ environment_scope: 'production' }] }] } @@ -666,7 +666,7 @@ RSpec.describe Projects::FeatureFlagsController do strategy_json = json_response['strategies'].first expect(strategy_json['name']).to eq('flexibleRollout') - expect(strategy_json['parameters']).to eq({ 'groupId' => 'default', 'rollout' => '15', 'stickiness' => 'DEFAULT' }) + expect(strategy_json['parameters']).to eq({ 'groupId' => 'default', 'rollout' => '15', 'stickiness' => 'default' }) expect(strategy_json['scopes'].count).to eq(1) scope_json = strategy_json['scopes'].first @@ -938,7 +938,7 @@ RSpec.describe Projects::FeatureFlagsController do it 'creates a flexibleRollout strategy' do put_request(new_version_flag, strategies_attributes: [{ name: 'flexibleRollout', - parameters: { groupId: 'default', rollout: '30', stickiness: 'DEFAULT' } + parameters: { groupId: 'default', rollout: '30', stickiness: 'default' } }]) expect(response).to have_gitlab_http_status(:ok) @@ -948,7 +948,7 @@ RSpec.describe Projects::FeatureFlagsController do expect(strategy_json['parameters']).to eq({ 'groupId' => 'default', 'rollout' => '30', - 'stickiness' => 'DEFAULT' + 'stickiness' => 'default' }) expect(strategy_json['scopes']).to eq([]) end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index d18369b8023..7f330681a82 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -51,7 +51,7 @@ FactoryBot.define do ci_job_token_scope_enabled { nil } end - before(:create) do |project, evaluator| + after(:build) do |project, evaluator| # Builds and MRs can't have higher visibility level than repository access level. builds_access_level = [evaluator.builds_access_level, evaluator.repository_access_level].min merge_requests_access_level = [evaluator.merge_requests_access_level, evaluator.repository_access_level].min @@ -67,21 +67,11 @@ FactoryBot.define do pages_access_level: evaluator.pages_access_level, metrics_dashboard_access_level: evaluator.metrics_dashboard_access_level, operations_access_level: evaluator.operations_access_level, - analytics_access_level: evaluator.analytics_access_level + analytics_access_level: evaluator.analytics_access_level, + container_registry_access_level: evaluator.container_registry_access_level } project.build_project_feature(hash) - - # This is not included in the `hash` above because the default_value_for in - # the ProjectFeature model overrides the value set by `build_project_feature` when - # evaluator.container_registry_access_level == ProjectFeature::DISABLED. - # - # This is because the default_value_for gem uses the _changed? method - # to determine if the default value should be applied. For new records, - # _changed? returns false if the value of the column is the same as - # the database default. - # See https://github.com/FooBarWidget/default_value_for/blob/release-3.4.0/lib/default_value_for.rb#L158. - project.project_feature.container_registry_access_level = evaluator.container_registry_access_level end after(:create) do |project, evaluator| diff --git a/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js b/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js index 02216370b79..07aa456e69e 100644 --- a/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js +++ b/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js @@ -66,15 +66,14 @@ describe('feature_flags/components/strategies/flexible_rollout.vue', () => { }); it('emits a change when the stickiness value changes', async () => { - stickinessSelect.setValue('USERID'); - await wrapper.vm.$nextTick(); + await stickinessSelect.setValue('userId'); expect(wrapper.emitted('change')).toEqual([ [ { parameters: { rollout: flexibleRolloutStrategy.parameters.rollout, groupId: PERCENT_ROLLOUT_GROUP_ID, - stickiness: 'USERID', + stickiness: 'userId', }, }, ], diff --git a/spec/frontend/feature_flags/mock_data.js b/spec/frontend/feature_flags/mock_data.js index b5f09ac1957..4c40c2acf01 100644 --- a/spec/frontend/feature_flags/mock_data.js +++ b/spec/frontend/feature_flags/mock_data.js @@ -76,7 +76,7 @@ export const percentRolloutStrategy = { export const flexibleRolloutStrategy = { name: ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT, - parameters: { rollout: '50', groupId: 'default', stickiness: 'DEFAULT' }, + parameters: { rollout: '50', groupId: 'default', stickiness: 'default' }, scopes: [], }; diff --git a/spec/migrations/20210722150102_operations_feature_flags_correct_flexible_rollout_values_spec.rb b/spec/migrations/20210722150102_operations_feature_flags_correct_flexible_rollout_values_spec.rb new file mode 100644 index 00000000000..130ad45ffc1 --- /dev/null +++ b/spec/migrations/20210722150102_operations_feature_flags_correct_flexible_rollout_values_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration!('operations_feature_flags_correct_flexible_rollout_values') + +RSpec.describe OperationsFeatureFlagsCorrectFlexibleRolloutValues, :migration do + let_it_be(:strategies) { table(:operations_strategies) } + + let(:namespace) { table(:namespaces).create!(name: 'feature_flag', path: 'feature_flag') } + let(:project) { table(:projects).create!(namespace_id: namespace.id) } + let(:feature_flag) { table(:operations_feature_flags).create!(project_id: project.id, active: true, name: 'foo', iid: 1) } + + describe "#up" do + described_class::STICKINESS.each do |old, new| + it "corrects parameters for flexible rollout stickiness #{old}" do + reversible_migration do |migration| + parameters = { groupId: "default", rollout: "100", stickiness: old } + strategy = create_strategy(parameters) + + migration.before -> { + expect(strategy.reload.parameters).to eq({ "groupId" => "default", "rollout" => "100", "stickiness" => old }) + } + + migration.after -> { + expect(strategy.reload.parameters).to eq({ "groupId" => "default", "rollout" => "100", "stickiness" => new }) + } + end + end + end + + it 'ignores other strategies' do + reversible_migration do |migration| + parameters = { "groupId" => "default", "rollout" => "100", "stickiness" => "USERID" } + strategy = create_strategy(parameters, name: 'default') + + migration.before -> { + expect(strategy.reload.parameters).to eq(parameters) + } + + migration.after -> { + expect(strategy.reload.parameters).to eq(parameters) + } + end + end + + it 'ignores other stickiness' do + reversible_migration do |migration| + parameters = { "groupId" => "default", "rollout" => "100", "stickiness" => "FOO" } + strategy = create_strategy(parameters) + + migration.before -> { + expect(strategy.reload.parameters).to eq(parameters) + } + + migration.after -> { + expect(strategy.reload.parameters).to eq(parameters) + } + end + end + end + + def create_strategy(params, name: 'flexibleRollout') + strategies.create!(name: name, parameters: params, feature_flag_id: feature_flag.id) + end +end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index e2700378f5f..b945fa34ebb 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -208,23 +208,9 @@ RSpec.describe Namespace do it { is_expected.to include_module(Gitlab::VisibilityLevel) } it { is_expected.to include_module(Namespaces::Traversal::Recursive) } it { is_expected.to include_module(Namespaces::Traversal::Linear) } - it { is_expected.to include_module(Namespaces::Traversal::RecursiveScopes) } - it { is_expected.to include_module(Namespaces::Traversal::LinearScopes) } end - context 'traversal scopes' do - context 'recursive' do - before do - stub_feature_flags(use_traversal_ids: false) - end - - it_behaves_like 'namespace traversal scopes' - end - - context 'linear' do - it_behaves_like 'namespace traversal scopes' - end - end + it_behaves_like 'linear namespace traversal' context 'traversal_ids on create' do context 'default traversal_ids' do diff --git a/spec/models/operations/feature_flags/strategy_spec.rb b/spec/models/operations/feature_flags/strategy_spec.rb index 0ecb49e75f3..9289e3beab5 100644 --- a/spec/models/operations/feature_flags/strategy_spec.rb +++ b/spec/models/operations/feature_flags/strategy_spec.rb @@ -112,7 +112,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do end context 'when the strategy name is flexibleRollout' do - valid_parameters = { rollout: '40', groupId: 'mygroup', stickiness: 'DEFAULT' } + valid_parameters = { rollout: '40', groupId: 'mygroup', stickiness: 'default' } where(invalid_parameters: [ nil, {}, @@ -133,7 +133,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do [ [:rollout, '10'], - [:stickiness, 'DEFAULT'], + [:stickiness, 'default'], [:groupId, 'mygroup'] ].permutation(3).each do |parameters| it "allows the parameters in the order #{parameters.map { |p| p.first }.join(', ')}" do @@ -151,7 +151,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do "\n", "\t", "\n10", "20\n", "\n100", "100\n", "\n ", nil]) with_them do it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do - parameters = { stickiness: 'DEFAULT', groupId: 'mygroup', rollout: invalid_value } + parameters = { stickiness: 'default', groupId: 'mygroup', rollout: invalid_value } strategy = described_class.create(feature_flag: feature_flag, name: 'flexibleRollout', parameters: parameters) @@ -165,7 +165,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do where(valid_value: %w[0 1 10 38 100 93]) with_them do it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do - parameters = { stickiness: 'DEFAULT', groupId: 'mygroup', rollout: valid_value } + parameters = { stickiness: 'default', groupId: 'mygroup', rollout: valid_value } strategy = described_class.create(feature_flag: feature_flag, name: 'flexibleRollout', parameters: parameters) @@ -180,7 +180,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do '!bad', '.bad', 'Bad', 'bad1', "", " ", "b" * 33, "ba_d", "ba\nd"]) with_them do it 'must be a string value of up to 32 lowercase characters' do - parameters = { stickiness: 'DEFAULT', groupId: invalid_value, rollout: '40' } + parameters = { stickiness: 'default', groupId: invalid_value, rollout: '40' } strategy = described_class.create(feature_flag: feature_flag, name: 'flexibleRollout', parameters: parameters) @@ -192,7 +192,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do where(valid_value: ["somegroup", "anothergroup", "okay", "g", "a" * 32]) with_them do it 'must be a string value of up to 32 lowercase characters' do - parameters = { stickiness: 'DEFAULT', groupId: valid_value, rollout: '40' } + parameters = { stickiness: 'default', groupId: valid_value, rollout: '40' } strategy = described_class.create(feature_flag: feature_flag, name: 'flexibleRollout', parameters: parameters) @@ -203,7 +203,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do end describe 'stickiness' do - where(invalid_value: [nil, " ", "default", "DEFAULT\n", "UserId", "USER", "USERID "]) + where(invalid_value: [nil, " ", "DEFAULT", "DEFAULT\n", "UserId", "USER", "USERID "]) with_them do it 'must be a string representing a supported stickiness setting' do parameters = { stickiness: invalid_value, groupId: 'mygroup', rollout: '40' } @@ -212,12 +212,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do parameters: parameters) expect(strategy.errors[:parameters]).to eq([ - 'stickiness parameter must be DEFAULT, USERID, SESSIONID, or RANDOM' + 'stickiness parameter must be default, userId, sessionId, or random' ]) end end - where(valid_value: %w[DEFAULT USERID SESSIONID RANDOM]) + where(valid_value: %w[default userId sessionId random]) with_them do it 'must be a string representing a supported stickiness setting' do parameters = { stickiness: valid_value, groupId: 'mygroup', rollout: '40' } @@ -425,7 +425,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do user_list: user_list, parameters: { groupId: 'default', rollout: '10', - stickiness: 'DEFAULT' }) + stickiness: 'default' }) expect(strategy.errors[:user_list]).to eq(['must be blank']) end @@ -435,7 +435,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do name: 'flexibleRollout', parameters: { groupId: 'default', rollout: '10', - stickiness: 'DEFAULT' }) + stickiness: 'default' }) expect(strategy.errors[:user_list]).to be_empty end diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb index 0ef5a2046ba..0e276191af1 100644 --- a/spec/models/project_feature_spec.rb +++ b/spec/models/project_feature_spec.rb @@ -218,5 +218,29 @@ RSpec.describe ProjectFeature do end end end + + context 'test build factory' do + let(:project) { build(:project, container_registry_access_level: level) } + + subject { project.container_registry_access_level } + + context 'private' do + let(:level) { ProjectFeature::PRIVATE } + + it { is_expected.to eq(level) } + end + + context 'enabled' do + let(:level) { ProjectFeature::ENABLED } + + it { is_expected.to eq(level) } + end + + context 'disabled' do + let(:level) { ProjectFeature::DISABLED } + + it { is_expected.to eq(level) } + end + end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 66d650b4f84..72c45de402e 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -318,7 +318,8 @@ RSpec.describe Project, factory_default: :keep do end it 'validates presence of project_feature' do - project = build(:project, project_feature: nil) + project = build(:project) + project.project_feature = nil expect(project).not_to be_valid end diff --git a/spec/requests/api/feature_flags_spec.rb b/spec/requests/api/feature_flags_spec.rb index 8edf8825fb2..8c8c6803a38 100644 --- a/spec/requests/api/feature_flags_spec.rb +++ b/spec/requests/api/feature_flags_spec.rb @@ -417,7 +417,7 @@ RSpec.describe API::FeatureFlags do version: 'new_version_flag', strategies: [{ name: 'flexibleRollout', - parameters: { groupId: 'default', rollout: '50', stickiness: 'DEFAULT' }, + parameters: { groupId: 'default', rollout: '50', stickiness: 'default' }, scopes: [{ environment_scope: 'staging' }] @@ -434,7 +434,7 @@ RSpec.describe API::FeatureFlags do expect(feature_flag.version).to eq('new_version_flag') expect(feature_flag.strategies.map { |s| s.slice(:name, :parameters).deep_symbolize_keys }).to eq([{ name: 'flexibleRollout', - parameters: { groupId: 'default', rollout: '50', stickiness: 'DEFAULT' } + parameters: { groupId: 'default', rollout: '50', stickiness: 'default' } }]) expect(feature_flag.strategies.first.scopes.map { |s| s.slice(:environment_scope).deep_symbolize_keys }).to eq([{ environment_scope: 'staging' @@ -630,7 +630,7 @@ RSpec.describe API::FeatureFlags do strategies: [{ id: strategy.id, name: 'flexibleRollout', - parameters: { groupId: 'default', rollout: '10', stickiness: 'DEFAULT' } + parameters: { groupId: 'default', rollout: '10', stickiness: 'default' } }] } @@ -642,7 +642,7 @@ RSpec.describe API::FeatureFlags do expect(result).to eq([{ id: strategy.id, name: 'flexibleRollout', - parameters: { groupId: 'default', rollout: '10', stickiness: 'DEFAULT' } + parameters: { groupId: 'default', rollout: '10', stickiness: 'default' } }]) end @@ -677,7 +677,7 @@ RSpec.describe API::FeatureFlags do params = { strategies: [{ name: 'flexibleRollout', - parameters: { groupId: 'default', rollout: '10', stickiness: 'DEFAULT' } + parameters: { groupId: 'default', rollout: '10', stickiness: 'default' } }] } @@ -694,7 +694,7 @@ RSpec.describe API::FeatureFlags do parameters: {} }, { name: 'flexibleRollout', - parameters: { groupId: 'default', rollout: '10', stickiness: 'DEFAULT' } + parameters: { groupId: 'default', rollout: '10', stickiness: 'default' } }]) end diff --git a/spec/support/shared_examples/namespaces/linear_traversal_examples.rb b/spec/support/shared_examples/namespaces/linear_traversal_examples.rb new file mode 100644 index 00000000000..2fd90c36953 --- /dev/null +++ b/spec/support/shared_examples/namespaces/linear_traversal_examples.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# Traversal examples common to linear and recursive methods are in +# spec/support/shared_examples/namespaces/traversal_examples.rb + +RSpec.shared_examples 'linear namespace traversal' do + context 'when use_traversal_ids feature flag is enabled' do + before do + stub_feature_flags(use_traversal_ids: true) + end + + context 'scopes' do + describe '.as_ids' do + let_it_be(:namespace1) { create(:group) } + let_it_be(:namespace2) { create(:group) } + + subject { Namespace.where(id: [namespace1, namespace2]).as_ids.pluck(:id) } + + it { is_expected.to contain_exactly(namespace1.id, namespace2.id) } + end + end + end +end diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb deleted file mode 100644 index bc17d104034..00000000000 --- a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'namespace traversal scopes' do - # Hierarchy 1 - let_it_be(:group_1) { create(:group) } - let_it_be(:nested_group_1) { create(:group, parent: group_1) } - let_it_be(:deep_nested_group_1) { create(:group, parent: nested_group_1) } - - # Hierarchy 2 - let_it_be(:group_2) { create(:group) } - let_it_be(:nested_group_2) { create(:group, parent: group_2) } - let_it_be(:deep_nested_group_2) { create(:group, parent: nested_group_2) } - - # All groups - let_it_be(:groups) do - [ - group_1, nested_group_1, deep_nested_group_1, - group_2, nested_group_2, deep_nested_group_2 - ] - end - - describe '.as_ids' do - subject { described_class.where(id: [group_1, group_2]).as_ids.pluck(:id) } - - it { is_expected.to contain_exactly(group_1.id, group_2.id) } - end - - describe '.without_sti_condition' do - subject { described_class.without_sti_condition } - - it { expect(subject.where_values_hash).not_to have_key(:type) } - end - - describe '.self_and_descendants' do - subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_descendants } - - it { is_expected.to contain_exactly(nested_group_1, deep_nested_group_1, nested_group_2, deep_nested_group_2) } - - context 'with duplicate descendants' do - subject { described_class.where(id: [group_1, group_2, nested_group_1]).self_and_descendants } - - it { is_expected.to match_array(groups) } - end - end - - describe '.self_and_descendant_ids' do - subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_descendant_ids.pluck(:id) } - - it { is_expected.to contain_exactly(nested_group_1.id, deep_nested_group_1.id, nested_group_2.id, deep_nested_group_2.id) } - end -end diff --git a/yarn.lock b/yarn.lock index cd8511bbd2f..7870aa23b58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3418,12 +3418,12 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@2, commander@^2.10.0, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@~2.20.0: - version "2.20.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" - integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== +commander@2, commander@^2.19.0, commander@^2.20.0, commander@^2.20.3, commander@~2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -commander@^6.0.0, commander@^6.2.0, commander@~6.2.1: +commander@^6.0.0, commander@^6.1.0, commander@^6.2.0, commander@~6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== @@ -10560,7 +10560,7 @@ semver-diff@^3.1.1: dependencies: semver "^6.3.0" -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -12882,14 +12882,14 @@ yarn-check-webpack-plugin@^1.2.0: dependencies: chalk "^2.4.2" -yarn-deduplicate@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/yarn-deduplicate/-/yarn-deduplicate-1.1.1.tgz#19b4a87654b66f55bf3a4bd6b153b4e4ab1b6e6d" - integrity sha512-2FDJ1dFmtvqhRmfja89ohYzpaheCYg7BFBSyaUq+kxK0y61C9oHv1XaQovCWGJtP2WU8PksQOgzMVV7oQOobzw== +yarn-deduplicate@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/yarn-deduplicate/-/yarn-deduplicate-3.1.0.tgz#3018d93e95f855f236a215b591fe8bc4bcabba3e" + integrity sha512-q2VZ6ThNzQpGfNpkPrkmV7x5HT9MOhCUsTxVTzyyZB0eSXz1NTodHn+r29DlLb+peKk8iXxzdUVhQG9pI7moFw== dependencies: "@yarnpkg/lockfile" "^1.1.0" - commander "^2.10.0" - semver "^5.3.0" + commander "^6.1.0" + semver "^7.3.2" yeast@0.1.2: version "0.1.2"