Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									2644e59eb5
								
							
						
					
					
						commit
						a303eb5d32
					
				
							
								
								
									
										4
									
								
								Gemfile
								
								
								
								
							
							
						
						
									
										4
									
								
								Gemfile
								
								
								
								
							|  | @ -402,7 +402,7 @@ end | ||||||
| 
 | 
 | ||||||
| group :development, :test do | group :development, :test do | ||||||
|   gem 'deprecation_toolkit', '~> 1.5.1', require: false # rubocop:todo Gemfile/MissingFeatureCategory |   gem 'deprecation_toolkit', '~> 1.5.1', require: false # rubocop:todo Gemfile/MissingFeatureCategory | ||||||
|   gem 'bullet', '~> 7.1.1' # rubocop:todo Gemfile/MissingFeatureCategory |   gem 'bullet', '~> 7.1.2' # rubocop:todo Gemfile/MissingFeatureCategory | ||||||
|   gem 'parser', '~> 3.2', '>= 3.2.2.4' # rubocop:todo Gemfile/MissingFeatureCategory |   gem 'parser', '~> 3.2', '>= 3.2.2.4' # rubocop:todo Gemfile/MissingFeatureCategory | ||||||
|   gem 'pry-byebug' # rubocop:todo Gemfile/MissingFeatureCategory |   gem 'pry-byebug' # rubocop:todo Gemfile/MissingFeatureCategory | ||||||
|   gem 'pry-rails', '~> 0.3.9' # rubocop:todo Gemfile/MissingFeatureCategory |   gem 'pry-rails', '~> 0.3.9' # rubocop:todo Gemfile/MissingFeatureCategory | ||||||
|  | @ -434,7 +434,7 @@ group :development, :test do | ||||||
| 
 | 
 | ||||||
|   gem 'knapsack', '~> 1.21.1', feature_category: :tooling |   gem 'knapsack', '~> 1.21.1', feature_category: :tooling | ||||||
|   gem 'crystalball', '~> 0.7.0', require: false, feature_category: :tooling |   gem 'crystalball', '~> 0.7.0', require: false, feature_category: :tooling | ||||||
|   gem 'test_file_finder', '~> 0.1.3', feature_category: :tooling |   gem 'test_file_finder', '~> 0.2.1', feature_category: :tooling | ||||||
| 
 | 
 | ||||||
|   gem 'simple_po_parser', '~> 1.1.6', require: false # rubocop:todo Gemfile/MissingFeatureCategory |   gem 'simple_po_parser', '~> 1.1.6', require: false # rubocop:todo Gemfile/MissingFeatureCategory | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -64,7 +64,7 @@ | ||||||
| {"name":"bootsnap","version":"1.17.0","platform":"ruby","checksum":"6b0ea4dd68f0d424968dcd13953c3f04b13a19a8761c540d3af13507fcfa1347"}, | {"name":"bootsnap","version":"1.17.0","platform":"ruby","checksum":"6b0ea4dd68f0d424968dcd13953c3f04b13a19a8761c540d3af13507fcfa1347"}, | ||||||
| {"name":"browser","version":"5.3.1","platform":"ruby","checksum":"62745301701ff2c6c5d32d077bb12532b20be261929dcb52c6781ed0d5658b3c"}, | {"name":"browser","version":"5.3.1","platform":"ruby","checksum":"62745301701ff2c6c5d32d077bb12532b20be261929dcb52c6781ed0d5658b3c"}, | ||||||
| {"name":"builder","version":"3.2.4","platform":"ruby","checksum":"99caf08af60c8d7f3a6b004029c4c3c0bdaebced6c949165fe98f1db27fbbc10"}, | {"name":"builder","version":"3.2.4","platform":"ruby","checksum":"99caf08af60c8d7f3a6b004029c4c3c0bdaebced6c949165fe98f1db27fbbc10"}, | ||||||
| {"name":"bullet","version":"7.1.1","platform":"ruby","checksum":"ad7789d9ad2bfe772f96620ba8f927e756c74525f2c03e7843d3518ce50e5b9c"}, | {"name":"bullet","version":"7.1.2","platform":"ruby","checksum":"429725c174cb74ca0ae99b9720bf22cab80be59ee9401805f7ecc9ac62cbb3bb"}, | ||||||
| {"name":"bundler-audit","version":"0.9.1","platform":"ruby","checksum":"bdc716fc21cd8652a6507b137e5bc51f5e0e4f6f106a114ab004c89d0200bd3d"}, | {"name":"bundler-audit","version":"0.9.1","platform":"ruby","checksum":"bdc716fc21cd8652a6507b137e5bc51f5e0e4f6f106a114ab004c89d0200bd3d"}, | ||||||
| {"name":"byebug","version":"11.1.3","platform":"ruby","checksum":"2485944d2bb21283c593d562f9ae1019bf80002143cc3a255aaffd4e9cf4a35b"}, | {"name":"byebug","version":"11.1.3","platform":"ruby","checksum":"2485944d2bb21283c593d562f9ae1019bf80002143cc3a255aaffd4e9cf4a35b"}, | ||||||
| {"name":"capybara","version":"3.39.2","platform":"ruby","checksum":"d6f0ca5f30897e64789428d4b047a0df105815a302069913578ac35d5ca99884"}, | {"name":"capybara","version":"3.39.2","platform":"ruby","checksum":"d6f0ca5f30897e64789428d4b047a0df105815a302069913578ac35d5ca99884"}, | ||||||
|  | @ -637,7 +637,7 @@ | ||||||
| {"name":"terminal-table","version":"3.0.2","platform":"ruby","checksum":"f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91"}, | {"name":"terminal-table","version":"3.0.2","platform":"ruby","checksum":"f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91"}, | ||||||
| {"name":"terser","version":"1.0.2","platform":"ruby","checksum":"80c2e0bc7e2db4e12e8529658f9e0820e13d685ae67d745bf981f269743bb28e"}, | {"name":"terser","version":"1.0.2","platform":"ruby","checksum":"80c2e0bc7e2db4e12e8529658f9e0820e13d685ae67d745bf981f269743bb28e"}, | ||||||
| {"name":"test-prof","version":"1.2.3","platform":"ruby","checksum":"c52a40194cb30f399ed3eb6beb4c45b5daad8b8eb418e8ef69089e4dc7e01fd6"}, | {"name":"test-prof","version":"1.2.3","platform":"ruby","checksum":"c52a40194cb30f399ed3eb6beb4c45b5daad8b8eb418e8ef69089e4dc7e01fd6"}, | ||||||
| {"name":"test_file_finder","version":"0.1.4","platform":"ruby","checksum":"bc36d8339eac4fb9dc36514a7c5f4d389ac2fb6d010716fc715c5c8fbb98eacd"}, | {"name":"test_file_finder","version":"0.2.1","platform":"ruby","checksum":"a5e9b369d80c76aefbb609acf5e11d89a048f35e565de3cc261c20112f0fcdb3"}, | ||||||
| {"name":"text","version":"1.3.1","platform":"ruby","checksum":"2fbbbc82c1ce79c4195b13018a87cbb00d762bda39241bb3cdc32792759dd3f4"}, | {"name":"text","version":"1.3.1","platform":"ruby","checksum":"2fbbbc82c1ce79c4195b13018a87cbb00d762bda39241bb3cdc32792759dd3f4"}, | ||||||
| {"name":"thor","version":"1.2.2","platform":"ruby","checksum":"2f93c652828cba9fcf4f65f5dc8c306f1a7317e05aad5835a13740122c17f24c"}, | {"name":"thor","version":"1.2.2","platform":"ruby","checksum":"2f93c652828cba9fcf4f65f5dc8c306f1a7317e05aad5835a13740122c17f24c"}, | ||||||
| {"name":"thread_safe","version":"0.3.6","platform":"java","checksum":"bb28394cd0924c068981adee71f36a81c85c92e7d74d3f62372bd51489a0e0c2"}, | {"name":"thread_safe","version":"0.3.6","platform":"java","checksum":"bb28394cd0924c068981adee71f36a81c85c92e7d74d3f62372bd51489a0e0c2"}, | ||||||
|  |  | ||||||
							
								
								
									
										10
									
								
								Gemfile.lock
								
								
								
								
							
							
						
						
									
										10
									
								
								Gemfile.lock
								
								
								
								
							|  | @ -329,7 +329,7 @@ GEM | ||||||
