From 3456ec38df0daa16e9512a68d47f48736742cb1b Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 15 Apr 2024 15:10:58 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../super_sidebar/components/help_center.vue | 2 +- .../ml/find_model_version_resolver.rb | 26 +++ .../types/ml/candidate_metadata_type.rb | 24 +++ app/graphql/types/ml/candidate_metric_type.rb | 28 +++ app/graphql/types/ml/candidate_param_type.rb | 24 +++ app/graphql/types/ml/candidate_type.rb | 12 ++ app/graphql/types/ml/model_type.rb | 4 + app/graphql/types/ml/model_version_type.rb | 4 + app/graphql/types/project_type.rb | 3 +- .../types/projects/branch_rule_type.rb | 4 +- app/mailers/emails/profile.rb | 5 +- app/mailers/previews/notify_preview.rb | 2 +- app/models/projects/branch_rule.rb | 2 +- app/services/notification_service.rb | 6 +- ...cess_token_about_to_expire_email.html.haml | 10 + ...ccess_token_about_to_expire_email.text.erb | 7 + ...ess_tokens_about_to_expire_email.html.haml | 15 -- ...cess_tokens_about_to_expire_email.text.erb | 13 -- app/views/shared/projects/_list.html.haml | 8 +- .../personal_access_tokens/expiring_worker.rb | 10 +- db/docs/vulnerability_exports.yml | 11 +- ...er_approvals_member_namespace_id_status.rb | 16 ++ db/schema_migrations/20240410061546 | 1 + db/structure.sql | 2 + .../audit_event_streaming/index.md | 2 +- doc/api/graphql/reference/index.md | 203 +++++++++++++++++- doc/architecture/blueprints/cells/index.md | 3 +- .../metrics/metrics_lifecycle.md | 22 ++ doc/subscriptions/subscription-add-ons.md | 15 +- doc/user/permissions.md | 4 +- doc/user/project/requirements/index.md | 12 +- .../ci/templates/Pages/Gatsby.gitlab-ci.yml | 2 +- lib/sidebars/groups/menus/settings_menu.rb | 1 - locale/gitlab.pot | 68 +++--- .../internal_events/product_group_renamer.rb | 81 +++++++ .../event_definition.yml | 18 ++ .../event_definition_from_another_group.yml | 18 ++ .../metric_definition.yml | 21 ++ .../product_group_renamer/product_groups.json | 7 + .../ml/find_model_version_resolver_spec.rb | 56 +++++ .../types/ml/candidate_metadata_type_spec.rb | 11 + .../types/ml/candidate_metric_type_spec.rb | 11 + .../types/ml/candidate_param_type_spec.rb | 11 + spec/graphql/types/ml/candidate_type_spec.rb | 39 ++++ spec/graphql/types/ml/model_type_spec.rb | 17 +- .../types/ml/model_version_type_spec.rb | 4 +- spec/lib/gitlab/database/sharding_key_spec.rb | 3 +- spec/mailers/emails/profile_spec.rb | 4 +- .../mutations/ml/models/create_spec.rb | 2 +- spec/requests/api/projects_spec.rb | 2 +- .../product_group_renamer_spec.rb | 61 ++++++ spec/services/notification_service_spec.rb | 10 +- .../shared/projects/_list.html.haml_spec.rb | 38 +++- .../expiring_worker_spec.rb | 2 +- 54 files changed, 872 insertions(+), 115 deletions(-) create mode 100644 app/graphql/resolvers/ml/find_model_version_resolver.rb create mode 100644 app/graphql/types/ml/candidate_metadata_type.rb create mode 100644 app/graphql/types/ml/candidate_metric_type.rb create mode 100644 app/graphql/types/ml/candidate_param_type.rb create mode 100644 app/views/notify/bot_resource_access_token_about_to_expire_email.html.haml create mode 100644 app/views/notify/bot_resource_access_token_about_to_expire_email.text.erb delete mode 100644 app/views/notify/resource_access_tokens_about_to_expire_email.html.haml delete mode 100644 app/views/notify/resource_access_tokens_about_to_expire_email.text.erb create mode 100644 db/migrate/20240410061546_add_index_for_member_approvals_member_namespace_id_status.rb create mode 100644 db/schema_migrations/20240410061546 create mode 100644 scripts/internal_events/product_group_renamer.rb create mode 100644 spec/fixtures/scripts/product_group_renamer/event_definition.yml create mode 100644 spec/fixtures/scripts/product_group_renamer/event_definition_from_another_group.yml create mode 100644 spec/fixtures/scripts/product_group_renamer/metric_definition.yml create mode 100644 spec/fixtures/scripts/product_group_renamer/product_groups.json create mode 100644 spec/graphql/resolvers/ml/find_model_version_resolver_spec.rb create mode 100644 spec/graphql/types/ml/candidate_metadata_type_spec.rb create mode 100644 spec/graphql/types/ml/candidate_metric_type_spec.rb create mode 100644 spec/graphql/types/ml/candidate_param_type_spec.rb create mode 100644 spec/scripts/internal_events/product_group_renamer_spec.rb diff --git a/app/assets/javascripts/super_sidebar/components/help_center.vue b/app/assets/javascripts/super_sidebar/components/help_center.vue index b9701e3c503..90ca222aecb 100644 --- a/app/assets/javascripts/super_sidebar/components/help_center.vue +++ b/app/assets/javascripts/super_sidebar/components/help_center.vue @@ -35,7 +35,7 @@ export default { shortcuts: __('Keyboard shortcuts'), version: __('Your GitLab version'), whatsnew: __("What's new"), - chat: s__('TanukiBot|GitLab Duo Chat'), + chat: s__('DuoChat|GitLab Duo Chat'), }, props: { sidebarData: { diff --git a/app/graphql/resolvers/ml/find_model_version_resolver.rb b/app/graphql/resolvers/ml/find_model_version_resolver.rb new file mode 100644 index 00000000000..fde8b7ac24b --- /dev/null +++ b/app/graphql/resolvers/ml/find_model_version_resolver.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Resolvers + module Ml + class FindModelVersionResolver < Resolvers::BaseResolver + extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1 + + type ::Types::Ml::ModelType, null: true + + argument :model_version_id, ::Types::GlobalIDType[::Ml::ModelVersion], + required: false, + description: 'Id of the version to be fetched.' + + def resolve(model_version_id:) + Gitlab::Graphql::Lazy.with_value(find_object(id: model_version_id)) do |model_version| + model_version if Ability.allowed?(current_user, :read_model_registry, model_version&.project) && + model_version.model_id == object.id + end + end + + def find_object(id:) + GitlabSchema.find_by_gid(id) + end + end + end +end diff --git a/app/graphql/types/ml/candidate_metadata_type.rb b/app/graphql/types/ml/candidate_metadata_type.rb new file mode 100644 index 00000000000..b2cccb47bc5 --- /dev/null +++ b/app/graphql/types/ml/candidate_metadata_type.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Types + module Ml + # rubocop: disable Graphql/AuthorizeTypes -- authorization in ModelDetailsResolver + class CandidateMetadataType < ::Types::BaseObject + graphql_name 'MlCandidateMetadata' + description 'Metadata for a candidate in the model registry' + + connection_type_class Types::LimitedCountableConnectionType + + field :id, ::Types::GlobalIDType[::Ml::CandidateMetadata], null: false, description: 'ID of the metadata.' + + field :name, ::GraphQL::Types::String, + null: true, + description: 'Name of the metadata entry.' + + field :value, ::GraphQL::Types::String, + null: false, + description: 'Value set for the metadata entry.' + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/ml/candidate_metric_type.rb b/app/graphql/types/ml/candidate_metric_type.rb new file mode 100644 index 00000000000..b0cb9ca18e8 --- /dev/null +++ b/app/graphql/types/ml/candidate_metric_type.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Types + module Ml + # rubocop: disable Graphql/AuthorizeTypes -- authorization in ModelDetailsResolver + class CandidateMetricType < ::Types::BaseObject + graphql_name 'MlCandidateMetric' + description 'Metric for a candidate in the model registry' + + connection_type_class Types::LimitedCountableConnectionType + + field :id, ::Types::GlobalIDType[::Ml::CandidateMetric], null: false, description: 'ID of the metric.' + + field :name, ::GraphQL::Types::String, + null: true, + description: 'Name of the metric.' + + field :step, ::GraphQL::Types::Int, + null: false, + description: 'Step at which the metric was measured.' + + field :value, ::GraphQL::Types::Float, + null: false, + description: 'Value set for the metric.' + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/ml/candidate_param_type.rb b/app/graphql/types/ml/candidate_param_type.rb new file mode 100644 index 00000000000..e73e176c4e5 --- /dev/null +++ b/app/graphql/types/ml/candidate_param_type.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Types + module Ml + # rubocop: disable Graphql/AuthorizeTypes -- authorization in ModelDetailsResolver + class CandidateParamType < ::Types::BaseObject + graphql_name 'MlCandidateParam' + description 'Parameter for a candidate in the model registry' + + connection_type_class Types::LimitedCountableConnectionType + + field :id, ::Types::GlobalIDType[::Ml::CandidateParam], null: false, description: 'ID of the parameter.' + + field :name, ::GraphQL::Types::String, + null: true, + description: 'Name of the parameter.' + + field :value, ::GraphQL::Types::String, + null: false, + description: 'Value set for the parameter.' + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/ml/candidate_type.rb b/app/graphql/types/ml/candidate_type.rb index 1c6ce26b9ce..9367d0995d3 100644 --- a/app/graphql/types/ml/candidate_type.rb +++ b/app/graphql/types/ml/candidate_type.rb @@ -31,6 +31,18 @@ module Types field :created_at, Types::TimeType, null: false, description: 'Date of creation.' + field :params, ::Types::Ml::CandidateParamType.connection_type, + null: false, + description: 'Parameters for the candidate.' + + field :metrics, ::Types::Ml::CandidateMetricType.connection_type, + null: false, + description: 'Metrics for the candidate.' + + field :metadata, ::Types::Ml::CandidateMetadataType.connection_type, + null: false, + description: 'Metadata entries for the candidate.' + field :ci_job, ::Types::Ci::JobType, null: true, description: 'CI information about the job that created the candidate.' diff --git a/app/graphql/types/ml/model_type.rb b/app/graphql/types/ml/model_type.rb index 4ea2446bd84..65ef6621f8b 100644 --- a/app/graphql/types/ml/model_type.rb +++ b/app/graphql/types/ml/model_type.rb @@ -38,6 +38,10 @@ module Types field :candidates, ::Types::Ml::CandidateType.connection_type, null: true, description: 'Version candidates of the model.' + + field :version, ::Types::Ml::ModelVersionType, null: true, + description: 'Version of the model.', + resolver: ::Resolvers::Ml::FindModelVersionResolver end # rubocop: enable Graphql/AuthorizeTypes end diff --git a/app/graphql/types/ml/model_version_type.rb b/app/graphql/types/ml/model_version_type.rb index 771ff249188..30443f537ac 100644 --- a/app/graphql/types/ml/model_version_type.rb +++ b/app/graphql/types/ml/model_version_type.rb @@ -14,6 +14,10 @@ module Types field :created_at, Types::TimeType, null: false, description: 'Date of creation.' field :version, ::GraphQL::Types::String, null: false, description: 'Name of the version.' + field :package_id, ::Types::GlobalIDType[::Packages::Package], + null: false, + description: 'Package for model version artifacts.' + field :candidate, ::Types::Ml::CandidateType, null: false, description: 'Metrics, params and metadata for the model version.' diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 0406c2ac273..fa8ccea0510 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -618,8 +618,7 @@ module Types resolver: Resolvers::Projects::ForkDetailsResolver, description: 'Details of the fork project compared to its upstream project.' - field :branch_rules, - Types::Projects::BranchRuleType.connection_type, + field :branch_rules, Types::Projects::BranchRuleType.connection_type, null: true, description: "Branch rules configured for the project.", resolver: Resolvers::Projects::BranchRulesResolver diff --git a/app/graphql/types/projects/branch_rule_type.rb b/app/graphql/types/projects/branch_rule_type.rb index d960361c4e6..c5bf4026b17 100644 --- a/app/graphql/types/projects/branch_rule_type.rb +++ b/app/graphql/types/projects/branch_rule_type.rb @@ -43,12 +43,12 @@ module Types field :created_at, Types::TimeType, - null: false, + null: true, description: 'Timestamp of when the branch rule was created.' field :updated_at, Types::TimeType, - null: false, + null: true, description: 'Timestamp of when the branch rule was last updated.' end end diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb index dbfbb9d684c..56f884bcbc2 100644 --- a/app/mailers/emails/profile.rb +++ b/app/mailers/emails/profile.rb @@ -60,9 +60,10 @@ module Emails end # rubocop: enable CodeReuse/ActiveRecord - def resource_access_tokens_about_to_expire_email(recipient, resource, token_names) + # resource owners are sent mail about expiring access tokens which belong to a bot user + def bot_resource_access_token_about_to_expire_email(recipient, resource, token_name) @user = recipient - @token_names = token_names + @token_name = token_name @days_to_expire = PersonalAccessToken::DAYS_TO_EXPIRE @resource = resource if resource.is_a?(Group) diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb index da289cdbb3b..d7102fb0226 100644 --- a/app/mailers/previews/notify_preview.rb +++ b/app/mailers/previews/notify_preview.rb @@ -65,7 +65,7 @@ class NotifyPreview < ActionMailer::Preview end def resource_access_token_about_to_expire_email - Notify.resource_access_tokens_about_to_expire_email(user, group, ['token_name']) + Notify.bot_resource_access_token_about_to_expire_email(user, group, 'token_name') end def access_token_created_email diff --git a/app/models/projects/branch_rule.rb b/app/models/projects/branch_rule.rb index a3e502faa38..a76fc6e5f6a 100644 --- a/app/models/projects/branch_rule.rb +++ b/app/models/projects/branch_rule.rb @@ -8,7 +8,7 @@ module Projects attr_reader :project, :protected_branch alias_method :branch_protection, :protected_branch - def_delegators(:protected_branch, :id, :name, :group, :default_branch?, :created_at, :updated_at) + def_delegators(:protected_branch, :id, :name, :group, :default_branch?, :created_at, :updated_at, :persisted?) def self.find(id) protected_branch = ProtectedBranch.find(id) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 3c40707d0c6..d33db5c7b34 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -75,15 +75,15 @@ class NotificationService end end - def resource_access_tokens_about_to_expire(bot_user, token_names) + def bot_resource_access_token_about_to_expire(bot_user, token_name) recipients = bot_user.resource_bot_owners.select { |owner| owner.can?(:receive_notifications) } resource = bot_user.resource_bot_resource recipients.each do |recipient| - mailer.resource_access_tokens_about_to_expire_email( + mailer.bot_resource_access_token_about_to_expire_email( recipient, resource, - token_names + token_name ).deliver_later end end diff --git a/app/views/notify/bot_resource_access_token_about_to_expire_email.html.haml b/app/views/notify/bot_resource_access_token_about_to_expire_email.html.haml new file mode 100644 index 00000000000..f61082bbf76 --- /dev/null +++ b/app/views/notify/bot_resource_access_token_about_to_expire_email.html.haml @@ -0,0 +1,10 @@ +%p + = _('Hi %{username}!') % { username: sanitize_name(@user.name) } +%p + - code_tag_pair = tag_pair(tag.code, :codeOpen, :codeClose) + = safe_format(_('Your %{resource_type} access token %{codeOpen}%{token_name}%{codeClose} for %{codeOpen}%{resource_path}%{codeClose} will expire in %{days_to_expire} or less.'), code_tag_pair, days_to_expire: pluralize(@days_to_expire, _('day')), token_name: @token_name, resource_path: @resource.full_path, resource_type: @resource.class.name) +%p + - link_start = ''.html_safe % { url: @target_url } + = html_escape(_('You can create a new one or check them in your %{link_start}Access Tokens%{link_end} settings.')) % { link_start: link_start, link_end: ''.html_safe } +%p + = @reason_text diff --git a/app/views/notify/bot_resource_access_token_about_to_expire_email.text.erb b/app/views/notify/bot_resource_access_token_about_to_expire_email.text.erb new file mode 100644 index 00000000000..00b553f02a6 --- /dev/null +++ b/app/views/notify/bot_resource_access_token_about_to_expire_email.text.erb @@ -0,0 +1,7 @@ +<%= _('Hi %{username}!') % { username: sanitize_name(@user.name) } %> + +<%= _('Your %{resource_type} access token %{token_name} for %{resource_path} will expire in %{days_to_expire} or less.') % { days_to_expire: pluralize(@days_to_expire, _('day')), token_name: @token_name, resource_path: "#{@resource.class.name.titleize}: #{@resource.full_path}", resource_type: "#{@resource.class.name.titleize}" }%> + +<%= _('You can create a new one or check them in your access token settings: %{target_url}') % { target_url: @target_url } %> + +<%= @reason_text %> diff --git a/app/views/notify/resource_access_tokens_about_to_expire_email.html.haml b/app/views/notify/resource_access_tokens_about_to_expire_email.html.haml deleted file mode 100644 index 35c2260f24f..00000000000 --- a/app/views/notify/resource_access_tokens_about_to_expire_email.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -%p - = _('Hi %{username}!') % { username: sanitize_name(@user.name) } -%p - = _('One or more of your resource access tokens will expire in %{days_to_expire} or less:') % { days_to_expire: pluralize(@days_to_expire, _('day')) } -%p - #{@resource.class.name.titleize}: #{@resource.full_path} -%p - %ul - - @token_names.each do |token| - %li= token -%p - - link_start = ''.html_safe % { url: @target_url } - = html_escape(_('You can create a new one or check them in your %{link_start}access tokens%{link_end} settings.')) % { link_start: link_start, link_end: ''.html_safe } -%p - = @reason_text diff --git a/app/views/notify/resource_access_tokens_about_to_expire_email.text.erb b/app/views/notify/resource_access_tokens_about_to_expire_email.text.erb deleted file mode 100644 index f57c3e7b0d0..00000000000 --- a/app/views/notify/resource_access_tokens_about_to_expire_email.text.erb +++ /dev/null @@ -1,13 +0,0 @@ -<%= _('Hi %{username}!') % { username: sanitize_name(@user.name) } %> - -<%= _('One or more of your resource access tokens will expire in %{days_to_expire} or less:') % { days_to_expire: pluralize(@days_to_expire, _('day')) } %> - -<%= "#{@resource.class.name.titleize}: #{@resource.full_path}" %> - -<% @token_names.each do |token| %> - - <%= token %> -<% end %> - -<%= _('You can create a new one or check them in your access token settings: %{target_url}') % { target_url: @target_url } %> - -<%= @reason_text %> diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index 98e5367ded8..b0926bf63b2 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -20,6 +20,7 @@ - own_projects_illustration_path = 'illustrations/empty-state/empty-projects-md.svg' - own_projects_current_user_empty_message_header = s_('UserProfile|You haven\'t created any personal projects') - own_projects_current_user_empty_message_description = s_('UserProfile|Your projects can be available publicly, internally, or privately, at your choice.') +- own_projects_current_user_cant_create_description = s_("UserProfile|You cannot create projects in your personal namespace. Contact your GitLab administrator.") - own_projects_visitor_empty_message = s_('UserProfile|There are no projects available to be displayed here') - explore_page_empty_message = s_('UserProfile|Explore public groups to find projects to contribute to') - new_project_button_label = _('New project') @@ -69,9 +70,10 @@ primary_button_link: explore_projects_button_link, visitor_empty_message: starred_projects_visitor_empty_message } - else + - user_has_limits = current_user&.projects_limit.to_i > 0 = render partial: 'shared/empty_states/profile_tabs', locals: { illustration_path: own_projects_illustration_path, current_user_empty_message_header: own_projects_current_user_empty_message_header, - current_user_empty_message_description: own_projects_current_user_empty_message_description, - primary_button_label: new_project_button_label, - primary_button_link: new_project_button_link, + current_user_empty_message_description: user_has_limits ? own_projects_current_user_empty_message_description : own_projects_current_user_cant_create_description, + primary_button_label: user_has_limits ? new_project_button_label : nil, + primary_button_link: user_has_limits ? new_project_button_link : nil, visitor_empty_message: defined?(explore_page) && explore_page ? explore_page_empty_message : own_projects_visitor_empty_message } diff --git a/app/workers/personal_access_tokens/expiring_worker.rb b/app/workers/personal_access_tokens/expiring_worker.rb index e4c39b2ae99..f2a11e44020 100644 --- a/app/workers/personal_access_tokens/expiring_worker.rb +++ b/app/workers/personal_access_tokens/expiring_worker.rb @@ -50,7 +50,7 @@ module PersonalAccessTokens # We're limiting to 100 tokens so we avoid loading too many tokens into memory. # At the time of writing this would only affect 69 users on GitLab.com - deliver_user_notifications(token_names, user) + deliver_user_notifications(user, token_names) expiring_user_tokens.update_all(expire_notification_delivered: true) end @@ -77,7 +77,7 @@ module PersonalAccessTokens bot_users.each do |user| with_context(user: user) do - expiring_user_token = user.personal_access_tokens.first + expiring_user_token = user.personal_access_tokens.first # bot user should not have more than 1 token execute_web_hooks(expiring_user_token, user) deliver_bot_notifications(expiring_user_token.name, user) @@ -91,8 +91,8 @@ module PersonalAccessTokens # rubocop: enable CodeReuse/ActiveRecord end - def deliver_bot_notifications(token_names, user) - notification_service.resource_access_tokens_about_to_expire(user, token_names) + def deliver_bot_notifications(token_name, user) + notification_service.bot_resource_access_token_about_to_expire(user, token_name) Gitlab::AppLogger.info( message: "Notifying Bot User resource owners about expiring tokens", @@ -101,7 +101,7 @@ module PersonalAccessTokens ) end - def deliver_user_notifications(token_names, user) + def deliver_user_notifications(user, token_names) notification_service.access_token_about_to_expire(user, token_names) Gitlab::AppLogger.info( diff --git a/db/docs/vulnerability_exports.yml b/db/docs/vulnerability_exports.yml index 823ec9ac252..4a975fa67f5 100644 --- a/db/docs/vulnerability_exports.yml +++ b/db/docs/vulnerability_exports.yml @@ -7,4 +7,13 @@ feature_categories: description: Stores metadata about exported Vulnerabilities CSV files introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27196 milestone: '13.0' -gitlab_schema: gitlab_main +gitlab_schema: gitlab_main_cell +allow_cross_joins: +- gitlab_main_clusterwide +allow_cross_transactions: +- gitlab_main_clusterwide +allow_cross_foreign_keys: +- gitlab_main_clusterwide +sharding_key: + project_id: projects + group_id: namespaces diff --git a/db/migrate/20240410061546_add_index_for_member_approvals_member_namespace_id_status.rb b/db/migrate/20240410061546_add_index_for_member_approvals_member_namespace_id_status.rb new file mode 100644 index 00000000000..a76569985f7 --- /dev/null +++ b/db/migrate/20240410061546_add_index_for_member_approvals_member_namespace_id_status.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddIndexForMemberApprovalsMemberNamespaceIdStatus < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '16.11' + + INDEX_NAME = 'index_member_approvals_on_member_namespace_id_status' + + def up + add_concurrent_index :member_approvals, [:member_namespace_id, :status], where: 'status = 0', name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :member_approvals, INDEX_NAME + end +end diff --git a/db/schema_migrations/20240410061546 b/db/schema_migrations/20240410061546 new file mode 100644 index 00000000000..5e532b6a2cc --- /dev/null +++ b/db/schema_migrations/20240410061546 @@ -0,0 +1 @@ +90dda8138e958b31a29a180fe5023417418da6862d8621e7b89d80e5d8abd3da \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 503eefcebe2..f0eab50afdd 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -25902,6 +25902,8 @@ CREATE INDEX index_member_approval_on_requested_by_id ON member_approvals USING CREATE INDEX index_member_approval_on_reviewed_by_id ON member_approvals USING btree (reviewed_by_id); +CREATE INDEX index_member_approvals_on_member_namespace_id_status ON member_approvals USING btree (member_namespace_id, status) WHERE (status = 0); + CREATE INDEX index_member_approvals_on_member_role_id ON member_approvals USING btree (member_role_id); CREATE INDEX index_member_approvals_on_user_id ON member_approvals USING btree (user_id); diff --git a/doc/administration/audit_event_streaming/index.md b/doc/administration/audit_event_streaming/index.md index 17856ebf24f..417818d40b0 100644 --- a/doc/administration/audit_event_streaming/index.md +++ b/doc/administration/audit_event_streaming/index.md @@ -470,7 +470,7 @@ To delete only the custom HTTP headers for a streaming destination: 1. On the left sidebar, at the bottom, select **Admin Area**. 1. Select **Monitoring > Audit Events**. 1. On the main area, select the **Streams** tab. -1. To the right of the item, **Edit** (**{pencil}**). +1. To the right of the item, select **Edit** (**{pencil}**). 1. Locate the **Custom HTTP headers** table. 1. Locate the header that you wish to remove. 1. To the right of the header, select **Delete** (**{remove}**). diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 4fdc2b46286..b7759f084b2 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -12843,6 +12843,117 @@ The edge type for [`MlCandidate`](#mlcandidate). | `cursor` | [`String!`](#string) | A cursor for use in pagination. | | `node` | [`MlCandidate`](#mlcandidate) | The item at the end of the edge. | +#### `MlCandidateMetadataConnection` + +The connection type for [`MlCandidateMetadata`](#mlcandidatemetadata). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `edges` | [`[MlCandidateMetadataEdge]`](#mlcandidatemetadataedge) | A list of edges. | +| `nodes` | [`[MlCandidateMetadata]`](#mlcandidatemetadata) | A list of nodes. | +| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +##### Fields with arguments + +###### `MlCandidateMetadataConnection.count` + +Limited count of collection. Returns limit + 1 for counts greater than the limit. + +Returns [`Int!`](#int). + +####### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `limit` | [`Int`](#int) | Limit value to be applied to the count query. Default is 1000. | + +#### `MlCandidateMetadataEdge` + +The edge type for [`MlCandidateMetadata`](#mlcandidatemetadata). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `cursor` | [`String!`](#string) | A cursor for use in pagination. | +| `node` | [`MlCandidateMetadata`](#mlcandidatemetadata) | The item at the end of the edge. | + +#### `MlCandidateMetricConnection` + +The connection type for [`MlCandidateMetric`](#mlcandidatemetric). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `edges` | [`[MlCandidateMetricEdge]`](#mlcandidatemetricedge) | A list of edges. | +| `nodes` | [`[MlCandidateMetric]`](#mlcandidatemetric) | A list of nodes. | +| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +##### Fields with arguments + +###### `MlCandidateMetricConnection.count` + +Limited count of collection. Returns limit + 1 for counts greater than the limit. + +Returns [`Int!`](#int). + +####### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `limit` | [`Int`](#int) | Limit value to be applied to the count query. Default is 1000. | + +#### `MlCandidateMetricEdge` + +The edge type for [`MlCandidateMetric`](#mlcandidatemetric). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `cursor` | [`String!`](#string) | A cursor for use in pagination. | +| `node` | [`MlCandidateMetric`](#mlcandidatemetric) | The item at the end of the edge. | + +#### `MlCandidateParamConnection` + +The connection type for [`MlCandidateParam`](#mlcandidateparam). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `edges` | [`[MlCandidateParamEdge]`](#mlcandidateparamedge) | A list of edges. | +| `nodes` | [`[MlCandidateParam]`](#mlcandidateparam) | A list of nodes. | +| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +##### Fields with arguments + +###### `MlCandidateParamConnection.count` + +Limited count of collection. Returns limit + 1 for counts greater than the limit. + +Returns [`Int!`](#int). + +####### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `limit` | [`Int`](#int) | Limit value to be applied to the count query. Default is 1000. | + +#### `MlCandidateParamEdge` + +The edge type for [`MlCandidateParam`](#mlcandidateparam). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `cursor` | [`String!`](#string) | A cursor for use in pagination. | +| `node` | [`MlCandidateParam`](#mlcandidateparam) | The item at the end of the edge. | + #### `MlModelConnection` The connection type for [`MlModel`](#mlmodel). @@ -16581,14 +16692,14 @@ Branch rules configured for a rule target. | ---- | ---- | ----------- | | `approvalRules` | [`ApprovalProjectRuleConnection`](#approvalprojectruleconnection) | Merge request approval rules configured for this branch rule. (see [Connections](#connections)) | | `branchProtection` | [`BranchProtection`](#branchprotection) | Branch protections configured for this branch rule. | -| `createdAt` | [`Time!`](#time) | Timestamp of when the branch rule was created. | +| `createdAt` | [`Time`](#time) | Timestamp of when the branch rule was created. | | `externalStatusChecks` | [`ExternalStatusCheckConnection`](#externalstatuscheckconnection) | External status checks configured for this branch rule. (see [Connections](#connections)) | | `id` | [`ProjectsBranchRuleID`](#projectsbranchruleid) | ID of the branch rule. | | `isDefault` | [`Boolean!`](#boolean) | Check if this branch rule protects the project's default branch. | | `isProtected` | [`Boolean!`](#boolean) | Check if this branch rule protects access for the branch. | | `matchingBranchesCount` | [`Int!`](#int) | Number of existing branches that match this branch rule. | | `name` | [`String!`](#string) | Name of the branch rule target. Includes wildcards. | -| `updatedAt` | [`Time!`](#time) | Timestamp of when the branch rule was last updated. | +| `updatedAt` | [`Time`](#time) | Timestamp of when the branch rule was last updated. | ### `BurnupChartDailyTotals` @@ -24616,9 +24727,49 @@ Candidate for a model version in the model registry. | `eid` | [`String!`](#string) | MLflow uuid for the candidate. | | `id` | [`MlCandidateID!`](#mlcandidateid) | ID of the candidate. | | `iid` | [`Int!`](#int) | IID of the candidate scoped to project. | +| `metadata` | [`MlCandidateMetadataConnection!`](#mlcandidatemetadataconnection) | Metadata entries for the candidate. (see [Connections](#connections)) | +| `metrics` | [`MlCandidateMetricConnection!`](#mlcandidatemetricconnection) | Metrics for the candidate. (see [Connections](#connections)) | | `name` | [`String`](#string) | Name of the candidate. | +| `params` | [`MlCandidateParamConnection!`](#mlcandidateparamconnection) | Parameters for the candidate. (see [Connections](#connections)) | | `status` | [`String`](#string) | Candidate status. | +### `MlCandidateMetadata` + +Metadata for a candidate in the model registry. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `id` | [`MlCandidateMetadataID!`](#mlcandidatemetadataid) | ID of the metadata. | +| `name` | [`String`](#string) | Name of the metadata entry. | +| `value` | [`String!`](#string) | Value set for the metadata entry. | + +### `MlCandidateMetric` + +Metric for a candidate in the model registry. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `id` | [`MlCandidateMetricID!`](#mlcandidatemetricid) | ID of the metric. | +| `name` | [`String`](#string) | Name of the metric. | +| `step` | [`Int!`](#int) | Step at which the metric was measured. | +| `value` | [`Float!`](#float) | Value set for the metric. | + +### `MlCandidateParam` + +Parameter for a candidate in the model registry. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `id` | [`MlCandidateParamID!`](#mlcandidateparamid) | ID of the parameter. | +| `name` | [`String`](#string) | Name of the parameter. | +| `value` | [`String!`](#string) | Value set for the parameter. | + ### `MlModel` Machine learning model in the model registry. @@ -24639,6 +24790,18 @@ Machine learning model in the model registry. #### Fields with arguments +##### `MlModel.version` + +Version of the model. + +Returns [`MlModelVersion`](#mlmodelversion). + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `modelVersionId` | [`MlModelVersionID`](#mlmodelversionid) | Id of the version to be fetched. | + ##### `MlModel.versions` Versions of the model. @@ -24669,6 +24832,7 @@ Version of a machine learning model. | `candidate` | [`MlCandidate!`](#mlcandidate) | Metrics, params and metadata for the model version. | | `createdAt` | [`Time!`](#time) | Date of creation. | | `id` | [`MlModelVersionID!`](#mlmodelversionid) | ID of the model version. | +| `packageId` | [`PackagesPackageID!`](#packagespackageid) | Package for model version artifacts. | | `version` | [`String!`](#string) | Name of the version. | ### `MonthlyUsage` @@ -25942,7 +26106,6 @@ Check permissions for the current user on a vulnerability finding. | `archived` | [`Boolean`](#boolean) | Indicates the archived status of the project. | | `autocloseReferencedIssues` | [`Boolean`](#boolean) | Indicates if issues referenced by merge requests and commits within the default branch are closed automatically. | | `avatarUrl` | [`String`](#string) | URL to avatar image file of the project. | -| `branchRules` | [`BranchRuleConnection`](#branchruleconnection) | Branch rules configured for the project. (see [Connections](#connections)) | | `ciAccessAuthorizedAgents` | [`ClusterAgentAuthorizationCiAccessConnection`](#clusteragentauthorizationciaccessconnection) | Authorized cluster agents for the project through ci_access keyword. (see [Connections](#connections)) | | `ciCdSettings` | [`ProjectCiCdSetting`](#projectcicdsetting) | CI/CD settings for the project. | | `ciConfigPathOrDefault` | [`String!`](#string) | Path of the CI configuration file. | @@ -26261,6 +26424,22 @@ four standard [pagination arguments](#pagination-arguments): | ---- | ---- | ----------- | | `id` | [`BoardID`](#boardid) | Find a board by its ID. | +##### `Project.branchRules` + +Branch rules configured for the project. + +Returns [`BranchRuleConnection`](#branchruleconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#pagination-arguments): +`before: String`, `after: String`, `first: Int`, and `last: Int`. + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `buildMissing` | [`Boolean`](#boolean) | Return unpersisted custom branch rules. | + ##### `Project.ciConfigVariables` CI/CD config variable. @@ -34873,6 +35052,24 @@ A `MlCandidateID` is a global ID. It is encoded as a string. An example `MlCandidateID` is: `"gid://gitlab/Ml::Candidate/1"`. +### `MlCandidateMetadataID` + +A `MlCandidateMetadataID` is a global ID. It is encoded as a string. + +An example `MlCandidateMetadataID` is: `"gid://gitlab/Ml::CandidateMetadata/1"`. + +### `MlCandidateMetricID` + +A `MlCandidateMetricID` is a global ID. It is encoded as a string. + +An example `MlCandidateMetricID` is: `"gid://gitlab/Ml::CandidateMetric/1"`. + +### `MlCandidateParamID` + +A `MlCandidateParamID` is a global ID. It is encoded as a string. + +An example `MlCandidateParamID` is: `"gid://gitlab/Ml::CandidateParam/1"`. + ### `MlModelID` A `MlModelID` is a global ID. It is encoded as a string. diff --git a/doc/architecture/blueprints/cells/index.md b/doc/architecture/blueprints/cells/index.md index 363cd367f41..2385bdc8366 100644 --- a/doc/architecture/blueprints/cells/index.md +++ b/doc/architecture/blueprints/cells/index.md @@ -319,8 +319,7 @@ For example, users on GitLab Dedicated don't have to have a different and unique ### Can different Cells communicate with each other? -Up until iteration 3, Cells communicate with each other only via a shared database that contains common data. -In iteration 4 we are going to evaluate the option of Cells calling each other via API to provide more isolation and reliability. +Not directly, our goal is to keep them isolated and only communicate using global services. ### How are Cells provisioned? diff --git a/doc/development/internal_analytics/metrics/metrics_lifecycle.md b/doc/development/internal_analytics/metrics/metrics_lifecycle.md index 6899f31a2b7..0c1bb5a1b9e 100644 --- a/doc/development/internal_analytics/metrics/metrics_lifecycle.md +++ b/doc/development/internal_analytics/metrics/metrics_lifecycle.md @@ -57,3 +57,25 @@ Currently, the [Metrics Dictionary](https://metrics.gitlab.com/) is built automa Do not remove the metric's YAML definition altogether. Some self-managed instances might not immediately update to the latest version of GitLab, and therefore continue to report the removed metric. The Analytics Instrumentation team requires a record of all removed metrics to identify and filter them. + +## Group name changes + +When the name of a group that owns events or metrics is changed, the `product_group` property should be updated in all metric and event definitions belonging to that group. + +The `product_group_renamer` script can update all the definitions so you do not have to do it manually. + +For example, if the group 5-min-app was renamed to 2-min-app, you can update the relevant files like this: + +```shell +$ ruby scripts/internal_events/product_group_renamer.rb 5-min-app 2-min-app +Updated '5-min-app' to '2-min-app' in 3 files + +Updated files: + config/metrics/schema/product_groups.json + config/metrics/counts_28d/20210216184517_p_ci_templates_5_min_production_app_monthly.yml + config/metrics/counts_7d/20210216184515_p_ci_templates_5_min_production_app_weekly.yml +``` + +After running the script, you must commit all the modified files to Git and create a merge request. + +If a group is split into multiple groups, you need to manually update the product_group. diff --git a/doc/subscriptions/subscription-add-ons.md b/doc/subscriptions/subscription-add-ons.md index 8c677dff6ff..c8fc855783a 100644 --- a/doc/subscriptions/subscription-add-ons.md +++ b/doc/subscriptions/subscription-add-ons.md @@ -66,10 +66,17 @@ Prerequisites: #### Configure network and proxy settings -For self-managed instances, you must update your firewalls and HTTP proxy servers -to allow outbound connections to `https://cloud.gitlab.com:443`. -To use an HTTP/S proxy, set the `gitLab_workhorse` and `gitLab_rails` -[web proxy environment variables](https://docs.gitlab.com/omnibus/settings/environment-variables.html). +For all self-managed AI features: + +- Your firewalls and HTTP/S proxy servers must allow outbound connections + to `gitlab.com` and `cloud.gitlab.com` on port `443`. +- Both `HTTP2` and the `'upgrade'` header must be allowed, because GitLab Duo + uses both REST and WebSockets. +- To use an HTTP/S proxy, both `gitLab_workhorse` and `gitLab_rails` must have the necessary + [web proxy environment variables](https://docs.gitlab.com/omnibus/settings/environment-variables.html) set. +- Check for restrictions on WebSocket (`wss://`) traffic to `wss://gitlab.com/-/cable` and other `.com` domains. + Network policy restrictions on `wss://` traffic can cause issues with some GitLab Duo Chat + services. Consider policy updates to allow these services. ### Assign seats in bulk diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 80903fd2af6..ae040de747d 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -208,8 +208,8 @@ The following table lists project permissions available for each role: | [Repository](project/repository/index.md):
Remove fork relationship | | | | | ✓ | | | [Repository](project/repository/index.md):
Force push to protected branches | | | | | | Not allowed for Guest, Reporter, Developer, Maintainer, or Owner. See [protected branches](project/protected_branches.md). | | [Repository](project/repository/index.md):
Remove protected branches by using the UI or API | | | | ✓ | ✓ | | -| [Requirements Management](project/requirements/index.md):
Archive / reopen | | ✓ | ✓ | ✓ | ✓ | | -| [Requirements Management](project/requirements/index.md):
Create / edit | | ✓ | ✓ | ✓ | ✓ | | +| [Requirements Management](project/requirements/index.md):
Archive / reopen | | ✓ | ✓ | ✓ | ✓ | Authors and assignees can archive and re-open even if they don’t have the Reporter role. | +| [Requirements Management](project/requirements/index.md):
Create / edit | | ✓ | ✓ | ✓ | ✓ | Authors and assignees can modify the title and description even if they don’t have the Reporter role.| | [Requirements Management](project/requirements/index.md):
Import / export | | ✓ | ✓ | ✓ | ✓ | | | [Security dashboard](application_security/security_dashboard/index.md):
Create issue from vulnerability finding | | | ✓ | ✓ | ✓ | | | [Security dashboard](application_security/security_dashboard/index.md):
Create vulnerability from vulnerability finding | | | ✓ | ✓ | ✓ | | diff --git a/doc/user/project/requirements/index.md b/doc/user/project/requirements/index.md index a733df8225e..a4fd5012931 100644 --- a/doc/user/project/requirements/index.md +++ b/doc/user/project/requirements/index.md @@ -69,13 +69,13 @@ next to the requirement title. ## Edit a requirement -> - The ability to mark a requirement as Satisfied [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218607) in GitLab 13.5. +> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/424961) in GitLab 16.11: Authors and assignees can edit requirements even if they don’t have the Reporter role. You can edit a requirement from the requirements list page. Prerequisites: -- You must have at least the Reporter role. +- You must have at least the Reporter role or be the author or assignee of the requirement. To edit a requirement: @@ -86,12 +86,14 @@ To edit a requirement: ## Archive a requirement +> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/424961) in GitLab 16.11: Authors and assignees can archive requirements even if they don’t have the Reporter role. + You can archive an open requirement while you're in the **Open** tab. Prerequisites: -- You must have at least the Reporter role. +- You must have at least the Reporter role or be the author or assignee of the requirement. To archive a requirement, select **Archive** (**{archive}**). @@ -99,11 +101,13 @@ As soon as a requirement is archived, it no longer appears in the **Open** tab. ## Reopen a requirement +> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/424961) in GitLab 16.11: Authors and assignees can re-open requirements even if they don’t have the Reporter role. + You can view the list of archived requirements in the **Archived** tab. Prerequisites: -- You must have at least the Reporter role. +- You must have at least the Reporter role or be the author or assignee of the requirement. ![archived requirements list](img/requirements_archived_list_view_v13_1.png) diff --git a/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml index c58db066d61..23792486d60 100644 --- a/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml @@ -4,7 +4,7 @@ # https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml default: - image: node:latest + image: node:21.7.3 # This folder is cached between builds # https://docs.gitlab.com/ee/ci/yaml/index.html#cache diff --git a/lib/sidebars/groups/menus/settings_menu.rb b/lib/sidebars/groups/menus/settings_menu.rb index d20dec2f8b9..8836f82037b 100644 --- a/lib/sidebars/groups/menus/settings_menu.rb +++ b/lib/sidebars/groups/menus/settings_menu.rb @@ -119,7 +119,6 @@ module Sidebars ) end - # overriden in ee/lib/ee/sidebars/groups/menus/settings_menu.rb def usage_quotas_menu_enabled? context.group.usage_quotas_enabled? end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 02d75676e82..158e5c7ebea 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -18948,6 +18948,33 @@ msgstr "" msgid "Due to inactivity, this project is scheduled to be deleted on %{deletion_date}. %{link_start}Why is this scheduled?%{link_end}" msgstr "" +msgid "DuoChat|Ask a question about GitLab" +msgstr "" + +msgid "DuoChat|For example, %{linkStart}what is a fork%{linkEnd}?" +msgstr "" + +msgid "DuoChat|GitLab Duo Chat" +msgstr "" + +msgid "DuoChat|Give feedback" +msgstr "" + +msgid "DuoChat|How to use GitLab" +msgstr "" + +msgid "DuoChat|The issue, epic, or code you're viewing" +msgstr "" + +msgid "DuoChat|There was an error communicating with GitLab Duo Chat. Please try again later." +msgstr "" + +msgid "DuoChat|Use AI to answer questions about things like:" +msgstr "" + +msgid "DuoChat|What is a fork?" +msgstr "" + msgid "DuoProTrialAlert|Dismiss Code Suggestions banner" msgstr "" @@ -35347,9 +35374,6 @@ msgstr "" msgid "One or more of your personal access tokens will expire in %{days_to_expire} days or less:" msgstr "" -msgid "One or more of your resource access tokens will expire in %{days_to_expire} or less:" -msgstr "" - msgid "Only %{workspaceType} members with %{permissions} can view or be notified about this %{issuableType}." msgstr "" @@ -50681,33 +50705,6 @@ msgstr "" msgid "Take a look at the documentation to discover all of GitLab’s capabilities." msgstr "" -msgid "TanukiBot|Ask a question about GitLab" -msgstr "" - -msgid "TanukiBot|For example, %{linkStart}what is a fork%{linkEnd}?" -msgstr "" - -msgid "TanukiBot|GitLab Duo Chat" -msgstr "" - -msgid "TanukiBot|Give feedback" -msgstr "" - -msgid "TanukiBot|How to use GitLab" -msgstr "" - -msgid "TanukiBot|The issue, epic, or code you're viewing" -msgstr "" - -msgid "TanukiBot|There was an error communicating with GitLab Duo Chat. Please try again later." -msgstr "" - -msgid "TanukiBot|Use AI to answer questions about things like:" -msgstr "" - -msgid "TanukiBot|What is a fork?" -msgstr "" - msgid "Target" msgstr "" @@ -55881,6 +55878,9 @@ msgstr "" msgid "UserProfile|You can create a group for several dependent projects" msgstr "" +msgid "UserProfile|You cannot create projects in your personal namespace. Contact your GitLab administrator." +msgstr "" + msgid "UserProfile|You do not have any followers" msgstr "" @@ -58791,7 +58791,7 @@ msgstr "" msgid "You can create a new SSH key by visiting %{link}" msgstr "" -msgid "You can create a new one or check them in your %{link_start}access tokens%{link_end} settings." +msgid "You can create a new one or check them in your %{link_start}Access Tokens%{link_end} settings." msgstr "" msgid "You can create a new one or check them in your %{pat_link_start}personal access tokens%{pat_link_end} settings." @@ -59378,6 +59378,12 @@ msgstr "" msgid "Your %{plan} plan will be applied to your group." msgstr "" +msgid "Your %{resource_type} access token %{codeOpen}%{token_name}%{codeClose} for %{codeOpen}%{resource_path}%{codeClose} will expire in %{days_to_expire} or less." +msgstr "" + +msgid "Your %{resource_type} access token %{token_name} for %{resource_path} will expire in %{days_to_expire} or less." +msgstr "" + msgid "Your %{spammable_entity_type} has been recognized as spam. Please, change the content or solve the reCAPTCHA to proceed." msgstr "" diff --git a/scripts/internal_events/product_group_renamer.rb b/scripts/internal_events/product_group_renamer.rb new file mode 100644 index 00000000000..2f2680c4172 --- /dev/null +++ b/scripts/internal_events/product_group_renamer.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +# !/usr/bin/env ruby +# +# Update group name in all relevant metric and event definition after a group name change. + +require 'json' +PRODUCT_GROUPS_SCHEMA_PATH = 'config/metrics/schema/product_groups.json' +ALL_METRIC_AND_EVENT_DEFINITIONS_GLOB = "{ee/,}config/{metrics/*,events}/*.yml" + +class ProductGroupRenamer + def initialize(schema_path, definitions_glob) + @schema_path = schema_path + @definitions_glob = definitions_glob + end + + def rename_product_group(old_name, new_name) + changed_files = [] + # Rename the product group in the schema + + current_schema = File.read(@schema_path) + product_group_schema = JSON.parse(current_schema) + + product_group_schema["enum"].delete(old_name) + product_group_schema["enum"].push(new_name) unless product_group_schema["enum"].include?(new_name) + product_group_schema["enum"].sort! + + new_schema = "#{JSON.pretty_generate(product_group_schema)}\n" + if new_schema != current_schema + File.write(@schema_path, new_schema) + changed_files << @schema_path + end + + # Rename product group in all metric and event definitions + Dir.glob(@definitions_glob).each do |file_path| + file_content = File.read(File.expand_path(file_path)) + + new_content = file_content.gsub(/product_group:\s*['"]?#{old_name}['"]?$/, "product_group: #{new_name}") + + if new_content != file_content + File.write(file_path, new_content) + changed_files << file_path + end + end + + changed_files + end +end + +if $PROGRAM_NAME == __FILE__ + if ARGV.length != 2 + puts <<~TEXT + Usage: + When a group is renamed, this script replaces the value for "product_group" in all matching event & metric definitions. + + Format: + ruby #{$PROGRAM_NAME} OLD_NAME NEW_NAME + + Example: + ruby #{$PROGRAM_NAME} pipeline_authoring renamed_pipeline_authoring + TEXT + exit + end + + old_name = ARGV[0] + new_name = ARGV[1] + + changed_files = ProductGroupRenamer + .new(PRODUCT_GROUPS_SCHEMA_PATH, ALL_METRIC_AND_EVENT_DEFINITIONS_GLOB) + .rename_product_group(old_name, new_name) + + puts "Updated '#{old_name}' to '#{new_name}' in #{changed_files.length} files" + puts + + if changed_files.any? + puts "Updated files:" + changed_files.each do |file_path| + puts " #{file_path}" + end + end +end diff --git a/spec/fixtures/scripts/product_group_renamer/event_definition.yml b/spec/fixtures/scripts/product_group_renamer/event_definition.yml new file mode 100644 index 00000000000..dd4b48e762a --- /dev/null +++ b/spec/fixtures/scripts/product_group_renamer/event_definition.yml @@ -0,0 +1,18 @@ +--- +description: Engineer uses Internal Event CLI to define a new event +internal_events: true +action: internal_events_cli_used +identifiers: +- project +- namespace +- user +product_group: a_group_name +milestone: '16.6' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/149010 +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate diff --git a/spec/fixtures/scripts/product_group_renamer/event_definition_from_another_group.yml b/spec/fixtures/scripts/product_group_renamer/event_definition_from_another_group.yml new file mode 100644 index 00000000000..57bf3a1b779 --- /dev/null +++ b/spec/fixtures/scripts/product_group_renamer/event_definition_from_another_group.yml @@ -0,0 +1,18 @@ +--- +description: Engineer uses Internal Event CLI to define a new event +internal_events: true +action: internal_events_cli_used +identifiers: +- project +- namespace +- user +product_group: another_group +milestone: '17.0' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/149010 +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate diff --git a/spec/fixtures/scripts/product_group_renamer/metric_definition.yml b/spec/fixtures/scripts/product_group_renamer/metric_definition.yml new file mode 100644 index 00000000000..12a581501da --- /dev/null +++ b/spec/fixtures/scripts/product_group_renamer/metric_definition.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.count_total_internal_events_cli_used +description: Total count of when an event was defined using the CLI +product_group: a_group_name +performance_indicator_type: [] +value_type: number +status: active +milestone: '17.0' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/149010 +time_frame: all +data_source: internal_events +data_category: optional +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +events: +- name: internal_events_cli_used diff --git a/spec/fixtures/scripts/product_group_renamer/product_groups.json b/spec/fixtures/scripts/product_group_renamer/product_groups.json new file mode 100644 index 00000000000..99fd56a0465 --- /dev/null +++ b/spec/fixtures/scripts/product_group_renamer/product_groups.json @@ -0,0 +1,7 @@ +{ + "type": "string", + "enum": [ + "a_better_group_name", + "another_group_name" + ] +} diff --git a/spec/graphql/resolvers/ml/find_model_version_resolver_spec.rb b/spec/graphql/resolvers/ml/find_model_version_resolver_spec.rb new file mode 100644 index 00000000000..1ac19889d6c --- /dev/null +++ b/spec/graphql/resolvers/ml/find_model_version_resolver_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Ml::FindModelVersionResolver, feature_category: :mlops do + include GraphqlHelpers + + describe '#resolve' do + let_it_be(:project) { create(:project) } + let_it_be(:model) { create(:ml_models, project: project) } + let_it_be(:model_version) { create(:ml_model_versions, model: model) } + let_it_be(:another_model_version) { create(:ml_model_versions) } + let_it_be(:owner) { project.owner } + + let(:current_user) { owner } + let(:args) { { model_version_id: global_id_of(model_version) } } + let(:read_model_registry) { true } + + subject { force(resolve(described_class, obj: model, ctx: { current_user: current_user }, args: args)) } + + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(current_user, :read_model_registry, project) + .and_return(read_model_registry) + end + + context 'when user is allowed and model version exists and belongs to model' do + it { is_expected.to eq(model_version) } + + context 'when user is nil' do + let(:current_user) { nil } + + it { is_expected.to eq(model_version) } + end + end + + context 'when user does not have permission' do + let(:read_model_registry) { false } + + it { is_expected.to be_nil } + end + + context 'when model version exists but does not belong to model' do + let(:args) { { model_version_id: global_id_of(another_model_version) } } + + it { is_expected.to be_nil } + end + + context 'when model version does not exist' do + let(:args) { { model_version_id: global_id_of(id: non_existing_record_id, model_name: 'Ml::ModelVersion') } } + + it { is_expected.to be_nil } + end + end +end diff --git a/spec/graphql/types/ml/candidate_metadata_type_spec.rb b/spec/graphql/types/ml/candidate_metadata_type_spec.rb new file mode 100644 index 00000000000..719739d76f6 --- /dev/null +++ b/spec/graphql/types/ml/candidate_metadata_type_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['MlCandidateMetadata'], feature_category: :mlops do + it 'has the expected fields' do + expected_fields = %w[id name value] + + expect(described_class).to include_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/ml/candidate_metric_type_spec.rb b/spec/graphql/types/ml/candidate_metric_type_spec.rb new file mode 100644 index 00000000000..f5c345b819b --- /dev/null +++ b/spec/graphql/types/ml/candidate_metric_type_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['MlCandidateMetric'], feature_category: :mlops do + it 'has the expected fields' do + expected_fields = %w[id name value step] + + expect(described_class).to include_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/ml/candidate_param_type_spec.rb b/spec/graphql/types/ml/candidate_param_type_spec.rb new file mode 100644 index 00000000000..a16b6a6428b --- /dev/null +++ b/spec/graphql/types/ml/candidate_param_type_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['MlCandidateParam'], feature_category: :mlops do + it 'has the expected fields' do + expected_fields = %w[id name value] + + expect(described_class).to include_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/ml/candidate_type_spec.rb b/spec/graphql/types/ml/candidate_type_spec.rb index 11a3e46ecc8..41219b0b09f 100644 --- a/spec/graphql/types/ml/candidate_type_spec.rb +++ b/spec/graphql/types/ml/candidate_type_spec.rb @@ -10,6 +10,9 @@ RSpec.describe GitlabSchema.types['MlCandidate'], feature_category: :mlops do let_it_be(:candidate) do model_version.candidate.tap do |c| c.update!(ci_build: create(:ci_build, pipeline: pipeline, user: current_user)) + c.metrics = [create(:ml_candidate_metrics, candidate: c)] + c.params = [create(:ml_candidate_params, candidate: c)] + c.metadata = [create(:ml_candidate_metadata, candidate: c)] end end @@ -33,6 +36,21 @@ RSpec.describe GitlabSchema.types['MlCandidate'], feature_category: :mlops do showPath artifactPath } + metrics { + nodes { + id + } + } + params { + nodes { + id + } + } + metadata { + nodes { + id + } + } } } } @@ -59,6 +77,27 @@ RSpec.describe GitlabSchema.types['MlCandidate'], feature_category: :mlops do '_links' => { 'showPath' => "/#{project.full_path}/-/ml/candidates/#{model_version.candidate.iid}", 'artifactPath' => "/#{project.full_path}/-/packages/#{model_version.package_id}" + }, + 'metrics' => { + 'nodes' => [ + { + 'id' => "gid://gitlab/Ml::CandidateMetric/#{candidate.metrics.first.id}" + } + ] + }, + 'params' => { + 'nodes' => [ + { + 'id' => "gid://gitlab/Ml::CandidateParam/#{candidate.params.first.id}" + } + ] + }, + 'metadata' => { + 'nodes' => [ + { + 'id' => "gid://gitlab/Ml::CandidateMetadata/#{candidate.metadata.first.id}" + } + ] } }) end diff --git a/spec/graphql/types/ml/model_type_spec.rb b/spec/graphql/types/ml/model_type_spec.rb index 43732743a6b..9b0cbeda2a1 100644 --- a/spec/graphql/types/ml/model_type_spec.rb +++ b/spec/graphql/types/ml/model_type_spec.rb @@ -7,10 +7,13 @@ RSpec.describe GitlabSchema.types['MlModel'], feature_category: :mlops do let_it_be(:project) { model.project } let_it_be(:candidates) { Array.new(2) { create(:ml_candidates, experiment: model.default_experiment) } } + let_it_be(:model_id) { GitlabSchema.id_from_object(model).to_s } + let_it_be(:model_version_id) { GitlabSchema.id_from_object(model.latest_version).to_s } + let(:query) do %( query { - mlModel(id: "gid://gitlab/Ml::Model/#{model.id}") { + mlModel(id: "#{model_id}") { id description name @@ -19,6 +22,9 @@ RSpec.describe GitlabSchema.types['MlModel'], feature_category: :mlops do latestVersion { id } + version(modelVersionId: "#{model_version_id}") { + id + } _links { showPath } @@ -33,7 +39,7 @@ RSpec.describe GitlabSchema.types['MlModel'], feature_category: :mlops do it 'includes all the fields' do expected_fields = %w[id name versions candidates version_count _links created_at latest_version description - candidate_count description] + candidate_count description version] expect(described_class).to include_graphql_fields(*expected_fields) end @@ -42,11 +48,14 @@ RSpec.describe GitlabSchema.types['MlModel'], feature_category: :mlops do model_data = data.dig('data', 'mlModel') expect(model_data).to eq({ - 'id' => "gid://gitlab/Ml::Model/#{model.id}", + 'id' => model_id, 'name' => model.name, 'description' => 'A description', 'latestVersion' => { - 'id' => "gid://gitlab/Ml::ModelVersion/#{model.latest_version.id}" + 'id' => model_version_id + }, + 'version' => { + 'id' => model_version_id }, 'versionCount' => 1, 'candidateCount' => 2, diff --git a/spec/graphql/types/ml/model_version_type_spec.rb b/spec/graphql/types/ml/model_version_type_spec.rb index 16d4e2350a4..b77c4d77791 100644 --- a/spec/graphql/types/ml/model_version_type_spec.rb +++ b/spec/graphql/types/ml/model_version_type_spec.rb @@ -15,6 +15,7 @@ RSpec.describe GitlabSchema.types['MlModelVersion'], feature_category: :mlops do latestVersion { id version + packageId candidate { id } @@ -33,7 +34,7 @@ RSpec.describe GitlabSchema.types['MlModelVersion'], feature_category: :mlops do subject(:data) { GitlabSchema.execute(query, context: { current_user: project.owner }).as_json } it 'includes all fields' do - expected_fields = %w[id version created_at _links candidate] + expected_fields = %w[id version created_at _links candidate package_id] expect(described_class).to include_graphql_fields(*expected_fields) end @@ -44,6 +45,7 @@ RSpec.describe GitlabSchema.types['MlModelVersion'], feature_category: :mlops do expect(version_data).to eq({ 'id' => "gid://gitlab/Ml::ModelVersion/#{model_version.id}", 'version' => model_version.version, + 'packageId' => "gid://gitlab/Packages::Package/#{model_version.package_id}", 'candidate' => { 'id' => "gid://gitlab/Ml::Candidate/#{model_version.candidate.id}" }, diff --git a/spec/lib/gitlab/database/sharding_key_spec.rb b/spec/lib/gitlab/database/sharding_key_spec.rb index e16a68bc185..70adee091f8 100644 --- a/spec/lib/gitlab/database/sharding_key_spec.rb +++ b/spec/lib/gitlab/database/sharding_key_spec.rb @@ -42,7 +42,8 @@ RSpec.describe 'new tables missing sharding_key', feature_category: :cell do 'sprints.group_id', 'subscription_add_on_purchases.namespace_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/444338 'temp_notes_backup.project_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/443667' - *['todos.project_id', 'todos.group_id'] + *['todos.project_id', 'todos.group_id'], + *['vulnerability_exports.project_id', 'vulnerability_exports.group_id'] ] end diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb index 37118a18c94..547fe878cc3 100644 --- a/spec/mailers/emails/profile_spec.rb +++ b/spec/mailers/emails/profile_spec.rb @@ -193,7 +193,7 @@ RSpec.describe Emails::Profile, feature_category: :user_profile do resource.add_developer(project_bot) end - subject { Notify.resource_access_tokens_about_to_expire_email(user, resource, [expiring_token.name]) } + subject { Notify.bot_resource_access_token_about_to_expire_email(user, resource, expiring_token.name) } it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' @@ -216,7 +216,7 @@ RSpec.describe Emails::Profile, feature_category: :user_profile do resource.add_reporter(project_bot) end - subject { Notify.resource_access_tokens_about_to_expire_email(user, resource, [expiring_token.name]) } + subject { Notify.bot_resource_access_token_about_to_expire_email(user, resource, expiring_token.name) } it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' diff --git a/spec/requests/api/graphql/mutations/ml/models/create_spec.rb b/spec/requests/api/graphql/mutations/ml/models/create_spec.rb index 959e67d63fb..fd8e4567c76 100644 --- a/spec/requests/api/graphql/mutations/ml/models/create_spec.rb +++ b/spec/requests/api/graphql/mutations/ml/models/create_spec.rb @@ -13,7 +13,7 @@ RSpec.describe 'Creation of a machine learning model', feature_category: :mlops let(:name) { 'some_name' } let(:description) { 'A description' } - let(:mutation) { graphql_mutation(:ml_model_create, input) } + let(:mutation) { graphql_mutation(:ml_model_create, input, nil, ['version']) } let(:mutation_response) { graphql_mutation_response(:ml_model_create) } context 'when user is not allowed write changes' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 3423da68488..40c291de1bc 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1971,7 +1971,7 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and end context 'user does not have access to view the private profile' do - it 'returns no projects' do + it 'returns no projects', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/444704' do get api(path, user) expect(response).to have_gitlab_http_status(:ok) diff --git a/spec/scripts/internal_events/product_group_renamer_spec.rb b/spec/scripts/internal_events/product_group_renamer_spec.rb new file mode 100644 index 00000000000..98d847d6088 --- /dev/null +++ b/spec/scripts/internal_events/product_group_renamer_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_relative '../../../scripts/internal_events/product_group_renamer' + +RSpec.describe ProductGroupRenamer, feature_category: :service_ping do + let(:renamer) { described_class.new(schema_path, definitions_glob) } + + context 'with real definitions', :aggregate_failures do + let(:schema_path) { PRODUCT_GROUPS_SCHEMA_PATH } + let(:definitions_glob) { ALL_METRIC_AND_EVENT_DEFINITIONS_GLOB } + + it 'reads all definitions files' do + allow(File).to receive(:read).and_call_original + + Gitlab::Tracking::EventDefinition.definitions.each do |event_definition| + expect(File).to receive(:read).with(event_definition.path) + expect(File).not_to receive(:write).with(event_definition.path) + end + + Gitlab::Usage::MetricDefinition.definitions.each_value do |metric_definition| + expect(File).to receive(:read).with(metric_definition.path) + expect(File).not_to receive(:write).with(metric_definition.path) + end + + renamer.rename_product_group('old_name', 'new_name') + end + end + + describe '#rename_product_group', :aggregate_failures do + let(:temp_dir) { Dir.mktmpdir } + let(:schema_path) { File.join(temp_dir, 'product_groups.json') } + let(:event_definition_path) { File.join(temp_dir, 'event_definition.yml') } + let(:metric_definition_path) { File.join(temp_dir, 'metric_definition.yml') } + let(:event_definition_from_another_group_path) do + File.join(temp_dir, 'event_definition_from_another_group.yml') + end + + let(:definitions_glob) { [event_definition_path, metric_definition_path, event_definition_from_another_group_path] } + + before do + FileUtils.cp_r(File.join('spec/fixtures/scripts/product_group_renamer', '.'), temp_dir) + end + + after do + FileUtils.rm_rf(temp_dir) + end + + it 'renames product group in the schema and the definitions' do + renamer.rename_product_group('a_group_name', 'a_better_group_name') + + schema_content = File.read(schema_path) + + expect(schema_content).to include('a_better_group_name') + expect(schema_content).not_to include('a_group_name') + expect(File.read(event_definition_path)).to include('product_group: a_better_group_name') + expect(File.read(metric_definition_path)).to include('product_group: a_better_group_name') + expect(File.read(event_definition_from_another_group_path)).not_to include('product_group: a_better_group_name') + end + end +end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 35e84e9cdb6..ad740c1b7dc 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -384,7 +384,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do let_it_be(:owner2) { create(:user) } subject(:notification_service) do - notification.resource_access_tokens_about_to_expire(project_bot, [expiring_token.name]) + notification.bot_resource_access_token_about_to_expire(project_bot, [expiring_token.name]) end context 'when the resource is a group' do @@ -402,13 +402,13 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do owner1, project_bot.resource_bot_resource, [expiring_token.name], - mail: "resource_access_tokens_about_to_expire_email" + mail: "bot_resource_access_token_about_to_expire_email" ).and( have_enqueued_email( owner2, project_bot.resource_bot_resource, [expiring_token.name], - mail: "resource_access_tokens_about_to_expire_email" + mail: "bot_resource_access_token_about_to_expire_email" ) ) ) @@ -430,13 +430,13 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do owner1, project_bot.resource_bot_resource, [expiring_token.name], - mail: "resource_access_tokens_about_to_expire_email" + mail: "bot_resource_access_token_about_to_expire_email" ).and( have_enqueued_email( owner2, project_bot.resource_bot_resource, [expiring_token.name], - mail: "resource_access_tokens_about_to_expire_email" + mail: "bot_resource_access_token_about_to_expire_email" ) ) ) diff --git a/spec/views/shared/projects/_list.html.haml_spec.rb b/spec/views/shared/projects/_list.html.haml_spec.rb index 2a840d19418..030f54bb8a6 100644 --- a/spec/views/shared/projects/_list.html.haml_spec.rb +++ b/spec/views/shared/projects/_list.html.haml_spec.rb @@ -3,10 +3,12 @@ require 'spec_helper' RSpec.describe 'shared/projects/_list' do - let(:group) { create(:group) } + let_it_be(:user) { build(:user) } + let_it_be(:group) { create(:group) } before do allow(view).to receive(:projects).and_return(projects) + allow(view).to receive(:current_user) { user } end context 'with projects' do @@ -99,5 +101,39 @@ RSpec.describe 'shared/projects/_list' do end end end + + context 'when projects_limit > 0' do + before do + allow(user).to receive(:projects_limit).and_return(1) + controller.params[:controller] = 'users' + controller.params[:username] = user.username + end + + it 'renders `New project` button' do + render + + expect(rendered).to have_link('New project') + expect(rendered).to have_content( + s_('UserProfile|Your projects can be available publicly, internally, or privately, at your choice.') + ) + end + end + + context 'when projects_limit is 0' do + before do + allow(user).to receive(:projects_limit).and_return(0) + controller.params[:controller] = 'users' + controller.params[:username] = user.username + end + + it 'does not render `New project` button' do + render + + expect(rendered).not_to have_link('New project') + expect(rendered).to have_content( + s_("UserProfile|You cannot create projects in your personal namespace. Contact your GitLab administrator.") + ) + end + end end end diff --git a/spec/workers/personal_access_tokens/expiring_worker_spec.rb b/spec/workers/personal_access_tokens/expiring_worker_spec.rb index 930e6ef8ff7..6b021da623d 100644 --- a/spec/workers/personal_access_tokens/expiring_worker_spec.rb +++ b/spec/workers/personal_access_tokens/expiring_worker_spec.rb @@ -8,7 +8,7 @@ RSpec.describe PersonalAccessTokens::ExpiringWorker, type: :worker, feature_cate shared_examples 'sends notification about expiry of bot user tokens' do it 'uses notification service to send the email' do expect_next_instance_of(NotificationService) do |notification_service| - expect(notification_service).to receive(:resource_access_tokens_about_to_expire) + expect(notification_service).to receive(:bot_resource_access_token_about_to_expire) .with(project_bot, expiring_token.name) end