|       msgpack (~> 1.2) |       msgpack (~> 1.2) | ||||||
|     browser (5.3.1) |     browser (5.3.1) | ||||||
|     builder (3.2.4) |     builder (3.2.4) | ||||||
|     bullet (7.1.1) |     bullet (7.1.2) | ||||||
|       activesupport (>= 3.0.0) |       activesupport (>= 3.0.0) | ||||||
|       uniform_notifier (~> 1.11) |       uniform_notifier (~> 1.11) | ||||||
|     bundler-audit (0.9.1) |     bundler-audit (0.9.1) | ||||||
|  | @ -1596,8 +1596,8 @@ GEM | ||||||
|     terser (1.0.2) |     terser (1.0.2) | ||||||
|       execjs (>= 0.3.0, < 3) |       execjs (>= 0.3.0, < 3) | ||||||
|     test-prof (1.2.3) |     test-prof (1.2.3) | ||||||
|     test_file_finder (0.1.4) |     test_file_finder (0.2.1) | ||||||
|       faraday (~> 1.0) |       faraday (>= 1.0, < 3.0, != 2.0.0) | ||||||
|     text (1.3.1) |     text (1.3.1) | ||||||
|     thor (1.2.2) |     thor (1.2.2) | ||||||
|     thread_safe (0.3.6) |     thread_safe (0.3.6) | ||||||
|  | @ -1771,7 +1771,7 @@ DEPENDENCIES | ||||||
|   better_errors (~> 2.10.1) |   better_errors (~> 2.10.1) | ||||||
|   bootsnap (~> 1.17.0) |   bootsnap (~> 1.17.0) | ||||||
|   browser (~> 5.3.1) |   browser (~> 5.3.1) | ||||||
|   bullet (~> 7.1.1) |   bullet (~> 7.1.2) | ||||||
|   bundler-audit (~> 0.9.1) |   bundler-audit (~> 0.9.1) | ||||||
|   bundler-checksum (~> 0.1.0)! |   bundler-checksum (~> 0.1.0)! | ||||||
|   capybara (~> 3.39, >= 3.39.2) |   capybara (~> 3.39, >= 3.39.2) | ||||||
|  | @ -2036,7 +2036,7 @@ DEPENDENCIES | ||||||
|   telesignenterprise (~> 2.2) |   telesignenterprise (~> 2.2) | ||||||
|   terser (= 1.0.2) |   terser (= 1.0.2) | ||||||
|   test-prof (~> 1.2.3) |   test-prof (~> 1.2.3) | ||||||
|   test_file_finder (~> 0.1.3) |   test_file_finder (~> 0.2.1) | ||||||
|   thrift (>= 0.16.0) |   thrift (>= 0.16.0) | ||||||
|   timfel-krb5-auth (~> 0.8) |   timfel-krb5-auth (~> 0.8) | ||||||
|   toml-rb (~> 2.2.0) |   toml-rb (~> 2.2.0) | ||||||
|  |  | ||||||
|  | @ -3,10 +3,11 @@ | ||||||
| module Projects | module Projects | ||||||
|   module Ml |   module Ml | ||||||
|     class ModelsIndexComponent < ViewComponent::Base |     class ModelsIndexComponent < ViewComponent::Base | ||||||
|       attr_reader :paginator |       attr_reader :paginator, :model_count | ||||||
| 
 | 
 | ||||||
|       def initialize(paginator:) |       def initialize(paginator:, model_count:) | ||||||
|         @paginator = paginator |         @paginator = paginator | ||||||
|  |         @model_count = model_count | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       private |       private | ||||||
|  | @ -14,7 +15,8 @@ module Projects | ||||||
|       def view_model |       def view_model | ||||||
|         vm = { |         vm = { | ||||||
|           models: models_view_model, |           models: models_view_model, | ||||||
|           page_info: page_info_view_model |           page_info: page_info_view_model, | ||||||
|  |           model_count: model_count | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         Gitlab::Json.generate(vm.deep_transform_keys { |k| k.to_s.camelize(:lower) }) |         Gitlab::Json.generate(vm.deep_transform_keys { |k| k.to_s.camelize(:lower) }) | ||||||
|  | @ -26,7 +28,8 @@ module Projects | ||||||
|             name: m.name, |             name: m.name, | ||||||
|             version: m.latest_version_name, |             version: m.latest_version_name, | ||||||
|             version_count: m.version_count, |             version_count: m.version_count, | ||||||
|             path: m.latest_package_path |             version_package_path: m.latest_package_path, | ||||||
|  |             version_path: m.latest_version_path | ||||||
|           } |           } | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|  |  | ||||||
|  | @ -15,9 +15,11 @@ module Projects | ||||||
|                         .transform_keys(&:underscore) |                         .transform_keys(&:underscore) | ||||||
|                         .permit(:name, :order_by, :sort) |                         .permit(:name, :order_by, :sort) | ||||||
| 
 | 
 | ||||||
|         @paginator = ::Projects::Ml::ModelFinder.new(@project, find_params) |         finder = ::Projects::Ml::ModelFinder.new(@project, find_params) | ||||||
|                                                 .execute | 
 | ||||||
|                                                 .keyset_paginate(cursor: params[:cursor], per_page: MAX_MODELS_PER_PAGE) |         @paginator = finder.execute.keyset_paginate(cursor: params[:cursor], per_page: MAX_MODELS_PER_PAGE) | ||||||
|  | 
 | ||||||
|  |         @model_count = finder.count | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       def show; end |       def show; end | ||||||
|  |  | ||||||
|  | @ -3,6 +3,8 @@ | ||||||
| module Projects | module Projects | ||||||
|   module Ml |   module Ml | ||||||
|     class ModelFinder |     class ModelFinder | ||||||
|  |       include Gitlab::Utils::StrongMemoize | ||||||
|  | 
 | ||||||
|       VALID_ORDER_BY = %w[name created_at id].freeze |       VALID_ORDER_BY = %w[name created_at id].freeze | ||||||
|       VALID_SORT = %w[asc desc].freeze |       VALID_SORT = %w[asc desc].freeze | ||||||
| 
 | 
 | ||||||
|  | @ -12,16 +14,26 @@ module Projects | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       def execute |       def execute | ||||||
|  |         relation | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       def count | ||||||
|  |         relation.length | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       private | ||||||
|  | 
 | ||||||
|  |       def relation | ||||||
|         @models = ::Ml::Model |         @models = ::Ml::Model | ||||||
|           .by_project(project) |             .by_project(project) | ||||||
|           .including_latest_version |             .including_latest_version | ||||||
|           .with_version_count |             .including_project | ||||||
|  |             .with_version_count | ||||||
| 
 | 
 | ||||||
|         @models = by_name |         @models = by_name | ||||||
|         ordered |         ordered | ||||||
|       end |       end | ||||||
| 
 |       strong_memoize_attr :relation | ||||||
|       private |  | ||||||
| 
 | 
 | ||||||
|       def by_name |       def by_name | ||||||
|         return models unless params[:name].present? |         return models unless params[:name].present? | ||||||
|  |  | ||||||
|  | @ -22,6 +22,7 @@ module Ml | ||||||
|     has_one :latest_version, -> { latest_by_model }, class_name: 'Ml::ModelVersion', inverse_of: :model |     has_one :latest_version, -> { latest_by_model }, class_name: 'Ml::ModelVersion', inverse_of: :model | ||||||
| 
 | 
 | ||||||
|     scope :including_latest_version, -> { includes(:latest_version) } |     scope :including_latest_version, -> { includes(:latest_version) } | ||||||
|  |     scope :including_project, -> { includes(:project) } | ||||||
|     scope :with_version_count, -> { |     scope :with_version_count, -> { | ||||||
|       left_outer_joins(:versions) |       left_outer_joins(:versions) | ||||||
|         .select("ml_models.*, count(ml_model_versions.id) as version_count") |         .select("ml_models.*, count(ml_model_versions.id) as version_count") | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ module Ml | ||||||
|     presents ::Ml::Model, as: :model |     presents ::Ml::Model, as: :model | ||||||
| 
 | 
 | ||||||
|     def latest_version_name |     def latest_version_name | ||||||
|       model.latest_version&.version |       latest_version&.version | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def version_count |     def version_count | ||||||
|  | @ -15,13 +15,21 @@ module Ml | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def latest_package_path |     def latest_package_path | ||||||
|       return unless model.latest_version&.package_id.present? |       latest_version&.package_path | ||||||
|  |     end | ||||||
| 
 | 
 | ||||||
|       Gitlab::Routing.url_helpers.project_package_path(model.project, model.latest_version.package_id) |     def latest_version_path | ||||||
|  |       latest_version&.path | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def path |     def path | ||||||
|       Gitlab::Routing.url_helpers.project_ml_model_path(model.project, model.id) |       project_ml_model_path(model.project, model.id) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     private | ||||||
|  | 
 | ||||||
|  |     def latest_version | ||||||
|  |       model.latest_version&.present | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -9,11 +9,17 @@ module Ml | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def path |     def path | ||||||
|       Gitlab::Routing.url_helpers.project_ml_model_version_path( |       project_ml_model_version_path( | ||||||
|         model_version.model.project, |         model_version.model.project, | ||||||
|         model_version.model, |         model_version.model, | ||||||
|         model_version |         model_version | ||||||
|       ) |       ) | ||||||
|     end |     end | ||||||
|  | 
 | ||||||
|  |     def package_path | ||||||
|  |       return unless model_version.package_id.present? | ||||||
|  | 
 | ||||||
|  |       project_package_path(model_version.project, model_version.package_id) | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -9,7 +9,12 @@ module MergeRequests | ||||||
|     def initialize(project:, current_user:, changes:, push_options:, params: {}) |     def initialize(project:, current_user:, changes:, push_options:, params: {}) | ||||||
|       super(project: project, current_user: current_user, params: params) |       super(project: project, current_user: current_user, params: params) | ||||||
| 
 | 
 | ||||||
|       @target_project = @project.default_merge_request_target |       @target_project = if push_options[:target_project] | ||||||
|  |                           Project.find_by_full_path(push_options[:target_project]) | ||||||
|  |                         else | ||||||
|  |                           @project.default_merge_request_target | ||||||
|  |                         end | ||||||
|  | 
 | ||||||
|       @changes = Gitlab::ChangesList.new(changes) |       @changes = Gitlab::ChangesList.new(changes) | ||||||
|       @push_options = push_options |       @push_options = push_options | ||||||
|       @errors = [] |       @errors = [] | ||||||
|  | @ -63,6 +68,10 @@ module MergeRequests | ||||||
|         return |         return | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |       unless project == target_project || project.in_fork_network_of?(target_project) | ||||||
|  |         errors << "Projects #{project.full_path} and #{target_project.full_path} are not in the same network" | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|       unless target_project.merge_requests_enabled? |       unless target_project.merge_requests_enabled? | ||||||
|         errors << "Merge requests are not enabled for project #{target_project.full_path}" |         errors << "Merge requests are not enabled for project #{target_project.full_path}" | ||||||
|       end |       end | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| - breadcrumb_title s_('ModelRegistry|Model registry') | - breadcrumb_title s_('ModelRegistry|Model registry') | ||||||
| - page_title s_('ModelRegistry|Model registry') | - page_title s_('ModelRegistry|Model registry') | ||||||
| 
 | 
 | ||||||
| = render(Projects::Ml::ModelsIndexComponent.new(paginator: @paginator)) | = render(Projects::Ml::ModelsIndexComponent.new(paginator: @paginator, model_count: @model_count)) | ||||||
|  |  | ||||||
|  | @ -0,0 +1,7 @@ | ||||||
|  | --- | ||||||
|  | migration_job_name: DeleteInvalidProtectedBranchMergeAccessLevels | ||||||
|  | description: Remove rows from protected_branch_merge_access_levels for groups that do not have project_group_links to the project for the associated protected branch | ||||||
|  | feature_category: source_code_management | ||||||
|  | introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427486 | ||||||
|  | milestone: 16.6 | ||||||
|  | queued_migration_version: 20231016173129 | ||||||
|  | @ -0,0 +1,7 @@ | ||||||
|  | --- | ||||||
|  | migration_job_name: DeleteInvalidProtectedBranchPushAccessLevels | ||||||
|  | description: Remove rows from protected_branch_push_access_levels for groups that do not have project_group_links to the project for the associated protected branch | ||||||
|  | feature_category: source_code_management | ||||||
|  | introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427486 | ||||||
|  | milestone: 16.6 | ||||||
|  | queued_migration_version: 20231016194927 | ||||||
|  | @ -0,0 +1,7 @@ | ||||||
|  | --- | ||||||
|  | migration_job_name: DeleteInvalidProtectedTagCreateAccessLevels | ||||||
|  | description: Remove rows from protected_tag_create_access_levels for groups that do not have project_group_links to the project for the associated protected tag | ||||||
|  | feature_category: source_code_management | ||||||
|  | introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427486 | ||||||
|  | milestone: 16.6 | ||||||
|  | queued_migration_version: 20231016194943 | ||||||
|  | @ -0,0 +1,25 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class AddTemporaryIndexToMergeAccessLevels < Gitlab::Database::Migration[2.2] | ||||||
|  |   disable_ddl_transaction! | ||||||
|  |   milestone '16.6' | ||||||
|  | 
 | ||||||
|  |   INDEX_NAME = 'tmp_idx_protected_branch_merge_access_levels_on_id_with_group' | ||||||
|  | 
 | ||||||
|  |   def up | ||||||
|  |     # Temporary index to be removed in 16.7 https://gitlab.com/gitlab-org/gitlab/-/issues/430843 | ||||||
|  |     add_concurrent_index( | ||||||
|  |       :protected_branch_merge_access_levels, | ||||||
|  |       %i[id], | ||||||
|  |       where: 'group_id IS NOT NULL', | ||||||
|  |       name: INDEX_NAME | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def down | ||||||
|  |     remove_concurrent_index_by_name( | ||||||
|  |       :protected_branch_merge_access_levels, | ||||||
|  |       INDEX_NAME | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,28 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class QueueDeleteInvalidProtectedBranchMergeAccessLevels < Gitlab::Database::Migration[2.1] | ||||||
|  |   MIGRATION = "DeleteInvalidProtectedBranchMergeAccessLevels" | ||||||
|  |   DELAY_INTERVAL = 2.minutes | ||||||
|  |   MAX_BATCH_SIZE = 10_000 | ||||||
|  |   BATCH_SIZE = 5_000 | ||||||
|  |   SUB_BATCH_SIZE = 500 | ||||||
|  | 
 | ||||||
|  |   disable_ddl_transaction! | ||||||
|  |   restrict_gitlab_migration gitlab_schema: :gitlab_main | ||||||
|  | 
 | ||||||
|  |   def up | ||||||
|  |     queue_batched_background_migration( | ||||||
|  |       MIGRATION, | ||||||
|  |       :protected_branch_merge_access_levels, | ||||||
|  |       :id, | ||||||
|  |       job_interval: DELAY_INTERVAL, | ||||||
|  |       queued_migration_version: '20231016173129', | ||||||
|  |       batch_size: BATCH_SIZE, | ||||||
|  |       sub_batch_size: SUB_BATCH_SIZE | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def down | ||||||
|  |     delete_batched_background_migration(MIGRATION, :protected_branch_merge_access_levels, :id, []) | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,25 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class AddTemporaryIndexToPushAccessLevels < Gitlab::Database::Migration[2.2] | ||||||
|  |   disable_ddl_transaction! | ||||||
|  | 
 | ||||||
|  |   milestone '16.6' | ||||||
|  |   INDEX_NAME = 'tmp_idx_protected_branch_push_access_levels_on_id_with_group' | ||||||
|  | 
 | ||||||
|  |   def up | ||||||
|  |     # Temporary index to be removed in 16.7 https://gitlab.com/gitlab-org/gitlab/-/issues/430843 | ||||||
|  |     add_concurrent_index( | ||||||
|  |       :protected_branch_push_access_levels, | ||||||
|  |       %i[id], | ||||||
|  |       where: 'group_id IS NOT NULL', | ||||||
|  |       name: INDEX_NAME | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def down | ||||||
|  |     remove_concurrent_index_by_name( | ||||||
|  |       :protected_branch_push_access_levels, | ||||||
|  |       INDEX_NAME | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,28 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class QueueDeleteInvalidProtectedBranchPushAccessLevels < Gitlab::Database::Migration[2.1] | ||||||
|  |   MIGRATION = "DeleteInvalidProtectedBranchPushAccessLevels" | ||||||
|  |   DELAY_INTERVAL = 2.minutes | ||||||
|  |   MAX_BATCH_SIZE = 10_000 | ||||||
|  |   BATCH_SIZE = 5_000 | ||||||
|  |   SUB_BATCH_SIZE = 500 | ||||||
|  | 
 | ||||||
|  |   disable_ddl_transaction! | ||||||
|  |   restrict_gitlab_migration gitlab_schema: :gitlab_main | ||||||
|  | 
 | ||||||
|  |   def up | ||||||
|  |     queue_batched_background_migration( | ||||||
|  |       MIGRATION, | ||||||
|  |       :protected_branch_push_access_levels, | ||||||
|  |       :id, | ||||||
|  |       job_interval: DELAY_INTERVAL, | ||||||
|  |       queued_migration_version: '20231016194927', | ||||||
|  |       batch_size: BATCH_SIZE, | ||||||
|  |       sub_batch_size: SUB_BATCH_SIZE | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def down | ||||||
|  |     delete_batched_background_migration(MIGRATION, :protected_branch_push_access_levels, :id, []) | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,25 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class AddTemporaryIndexToCreateAccessLevels < Gitlab::Database::Migration[2.2] | ||||||
|  |   disable_ddl_transaction! | ||||||
|  |   milestone '16.6' | ||||||
|  | 
 | ||||||
|  |   INDEX_NAME = 'tmp_idx_protected_tag_create_access_levels_on_id_with_group' | ||||||
|  | 
 | ||||||
|  |   def up | ||||||
|  |     # Temporary index to be removed in 16.7 https://gitlab.com/gitlab-org/gitlab/-/issues/430843 | ||||||
|  |     add_concurrent_index( | ||||||
|  |       :protected_tag_create_access_levels, | ||||||
|  |       %i[id], | ||||||
|  |       where: 'group_id IS NOT NULL', | ||||||
|  |       name: INDEX_NAME | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def down | ||||||
|  |     remove_concurrent_index_by_name( | ||||||
|  |       :protected_tag_create_access_levels, | ||||||
|  |       INDEX_NAME | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,27 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class QueueDeleteInvalidProtectedTagCreateAccessLevels < Gitlab::Database::Migration[2.1] | ||||||
|  |   MIGRATION = "DeleteInvalidProtectedTagCreateAccessLevels" | ||||||
|  |   DELAY_INTERVAL = 2.minutes | ||||||
|  |   BATCH_SIZE = 10_000 | ||||||
|  |   SUB_BATCH_SIZE = 500 | ||||||
|  | 
 | ||||||
|  |   disable_ddl_transaction! | ||||||
|  |   restrict_gitlab_migration gitlab_schema: :gitlab_main | ||||||
|  | 
 | ||||||
|  |   def up | ||||||
|  |     queue_batched_background_migration( | ||||||
|  |       MIGRATION, | ||||||
|  |       :protected_tag_create_access_levels, | ||||||
|  |       :id, | ||||||
|  |       job_interval: DELAY_INTERVAL, | ||||||
|  |       queued_migration_version: '20231016194943', | ||||||
|  |       batch_size: BATCH_SIZE, | ||||||
|  |       sub_batch_size: SUB_BATCH_SIZE | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def down | ||||||
|  |     delete_batched_background_migration(MIGRATION, :protected_tag_create_access_levels, :id, []) | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | b720259efa4eb9fe75a3352a64b9e14ae7b048240daf34c40a66cc5ef409dcc0 | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | f886678df9907d2bf60f98c0184c91604069cd613b541a0476e30789f327df15 | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | 0f2e4b7fc2658b5063dbe8dea6c881fb59a9d99ed53332ae1bdb5578343c3e89 | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | 44474805c7858d07d093650e43f3313746976e4c523b408d029e32829a5b7301 | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | 43de9dd5e63a80c51aa21e42b7f41d03b6d36143afa45cf45ead6ee0cc8152cc | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | b17f7eaff454fab3e46e438d81fdebab14776322af261e0f2a12ceb69a5623a8 | ||||||
|  | @ -35071,6 +35071,12 @@ CREATE INDEX tmp_idx_orphaned_approval_merge_request_rules ON approval_merge_req | ||||||
| 
 | 
 | ||||||
| CREATE INDEX tmp_idx_orphaned_approval_project_rules ON approval_project_rules USING btree (id) WHERE ((report_type = ANY (ARRAY[2, 4])) AND (security_orchestration_policy_configuration_id IS NULL)); | CREATE INDEX tmp_idx_orphaned_approval_project_rules ON approval_project_rules USING btree (id) WHERE ((report_type = ANY (ARRAY[2, 4])) AND (security_orchestration_policy_configuration_id IS NULL)); | ||||||
| 
 | 
 | ||||||
|  | CREATE INDEX tmp_idx_protected_branch_merge_access_levels_on_id_with_group ON protected_branch_merge_access_levels USING btree (id) WHERE (group_id IS NOT NULL); | ||||||
|  | 
 | ||||||
|  | CREATE INDEX tmp_idx_protected_branch_push_access_levels_on_id_with_group ON protected_branch_push_access_levels USING btree (id) WHERE (group_id IS NOT NULL); | ||||||
|  | 
 | ||||||
|  | CREATE INDEX tmp_idx_protected_tag_create_access_levels_on_id_with_group ON protected_tag_create_access_levels USING btree (id) WHERE (group_id IS NOT NULL); | ||||||
|  | 
 | ||||||
| CREATE INDEX tmp_index_ci_job_artifacts_on_expire_at_where_locked_unknown ON ci_job_artifacts USING btree (expire_at, job_id) WHERE ((locked = 2) AND (expire_at IS NOT NULL)); | CREATE INDEX tmp_index_ci_job_artifacts_on_expire_at_where_locked_unknown ON ci_job_artifacts USING btree (expire_at, job_id) WHERE ((locked = 2) AND (expire_at IS NOT NULL)); | ||||||
| 
 | 
 | ||||||
| CREATE INDEX tmp_index_cis_vulnerability_reads_on_id ON vulnerability_reads USING btree (id) WHERE (report_type = 7); | CREATE INDEX tmp_index_cis_vulnerability_reads_on_id ON vulnerability_reads USING btree (id) WHERE (report_type = 7); | ||||||
|  |  | ||||||
|  | @ -120,51 +120,46 @@ However, Service-Integration will establish certain necessary and optional requi | ||||||
| 
 | 
 | ||||||
| ###### Ease of Use, Ownership Requirements | ###### Ease of Use, Ownership Requirements | ||||||
| 
 | 
 | ||||||
| 1. <a name="R100">`R100`</a>: Required: the platform should be easy to use: imagine Heroku with [GitLab Production Readiness-approved](https://about.gitlab.com/handbook/engineering/infrastructure/production/readiness/) defaults. | | ID | Required | Detail | Epic/Issue | Done? | | ||||||
| 1. <a name="R110">`R110`</a>: Required: with the exception of an Infrastructure-led onboarding process, services are owned, deployed and managed by stage-group teams. In other words,services follow a "You Build It, You Run It" model of ownership. | |---|---|---|---|---| | ||||||
| 1. <a name="R120">`R120`</a>: Required: programming-language agnostic: no requirements for services. Services should be packaged as container images. | | `R100` | Required    | The platform should be easy to use: imagine Heroku with [GitLab Production Readiness-approved](https://about.gitlab.com/handbook/engineering/infrastructure/production/readiness/) defaults. | [Runway to [BETA] : Increased Adoption and Self Service](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/1115) | **{dotted-circle}** No | | ||||||
| 1. <a name="R130">`R130`</a>: Recommended: Each service should be evaluated against the GitLab.com [Service Maturity Model](https://about.gitlab.com/handbook/engineering/infrastructure/service-maturity-model/). | | `R110` | Required    | With the exception of an Infrastructure-led onboarding process, services are owned, deployed and managed by stage-group teams. In other words,services follow a “You Build It, You Run It” model of ownership.| [[Paused] Discussion: Tiered Support Model for Runway](https://gitlab.com/gitlab-com/gl-infra/platform/runway/team/-/issues/97) | **{dotted-circle}** No | | ||||||
| 1. <a name="R140">`R140`</a>: Recommended: services using the platform have expedited production-readiness processes. | | `R120` | Required    | Programming-language agnostic: no requirements for services. Services should be packaged as container images.| [Runway to [BETA] : Increased Adoption and Self Service](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/1115) | **{dotted-circle}** No | | ||||||
|    1. Production-readiness requirements graded by service maturity: low-traffic, low-maturity experimental services will have lower requirement thresholds than more mature services. | | `R130` | Recommended | Each service should be evaluated against the GitLab.com [Service Maturity Model](https://about.gitlab.com/handbook/engineering/infrastructure/service-maturity-model/).| [Discussion: Introduce an 'Infrastructure Well-Architected Service Framework'](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/2537) | **{dotted-circle}** No | | ||||||
|    1. By default, the platform should provide services with defaults that would pass production-readiness review for the lowest service maturity-level. | | `R140` | Recommended | Services using the platform have expedited production-readiness processes. {::nomarkdown}<ol><li>Production-readiness requirements graded by service maturity: low-traffic, low-maturity experimental services will have lower requirement thresholds than more mature services. </li><li> By default, the platform should provide services with defaults that would pass production-readiness review for the lowest service maturity-level. </li><li> At introduction, lowest maturity services can be deployed without production readiness, provided the meet certain automatically validated requirements. This removes Infrastructure gate-keeping from being a blocker to experimental service delivery.</li></ol>{:/} | | | | ||||||
|    1. At introduction, lowest maturity services can be deployed without production readiness, provided the meet certain automatically validated requirements. This removes Infrastructure gate-keeping from being a blocker to experimental service delivery. |  | ||||||
| 
 | 
 | ||||||
| ###### Observability Requirements | ###### Observability Requirements | ||||||
| 
 | 
 | ||||||
| 1. <a name="R200">`R200`</a>: Required: the platform must provide SLIs for services out-of-the-box. | | ID | Required | Detail | Epic/Issue | Done? | | ||||||
|    1. While it is recommended that services expose internal metrics, it is not mandatory. The platform will provide monitoring from the load-balancer. This is to speed up deployment by removing barriers to experimentation. | |---|---|---|---|---| | ||||||
|    1. For services that provide internal metrics scrape endpoints, the platform must be configurable to collect these. | | `R200` | Required | The platform must provide SLIs for services out-of-the-box.{::nomarkdown}<ol><li>While it is recommended that services expose internal metrics, it is not mandatory. The platform will provide monitoring from the load-balancer. This is to speed up deployment by removing barriers to experimentation.</li><li>For services that provide internal metrics scrape endpoints, the platform must be configurable to collect these.</li><li>The platform must provide generic load-balancer level SLIs for all services. Service owners must be able to select from constructing SLIs from internal application metrics, the platform-provided external SLIs, or a combination of both.</li></ol>{:/} | [Observability: Default Metrics](https://gitlab.com/gitlab-com/gl-infra/platform/runway/team/-/issues/72), [Observability: Custom Metrics](https://gitlab.com/gitlab-com/gl-infra/platform/runway/team/-/issues/67) | **{check-circle}** Yes | | ||||||
|    1. The platform must provide generic load-balancer level SLIs for all services. Service owners must be able to select from constructing SLIs from internal application metrics, the platform-provided external SLIs, or a combination of both. | | `R210` | Required | Observability dashboards, rules, alerts (with per-term routing) must be generated from a manifest. | [Observability: Metrics Catalog](https://gitlab.com/gitlab-com/gl-infra/platform/runway/team/-/issues/74) | **{check-circle}** Yes | | ||||||
| 1. <a name="R210">`R210`</a>: Required: Observability dashboards, rules, alerts (with per-term routing) must be generated from a manifest. | | `R220` | Required | Standardized logging infrastructure.{::nomarkdown}<ol><li>Mandate that all logging emitted from services must be Structured JSON. Text logs are permitted but not recommended.</li><li>See <a href="#common-service-libraries">Common Service Libraries</a> for more details of building common SDKs for observability.</li></ol>{:/}  | [Observability: Logs in Elasticsearch for model-gateway](https://gitlab.com/gitlab-com/gl-infra/platform/runway/team/-/issues/75), [Observability: Runway logs available to users](https://gitlab.com/gitlab-com/gl-infra/platform/runway/team/-/issues/84) | | | ||||||
| 1. <a name="R220">`R220`</a>:Required: standardized logging infrastructure. |  | ||||||
|    1. Mandate that all logging emitted from services must be Structured JSON. Text logs are permitted but not recommended. |  | ||||||
|    1. See [Common Service Libraries](#common-service-libraries) for more details of building common SDKs for observability. |  | ||||||
| 
 | 
 | ||||||
| ###### Deployment Requirements | ###### Deployment Requirements | ||||||
| 
 | 
 | ||||||
| 1. <a name="R300">`R300`</a>: Required: No secrets stored in CI/CD. | | ID | Required | Detail | Epic/Issue | Done? | | ||||||
|    1. Authentication with Cloud Provider Resources should be exclusively via OIDC, managed as part of the platform. | |---|---|---|---|---| | ||||||
|    1. Secrets should be stored in the Infrastructure-provided Hashicorp Vault for the environment and passed to applications through files or environment variables. | | `R300` | Required | No secrets stored in CI/CD. {::nomarkdown} <ol><li>Authentication with Cloud Provider Resources should be exclusively via OIDC, managed as part of the platform.</li><li> Secrets should be stored in the Infrastructure-provided Hashicorp Vault for the environment and passed to applications through files or environment variables. </li><li>Generation and management of service account tokens should be done declaratively, without manual interaction.</li></ul>{:/} | [Secrets Management](https://gitlab.com/gitlab-com/gl-infra/platform/runway/team/-/issues/52) | **{dotted-circle}** No | | ||||||
|    1. Generation and management of service account tokens should be done declaratively, without manual interaction. | | `R310` | Required | Multiple environment should be supported, eg Staging and Production. |  | **{check-circle}** Yes | | ||||||
| 1. <a name="R310">`R310`</a>: Required: multiple environment should be supported, eg Staging and Production. | | `R320` | Required | The platform should be cost-effective. Kubernetes clusters should support multiple services and teams. |  |  | | ||||||
| 1. <a name="R320">`R320`</a>: Required: the platform should be cost-effective. Kubernetes clusters should support multiple services and teams. | | `R330` | Recommended | Gradual rollouts, rollbacks, blue-green deployments. |  |  | | ||||||
| 1. <a name="R330">`R330`</a>: Recommended: gradual rollouts, rollbacks, blue-green deployments. | | `R340` | Required | Services should be isolated from one another. |  |  | | ||||||
| 1. <a name="R340">`R340`</a>: Required: services should be isolated from one another. | | `R350` | Recommended | Services should have the ability to specify node characteristic requirements (eg, GPU). |  |  | | ||||||
| 1. <a name="R350">`R350`</a>: Recommended: services should have the ability to specify node characteristic requirements (eg, GPU). | | `R360` | Required | Developers should not need knowledge of Helm, Kubernetes, Prometheus in order to deploy. All required values are configured and validated in project-hosted manifest before generating Kubernetes manifests, Prometheus rules, etc. |  |  | | ||||||
| 1. <a name="R360">`R360`</a>: Required: Developers should not need knowledge of Helm, Kubernetes, Prometheus in order to deploy. All required values are configured and validated in project-hosted manifest before generating Kubernetes manifests, Prometheus rules, etc. | | `R370` |  | Initially services should be synchronous only - using REST or GRPC requests.{::nomarkdown}<ol><li>This does not however preclude long-running HTTP(s) requests, for example long-polling or Websocket requests.</li></ol>{:/} |  |  | | ||||||
| 1. <a name="R370">`R370`</a>: Initially services should be synchronous only - using REST or GRPC requests. | | `R390` |  | Each service hosted in its own GitLab repository with deployment manifest stored in the repository.  {::nomarkdown}<ol><li>Continuous deployments that are initiated from the CI pipeline of the corresponding GitLab repository.</li></ol>{:/}  | | | | ||||||
|     1. This does not however preclude long-running HTTP(s) requests, for example long-polling or Websocket requests. |  | ||||||
| 1. <a name="R390">`R390`</a>: Each service hosted in its own GitLab repository with deployment manifest stored in the repository. |  | ||||||
|     1. Continuous deployments that are initiated from the CI pipeline of the corresponding GitLab repository. |  | ||||||
| 
 | 
 | ||||||
| ##### Security Requirements | ##### Security Requirements | ||||||
| 
 | 
 | ||||||
| 1. <a name="R400">`R400`</a>: stateful services deployed on the platform that utilize their own stateful storage (for example, custom deployed Postgres instance), must not store application security tokens, cloud-provider service keys or other long-lived security tokens in their stateful stores. | | ID | Required | Detail | Epic/Issue | Done? | | ||||||
| 1. <a name="R410">`R410`</a>: long-lived shared secrets are discouraged, and should be referenced in the service manifest as such, to allow for accounting and monitoring. | |---|---|---|---|---| | ||||||
| 1. <a name="R420">`R420`</a>: services using long-lived shared secrets should ensure that secret rotation can take place without downtime. | | `R400` |  | Stateful services deployed on the platform that utilize their own stateful storage (for example, custom deployed Postgres instance), must not store application security tokens, cloud-provider service keys or other long-lived security tokens in their stateful stores. |  |  | | ||||||
|     1. During a rotation, old and new generations of secrets should pass authentication, allowing gradual roll-out of new secrets. | | `R410` |  | Long-lived shared secrets are discouraged, and should be referenced in the service manifest as such, to allow for accounting and monitoring. |  |  | | ||||||
|  | | `R420` |  | Services using long-lived shared secrets should ensure that secret rotation can take place without downtime. {::nomarkdown}<ol><li>During a rotation, old and new generations of secrets should pass authentication, allowing gradual roll-out of new secrets.</li></ol>{:/} |  |  | | ||||||
| 
 | 
 | ||||||
| ##### Common Service Libraries | ##### Common Service Libraries | ||||||
| 
 | 
 | ||||||
| 1. <a name="R500">`R500`</a>: Experimental services would be strongly encouraged to adopt and use [LabKit](https://gitlab.com/gitlab-org/labkit) (for Go services), or [LabKit-Ruby](https://gitlab.com/gitlab-org/ruby/gems/labkit-ruby) for observability, context, correlation, FIPs verification, etc. | | ID | Required | Detail | Epic/Issue | Done? | | ||||||
|    1. At present, there is no LabKit-Python library, but some experiments will run in Python, so building a library to providing observability, context, correlation services in Python will be required. | |---|---|---|---|---| | ||||||
|  | | `R500` | Required | Experimental services would be strongly encouraged to adopt and use [LabKit](https://gitlab.com/gitlab-org/labkit) (for Go services), or [LabKit-Ruby](https://gitlab.com/gitlab-org/ruby/gems/labkit-ruby) for observability, context, correlation, FIPs verification, etc. {::nomarkdown}<ol><li>At present, there is no LabKit-Python library, but some experiments will run in Python, so building a library to providing observability, context, correlation services in Python will be required. </li></ol>{:/} | | | | ||||||
|  |  | ||||||
|  | @ -220,7 +220,7 @@ The following example projects can help you get started with the agent. | ||||||
| 
 | 
 | ||||||
| > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340882) in GitLab 14.8, GitLab warns you on the agent's list page to update the agent version installed on your cluster. | > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340882) in GitLab 14.8, GitLab warns you on the agent's list page to update the agent version installed on your cluster. | ||||||
| 
 | 
 | ||||||
| For the best experience, the version of the agent installed in your cluster should match the GitLab major and minor version. The previous minor version is also supported. For example, if your GitLab version is v14.9.4 (major version 14, minor version 9), then versions v14.9.0 and v14.9.1 of the agent are ideal, but any v14.8.x version of the agent is also supported. See [the release page](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/releases) of the GitLab agent. | For the best experience, the version of the agent installed in your cluster should match the GitLab major and minor version. The previous and next minor versions are also supported. For example, if your GitLab version is v14.9.4 (major version 14, minor version 9), then versions v14.9.0 and v14.9.1 of the agent are ideal, but any v14.8.x or v14.10.x version of the agent is also supported. See [the release page](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/releases) of the GitLab agent. | ||||||
| 
 | 
 | ||||||
| ### Update the agent version | ### Update the agent version | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -45,7 +45,8 @@ Git push options can perform actions for merge requests while pushing changes: | ||||||
| | Push option                                  | Description | | | Push option                                  | Description | | ||||||
| |----------------------------------------------|-------------| | |----------------------------------------------|-------------| | ||||||
| | `merge_request.create`                       | Create a new merge request for the pushed branch. | | | `merge_request.create`                       | Create a new merge request for the pushed branch. | | ||||||
| | `merge_request.target=<branch_name>`         | Set the target of the merge request to a particular branch or upstream project, such as: `git push -o merge_request.target=project_path/branch`. | | | `merge_request.target=<branch_name>`         | Set the target of the merge request to a particular branch, such as: `git push -o merge_request.target=branch_name`. | | ||||||
|  | | `merge_request.target_project=<project>`     | Set the target of the merge request to a particular upstream project, such as: `git push -o merge_request.target_project=path/to/project`. Introduced in [GitLab 16.6](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132475). | | ||||||
| | `merge_request.merge_when_pipeline_succeeds` | Set the merge request to [merge when its pipeline succeeds](merge_requests/merge_when_pipeline_succeeds.md). | | | `merge_request.merge_when_pipeline_succeeds` | Set the merge request to [merge when its pipeline succeeds](merge_requests/merge_when_pipeline_succeeds.md). | | ||||||
| | `merge_request.remove_source_branch`         | Set the merge request to remove the source branch when it's merged. | | | `merge_request.remove_source_branch`         | Set the merge request to remove the source branch when it's merged. | | ||||||
| | `merge_request.title="<title>"`              | Set the title of the merge request. For example: `git push -o merge_request.title="The title I want"`. | | | `merge_request.title="<title>"`              | Set the title of the merge request. For example: `git push -o merge_request.title="The title I want"`. | | ||||||
|  |  | ||||||
|  | @ -0,0 +1,28 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | module Gitlab | ||||||
|  |   module BackgroundMigration | ||||||
|  |     # A job to remove protected_branch_merge_access_levels for groups that do not have project_group_links | ||||||
|  |     # to the project for the associated protected branch | ||||||
|  |     class DeleteInvalidProtectedBranchMergeAccessLevels < BatchedMigrationJob | ||||||
|  |       operation_name :delete_invalid_protected_branch_merge_access_levels | ||||||
|  |       scope_to ->(relation) { relation.where.not(group_id: nil) } | ||||||
|  |       feature_category :source_code_management | ||||||
|  | 
 | ||||||
|  |       def perform | ||||||
|  |         each_sub_batch do |sub_batch| | ||||||
|  |           sub_batch | ||||||
|  |           .joins('INNER JOIN protected_branches ON protected_branches.id = protected_branch_id') | ||||||
|  |           .joins(%( | ||||||
|  |             LEFT OUTER JOIN project_group_links pgl | ||||||
|  |               ON pgl.group_id = protected_branch_merge_access_levels.group_id | ||||||
|  |               AND pgl.project_id = protected_branches.project_id | ||||||
|  |           )) | ||||||
|  |           .where(%( | ||||||
|  |             pgl.id IS NULL | ||||||
|  |           )).delete_all | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,28 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | module Gitlab | ||||||
|  |   module BackgroundMigration | ||||||
|  |     # A job to remove protected_branch_push_access_levels for groups that do not have project_group_links | ||||||
|  |     # to the project for the associated protected branch | ||||||
|  |     class DeleteInvalidProtectedBranchPushAccessLevels < BatchedMigrationJob | ||||||
|  |       operation_name :delete_invalid_protected_branch_push_access_levels | ||||||
|  |       scope_to ->(relation) { relation.where.not(group_id: nil) } | ||||||
|  |       feature_category :source_code_management | ||||||
|  | 
 | ||||||
|  |       def perform | ||||||
|  |         each_sub_batch do |sub_batch| | ||||||
|  |           sub_batch | ||||||
|  |           .joins('INNER JOIN protected_branches ON protected_branches.id = protected_branch_id') | ||||||
|  |           .joins(%( | ||||||
|  |             LEFT OUTER JOIN project_group_links pgl | ||||||
|  |               ON pgl.group_id = protected_branch_push_access_levels.group_id | ||||||
|  |               AND pgl.project_id = protected_branches.project_id | ||||||
|  |           )) | ||||||
|  |           .where(%( | ||||||
|  |             pgl.id IS NULL | ||||||
|  |           )).delete_all | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,28 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | module Gitlab | ||||||
|  |   module BackgroundMigration | ||||||
|  |     # A job to remove protected_tag_create_access_levels for groups that do not have project_group_links | ||||||
|  |     # to the project for the associated protected branch | ||||||
|  |     class DeleteInvalidProtectedTagCreateAccessLevels < BatchedMigrationJob | ||||||
|  |       operation_name :delete_invalid_protected_tag_create_access_levels | ||||||
|  |       scope_to ->(relation) { relation.where.not(group_id: nil) } | ||||||
|  |       feature_category :source_code_management | ||||||
|  | 
 | ||||||
|  |       def perform | ||||||
|  |         each_sub_batch do |sub_batch| | ||||||
|  |           sub_batch | ||||||
|  |           .joins('INNER JOIN protected_tags ON protected_tags.id = protected_tag_id') | ||||||
|  |           .joins(%( | ||||||
|  |             LEFT OUTER JOIN project_group_links pgl | ||||||
|  |               ON pgl.group_id = protected_tag_create_access_levels.group_id | ||||||
|  |               AND pgl.project_id = protected_tags.project_id | ||||||
|  |           )) | ||||||
|  |           .where(%( | ||||||
|  |             pgl.id IS NULL | ||||||
|  |           )).delete_all | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -14,6 +14,7 @@ module Gitlab | ||||||
|           :milestone, |           :milestone, | ||||||
|           :remove_source_branch, |           :remove_source_branch, | ||||||
|           :target, |           :target, | ||||||
|  |           :target_project, | ||||||
|           :title, |           :title, | ||||||
|           :unassign, |           :unassign, | ||||||
|           :unlabel |           :unlabel | ||||||
|  |  | ||||||
|  | @ -23,7 +23,7 @@ RSpec.describe Projects::Ml::ModelsIndexComponent, type: :component, feature_cat | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   subject(:component) do |   subject(:component) do | ||||||
|     described_class.new(paginator: paginator) |     described_class.new(model_count: 5, paginator: paginator) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe 'rendered' do |   describe 'rendered' do | ||||||
|  | @ -43,13 +43,15 @@ RSpec.describe Projects::Ml::ModelsIndexComponent, type: :component, feature_cat | ||||||
|           { |           { | ||||||
|             'name' => model1.name, |             'name' => model1.name, | ||||||
|             'version' => model1.latest_version.version, |             'version' => model1.latest_version.version, | ||||||
|             'path' => "/#{project.full_path}/-/packages/#{model1.latest_version.package_id}", |             'versionPackagePath' => "/#{project.full_path}/-/packages/#{model1.latest_version.package_id}", | ||||||
|  |             'versionPath' => "/#{project.full_path}/-/ml/models/#{model1.id}/versions/#{model1.latest_version.id}", | ||||||
|             'versionCount' => 1 |             'versionCount' => 1 | ||||||
|           }, |           }, | ||||||
|           { |           { | ||||||
|             'name' => model2.name, |             'name' => model2.name, | ||||||
|             'version' => nil, |             'version' => nil, | ||||||
|             'path' => nil, |             'versionPackagePath' => nil, | ||||||
|  |             'versionPath' => nil, | ||||||
|             'versionCount' => 0 |             'versionCount' => 0 | ||||||
|           } |           } | ||||||
|         ], |         ], | ||||||
|  | @ -58,7 +60,8 @@ RSpec.describe Projects::Ml::ModelsIndexComponent, type: :component, feature_cat | ||||||
|           'hasPreviousPage' => false, |           'hasPreviousPage' => false, | ||||||
|           'startCursor' => 'abcde', |           'startCursor' => 'abcde', | ||||||
|           'endCursor' => 'defgh' |           'endCursor' => 'defgh' | ||||||
|         } |         }, | ||||||
|  |         'modelCount' => 5 | ||||||
|       }) |       }) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -19,7 +19,7 @@ RSpec.describe 'Groups > Members > Sort members', :js, feature_category: :groups | ||||||
|   def expect_sort_by(text, sort_direction) |   def expect_sort_by(text, sort_direction) | ||||||
|     within('[data-testid="members-sort-dropdown"]') do |     within('[data-testid="members-sort-dropdown"]') do | ||||||
|       expect(page).to have_css('button[aria-haspopup="menu"]', text: text) |       expect(page).to have_css('button[aria-haspopup="menu"]', text: text) | ||||||
|       expect(page).to have_button("Sorting Direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}") |       expect(page).to have_button("Sort direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}") | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -149,7 +149,7 @@ RSpec.describe 'Projects > Members > Sorting', :js, feature_category: :groups_an | ||||||
|   def expect_sort_by(text, sort_direction) |   def expect_sort_by(text, sort_direction) | ||||||
|     within('[data-testid="members-sort-dropdown"]') do |     within('[data-testid="members-sort-dropdown"]') do | ||||||
|       expect(page).to have_css('button[aria-haspopup="menu"]', text: text) |       expect(page).to have_css('button[aria-haspopup="menu"]', text: text) | ||||||
|       expect(page).to have_button("Sorting Direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}") |       expect(page).to have_button("Sort direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}") | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -19,9 +19,11 @@ RSpec.describe Projects::Ml::ModelFinder, feature_category: :mlops do | ||||||
|       is_expected.to eq([model3, model2, model1]) |       is_expected.to eq([model3, model2, model1]) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it 'including the latest version', :aggregate_failures do |     it 'including the latest version and project', :aggregate_failures do | ||||||
|       expect(models[0].association_cached?(:latest_version)).to be(true) |       expect(models[0].association_cached?(:latest_version)).to be(true) | ||||||
|  |       expect(models[0].association_cached?(:project)).to be(true) | ||||||
|       expect(models[1].association_cached?(:latest_version)).to be(true) |       expect(models[1].association_cached?(:latest_version)).to be(true) | ||||||
|  |       expect(models[1].association_cached?(:project)).to be(true) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it 'does not return models belonging to a different project' do |     it 'does not return models belonging to a different project' do | ||||||
|  |  | ||||||
|  | @ -0,0 +1,76 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require 'spec_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe Gitlab::BackgroundMigration::DeleteInvalidProtectedBranchMergeAccessLevels, | ||||||
|  |   feature_category: :source_code_management do | ||||||
|  |   let(:projects_table) { table(:projects) } | ||||||
|  |   let(:protected_branches_table) { table(:protected_branches) } | ||||||
|  |   let(:namespaces_table) { table(:namespaces) } | ||||||
|  |   let(:protected_branch_merge_access_levels_table) { table(:protected_branch_merge_access_levels) } | ||||||
|  |   let(:project_group_links_table) { table(:project_group_links) } | ||||||
|  |   let(:users_table) { table(:users) } | ||||||
|  | 
 | ||||||
|  |   let(:user1) { users_table.create!(name: 'user1', email: 'user1@example.com', projects_limit: 5) } | ||||||
|  | 
 | ||||||
|  |   let(:project_group) { namespaces_table.create!(name: 'group-1', path: 'group-1', type: 'Group') } | ||||||
|  |   let(:project_namespace) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-2', type: 'Project') } | ||||||
|  |   let!(:project_1) do | ||||||
|  |     projects_table | ||||||
|  |     .create!( | ||||||
|  |       name: 'project1', | ||||||
|  |       path: 'path1', | ||||||
|  |       namespace_id: project_group.id, | ||||||
|  |       project_namespace_id: project_namespace.id, | ||||||
|  |       visibility_level: 0 | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   subject(:perform_migration) do | ||||||
|  |     described_class.new(start_id: protected_branch_merge_access_levels_table.minimum(:id), | ||||||
|  |       end_id: protected_branch_merge_access_levels_table.maximum(:id), | ||||||
|  |       batch_table: :protected_branch_merge_access_levels, | ||||||
|  |       batch_column: :id, | ||||||
|  |       sub_batch_size: 1, | ||||||
|  |       pause_ms: 0, | ||||||
|  |       connection: ApplicationRecord.connection) | ||||||
|  |                    .perform | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   context 'when there are merge access levels' do | ||||||
|  |     let(:protected_branch1) { protected_branches_table.create!(project_id: project_1.id, name: 'name') } | ||||||
|  |     let!(:merge_access_level_for_user) do | ||||||
|  |       protected_branch_merge_access_levels_table.create!( | ||||||
|  |         protected_branch_id: protected_branch1.id, | ||||||
|  |         user_id: user1.id | ||||||
|  |       ) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     let(:invited_group) { namespaces_table.create!(name: 'group-2', path: 'group-2', type: 'Group') } | ||||||
|  |     let!(:invited_group_link) do | ||||||
|  |       project_group_links_table.create!(project_id: project_1.id, group_id: invited_group.id) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     let!(:merge_access_level_with_linked_group) do | ||||||
|  |       protected_branch_merge_access_levels_table.create!( | ||||||
|  |         protected_branch_id: protected_branch1.id, | ||||||
|  |         group_id: invited_group.id | ||||||
|  |       ) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     let!(:merge_access_level_with_unlinked_group) do | ||||||
|  |       protected_branch_merge_access_levels_table.create!( | ||||||
|  |         protected_branch_id: protected_branch1.id, | ||||||
|  |         group_id: project_group.id | ||||||
|  |       ) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'deletes merge access levels with groups that do not have project_group_links to the project' do | ||||||
|  |       expect { subject }.to change { protected_branch_merge_access_levels_table.count }.from(3).to(2) | ||||||
|  |       expect(protected_branch_merge_access_levels_table.all).to contain_exactly( | ||||||
|  |         merge_access_level_with_linked_group, | ||||||
|  |         merge_access_level_for_user | ||||||
|  |       ) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,76 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require 'spec_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe Gitlab::BackgroundMigration::DeleteInvalidProtectedBranchPushAccessLevels, | ||||||
|  |   feature_category: :source_code_management do | ||||||
|  |   let(:projects_table) { table(:projects) } | ||||||
|  |   let(:protected_branches_table) { table(:protected_branches) } | ||||||
|  |   let(:namespaces_table) { table(:namespaces) } | ||||||
|  |   let(:protected_branch_push_access_levels_table) { table(:protected_branch_push_access_levels) } | ||||||
|  |   let(:project_group_links_table) { table(:project_group_links) } | ||||||
|  |   let(:users_table) { table(:users) } | ||||||
|  | 
 | ||||||
|  |   let(:user1) { users_table.create!(name: 'user1', email: 'user1@example.com', projects_limit: 5) } | ||||||
|  | 
 | ||||||
|  |   let(:project_group) { namespaces_table.create!(name: 'group-1', path: 'group-1', type: 'Group') } | ||||||
|  |   let(:project_namespace) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-2', type: 'Project') } | ||||||
|  |   let!(:project_1) do | ||||||
|  |     projects_table | ||||||
|  |     .create!( | ||||||
|  |       name: 'project1', | ||||||
|  |       path: 'path1', | ||||||
|  |       namespace_id: project_group.id, | ||||||
|  |       project_namespace_id: project_namespace.id, | ||||||
|  |       visibility_level: 0 | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   subject(:perform_migration) do | ||||||
|  |     described_class.new(start_id: protected_branch_push_access_levels_table.minimum(:id), | ||||||
|  |       end_id: protected_branch_push_access_levels_table.maximum(:id), | ||||||
|  |       batch_table: :protected_branch_push_access_levels, | ||||||
|  |       batch_column: :id, | ||||||
|  |       sub_batch_size: 1, | ||||||
|  |       pause_ms: 0, | ||||||
|  |       connection: ApplicationRecord.connection) | ||||||
|  |                    .perform | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   context 'when there are push access levels' do | ||||||
|  |     let(:protected_branch1) { protected_branches_table.create!(project_id: project_1.id, name: 'name') } | ||||||
|  |     let!(:push_access_level_for_user) do | ||||||
|  |       protected_branch_push_access_levels_table.create!( | ||||||
|  |         protected_branch_id: protected_branch1.id, | ||||||
|  |         user_id: user1.id | ||||||
|  |       ) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     let(:invited_group) { namespaces_table.create!(name: 'group-2', path: 'group-2', type: 'Group') } | ||||||
|  |     let!(:invited_group_link) do | ||||||
|  |       project_group_links_table.create!(project_id: project_1.id, group_id: invited_group.id) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     let!(:push_access_level_with_linked_group) do | ||||||
|  |       protected_branch_push_access_levels_table.create!( | ||||||
|  |         protected_branch_id: protected_branch1.id, | ||||||
|  |         group_id: invited_group.id | ||||||
|  |       ) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     let!(:push_access_level_with_unlinked_group) do | ||||||
|  |       protected_branch_push_access_levels_table.create!( | ||||||
|  |         protected_branch_id: protected_branch1.id, | ||||||
|  |         group_id: project_group.id | ||||||
|  |       ) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'deletes push access levels with groups that do not have project_group_links to the project' do | ||||||
|  |       expect { subject }.to change { protected_branch_push_access_levels_table.count }.from(3).to(2) | ||||||
|  |       expect(protected_branch_push_access_levels_table.all).to contain_exactly( | ||||||
|  |         push_access_level_with_linked_group, | ||||||
|  |         push_access_level_for_user | ||||||
|  |       ) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,76 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require 'spec_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe Gitlab::BackgroundMigration::DeleteInvalidProtectedTagCreateAccessLevels, | ||||||
|  |   feature_category: :source_code_management do | ||||||
|  |   let(:projects_table) { table(:projects) } | ||||||
|  |   let(:protected_tags_table) { table(:protected_tags) } | ||||||
|  |   let(:namespaces_table) { table(:namespaces) } | ||||||
|  |   let(:protected_tag_create_access_levels_table) { table(:protected_tag_create_access_levels) } | ||||||
|  |   let(:project_group_links_table) { table(:project_group_links) } | ||||||
|  |   let(:users_table) { table(:users) } | ||||||
|  | 
 | ||||||
|  |   let(:user1) { users_table.create!(name: 'user1', email: 'user1@example.com', projects_limit: 5) } | ||||||
|  | 
 | ||||||
|  |   let(:project_group) { namespaces_table.create!(name: 'group-1', path: 'group-1', type: 'Group') } | ||||||
|  |   let(:project_namespace) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-2', type: 'Project') } | ||||||
|  |   let!(:project_1) do | ||||||
|  |     projects_table | ||||||
|  |     .create!( | ||||||
|  |       name: 'project1', | ||||||
|  |       path: 'path1', | ||||||
|  |       namespace_id: project_group.id, | ||||||
|  |       project_namespace_id: project_namespace.id, | ||||||
|  |       visibility_level: 0 | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   subject(:perform_migration) do | ||||||
|  |     described_class.new(start_id: protected_tag_create_access_levels_table.minimum(:id), | ||||||
|  |       end_id: protected_tag_create_access_levels_table.maximum(:id), | ||||||
|  |       batch_table: :protected_tag_create_access_levels, | ||||||
|  |       batch_column: :id, | ||||||
|  |       sub_batch_size: 1, | ||||||
|  |       pause_ms: 0, | ||||||
|  |       connection: ApplicationRecord.connection) | ||||||
|  |                    .perform | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   context 'when there are push access levels' do | ||||||
|  |     let(:protected_tag) { protected_tags_table.create!(project_id: project_1.id, name: 'name') } | ||||||
|  |     let!(:push_access_level_for_user) do | ||||||
|  |       protected_tag_create_access_levels_table.create!( | ||||||
|  |         protected_tag_id: protected_tag.id, | ||||||
|  |         user_id: user1.id | ||||||
|  |       ) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     let(:invited_group) { namespaces_table.create!(name: 'group-2', path: 'group-2', type: 'Group') } | ||||||
|  |     let!(:invited_group_link) do | ||||||
|  |       project_group_links_table.create!(project_id: project_1.id, group_id: invited_group.id) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     let!(:push_access_level_with_linked_group) do | ||||||
|  |       protected_tag_create_access_levels_table.create!( | ||||||
|  |         protected_tag_id: protected_tag.id, | ||||||
|  |         group_id: invited_group.id | ||||||
|  |       ) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     let!(:push_access_level_with_unlinked_group) do | ||||||
|  |       protected_tag_create_access_levels_table.create!( | ||||||
|  |         protected_tag_id: protected_tag.id, | ||||||
|  |         group_id: project_group.id | ||||||
|  |       ) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'deletes push access levels with groups that do not have project_group_links to the project' do | ||||||
|  |       expect { subject }.to change { protected_tag_create_access_levels_table.count }.from(3).to(2) | ||||||
|  |       expect(protected_tag_create_access_levels_table.all).to contain_exactly( | ||||||
|  |         push_access_level_with_linked_group, | ||||||
|  |         push_access_level_for_user | ||||||
|  |       ) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,26 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require 'spec_helper' | ||||||
|  | require_migration! | ||||||
|  | 
 | ||||||
|  | RSpec.describe QueueDeleteInvalidProtectedBranchMergeAccessLevels, feature_category: :source_code_management do | ||||||
|  |   let!(:batched_migration) { described_class::MIGRATION } | ||||||
|  | 
 | ||||||
|  |   it 'schedules a new batched migration' do | ||||||
|  |     reversible_migration do |migration| | ||||||
|  |       migration.before -> { | ||||||
|  |         expect(batched_migration).not_to have_scheduled_batched_migration | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       migration.after -> { | ||||||
|  |         expect(batched_migration).to have_scheduled_batched_migration( | ||||||
|  |           table_name: :protected_branch_merge_access_levels, | ||||||
|  |           column_name: :id, | ||||||
|  |           interval: described_class::DELAY_INTERVAL, | ||||||
|  |           batch_size: described_class::BATCH_SIZE, | ||||||
|  |           sub_batch_size: described_class::SUB_BATCH_SIZE | ||||||
|  |         ) | ||||||
|  |       } | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,26 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require 'spec_helper' | ||||||
|  | require_migration! | ||||||
|  | 
 | ||||||
|  | RSpec.describe QueueDeleteInvalidProtectedBranchPushAccessLevels, feature_category: :source_code_management do | ||||||
|  |   let!(:batched_migration) { described_class::MIGRATION } | ||||||
|  | 
 | ||||||
|  |   it 'schedules a new batched migration' do | ||||||
|  |     reversible_migration do |migration| | ||||||
|  |       migration.before -> { | ||||||
|  |         expect(batched_migration).not_to have_scheduled_batched_migration | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       migration.after -> { | ||||||
|  |         expect(batched_migration).to have_scheduled_batched_migration( | ||||||
|  |           table_name: :protected_branch_push_access_levels, | ||||||
|  |           column_name: :id, | ||||||
|  |           interval: described_class::DELAY_INTERVAL, | ||||||
|  |           batch_size: described_class::BATCH_SIZE, | ||||||
|  |           sub_batch_size: described_class::SUB_BATCH_SIZE | ||||||
|  |         ) | ||||||
|  |       } | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,26 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require 'spec_helper' | ||||||
|  | require_migration! | ||||||
|  | 
 | ||||||
|  | RSpec.describe QueueDeleteInvalidProtectedTagCreateAccessLevels, feature_category: :source_code_management do | ||||||
|  |   let!(:batched_migration) { described_class::MIGRATION } | ||||||
|  | 
 | ||||||
|  |   it 'schedules a new batched migration' do | ||||||
|  |     reversible_migration do |migration| | ||||||
|  |       migration.before -> { | ||||||
|  |         expect(batched_migration).not_to have_scheduled_batched_migration | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       migration.after -> { | ||||||
|  |         expect(batched_migration).to have_scheduled_batched_migration( | ||||||
|  |           table_name: :protected_tag_create_access_levels, | ||||||
|  |           column_name: :id, | ||||||
|  |           interval: described_class::DELAY_INTERVAL, | ||||||
|  |           batch_size: described_class::BATCH_SIZE, | ||||||
|  |           sub_batch_size: described_class::SUB_BATCH_SIZE | ||||||
|  |         ) | ||||||
|  |       } | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -78,6 +78,14 @@ RSpec.describe Ml::Model, feature_category: :mlops do | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   describe '.including_project' do | ||||||
|  |     subject { described_class.including_project } | ||||||
|  | 
 | ||||||
|  |     it 'loads latest version' do | ||||||
|  |       expect(subject.first.association_cached?(:project)).to be(true) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   describe 'with_version_count' do |   describe 'with_version_count' do | ||||||
|     let(:model) { existing_model } |     let(:model) { existing_model } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -58,6 +58,22 @@ RSpec.describe Ml::ModelPresenter, feature_category: :mlops do | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   describe '#latest_version_path' do | ||||||
|  |     subject { model.present.latest_version_path } | ||||||
|  | 
 | ||||||
|  |     context 'when model version does not have package' do | ||||||
|  |       let(:model) { model1 } | ||||||
|  | 
 | ||||||
|  |       it { is_expected.to be_nil } | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when latest model version has package' do | ||||||
|  |       let(:model) { model2 } | ||||||
|  | 
 | ||||||
|  |       it { is_expected.to eq("/#{project.full_path}/-/ml/models/#{model.id}/versions/#{model.latest_version.id}") } | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   describe '#path' do |   describe '#path' do | ||||||
|     subject { model1.present.path } |     subject { model1.present.path } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ require 'spec_helper' | ||||||
| RSpec.describe Ml::ModelVersionPresenter, feature_category: :mlops do | RSpec.describe Ml::ModelVersionPresenter, feature_category: :mlops do | ||||||
|   let_it_be(:project) { build_stubbed(:project) } |   let_it_be(:project) { build_stubbed(:project) } | ||||||
|   let_it_be(:model) { build_stubbed(:ml_models, name: 'a_model', project: project) } |   let_it_be(:model) { build_stubbed(:ml_models, name: 'a_model', project: project) } | ||||||
|   let_it_be(:model_version) { build_stubbed(:ml_model_versions, model: model, version: '1.1.1') } |   let_it_be(:model_version) { build_stubbed(:ml_model_versions, :with_package, model: model, version: '1.1.1') } | ||||||
|   let_it_be(:presenter) { model_version.present } |   let_it_be(:presenter) { model_version.present } | ||||||
| 
 | 
 | ||||||
|   describe '.display_name' do |   describe '.display_name' do | ||||||
|  | @ -15,8 +15,14 @@ RSpec.describe Ml::ModelVersionPresenter, feature_category: :mlops do | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '#path' do |   describe '#path' do | ||||||
|     subject { model_version.present.path } |     subject { presenter.path } | ||||||
| 
 | 
 | ||||||
|     it { is_expected.to eq("/#{project.full_path}/-/ml/models/#{model.id}/versions/#{model_version.id}") } |     it { is_expected.to eq("/#{project.full_path}/-/ml/models/#{model.id}/versions/#{model_version.id}") } | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   describe '#package_path' do | ||||||
|  |     subject { presenter.package_path } | ||||||
|  | 
 | ||||||
|  |     it { is_expected.to eq("/#{project.full_path}/-/packages/#{model_version.package_id}") } | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -43,17 +43,26 @@ RSpec.describe Projects::Ml::ModelsController, feature_category: :mlops do | ||||||
|       index_request |       index_request | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it 'fetches the correct models' do |     it 'fetches the correct variables', :aggregate_failures do | ||||||
|  |       stub_const("Projects::Ml::ModelsController::MAX_MODELS_PER_PAGE", 2) | ||||||
|  | 
 | ||||||
|       index_request |       index_request | ||||||
| 
 | 
 | ||||||
|       expect(assigns(:paginator).records).to match_array([model3, model2, model1]) |       page_models = [model3, model2] | ||||||
|  |       all_models = [model3, model2, model1] | ||||||
|  | 
 | ||||||
|  |       expect(assigns(:paginator).records).to match_array(page_models) | ||||||
|  |       expect(assigns(:model_count)).to be all_models.count | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it 'does not perform N+1 sql queries' do |     it 'does not perform N+1 sql queries' do | ||||||
|  |       list_models | ||||||
|  | 
 | ||||||
|       control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { list_models } |       control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { list_models } | ||||||
| 
 | 
 | ||||||
|       create_list(:ml_model_versions, 2, model: model1) |       create_list(:ml_model_versions, 2, model: model1) | ||||||
|       create_list(:ml_model_versions, 2, model: model2) |       create_list(:ml_model_versions, 2, model: model2) | ||||||
|  |       create_list(:ml_models, 4, project: project) | ||||||
| 
 | 
 | ||||||
|       expect { list_models }.not_to exceed_all_query_limit(control_count) |       expect { list_models }.not_to exceed_all_query_limit(control_count) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -54,6 +54,17 @@ RSpec.describe MergeRequests::PushOptionsHandlerService, feature_category: :sour | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   shared_examples_for 'a service that can set the target project of a merge request' do | ||||||
|  |     subject(:last_mr) { MergeRequest.last } | ||||||
|  | 
 | ||||||
|  |     it 'creates a merge request with the correct target project' do | ||||||
|  |       project_path = push_options[:target_project] || project.default_merge_request_target.full_path | ||||||
|  | 
 | ||||||
|  |       expect { service.execute }.to change { MergeRequest.count }.by(1) | ||||||
|  |       expect(last_mr.target_project.full_path).to eq(project_path) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   shared_examples_for 'a service that can set the title of a merge request' do |   shared_examples_for 'a service that can set the title of a merge request' do | ||||||
|     subject(:last_mr) { MergeRequest.last } |     subject(:last_mr) { MergeRequest.last } | ||||||
| 
 | 
 | ||||||
|  | @ -347,6 +358,31 @@ RSpec.describe MergeRequests::PushOptionsHandlerService, feature_category: :sour | ||||||
|     it_behaves_like 'with the project default branch' |     it_behaves_like 'with the project default branch' | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   describe '`target_project` push option' do | ||||||
|  |     let(:changes) { new_branch_changes } | ||||||
|  |     let(:double_forked_project) { fork_project(forked_project, user1, repository: true) } | ||||||
|  |     let(:service) { described_class.new(project: double_forked_project, current_user: user1, changes: changes, push_options: push_options) } | ||||||
|  |     let(:push_options) { { create: true, target_project: target_project.full_path } } | ||||||
|  | 
 | ||||||
|  |     context 'to self' do | ||||||
|  |       let(:target_project) { double_forked_project } | ||||||
|  | 
 | ||||||
|  |       it_behaves_like 'a service that can set the target project of a merge request' | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'to intermediate project' do | ||||||
|  |       let(:target_project) { forked_project } | ||||||
|  | 
 | ||||||
|  |       it_behaves_like 'a service that can set the target project of a merge request' | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'to base project' do | ||||||
|  |       let(:target_project) { project } | ||||||
|  | 
 | ||||||
|  |       it_behaves_like 'a service that can set the target project of a merge request' | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   describe '`title` push option' do |   describe '`title` push option' do | ||||||
|     let(:push_options) { { title: title } } |     let(:push_options) { { title: title } } | ||||||
| 
 | 
 | ||||||
|  | @ -861,6 +897,17 @@ RSpec.describe MergeRequests::PushOptionsHandlerService, feature_category: :sour | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   describe 'when the target project does not exist' do | ||||||
|  |     let(:push_options) { { create: true, target: 'my-branch', target_project: 'does-not-exist' } } | ||||||
|  |     let(:changes) { default_branch_changes } | ||||||
|  | 
 | ||||||
|  |     it 'records an error', :sidekiq_inline do | ||||||
|  |       service.execute | ||||||
|  | 
 | ||||||
|  |       expect(service.errors).to eq(["User access was denied"]) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   describe 'when user does not have access to target project' do |   describe 'when user does not have access to target project' do | ||||||
|     let(:push_options) { { create: true, target: 'my-branch' } } |     let(:push_options) { { create: true, target: 'my-branch' } } | ||||||
|     let(:changes) { default_branch_changes } |     let(:changes) { default_branch_changes } | ||||||
|  | @ -890,6 +937,18 @@ RSpec.describe MergeRequests::PushOptionsHandlerService, feature_category: :sour | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   describe 'when projects are unrelated' do | ||||||
|  |     let(:unrelated_project) { create(:project, :public, :repository, group: child_group) } | ||||||
|  |     let(:push_options) { { create: true, target_project: unrelated_project.full_path } } | ||||||
|  |     let(:changes) { new_branch_changes } | ||||||
|  | 
 | ||||||
|  |     it 'records an error' do | ||||||
|  |       service.execute | ||||||
|  | 
 | ||||||
|  |       expect(service.errors).to eq(["Projects #{project.full_path} and #{unrelated_project.full_path} are not in the same network"]) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   describe 'when MR has ActiveRecord errors' do |   describe 'when MR has ActiveRecord errors' do | ||||||
|     let(:push_options) { { create: true } } |     let(:push_options) { { create: true } } | ||||||
|     let(:changes) { new_branch_changes } |     let(:changes) { new_branch_changes } | ||||||
|  |  | ||||||
|  | @ -158,7 +158,7 @@ def click_sort_option(option, ascending) | ||||||
|   wait_for_requests |   wait_for_requests | ||||||
| 
 | 
 | ||||||
|   # Reset the sort direction |   # Reset the sort direction | ||||||
|   if page.has_selector?('button[aria-label="Sorting Direction: Ascending"]', wait: 0) && !ascending |   if page.has_selector?('button[aria-label="Sort direction: Ascending"]', wait: 0) && !ascending | ||||||
|     click_button 'Sort direction' |     click_button 'Sort direction' | ||||||
| 
 | 
 | ||||||
|     wait_for_requests |     wait_for_requests | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue