diff --git a/.rubocop_todo/gitlab/bounded_contexts.yml b/.rubocop_todo/gitlab/bounded_contexts.yml index 678dca94adf..5a696cc9ec3 100644 --- a/.rubocop_todo/gitlab/bounded_contexts.yml +++ b/.rubocop_todo/gitlab/bounded_contexts.yml @@ -141,7 +141,6 @@ Gitlab/BoundedContexts: - 'app/graphql/mutations/alert_management/http_integration/reset_token.rb' - 'app/graphql/mutations/alert_management/http_integration/update.rb' - 'app/graphql/mutations/alert_management/prometheus_integration/create.rb' - - 'app/graphql/mutations/alert_management/prometheus_integration/prometheus_integration_base.rb' - 'app/graphql/mutations/alert_management/prometheus_integration/reset_token.rb' - 'app/graphql/mutations/alert_management/prometheus_integration/update.rb' - 'app/graphql/mutations/alert_management/update_alert_status.rb' diff --git a/.rubocop_todo/graphql/graphql_name.yml b/.rubocop_todo/graphql/graphql_name.yml index b03e1202663..b9eabed7f56 100644 --- a/.rubocop_todo/graphql/graphql_name.yml +++ b/.rubocop_todo/graphql/graphql_name.yml @@ -3,7 +3,6 @@ GraphQL/GraphqlName: Exclude: - 'app/graphql/mutations/alert_management/base.rb' - 'app/graphql/mutations/alert_management/http_integration/http_integration_base.rb' - - 'app/graphql/mutations/alert_management/prometheus_integration/prometheus_integration_base.rb' - 'app/graphql/mutations/award_emojis/base.rb' - 'app/graphql/mutations/base_mutation.rb' - 'app/graphql/mutations/boards/lists/base_create.rb' diff --git a/Gemfile b/Gemfile index 53ea06ee467..551ee1a3a9b 100644 --- a/Gemfile +++ b/Gemfile @@ -565,7 +565,7 @@ group :development, :test, :coverage do gem 'simplecov', '~> 0.22', require: false, feature_category: :tooling gem 'simplecov-lcov', '~> 0.8.0', require: false, feature_category: :tooling gem 'simplecov-cobertura', '~> 2.1.0', require: false, feature_category: :tooling - gem 'undercover', '~> 0.6.0', require: false, feature_category: :tooling + gem 'undercover', '~> 0.7.0', require: false, feature_category: :tooling end # Gems required in omnibus-gitlab pipeline diff --git a/Gemfile.checksum b/Gemfile.checksum index 669907a8df6..0c4f06de117 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -773,7 +773,7 @@ {"name":"typhoeus","version":"1.4.1","platform":"ruby","checksum":"1c17db8364bd45ab302dc61e460173c3e69835896be88a3df07c206d5c55ef7c"}, {"name":"tzinfo","version":"2.0.6","platform":"ruby","checksum":"8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b"}, {"name":"uber","version":"0.1.0","platform":"ruby","checksum":"5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc"}, -{"name":"undercover","version":"0.6.4","platform":"ruby","checksum":"3c34fcf129b52a4993065c52612a65e5e05e77f0cac3f4f8f388114fb129ec1a"}, +{"name":"undercover","version":"0.7.0","platform":"ruby","checksum":"1aa906581bd4d91b093a313496b8ef522e41a50b6c6689e228fd2587d3cca74d"}, {"name":"unf","version":"0.1.4","platform":"java","checksum":"49a5972ec0b3d091d3b0b2e00113f2f342b9b212f0db855eb30a629637f6d302"}, {"name":"unf","version":"0.1.4","platform":"ruby","checksum":"4999517a531f2a955750f8831941891f6158498ec9b6cb1c81ce89388e63022e"}, {"name":"unf_ext","version":"0.0.8.2","platform":"ruby","checksum":"90b9623ee359cc4878461c5d2eab7d3d3ce5801a680a9e7ac83b8040c5b742fa"}, diff --git a/Gemfile.lock b/Gemfile.lock index 2ec3ebab72b..d162bd76dc9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1968,12 +1968,14 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) - undercover (0.6.4) + undercover (0.7.0) base64 bigdecimal imagen (>= 0.2.0) rainbow (>= 2.1, < 4.0) rugged (>= 0.27, < 1.10) + simplecov + simplecov_json_formatter unf (0.1.4) unf_ext unf_ext (0.0.8.2) @@ -2412,7 +2414,7 @@ DEPENDENCIES truncato (~> 0.7.13) tty-prompt (~> 0.23) typhoeus (~> 1.4.0) - undercover (~> 0.6.0) + undercover (~> 0.7.0) unicode-emoji (~> 4.0) unleash (~> 3.2.2) uri (= 0.13.2) diff --git a/Gemfile.next.checksum b/Gemfile.next.checksum index 276671ed136..8c10e3bbf47 100644 --- a/Gemfile.next.checksum +++ b/Gemfile.next.checksum @@ -773,7 +773,7 @@ {"name":"typhoeus","version":"1.4.1","platform":"ruby","checksum":"1c17db8364bd45ab302dc61e460173c3e69835896be88a3df07c206d5c55ef7c"}, {"name":"tzinfo","version":"2.0.6","platform":"ruby","checksum":"8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b"}, {"name":"uber","version":"0.1.0","platform":"ruby","checksum":"5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc"}, -{"name":"undercover","version":"0.6.4","platform":"ruby","checksum":"3c34fcf129b52a4993065c52612a65e5e05e77f0cac3f4f8f388114fb129ec1a"}, +{"name":"undercover","version":"0.7.0","platform":"ruby","checksum":"1aa906581bd4d91b093a313496b8ef522e41a50b6c6689e228fd2587d3cca74d"}, {"name":"unf","version":"0.1.4","platform":"java","checksum":"49a5972ec0b3d091d3b0b2e00113f2f342b9b212f0db855eb30a629637f6d302"}, {"name":"unf","version":"0.1.4","platform":"ruby","checksum":"4999517a531f2a955750f8831941891f6158498ec9b6cb1c81ce89388e63022e"}, {"name":"unf_ext","version":"0.0.8.2","platform":"ruby","checksum":"90b9623ee359cc4878461c5d2eab7d3d3ce5801a680a9e7ac83b8040c5b742fa"}, diff --git a/Gemfile.next.lock b/Gemfile.next.lock index 21674891afe..8ed8113c68e 100644 --- a/Gemfile.next.lock +++ b/Gemfile.next.lock @@ -1962,12 +1962,14 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) - undercover (0.6.4) + undercover (0.7.0) base64 bigdecimal imagen (>= 0.2.0) rainbow (>= 2.1, < 4.0) rugged (>= 0.27, < 1.10) + simplecov + simplecov_json_formatter unf (0.1.4) unf_ext unf_ext (0.0.8.2) @@ -2407,7 +2409,7 @@ DEPENDENCIES truncato (~> 0.7.13) tty-prompt (~> 0.23) typhoeus (~> 1.4.0) - undercover (~> 0.6.0) + undercover (~> 0.7.0) unicode-emoji (~> 4.0) unleash (~> 3.2.2) uri (= 0.13.2) diff --git a/app/graphql/mutations/alert_management/prometheus_integration/create.rb b/app/graphql/mutations/alert_management/prometheus_integration/create.rb index 665ce96f0f9..943929da396 100644 --- a/app/graphql/mutations/alert_management/prometheus_integration/create.rb +++ b/app/graphql/mutations/alert_management/prometheus_integration/create.rb @@ -1,44 +1,30 @@ # frozen_string_literal: true +# Deprecated: +# Remove from MutationType during any major release. module Mutations module AlertManagement module PrometheusIntegration - class Create < PrometheusIntegrationBase + class Create < HttpIntegration::Create graphql_name 'PrometheusIntegrationCreate' - include FindsProject + field :integration, + Types::AlertManagement::PrometheusIntegrationType, + null: true, + description: "Newly created integration." - argument :project_path, GraphQL::Types::ID, - required: true, - description: 'Project to create the integration in.' - - argument :active, GraphQL::Types::Boolean, - required: true, - description: 'Whether the integration is receiving alerts.' + argument :name, GraphQL::Types::String, + required: false, + description: 'Name of the integration.', + default_value: 'Prometheus' argument :api_url, GraphQL::Types::String, required: false, - description: 'Endpoint at which Prometheus can be queried.' + description: 'Endpoint at which Prometheus can be queried.', + deprecated: { reason: 'Feature removed in 16.0', milestone: '18.2' } def resolve(args) - project = authorized_find!(args[:project_path]) - - return integration_exists if project.prometheus_integration - - result = ::Projects::Operations::UpdateService.new( - project, - current_user, - **integration_attributes(args), - **token_attributes - ).execute - - response(project.prometheus_integration, result) - end - - private - - def integration_exists - response(nil, message: _('Multiple Prometheus integrations are not supported')) + super(args.merge(name: 'Prometheus', type_identifier: :prometheus)) end end end diff --git a/app/graphql/mutations/alert_management/prometheus_integration/prometheus_integration_base.rb b/app/graphql/mutations/alert_management/prometheus_integration/prometheus_integration_base.rb deleted file mode 100644 index 19fb514d3a5..00000000000 --- a/app/graphql/mutations/alert_management/prometheus_integration/prometheus_integration_base.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -module Mutations - module AlertManagement - module PrometheusIntegration - class PrometheusIntegrationBase < BaseMutation - field :integration, - Types::AlertManagement::PrometheusIntegrationType, - null: true, - description: "Newly created integration." - - authorize :admin_project - - private - - def response(integration, result) - { - integration: integration, - errors: Array(result[:message]) - } - end - - def integration_attributes(args) - { - prometheus_integration_attributes: { - manual_configuration: args[:active], - api_url: args[:api_url] - }.compact - } - end - - def token_attributes - { alerting_setting_attributes: { regenerate_token: true } } - end - end - end - end -end diff --git a/app/graphql/mutations/alert_management/prometheus_integration/reset_token.rb b/app/graphql/mutations/alert_management/prometheus_integration/reset_token.rb index 15e6763b1ee..73f045791ff 100644 --- a/app/graphql/mutations/alert_management/prometheus_integration/reset_token.rb +++ b/app/graphql/mutations/alert_management/prometheus_integration/reset_token.rb @@ -1,25 +1,29 @@ # frozen_string_literal: true +# Deprecated: +# Remove from MutationType during any major release. module Mutations module AlertManagement module PrometheusIntegration - class ResetToken < PrometheusIntegrationBase + class ResetToken < HttpIntegration::ResetToken graphql_name 'PrometheusIntegrationResetToken' + field :integration, + Types::AlertManagement::PrometheusIntegrationType, + null: true, + description: "Updated integration." + argument :id, Types::GlobalIDType[::Integrations::Prometheus], required: true, description: "ID of the integration to mutate." - def resolve(id:) - integration = authorized_find!(id: id) + def authorized_find!(**) + integration = super&.project + &.alert_management_http_integrations + &.for_endpoint_identifier('legacy-prometheus') + &.take - result = ::Projects::Operations::UpdateService.new( - integration.project, - current_user, - token_attributes - ).execute - - response integration, result + integration || raise_resource_not_available_error! end end end diff --git a/app/graphql/mutations/alert_management/prometheus_integration/update.rb b/app/graphql/mutations/alert_management/prometheus_integration/update.rb index 593624aaafd..740ef58862f 100644 --- a/app/graphql/mutations/alert_management/prometheus_integration/update.rb +++ b/app/graphql/mutations/alert_management/prometheus_integration/update.rb @@ -1,33 +1,38 @@ # frozen_string_literal: true +# Deprecated: +# Remove from MutationType during any major release. module Mutations module AlertManagement module PrometheusIntegration - class Update < PrometheusIntegrationBase + class Update < HttpIntegration::Update graphql_name 'PrometheusIntegrationUpdate' + field :integration, + Types::AlertManagement::PrometheusIntegrationType, + null: true, + description: "Updated integration." + argument :id, Types::GlobalIDType[::Integrations::Prometheus], required: true, description: "ID of the integration to mutate." - argument :active, GraphQL::Types::Boolean, - required: false, - description: "Whether the integration is receiving alerts." - argument :api_url, GraphQL::Types::String, required: false, - description: "Endpoint at which Prometheus can be queried." + description: "Endpoint at which Prometheus can be queried.", + deprecated: { reason: 'Feature removed in 16.0', milestone: '18.2' } def resolve(args) - integration = authorized_find!(id: args[:id]) + super(args.except(:name)) + end - result = ::Projects::Operations::UpdateService.new( - integration.project, - current_user, - integration_attributes(args) - ).execute + def authorized_find!(**) + integration = super&.project + &.alert_management_http_integrations + &.for_endpoint_identifier('legacy-prometheus') + &.take - response integration.reset, result + integration || raise_resource_not_available_error! end end end diff --git a/app/graphql/resolvers/alert_management/integrations_resolver.rb b/app/graphql/resolvers/alert_management/integrations_resolver.rb index 44effb17072..992d387d454 100644 --- a/app/graphql/resolvers/alert_management/integrations_resolver.rb +++ b/app/graphql/resolvers/alert_management/integrations_resolver.rb @@ -14,48 +14,37 @@ module Resolvers type Types::AlertManagement::IntegrationType.connection_type, null: true def resolve(id: nil) + return [] unless Ability.allowed?(current_user, :admin_operations, project) + if id integrations_by(gid: id) else - http_integrations + prometheus_integrations + http_integrations end end private def integrations_by(gid:) - object = GitlabSchema.object_from_id(gid, expected_type: expected_integration_types) + object = GitlabSchema.object_from_id(gid, expected_type: [ + ::AlertManagement::HttpIntegration, + ::Integrations::Prometheus + ]) + defer { object }.then do |integration| - ret = integration if project == integration&.project - Array.wrap(ret) + next [] unless integration&.project == project + + if integration.is_a?(::Integrations::Prometheus) + project.alert_management_http_integrations + .for_endpoint_identifier('legacy-prometheus').to_a + else + [integration] + end end end - def prometheus_integrations - return [] unless prometheus_integrations_allowed? - - Array(project.prometheus_integration) - end - def http_integrations - return [] unless http_integrations_allowed? - - ::AlertManagement::HttpIntegrationsFinder.new(project, { type_identifier: :http }).execute - end - - def prometheus_integrations_allowed? - Ability.allowed?(current_user, :admin_project, project) - end - - def http_integrations_allowed? - Ability.allowed?(current_user, :admin_operations, project) - end - - def expected_integration_types - [].tap do |types| - types << ::AlertManagement::HttpIntegration if http_integrations_allowed? - types << ::Integrations::Prometheus if prometheus_integrations_allowed? - end + ::AlertManagement::HttpIntegrationsFinder.new(project).execute end end end diff --git a/app/graphql/types/alert_management/http_integration_type.rb b/app/graphql/types/alert_management/http_integration_type.rb index e14a0f0f0d5..b05f3b49d73 100644 --- a/app/graphql/types/alert_management/http_integration_type.rb +++ b/app/graphql/types/alert_management/http_integration_type.rb @@ -9,14 +9,6 @@ module Types implements Types::AlertManagement::IntegrationType authorize :admin_operations - - def type - object.type_identifier.to_sym - end - - def api_url - nil - end end end end diff --git a/app/graphql/types/alert_management/integration_type.rb b/app/graphql/types/alert_management/integration_type.rb index 6cc287a59ba..0b21207f3f5 100644 --- a/app/graphql/types/alert_management/integration_type.rb +++ b/app/graphql/types/alert_management/integration_type.rb @@ -39,11 +39,20 @@ module Types field :api_url, GraphQL::Types::String, null: true, - description: 'URL at which Prometheus metrics can be queried to populate the metrics dashboard.' + description: 'URL at which Prometheus metrics can be queried to populate the metrics dashboard.', + deprecated: { reason: 'Feature removed in 16.0', milestone: '18.2' } + + def type + object.type_identifier.to_sym + end + + def api_url + nil + end definition_methods do def resolve_type(object, context) - if object.is_a?(::Integrations::Prometheus) + if object.type_identifier == 'prometheus' Types::AlertManagement::PrometheusIntegrationType else Types::AlertManagement::HttpIntegrationType diff --git a/app/graphql/types/alert_management/prometheus_integration_type.rb b/app/graphql/types/alert_management/prometheus_integration_type.rb index 0f61eeaa177..a47a7f79eb4 100644 --- a/app/graphql/types/alert_management/prometheus_integration_type.rb +++ b/app/graphql/types/alert_management/prometheus_integration_type.rb @@ -1,38 +1,16 @@ # frozen_string_literal: true +# Deprecated: +# Remove with PrometheusIntegration mutations during any major release. module Types module AlertManagement class PrometheusIntegrationType < ::Types::BaseObject graphql_name 'AlertManagementPrometheusIntegration' - description 'An endpoint and credentials used to accept Prometheus alerts for a project' - - include ::Gitlab::Routing + description '**DEPRECATED - Use AlertManagementHttpIntegration directly** An endpoint and credentials used to accept Prometheus alerts for a project' implements Types::AlertManagement::IntegrationType - authorize :admin_project - - alias_method :prometheus_integration, :object - - def name - prometheus_integration.title - end - - def type - :prometheus - end - - def token - prometheus_integration.project&.alerting_setting&.token - end - - def url - prometheus_integration.project && notify_project_prometheus_alerts_url(prometheus_integration.project, format: :json) - end - - def active - prometheus_integration.manual_configuration? - end + authorize :admin_operations end end end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index c21783bc4d0..76d09fa958d 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -26,9 +26,15 @@ module Types mount_mutation Mutations::Security::CiConfiguration::ConfigureSast mount_mutation Mutations::Security::CiConfiguration::ConfigureSastIac mount_mutation Mutations::Security::CiConfiguration::ConfigureSecretDetection - mount_mutation Mutations::AlertManagement::PrometheusIntegration::Create - mount_mutation Mutations::AlertManagement::PrometheusIntegration::Update - mount_mutation Mutations::AlertManagement::PrometheusIntegration::ResetToken + mount_mutation Mutations::AlertManagement::PrometheusIntegration::Create, deprecated: { + reason: 'Use HttpIntegrationCreate', milestone: '18.2' + } + mount_mutation Mutations::AlertManagement::PrometheusIntegration::Update, deprecated: { + reason: 'Use HttpIntegrationUpdate', milestone: '18.2' + } + mount_mutation Mutations::AlertManagement::PrometheusIntegration::ResetToken, deprecated: { + reason: 'Use HttpIntegrationResetToken', milestone: '18.2' + } mount_mutation Mutations::AwardEmojis::Add mount_mutation Mutations::AwardEmojis::Remove mount_mutation Mutations::AwardEmojis::Toggle diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index e3e690e672d..e1b6730429e 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -537,7 +537,8 @@ module Types field :alert_management_integrations, Types::AlertManagement::IntegrationType.connection_type, null: true, description: 'Integrations which can receive alerts for the project.', - resolver: Resolvers::AlertManagement::IntegrationsResolver + resolver: Resolvers::AlertManagement::IntegrationsResolver, + deprecated: { reason: 'Use `alertManagementHttpIntegrations`', milestone: '18.2' } field :alert_management_http_integrations, Types::AlertManagement::HttpIntegrationType.connection_type, null: true, diff --git a/app/models/concerns/triggerable_hooks.rb b/app/models/concerns/triggerable_hooks.rb index 64e4a3311dc..a892593946b 100644 --- a/app/models/concerns/triggerable_hooks.rb +++ b/app/models/concerns/triggerable_hooks.rb @@ -32,6 +32,7 @@ module TriggerableHooks job_hooks: :job_events, member_hooks: :member_events, merge_request_hooks: :merge_requests_events, + milestone_hooks: :milestone_events, note_hooks: :note_events, pipeline_hooks: :pipeline_events, project_hooks: :project_events, diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index 1226a76433b..c15482d071e 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -15,6 +15,7 @@ class ProjectHook < WebHook :issue_hooks, :job_hooks, :merge_request_hooks, + :milestone_hooks, :note_hooks, :pipeline_hooks, :push_hooks, diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 358b4794764..7bdbdf49f7c 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -99,6 +99,10 @@ class Milestone < ApplicationRecord state :active end + def hook_attrs + Gitlab::HookData::MilestoneBuilder.new(self).build + end + # Searches for timeboxes with a matching title. # # This method uses ILIKE on PostgreSQL diff --git a/app/services/concerns/integrations/project_test_data.rb b/app/services/concerns/integrations/project_test_data.rb index 185b57a1384..f71cf87832c 100644 --- a/app/services/concerns/integrations/project_test_data.rb +++ b/app/services/concerns/integrations/project_test_data.rb @@ -82,6 +82,10 @@ module Integrations release.to_hook_data('create') end + def milestone_events_data + Gitlab::DataBuilder::Milestone.build_sample(project) + end + def emoji_events_data no_data_error(s_('TestHooks|Ensure the project has notes.')) unless project.notes.any? diff --git a/app/services/integrations/test/project_service.rb b/app/services/integrations/test/project_service.rb index 1e077d49e5a..3334b241ddd 100644 --- a/app/services/integrations/test/project_service.rb +++ b/app/services/integrations/test/project_service.rb @@ -14,7 +14,7 @@ module Integrations private - def data + def data # rubocop:disable Metrics/CyclomaticComplexity -- despite a high count of cases, this isn't that complex strong_memoize(:data) do case event || integration.default_test_event when 'push', 'tag_push' @@ -35,6 +35,8 @@ module Integrations deployment_events_data when 'release' releases_events_data + when 'milestone' + milestone_events_data when 'award_emoji' emoji_events_data when 'current_user' diff --git a/app/services/milestones/base_service.rb b/app/services/milestones/base_service.rb index 0d7d855bf5e..ecfe7db325e 100644 --- a/app/services/milestones/base_service.rb +++ b/app/services/milestones/base_service.rb @@ -2,14 +2,26 @@ module Milestones class BaseService < ::BaseService - # Parent can either a group or a project attr_accessor :parent, :current_user, :params def initialize(parent, user, params = {}) + # Parent can either a group or a project @parent = parent @current_user = user @params = params.dup + super end + + private + + def execute_hooks(milestone, action) + # At the moment, only project milestones support webhooks, not group milestones + return unless milestone.project_milestone? + return unless milestone.parent.has_active_hooks?(:milestone_hooks) + + payload = Gitlab::DataBuilder::Milestone.build(milestone, action) + milestone.parent.execute_hooks(payload, :milestone_hooks) + end end end diff --git a/app/services/milestones/close_service.rb b/app/services/milestones/close_service.rb index a252f5c144e..616c6d85b7c 100644 --- a/app/services/milestones/close_service.rb +++ b/app/services/milestones/close_service.rb @@ -5,6 +5,7 @@ module Milestones def execute(milestone) if milestone.close && milestone.project_milestone? event_service.close_milestone(milestone, current_user) + execute_hooks(milestone, 'close') end milestone diff --git a/app/services/milestones/create_service.rb b/app/services/milestones/create_service.rb index e8a14adc10d..d8a9be6aafe 100644 --- a/app/services/milestones/create_service.rb +++ b/app/services/milestones/create_service.rb @@ -9,6 +9,7 @@ module Milestones if milestone.save && milestone.project_milestone? event_service.open_milestone(milestone, current_user) + execute_hooks(milestone, 'create') end milestone diff --git a/app/services/milestones/destroy_service.rb b/app/services/milestones/destroy_service.rb index 6966764634f..dc605472e37 100644 --- a/app/services/milestones/destroy_service.rb +++ b/app/services/milestones/destroy_service.rb @@ -14,6 +14,7 @@ module Milestones return unless milestone.destroyed? + execute_hooks(milestone, 'delete') if milestone.project_milestone? milestone end diff --git a/app/services/milestones/reopen_service.rb b/app/services/milestones/reopen_service.rb index 125a3ec1367..a4a9ccfd401 100644 --- a/app/services/milestones/reopen_service.rb +++ b/app/services/milestones/reopen_service.rb @@ -5,6 +5,7 @@ module Milestones def execute(milestone) if milestone.activate && milestone.project_milestone? event_service.reopen_milestone(milestone, current_user) + execute_hooks(milestone, 'reopen') end milestone diff --git a/app/services/test_hooks/project_service.rb b/app/services/test_hooks/project_service.rb index b183210edb3..25c81c3a498 100644 --- a/app/services/test_hooks/project_service.rb +++ b/app/services/test_hooks/project_service.rb @@ -35,6 +35,8 @@ module TestHooks wiki_page_events_data when 'releases_events' releases_events_data + when 'milestone_events' + milestone_events_data when 'emoji_events' emoji_events_data when 'resource_access_token_events' diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index a3c503c9efc..3be7461410d 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -58,6 +58,10 @@ = form.gitlab_ui_checkbox_component :releases_events, integration_webhook_event_human_name(:releases_events), help_text: s_('Webhooks|A release is created, updated, or deleted.') + %li.gl-pb-3 + = form.gitlab_ui_checkbox_component :milestone_events, + integration_webhook_event_human_name(:milestone_events), + help_text: s_('Webhooks|A milestone is created, closed, reopened, or deleted.') %li.gl-pb-3 - emoji_help_link = link_to s_('Which emoji events trigger webhooks'), help_page_path('user/project/integrations/webhook_events.md', anchor: 'emoji-events') = form.gitlab_ui_checkbox_component :emoji_events, diff --git a/config/feature_flags/beta/validate_lfs_object_access.yml b/config/feature_flags/beta/validate_lfs_object_access.yml index ef4ce8c5f2c..6f61a46acf4 100644 --- a/config/feature_flags/beta/validate_lfs_object_access.yml +++ b/config/feature_flags/beta/validate_lfs_object_access.yml @@ -6,4 +6,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/547584 milestone: '18.1' group: group::source code type: beta -default_enabled: false \ No newline at end of file +default_enabled: true \ No newline at end of file diff --git a/db/migrate/20250613162310_add_milestone_events_to_web_hooks.rb b/db/migrate/20250613162310_add_milestone_events_to_web_hooks.rb new file mode 100644 index 00000000000..0357de580ff --- /dev/null +++ b/db/migrate/20250613162310_add_milestone_events_to_web_hooks.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddMilestoneEventsToWebHooks < Gitlab::Database::Migration[2.3] + milestone '18.2' + + def change + add_column :web_hooks, :milestone_events, :boolean, null: false, default: false + end +end diff --git a/db/schema_migrations/20250613162310 b/db/schema_migrations/20250613162310 new file mode 100644 index 00000000000..16d68416d52 --- /dev/null +++ b/db/schema_migrations/20250613162310 @@ -0,0 +1 @@ +ecd7624a83f30d66eb361cdfa56d857162c5ff00a2f9ae3ce256013f003f4d1b \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index f1a97c0fc05..f8002cd2c90 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -26012,6 +26012,7 @@ CREATE TABLE web_hooks ( project_events boolean DEFAULT false NOT NULL, vulnerability_events boolean DEFAULT false NOT NULL, member_approval_events boolean DEFAULT false NOT NULL, + milestone_events boolean DEFAULT false NOT NULL, CONSTRAINT check_1e4d5cbdc5 CHECK ((char_length(name) <= 255)), CONSTRAINT check_23a96ad211 CHECK ((char_length(description) <= 2048)), CONSTRAINT check_69ef76ee0c CHECK ((char_length(custom_webhook_template) <= 4096)) diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md index 7232876d85e..de15f5a29c1 100644 --- a/doc/administration/raketasks/maintenance.md +++ b/doc/administration/raketasks/maintenance.md @@ -552,7 +552,8 @@ gitlab-rake gitlab:db:schema_checker:run {{< /history >}} The `gitlab:db:sos` command gathers configuration, performance, and diagnostic data about your GitLab -database to help you troubleshoot issues. Where you run this command depends on your configuration: +database to help you troubleshoot issues. Where you run this command depends on your configuration. Make sure +to run this command relative to where GitLab is installed `(/gitlab)`. - **Scaled GitLab**: on your Puma or Sidekiq server. - **Cloud native install**: on the toolbox pod. @@ -561,9 +562,9 @@ database to help you troubleshoot issues. Where you run this command depends on Modify the command as needed: - **Default path** - To run the command with the default file path (`/var/opt/gitlab/gitlab-rails/tmp/sos.zip`), run `gitlab-rake gitlab:db:sos`. -- **Custom path** - To change the file path, run `gitlab-rake gitlab:db:sos["custom/path/to/file.zip"]`. +- **Custom path** - To change the file path, run `gitlab-rake gitlab:db:sos["/absolute/custom/path/to/file.zip"]`. - **Zsh users** - If you have not modified your Zsh configuration, you must add quotation marks - around the entire command, like this: `gitlab-rake "gitlab:db:sos[custom/path/to/file.zip]"` + around the entire command, like this: `gitlab-rake "gitlab:db:sos[/absolute/custom/path/to/file.zip]"` The Rake task runs for five minutes. It creates a compressed folder in the path you specify. The compressed folder contains a large number of files. diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index a029f685afb..f8e3708207f 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -2614,6 +2614,7 @@ Input type: `AuditEventsAmazonS3ConfigurationUpdateInput` | Name | Type | Description | | ---- | ---- | ----------- | | `accessKeyXid` | [`String`](#string) | Access key ID of the Amazon S3 account. | +| `active` | [`Boolean`](#boolean) | Active status of the destination. | | `awsRegion` | [`String`](#string) | AWS region where the bucket is created. | | `bucketName` | [`String`](#string) | Name of the bucket where the audit events would be logged. | | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | @@ -2776,6 +2777,7 @@ Input type: `AuditEventsInstanceAmazonS3ConfigurationUpdateInput` | Name | Type | Description | | ---- | ---- | ----------- | | `accessKeyXid` | [`String`](#string) | Access key ID of the Amazon S3 account. | +| `active` | [`Boolean`](#boolean) | Active status of the destination. | | `awsRegion` | [`String`](#string) | AWS region where the bucket is created. | | `bucketName` | [`String`](#string) | Name of the bucket where the audit events would be logged. | | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | @@ -10014,6 +10016,11 @@ Input type: `ProjectUpdateComplianceFrameworksInput` ### `Mutation.prometheusIntegrationCreate` +{{< details >}} +**Deprecated** in GitLab 18.2. +Use HttpIntegrationCreate. +{{< /details >}} + Input type: `PrometheusIntegrationCreateInput` #### Arguments @@ -10021,9 +10028,13 @@ Input type: `PrometheusIntegrationCreateInput` | Name | Type | Description | | ---- | ---- | ----------- | | `active` | [`Boolean!`](#boolean) | Whether the integration is receiving alerts. | -| `apiUrl` | [`String`](#string) | Endpoint at which Prometheus can be queried. | +| `apiUrl` {{< icon name="warning-solid" >}} | [`String`](#string) | **Deprecated**: Feature removed in 16.0. Deprecated in GitLab 18.2. | | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `name` | [`String`](#string) | Name of the integration. | +| `payloadAttributeMappings` | [`[AlertManagementPayloadAlertFieldInput!]`](#alertmanagementpayloadalertfieldinput) | Custom mapping of GitLab alert attributes to fields from the payload example. | +| `payloadExample` | [`JsonString`](#jsonstring) | Example of an alert payload. | | `projectPath` | [`ID!`](#id) | Project to create the integration in. | +| `type` | [`AlertManagementIntegrationType`](#alertmanagementintegrationtype) | Type of integration to create. Cannot be changed after creation. | #### Fields @@ -10035,6 +10046,11 @@ Input type: `PrometheusIntegrationCreateInput` ### `Mutation.prometheusIntegrationResetToken` +{{< details >}} +**Deprecated** in GitLab 18.2. +Use HttpIntegrationResetToken. +{{< /details >}} + Input type: `PrometheusIntegrationResetTokenInput` #### Arguments @@ -10050,10 +10066,15 @@ Input type: `PrometheusIntegrationResetTokenInput` | ---- | ---- | ----------- | | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | `errors` | [`[String!]!`](#string) | Errors encountered during the mutation. | -| `integration` | [`AlertManagementPrometheusIntegration`](#alertmanagementprometheusintegration) | Newly created integration. | +| `integration` | [`AlertManagementPrometheusIntegration`](#alertmanagementprometheusintegration) | Updated integration. | ### `Mutation.prometheusIntegrationUpdate` +{{< details >}} +**Deprecated** in GitLab 18.2. +Use HttpIntegrationUpdate. +{{< /details >}} + Input type: `PrometheusIntegrationUpdateInput` #### Arguments @@ -10061,9 +10082,12 @@ Input type: `PrometheusIntegrationUpdateInput` | Name | Type | Description | | ---- | ---- | ----------- | | `active` | [`Boolean`](#boolean) | Whether the integration is receiving alerts. | -| `apiUrl` | [`String`](#string) | Endpoint at which Prometheus can be queried. | +| `apiUrl` {{< icon name="warning-solid" >}} | [`String`](#string) | **Deprecated**: Feature removed in 16.0. Deprecated in GitLab 18.2. | | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | `id` | [`IntegrationsPrometheusID!`](#integrationsprometheusid) | ID of the integration to mutate. | +| `name` | [`String`](#string) | Name of the integration. | +| `payloadAttributeMappings` | [`[AlertManagementPayloadAlertFieldInput!]`](#alertmanagementpayloadalertfieldinput) | Custom mapping of GitLab alert attributes to fields from the payload example. | +| `payloadExample` | [`JsonString`](#jsonstring) | Example of an alert payload. | #### Fields @@ -10071,7 +10095,7 @@ Input type: `PrometheusIntegrationUpdateInput` | ---- | ---- | ----------- | | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | `errors` | [`[String!]!`](#string) | Errors encountered during the mutation. | -| `integration` | [`AlertManagementPrometheusIntegration`](#alertmanagementprometheusintegration) | Newly created integration. | +| `integration` | [`AlertManagementPrometheusIntegration`](#alertmanagementprometheusintegration) | Updated integration. | ### `Mutation.promoteToEpic` @@ -22006,7 +22030,7 @@ An endpoint and credentials used to accept alerts for a project. | Name | Type | Description | | ---- | ---- | ----------- | | `active` | [`Boolean`](#boolean) | Whether the endpoint is currently accepting alerts. | -| `apiUrl` | [`String`](#string) | URL at which Prometheus metrics can be queried to populate the metrics dashboard. | +| `apiUrl` {{< icon name="warning-solid" >}} | [`String`](#string) | **Deprecated** in GitLab 18.2. Feature removed in 16.0. | | `id` | [`ID!`](#id) | ID of the integration. | | `name` | [`String`](#string) | Name of the integration. | | `payloadAlertFields` | [`[AlertManagementPayloadAlertField!]`](#alertmanagementpayloadalertfield) | Extract alert fields from payload example for custom mapping. | @@ -22043,14 +22067,14 @@ Parsed field (with its name) from an alert used for custom mappings. ### `AlertManagementPrometheusIntegration` -An endpoint and credentials used to accept Prometheus alerts for a project. +**DEPRECATED - Use AlertManagementHttpIntegration directly** An endpoint and credentials used to accept Prometheus alerts for a project. #### Fields | Name | Type | Description | | ---- | ---- | ----------- | | `active` | [`Boolean`](#boolean) | Whether the endpoint is currently accepting alerts. | -| `apiUrl` | [`String`](#string) | URL at which Prometheus metrics can be queried to populate the metrics dashboard. | +| `apiUrl` {{< icon name="warning-solid" >}} | [`String`](#string) | **Deprecated** in GitLab 18.2. Feature removed in 16.0. | | `id` | [`ID!`](#id) | ID of the integration. | | `name` | [`String`](#string) | Name of the integration. | | `token` | [`String`](#string) | Token used to authenticate alert notification requests. | @@ -22066,6 +22090,7 @@ Stores Amazon S3 configurations for audit event streaming. | Name | Type | Description | | ---- | ---- | ----------- | | `accessKeyXid` | [`String!`](#string) | Access key ID of the Amazon S3 account. | +| `active` | [`Boolean!`](#boolean) | Active status of the destination. | | `awsRegion` | [`String!`](#string) | AWS region where the bucket is created. | | `bucketName` | [`String!`](#string) | Name of the bucket where the audit events would be logged. | | `group` | [`Group!`](#group) | Group the configuration belongs to. | @@ -31021,6 +31046,7 @@ Stores instance level Amazon S3 configurations for audit event streaming. | Name | Type | Description | | ---- | ---- | ----------- | | `accessKeyXid` | [`String!`](#string) | Access key ID of the Amazon S3 account. | +| `active` | [`Boolean!`](#boolean) | Active status of the destination. | | `awsRegion` | [`String!`](#string) | AWS region where the bucket is created. | | `bucketName` | [`String!`](#string) | Name of the bucket where the audit events would be logged. | | `id` | [`ID!`](#id) | ID of the configuration. | @@ -36584,6 +36610,11 @@ four standard [pagination arguments](#pagination-arguments): Integrations which can receive alerts for the project. +{{< details >}} +**Deprecated** in GitLab 18.2. +Use `alertManagementHttpIntegrations`. +{{< /details >}} + Returns [`AlertManagementIntegrationConnection`](#alertmanagementintegrationconnection). This field returns a [connection](#connections). It accepts the @@ -49567,7 +49598,7 @@ Implementations: | Name | Type | Description | | ---- | ---- | ----------- | | `active` | [`Boolean`](#boolean) | Whether the endpoint is currently accepting alerts. | -| `apiUrl` | [`String`](#string) | URL at which Prometheus metrics can be queried to populate the metrics dashboard. | +| `apiUrl` {{< icon name="warning-solid" >}} | [`String`](#string) | **Deprecated** in GitLab 18.2. Feature removed in 16.0. | | `id` | [`ID!`](#id) | ID of the integration. | | `name` | [`String`](#string) | Name of the integration. | | `token` | [`String`](#string) | Token used to authenticate alert notification requests. | @@ -49586,6 +49617,7 @@ Implementations: | Name | Type | Description | | ---- | ---- | ----------- | | `accessKeyXid` | [`String!`](#string) | Access key ID of the Amazon S3 account. | +| `active` | [`Boolean!`](#boolean) | Active status of the destination. | | `awsRegion` | [`String!`](#string) | AWS region where the bucket is created. | | `bucketName` | [`String!`](#string) | Name of the bucket where the audit events would be logged. | | `id` | [`ID!`](#id) | ID of the configuration. | diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 4f538b3b3a8..657760c69b6 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -1386,7 +1386,7 @@ Supported attributes: |---------------------|----------------|----------|-------------| | `id` | integer or string | Yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths) owned by the authenticated user. | | `merge_request_iid` | integer | Yes | The internal ID of the merge request. | -| `block_id` | integer | Yes | The internal ID of the block. | +| `block_id` | integer | Yes | The ID of the block. | Example request: diff --git a/doc/api/openapi/openapi_v2.yaml b/doc/api/openapi/openapi_v2.yaml index d9675e4d8da..fbf92bfa947 100644 --- a/doc/api/openapi/openapi_v2.yaml +++ b/doc/api/openapi/openapi_v2.yaml @@ -29125,6 +29125,7 @@ paths: - issues_events - job_events - merge_requests_events + - milestone_events - note_events - pipeline_events - push_events @@ -59246,6 +59247,8 @@ definitions: type: boolean releases_events: type: boolean + milestone_events: + type: boolean emoji_events: type: boolean resource_access_token_events: @@ -59304,6 +59307,9 @@ definitions: releases_events: type: boolean description: Trigger hook on release events + milestone_events: + type: boolean + description: Trigger hook on milestone events emoji_events: type: boolean description: Trigger hook on emoji events @@ -59421,6 +59427,9 @@ definitions: releases_events: type: boolean description: Trigger hook on release events + milestone_events: + type: boolean + description: Trigger hook on milestone events emoji_events: type: boolean description: Trigger hook on emoji events diff --git a/doc/api/project_webhooks.md b/doc/api/project_webhooks.md index 0918a17b462..52279498140 100644 --- a/doc/api/project_webhooks.md +++ b/doc/api/project_webhooks.md @@ -70,6 +70,7 @@ Example response: "wiki_page_events": true, "deployment_events": true, "releases_events": true, + "milestone_events": true, "feature_flag_events": true, "enable_ssl_verification": true, "repository_update_events": false, @@ -431,6 +432,7 @@ Supported attributes: | `branch_filter_strategy` | string | No | Filter push events by branch. Possible values are `wildcard` (default), `regex`, and `all_branches`. | | `push_events` | boolean | No | Trigger project webhook on push events. | | `releases_events` | boolean | No | Trigger project webhook on release events. | +| `milestone_events` | boolean | No | Trigger project webhook on milestone events. | | `tag_push_events` | boolean | No | Trigger project webhook on tag push events. | | `token` | string | No | Secret token to validate received payloads; the token isn't returned in the response. | | `wiki_page_events` | boolean | No | Trigger project webhook on wiki events. | @@ -475,6 +477,7 @@ Supported attributes: | `branch_filter_strategy` | string | No | Filter push events by branch. Possible values are `wildcard` (default), `regex`, and `all_branches`. | | `push_events` | boolean | No | Trigger project webhook on push events. | | `releases_events` | boolean | No | Trigger project webhook on release events. | +| `milestone_events` | boolean | No | Trigger project webhook on milestone events. | | `tag_push_events` | boolean | No | Trigger project webhook on tag push events. | | `token` | string | No | Secret token to validate received payloads. Not returned in the response. When you change the webhook URL, the secret token is reset and not retained. | | `wiki_page_events` | boolean | No | Trigger project webhook on wiki page events. | @@ -531,7 +534,7 @@ Supported attributes: |:----------|:------------------|:---------|:------------| | `hook_id` | integer | Yes | ID of the project webhook. | | `id` | integer or string | Yes | ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths). | -| `trigger` | string | Yes | One of `push_events`, `tag_push_events`, `issues_events`, `confidential_issues_events`, `note_events`, `merge_requests_events`, `job_events`, `pipeline_events`, `wiki_page_events`, `releases_events`, `emoji_events`, or `resource_access_token_events`. | +| `trigger` | string | Yes | One of `push_events`, `tag_push_events`, `issues_events`, `confidential_issues_events`, `note_events`, `merge_requests_events`, `job_events`, `pipeline_events`, `wiki_page_events`, `releases_events`, `milestone_events`, `emoji_events`, or `resource_access_token_events`. | Example response: diff --git a/doc/user/project/integrations/webhook_events.md b/doc/user/project/integrations/webhook_events.md index f8b86b3ca97..c5efc1b0621 100644 --- a/doc/user/project/integrations/webhook_events.md +++ b/doc/user/project/integrations/webhook_events.md @@ -37,6 +37,7 @@ Event type | Trigger [Deployment event](#deployment-events) | A deployment starts, succeeds, fails, or is canceled. [Feature flag event](#feature-flag-events) | A feature flag is turned on or off. [Release event](#release-events) | A release is created, edited, or deleted. +[Milestone event](#milestone-events) | A milestone is created, closed, reopened, or deleted. [Emoji event](#emoji-events) | An emoji reaction is added or removed. [Project or group access token event](#project-and-group-access-token-events) | A project or group access token will expire in seven days. [Vulnerability event](#vulnerability-events) | A vulnerability is created or updated. @@ -2137,6 +2138,69 @@ Payload example: } ``` +## Milestone events + +{{< history >}} + +- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14213) in GitLab 18.2. + +{{< /history >}} + +Milestone events are triggered when a milestone is created, closed, reopened, or deleted. + +The available values for `object_attributes.action` in the payload are: + +- `create` +- `close` +- `reopen` + +Request header: + +```plaintext +X-Gitlab-Event: Milestone Hook +``` + +Payload example: + +```json +{ + "object_kind": "milestone", + "event_type": "milestone", + "project": { + "id": 1, + "name": "Gitlab Test", + "description": "Aut reprehenderit ut est.", + "web_url": "http://example.com/gitlabhq/gitlab-test", + "avatar_url": null, + "git_ssh_url": "git@example.com:gitlabhq/gitlab-test.git", + "git_http_url": "http://example.com/gitlabhq/gitlab-test.git", + "namespace": "GitlabHQ", + "visibility_level": 20, + "path_with_namespace": "gitlabhq/gitlab-test", + "default_branch": "master", + "ci_config_path": null, + "homepage": "http://example.com/gitlabhq/gitlab-test", + "url": "http://example.com/gitlabhq/gitlab-test.git", + "ssh_url": "git@example.com:gitlabhq/gitlab-test.git", + "http_url": "http://example.com/gitlabhq/gitlab-test.git" + }, + "object_attributes": { + "id": 61, + "iid": 10, + "title": "v1.0", + "description": "First stable release", + "state": "active", + "created_at": "2025-06-16 14:10:57 UTC", + "updated_at": "2025-06-16 14:10:57 UTC", + "due_date": "2025-06-30", + "start_date": "2025-06-16", + "group_id": null, + "project_id": 1 + }, + "action": "create" +} +``` + ## Emoji events {{< history >}} diff --git a/lib/api/entities/project_hook.rb b/lib/api/entities/project_hook.rb index a1ede6fe277..b81720fa28f 100644 --- a/lib/api/entities/project_hook.rb +++ b/lib/api/entities/project_hook.rb @@ -14,6 +14,7 @@ module API expose :feature_flag_events, documentation: { type: 'boolean' } expose :job_events, documentation: { type: 'boolean' } expose :releases_events, documentation: { type: 'boolean' } + expose :milestone_events, documentation: { type: 'boolean' } expose :emoji_events, documentation: { type: 'boolean' } expose :resource_access_token_events, documentation: { type: 'boolean' } expose :vulnerability_events, documentation: { type: 'boolean' } diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index 410ff2e441e..bd0d4fec7d6 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -38,6 +38,7 @@ module API optional :deployment_events, type: Boolean, desc: "Trigger hook on deployment events" optional :feature_flag_events, type: Boolean, desc: "Trigger hook on feature flag events" optional :releases_events, type: Boolean, desc: "Trigger hook on release events" + optional :milestone_events, type: Boolean, desc: "Trigger hook on milestone events" optional :emoji_events, type: Boolean, desc: "Trigger hook on emoji events" optional :resource_access_token_events, type: Boolean, desc: "Trigger hook on project access token expiry events" optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook" diff --git a/lib/gitlab/data_builder/milestone.rb b/lib/gitlab/data_builder/milestone.rb new file mode 100644 index 00000000000..6f6e269c082 --- /dev/null +++ b/lib/gitlab/data_builder/milestone.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Gitlab + module DataBuilder + module Milestone + extend self + + SAMPLE_DATA = { + id: 1, + iid: 1, + title: 'Sample milestone', + description: 'Sample milestone description', + state: 'active', + created_at: Time.current, + updated_at: Time.current, + due_date: 1.week.from_now, + start_date: Time.current + }.freeze + + def build(milestone, action) + { + object_kind: 'milestone', + event_type: 'milestone', + project: milestone.project&.hook_attrs, + object_attributes: milestone.hook_attrs, + action: action + } + end + + def build_sample(project) + milestone = project.milestones.first || ::Milestone.new(SAMPLE_DATA.merge(project: project)) + build(milestone, 'create') + end + end + end +end diff --git a/lib/gitlab/hook_data/milestone_builder.rb b/lib/gitlab/hook_data/milestone_builder.rb new file mode 100644 index 00000000000..f2022a956eb --- /dev/null +++ b/lib/gitlab/hook_data/milestone_builder.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Gitlab + module HookData + class MilestoneBuilder < BaseBuilder + SAFE_HOOK_ATTRIBUTES = %i[ + id + iid + title + description + state + created_at + updated_at + due_date + start_date + project_id + ].freeze + + alias_method :milestone, :object + + def build + milestone + .attributes + .with_indifferent_access + .slice(*SAFE_HOOK_ATTRIBUTES) + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2a88fdf2aef..2752406a9ea 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2433,9 +2433,6 @@ msgstr "" msgid "AICatalog|Provide default instructions or context that will be included with every user interaction." msgstr "" -msgid "AICatalog|Released %{fullDate}" -msgstr "" - msgid "AICatalog|Run" msgstr "" @@ -17002,21 +16999,33 @@ msgstr "" msgid "ComplianceViolation|Audit event captured" msgstr "" +msgid "ComplianceViolation|Control" +msgstr "" + msgid "ComplianceViolation|Detected" msgstr "" msgid "ComplianceViolation|Dismissed" msgstr "" +msgid "ComplianceViolation|Framework" +msgstr "" + msgid "ComplianceViolation|In review" msgstr "" msgid "ComplianceViolation|Registered event IP" msgstr "" +msgid "ComplianceViolation|Requirement" +msgstr "" + msgid "ComplianceViolation|Resolved" msgstr "" +msgid "ComplianceViolation|Violation created based on associated framework" +msgstr "" + msgid "Compliance|Framework deleted successfully" msgstr "" @@ -28163,6 +28172,9 @@ msgstr "" msgid "Geo|Replication Details" msgstr "" +msgid "Geo|Replication failure" +msgstr "" + msgid "Geo|Replication information" msgstr "" @@ -28382,6 +28394,9 @@ msgstr "" msgid "Geo|Verification concurrency limit" msgstr "" +msgid "Geo|Verification failure" +msgstr "" + msgid "Geo|Verification information" msgstr "" @@ -31536,6 +31551,9 @@ msgstr "" msgid "Hide details" msgstr "" +msgid "Hide errors" +msgstr "" + msgid "Hide file browser" msgstr "" @@ -40370,9 +40388,6 @@ msgstr "" msgid "Multiple IP address ranges are supported. Does not affect access to the group's settings." msgstr "" -msgid "Multiple Prometheus integrations are not supported" -msgstr "" - msgid "Multiple components '%{name}' have '%{attribute}' attribute" msgstr "" @@ -59015,6 +59030,9 @@ msgstr "" msgid "Show epics in global search results" msgstr "" +msgid "Show errors" +msgstr "" + msgid "Show file browser" msgstr "" @@ -69486,6 +69504,9 @@ msgstr "" msgid "Webhooks|A merge request is created, updated, or merged." msgstr "" +msgid "Webhooks|A milestone is created, closed, reopened, or deleted." +msgstr "" + msgid "Webhooks|A new tag is pushed to the repository." msgstr "" @@ -73049,6 +73070,9 @@ msgstr "" msgid "added a Zoom call to this issue" msgstr "" +msgid "agent" +msgstr "" + msgid "agents" msgstr "" diff --git a/spec/factories/alert_management/http_integrations.rb b/spec/factories/alert_management/http_integrations.rb index 1a46215de47..f6cdfc788b4 100644 --- a/spec/factories/alert_management/http_integrations.rb +++ b/spec/factories/alert_management/http_integrations.rb @@ -21,8 +21,15 @@ FactoryBot.define do initialize_with { new(**attributes) } + trait :prometheus do + type_identifier { :prometheus } + end + factory :alert_management_prometheus_integration, traits: [:prometheus] do + type_identifier { :prometheus } + trait :legacy do + name { 'Prometheus' } endpoint_identifier { 'legacy-prometheus' } end end diff --git a/spec/factories/container_registry/protection/tag_rules.rb b/spec/factories/container_registry/protection/tag_rules.rb index c7b5ae73ba8..73342bbaf1b 100644 --- a/spec/factories/container_registry/protection/tag_rules.rb +++ b/spec/factories/container_registry/protection/tag_rules.rb @@ -7,9 +7,4 @@ FactoryBot.define do minimum_access_level_for_delete { :maintainer } minimum_access_level_for_push { :maintainer } end - - trait :immutable do - minimum_access_level_for_delete { nil } - minimum_access_level_for_push { nil } - end end diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb index 482cec1195d..56959e051f8 100644 --- a/spec/factories/project_hooks.rb +++ b/spec/factories/project_hooks.rb @@ -30,6 +30,7 @@ FactoryBot.define do deployment_events { true } feature_flag_events { true } releases_events { true } + milestone_events { true } emoji_events { true } vulnerability_events { true } end diff --git a/spec/features/projects/settings/webhooks_settings_spec.rb b/spec/features/projects/settings/webhooks_settings_spec.rb index 062bcf90796..f1f824a8d65 100644 --- a/spec/features/projects/settings/webhooks_settings_spec.rb +++ b/spec/features/projects/settings/webhooks_settings_spec.rb @@ -43,6 +43,7 @@ RSpec.describe 'Projects > Settings > Webhook Settings', feature_category: :webh expect(page).to have_content('Comment') expect(page).to have_content('Merge request events') expect(page).to have_content('Pipeline events') + expect(page).to have_content('Milestone events') expect(page).to have_content('Wiki page events') expect(page).to have_content('Releases events') expect(page).to have_content('Emoji events') diff --git a/spec/fixtures/api/schemas/public_api/v4/project_hook.json b/spec/fixtures/api/schemas/public_api/v4/project_hook.json index 238f5839f77..fb9cc717288 100644 --- a/spec/fixtures/api/schemas/public_api/v4/project_hook.json +++ b/spec/fixtures/api/schemas/public_api/v4/project_hook.json @@ -117,6 +117,9 @@ "releases_events": { "type": "boolean" }, + "milestone_events": { + "type": "boolean" + }, "emoji_events": { "type": "boolean" }, diff --git a/spec/graphql/mutations/alert_management/prometheus_integration/create_spec.rb b/spec/graphql/mutations/alert_management/prometheus_integration/create_spec.rb index 13575b7676b..043f57291d7 100644 --- a/spec/graphql/mutations/alert_management/prometheus_integration/create_spec.rb +++ b/spec/graphql/mutations/alert_management/prometheus_integration/create_spec.rb @@ -4,13 +4,13 @@ require 'spec_helper' RSpec.describe Mutations::AlertManagement::PrometheusIntegration::Create, feature_category: :api do include GraphqlHelpers + let_it_be(:current_user) { create(:user) } let_it_be(:project) { create(:project) } - let(:api_url) { 'http://prometheus.com/' } - let(:args) { { project_path: project.full_path, active: true, api_url: api_url } } + let(:args) { { project_path: project.full_path, active: true, api_url: 'http://prometheus.com/' } } - specify { expect(described_class).to require_graphql_authorizations(:admin_project) } + specify { expect(described_class).to require_graphql_authorizations(:admin_operations) } describe '#resolve' do subject(:resolve) { mutation_for(project, current_user).resolve(args) } @@ -20,49 +20,31 @@ RSpec.describe Mutations::AlertManagement::PrometheusIntegration::Create, featur project.add_maintainer(current_user) end - context 'when Prometheus Integration already exists' do - let_it_be(:existing_integration) { create(:prometheus_integration, project: project) } - - it 'returns errors' do - expect(resolve).to eq( - integration: nil, - errors: ['Multiple Prometheus integrations are not supported'] - ) - end - end - - context 'when api_url is nil' do - let(:api_url) { nil } - - it 'creates the integration' do - expect { resolve }.to change(::Alerting::ProjectAlertingSetting, :count).by(1) - end - end - - context 'when UpdateService responds with success' do + context 'when HttpIntegrations::CreateService responds with success' do it 'returns the integration with no errors' do expect(resolve).to eq( - integration: ::Integrations::Prometheus.last!, + integration: ::AlertManagement::HttpIntegration.last!, errors: [] ) - end - - it 'creates a corresponding token' do - expect { resolve }.to change(::Integrations::Prometheus, :count).by(1) + expect(resolve[:integration]).to have_attributes( + active: true, + name: 'Prometheus', + type_identifier: 'prometheus' + ) end end - context 'when UpdateService responds with an error' do + context 'when HttpIntegrations::CreateService responds with an error' do before do - allow_any_instance_of(::Projects::Operations::UpdateService) + allow_any_instance_of(::AlertManagement::HttpIntegrations::CreateService) .to receive(:execute) - .and_return({ status: :error, message: 'An error occurred' }) + .and_return(ServiceResponse.error(payload: { integration: nil }, message: 'An integration already exists')) end it 'returns errors' do expect(resolve).to eq( integration: nil, - errors: ['An error occurred'] + errors: ['An integration already exists'] ) end end diff --git a/spec/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb b/spec/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb index 1da4e644c31..c7bb0cfb96e 100644 --- a/spec/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb +++ b/spec/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb @@ -7,11 +7,12 @@ RSpec.describe Mutations::AlertManagement::PrometheusIntegration::ResetToken, fe let_it_be(:current_user) { create(:user) } let_it_be(:project) { create(:project) } - let_it_be(:integration) { create(:prometheus_integration, project: project) } + let_it_be_with_reload(:integration) { create(:alert_management_prometheus_integration, :legacy, project: project) } + let_it_be_with_reload(:old_integration) { create(:prometheus_integration, project: project) } - let(:args) { { id: GitlabSchema.id_from_object(integration) } } + let(:args) { { id: GitlabSchema.id_from_object(old_integration) } } - specify { expect(described_class).to require_graphql_authorizations(:admin_project) } + specify { expect(described_class).to require_graphql_authorizations(:admin_operations) } describe '#resolve' do subject(:resolve) { mutation_for(project, current_user).resolve(**args) } @@ -21,8 +22,10 @@ RSpec.describe Mutations::AlertManagement::PrometheusIntegration::ResetToken, fe project.add_maintainer(current_user) end - context 'when ::Projects::Operations::UpdateService responds with success' do + context 'when ::AlertManagement::HttpIntegrations::UpdateService responds with success' do it 'returns the integration with no errors' do + expect { resolve }.to change { integration.reload.token } + expect(resolve).to eq( integration: integration, errors: [] @@ -30,20 +33,42 @@ RSpec.describe Mutations::AlertManagement::PrometheusIntegration::ResetToken, fe end end - context 'when ::Projects::Operations::UpdateService responds with an error' do + context 'when ::AlertManagement::HttpIntegrations::UpdateService responds with an error' do before do - allow_any_instance_of(::Projects::Operations::UpdateService) + allow_any_instance_of(::AlertManagement::HttpIntegrations::UpdateService) .to receive(:execute) - .and_return({ status: :error, message: 'An error occurred' }) + .and_return(ServiceResponse.error(payload: { integration: nil }, message: 'An error occurred')) end it 'returns errors' do expect(resolve).to eq( - integration: integration, + integration: nil, errors: ['An error occurred'] ) end end + + context 'when prometheus_integration does not exist' do + before do + old_integration.destroy! + end + + it 'raises an error if the resource is not accessible to the user' do + expect(args[:id]).to be_present + + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when prometheus_integration does not have corresponding AlertManagement::HttpIntegration' do + before do + integration.destroy! + end + + it 'raises an error if the resource is not accessible to the user' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end end context 'when resource is not accessible to the user' do diff --git a/spec/graphql/mutations/alert_management/prometheus_integration/update_spec.rb b/spec/graphql/mutations/alert_management/prometheus_integration/update_spec.rb index 3bba889b9a0..802ebe005fe 100644 --- a/spec/graphql/mutations/alert_management/prometheus_integration/update_spec.rb +++ b/spec/graphql/mutations/alert_management/prometheus_integration/update_spec.rb @@ -7,11 +7,12 @@ RSpec.describe Mutations::AlertManagement::PrometheusIntegration::Update, featur let_it_be(:current_user) { create(:user) } let_it_be(:project) { create(:project) } - let_it_be(:integration) { create(:prometheus_integration, project: project) } + let_it_be_with_reload(:old_integration) { create(:prometheus_integration, project: project) } + let_it_be_with_reload(:integration) { create(:alert_management_prometheus_integration, :legacy, project: project) } - let(:args) { { id: GitlabSchema.id_from_object(integration), active: false, api_url: 'http://new-url.com' } } + let(:args) { { id: GitlabSchema.id_from_object(old_integration), active: false, api_url: 'http://new-url.com' } } - specify { expect(described_class).to require_graphql_authorizations(:admin_project) } + specify { expect(described_class).to require_graphql_authorizations(:admin_operations) } describe '#resolve' do subject(:resolve) { mutation_for(project, current_user).resolve(args) } @@ -21,29 +22,52 @@ RSpec.describe Mutations::AlertManagement::PrometheusIntegration::Update, featur project.add_maintainer(current_user) end - context 'when ::Projects::Operations::UpdateService responds with success' do + context 'when ::AlertManagement::HttpIntegrations::UpdateService responds with success' do it 'returns the integration with no errors' do expect(resolve).to eq( integration: integration, errors: [] ) + expect(integration.reload.active?).to be(false) end end - context 'when ::Projects::Operations::UpdateService responds with an error' do + context 'when ::AlertManagement::HttpIntegrations::UpdateService responds with an error' do before do - allow_any_instance_of(::Projects::Operations::UpdateService) + allow_any_instance_of(::AlertManagement::HttpIntegrations::UpdateService) .to receive(:execute) - .and_return({ status: :error, message: 'An error occurred' }) + .and_return(ServiceResponse.error(payload: { integration: nil }, message: 'An error occurred')) end it 'returns errors' do expect(resolve).to eq( - integration: integration, + integration: nil, errors: ['An error occurred'] ) end end + + context 'when prometheus_integration does not exist' do + before do + old_integration.destroy! + end + + it 'raises an error if the resource is not accessible to the user' do + expect(args[:id]).to be_present + + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when prometheus_integration does not have corresponding AlertManagement::HttpIntegration' do + before do + integration.destroy! + end + + it 'raises an error if the resource is not accessible to the user' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end end context 'when resource is not accessible to the user' do diff --git a/spec/graphql/resolvers/alert_management/integrations_resolver_spec.rb b/spec/graphql/resolvers/alert_management/integrations_resolver_spec.rb index ed2e7d35ee3..638d72508d8 100644 --- a/spec/graphql/resolvers/alert_management/integrations_resolver_spec.rb +++ b/spec/graphql/resolvers/alert_management/integrations_resolver_spec.rb @@ -8,13 +8,17 @@ RSpec.describe Resolvers::AlertManagement::IntegrationsResolver, feature_categor let_it_be(:current_user) { create(:user) } let_it_be(:project) { create(:project) } let_it_be(:project2) { create(:project) } - let_it_be(:prometheus_integration) { create(:prometheus_integration, project: project) } + let_it_be(:active_http_integration) { create(:alert_management_http_integration, project: project) } let_it_be(:inactive_http_integration) { create(:alert_management_http_integration, :inactive, project: project) } let_it_be(:other_proj_integration) { create(:alert_management_http_integration, project: project2) } - let_it_be(:other_proj_prometheus_integration) { create(:prometheus_integration, project: project2) } + + let_it_be(:prometheus_integration) { create(:prometheus_integration, project: project) } let_it_be(:migrated_integration) { create(:alert_management_prometheus_integration, :legacy, project: project) } + let_it_be(:alt_prometheus_integration) { create(:prometheus_integration, project: project2) } + let_it_be(:alt_migrated_integration) { create(:alert_management_prometheus_integration, :legacy, project: project2) } + let(:params) { {} } subject { sync(resolve_http_integrations(params)) } @@ -33,7 +37,7 @@ RSpec.describe Resolvers::AlertManagement::IntegrationsResolver, feature_categor project2.add_maintainer(current_user) end - it { is_expected.to contain_exactly(active_http_integration, prometheus_integration) } + it { is_expected.to contain_exactly(active_http_integration, migrated_integration) } context 'when HTTP Integration ID is given' do context 'when integration is from the current project' do @@ -53,11 +57,11 @@ RSpec.describe Resolvers::AlertManagement::IntegrationsResolver, feature_categor context 'when integration is from the current project' do let(:params) { { id: global_id_of(prometheus_integration) } } - it { is_expected.to contain_exactly(prometheus_integration) } + it { is_expected.to contain_exactly(migrated_integration) } end context 'when integration is from other project' do - let(:params) { { id: global_id_of(other_proj_prometheus_integration) } } + let(:params) { { id: global_id_of(alt_prometheus_integration) } } it { is_expected.to be_empty } end diff --git a/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb b/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb index 8c0bb96488f..7ccc70528c8 100644 --- a/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb +++ b/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb @@ -2,11 +2,11 @@ require 'spec_helper' -RSpec.describe GitlabSchema.types['AlertManagementPrometheusIntegration'] do +RSpec.describe GitlabSchema.types['AlertManagementPrometheusIntegration'], feature_category: :incident_management do include GraphqlHelpers specify { expect(described_class.graphql_name).to eq('AlertManagementPrometheusIntegration') } - specify { expect(described_class).to require_graphql_authorizations(:admin_project) } + specify { expect(described_class).to require_graphql_authorizations(:admin_operations) } describe 'resolvers' do shared_examples_for 'has field with value' do |field_name| @@ -17,11 +17,11 @@ RSpec.describe GitlabSchema.types['AlertManagementPrometheusIntegration'] do end end - let_it_be_with_reload(:integration) { create(:prometheus_integration) } + let_it_be_with_reload(:integration) { create(:alert_management_prometheus_integration, :legacy) } let_it_be(:user) { create(:user, maintainer_of: integration.project) } it_behaves_like 'has field with value', 'name' do - let(:value) { integration.title } + let(:value) { integration.name } end it_behaves_like 'has field with value', 'type' do @@ -29,7 +29,7 @@ RSpec.describe GitlabSchema.types['AlertManagementPrometheusIntegration'] do end it_behaves_like 'has field with value', 'token' do - let(:value) { nil } + let(:value) { integration.token } end it_behaves_like 'has field with value', 'url' do @@ -37,33 +37,7 @@ RSpec.describe GitlabSchema.types['AlertManagementPrometheusIntegration'] do end it_behaves_like 'has field with value', 'active' do - let(:value) { integration.manual_configuration? } - end - - context 'with alerting setting' do - let_it_be(:alerting_setting) { create(:project_alerting_setting, project: integration.project) } - - it_behaves_like 'has field with value', 'token' do - let(:value) { alerting_setting.token } - end - end - - describe 'a group integration' do - let_it_be(:group) { create(:group) } - let_it_be(:integration) { create(:prometheus_integration, :group, group: group) } - - # Since it is impossible to authorize the parent here, given that the - # project is nil, all fields should be redacted: - - described_class.fields.each_key do |field_name| - context "field: #{field_name}" do - it 'is redacted' do - expect do - resolve_field(field_name, integration, current_user: user) - end.to raise_error(GraphqlHelpers::UnauthorizedObject) - end - end - end + let(:value) { integration.active } end end end diff --git a/spec/lib/gitlab/data_builder/milestone_spec.rb b/spec/lib/gitlab/data_builder/milestone_spec.rb new file mode 100644 index 00000000000..8eb13c8ac42 --- /dev/null +++ b/spec/lib/gitlab/data_builder/milestone_spec.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::DataBuilder::Milestone, feature_category: :team_planning do + let_it_be(:project) { create(:project) } + + shared_examples 'builds milestone hook data' do + it { expect(data).to be_a(Hash) } + + it 'includes the correct structure' do + expect(data[:object_kind]).to eq('milestone') + expect(data[:event_type]).to eq('milestone') + expect(data[:action]).to eq(action) + end + end + + describe '.build' do + let(:milestone) { create(:milestone, project: project) } + let(:action) { 'create' } + + subject(:data) { described_class.build(milestone, action) } + + it_behaves_like 'builds milestone hook data' + + it 'includes project data' do + expect(data[:project]).to eq(milestone.project.hook_attrs) + end + + it 'includes milestone attributes' do + object_attributes = data[:object_attributes] + + expect(object_attributes[:id]).to eq(milestone.id) + expect(object_attributes[:iid]).to eq(milestone.iid) + expect(object_attributes[:title]).to eq(milestone.title) + expect(object_attributes[:description]).to eq(milestone.description) + expect(object_attributes[:state]).to eq(milestone.state) + expect(object_attributes[:created_at]).to eq(milestone.created_at) + expect(object_attributes[:updated_at]).to eq(milestone.updated_at) + expect(object_attributes[:due_date]).to eq(milestone.due_date) + expect(object_attributes[:start_date]).to eq(milestone.start_date) + expect(object_attributes[:project_id]).to eq(milestone.project_id) + end + + context 'with different actions' do + %w[create close reopen].each do |test_action| + context "when action is #{test_action}" do + let(:action) { test_action } + + it "sets the action to #{test_action}" do + expect(data[:action]).to eq(test_action) + end + end + end + end + + context 'with milestone having dates' do + let(:milestone) { create(:milestone, project: project, due_date: 1.week.from_now, start_date: 1.day.ago) } + + it 'includes the date information' do + expect(data[:object_attributes][:due_date]).to eq(milestone.due_date) + expect(data[:object_attributes][:start_date]).to eq(milestone.start_date) + end + end + + context 'with milestone having no dates' do + let(:milestone) { create(:milestone, project: project, due_date: nil, start_date: nil) } + + it 'includes nil date information' do + expect(data[:object_attributes][:due_date]).to be_nil + expect(data[:object_attributes][:start_date]).to be_nil + end + end + + context 'with closed milestone' do + let(:milestone) { create(:milestone, :closed, project: project) } + + it 'includes the correct state' do + expect(data[:object_attributes][:state]).to eq('closed') + end + end + + include_examples 'project hook data' + end + + describe '.build_sample' do + let(:action) { 'create' } + + context 'when project has existing milestones' do + subject(:data) { described_class.build_sample(project) } + + let_it_be(:existing_milestone) { create(:milestone, project: project) } + + it_behaves_like 'builds milestone hook data' + + it 'includes project data' do + expect(data[:project]).to eq(project.hook_attrs) + end + + it 'uses the first existing milestone' do + expect(data[:object_attributes][:id]).to eq(existing_milestone.id) + expect(data[:object_attributes][:title]).to eq(existing_milestone.title) + end + + include_examples 'project hook data' + end + + context 'when project has no milestones' do + subject(:data) { described_class.build_sample(clean_project) } + + let_it_be(:clean_project) { create(:project) } + + it_behaves_like 'builds milestone hook data' + + it 'includes project data' do + expect(data[:project]).to eq(clean_project.hook_attrs) + end + + it 'creates a sample milestone with predefined data' do + expect(data[:object_attributes][:title]).to eq('Sample milestone') + expect(data[:object_attributes][:description]).to eq('Sample milestone description') + expect(data[:object_attributes][:state]).to eq('active') + end + + include_examples 'project hook data' do + let(:project) { clean_project } + end + end + end +end diff --git a/spec/lib/gitlab/hook_data/milestone_builder_spec.rb b/spec/lib/gitlab/hook_data/milestone_builder_spec.rb new file mode 100644 index 00000000000..ef26dc9dd5d --- /dev/null +++ b/spec/lib/gitlab/hook_data/milestone_builder_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::HookData::MilestoneBuilder, feature_category: :webhooks do + let_it_be(:milestone) { create(:milestone) } + + let(:builder) { described_class.new(milestone) } + + describe '#build' do + subject(:data) { builder.build } + + it 'includes safe attributes' do + expect(data.keys).to match_array(described_class::SAFE_HOOK_ATTRIBUTES.map(&:to_s)) + end + + it 'returns indifferent access hash' do + expect(data).to be_a(ActiveSupport::HashWithIndifferentAccess) + end + + it 'includes correct milestone data' do + expect(data['id']).to eq(milestone.id) + expect(data['iid']).to eq(milestone.iid) + expect(data['title']).to eq(milestone.title) + expect(data['state']).to eq(milestone.state) + expect(data['project_id']).to eq(milestone.project_id) + end + end +end diff --git a/spec/lib/gitlab/import_export/project/relation_factory_spec.rb b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb index f898b18e200..439fedd3243 100644 --- a/spec/lib/gitlab/import_export/project/relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb @@ -67,6 +67,7 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory, :use_clean_rails_ 'job_events' => false, 'wiki_page_events' => true, 'releases_events' => false, + 'milestone_events' => false, 'emoji_events' => false, 'resource_access_token_events' => false, 'token' => token diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 516c94336f4..a85a4805f0c 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -619,6 +619,7 @@ ProjectHook: - confidential_note_events - repository_update_events - releases_events +- milestone_events - emoji_events - resource_access_token_events ProtectedBranch: diff --git a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb index 0ab87941e6b..cc42c764f98 100644 --- a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb +++ b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb @@ -38,21 +38,7 @@ RSpec.describe 'Creating a new Prometheus Integration', feature_category: :incid let(:mutation_response) { graphql_mutation_response(:prometheus_integration_create) } - it 'creates a new integration' do - post_graphql_mutation(mutation, current_user: current_user) - - new_integration = ::Integrations::Prometheus.last! - integration_response = mutation_response['integration'] - - expect(response).to have_gitlab_http_status(:success) - expect(integration_response['id']).to eq(GitlabSchema.id_from_object(new_integration).to_s) - expect(integration_response['type']).to eq('PROMETHEUS') - expect(integration_response['name']).to eq(new_integration.title) - expect(integration_response['active']).to eq(new_integration.manual_configuration?) - expect(integration_response['token']).to eq(new_integration.project.alerting_setting.token) - expect(integration_response['url']).to eq("http://localhost/#{project.full_path}/prometheus/alerts/notify.json") - expect(integration_response['apiUrl']).to eq(new_integration.api_url) - end + it_behaves_like 'creating a new HTTP integration', 'PROMETHEUS' context 'without api url' do let(:api_url) { nil } diff --git a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb index 7b00c9e3802..b77d8c34e11 100644 --- a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb +++ b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb @@ -7,11 +7,12 @@ RSpec.describe 'Resetting a token on an existing Prometheus Integration', featur let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, maintainers: user) } - let_it_be(:integration) { create(:prometheus_integration, project: project) } + let_it_be(:old_integration) { create(:prometheus_integration, project: project) } + let_it_be(:integration) { create(:alert_management_prometheus_integration, :legacy, project: project) } let(:mutation) do variables = { - id: GitlabSchema.id_from_object(integration).to_s + id: GitlabSchema.id_from_object(old_integration).to_s } graphql_mutation(:prometheus_integration_reset_token, variables) do <<~QL @@ -27,29 +28,15 @@ RSpec.describe 'Resetting a token on an existing Prometheus Integration', featur let(:mutation_response) { graphql_mutation_response(:prometheus_integration_reset_token) } - it 'creates a token' do - post_graphql_mutation(mutation, current_user: user) + it 'updates the token' do + expect { post_graphql_mutation(mutation, current_user: user) } + .to change { integration.reload.token } + integration_response = mutation_response['integration'] expect(response).to have_gitlab_http_status(:success) expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s) expect(integration_response['token']).not_to be_nil - expect(integration_response['token']).to eq(project.alerting_setting.token) - end - - context 'with an existing alerting setting' do - let_it_be(:alerting_setting) { create(:project_alerting_setting, project: project) } - - it 'updates the token' do - previous_token = alerting_setting.token - - post_graphql_mutation(mutation, current_user: user) - integration_response = mutation_response['integration'] - - expect(response).to have_gitlab_http_status(:success) - expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s) - expect(integration_response['token']).not_to eq(previous_token) - expect(integration_response['token']).to eq(alerting_setting.reload.token) - end + expect(integration_response['token']).to eq(integration.token) end end diff --git a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/update_spec.rb b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/update_spec.rb index 3219127fb5a..125ce0f31d6 100644 --- a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/update_spec.rb +++ b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/update_spec.rb @@ -7,13 +7,14 @@ RSpec.describe 'Updating an existing Prometheus Integration', feature_category: let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, maintainers: user) } - let_it_be(:integration) { create(:prometheus_integration, project: project) } + let_it_be(:old_integration) { create(:prometheus_integration, project: project) } + let_it_be(:integration) { create(:alert_management_prometheus_integration, :legacy, project: project) } let(:mutation) do variables = { - id: GitlabSchema.id_from_object(integration).to_s, + id: GitlabSchema.id_from_object(old_integration).to_s, api_url: 'http://modified-url.com', - active: true + active: false } graphql_mutation(:prometheus_integration_update, variables) do <<~QL @@ -37,7 +38,7 @@ RSpec.describe 'Updating an existing Prometheus Integration', feature_category: expect(response).to have_gitlab_http_status(:success) expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s) - expect(integration_response['apiUrl']).to eq('http://modified-url.com') - expect(integration_response['active']).to be_truthy + expect(integration_response['apiUrl']).to be_nil + expect(integration_response['active']).to be_falsey end end diff --git a/spec/requests/api/graphql/project/alert_management/integrations_spec.rb b/spec/requests/api/graphql/project/alert_management/integrations_spec.rb index c4d3a217027..5266a7a4748 100644 --- a/spec/requests/api/graphql/project/alert_management/integrations_spec.rb +++ b/spec/requests/api/graphql/project/alert_management/integrations_spec.rb @@ -7,8 +7,8 @@ RSpec.describe 'getting Alert Management Integrations', feature_category: :incid let_it_be(:project) { create(:project, :repository) } let_it_be(:current_user) { create(:user) } - let_it_be(:prometheus_integration) { create(:prometheus_integration, project: project) } - let_it_be(:project_alerting_setting) { create(:project_alerting_setting, project: project) } + let_it_be(:old_prometheus_integration) { create(:prometheus_integration, project: project) } + let_it_be(:prometheus_integration) { create(:alert_management_prometheus_integration, :legacy, project: project) } let_it_be(:active_http_integration) { create(:alert_management_http_integration, project: project) } let_it_be(:inactive_http_integration) { create(:alert_management_http_integration, :inactive, project: project) } let_it_be(:other_project_http_integration) { create(:alert_management_http_integration) } @@ -63,12 +63,7 @@ RSpec.describe 'getting Alert Management Integrations', feature_category: :incid ), a_graphql_entity_for( prometheus_integration, - 'type' => 'PROMETHEUS', - 'name' => 'Prometheus', - 'active' => prometheus_integration.manual_configuration?, - 'token' => project_alerting_setting.token, - 'url' => "http://#{Gitlab.config.gitlab.host}/#{project.full_path}/prometheus/alerts/notify.json", - 'apiUrl' => prometheus_integration.api_url + :name, :active, :token, :url, type: 'PROMETHEUS', api_url: nil ) ] end @@ -87,19 +82,14 @@ RSpec.describe 'getting Alert Management Integrations', feature_category: :incid end context 'when Prometheus Integration ID is given' do - let(:params) { { id: global_id_of(prometheus_integration) } } + let(:params) { { id: global_id_of(old_prometheus_integration) } } it_behaves_like 'a working graphql query' it 'returns the correct properties of the Prometheus Integration' do expect(integrations).to contain_exactly a_graphql_entity_for( prometheus_integration, - 'type' => 'PROMETHEUS', - 'name' => 'Prometheus', - 'active' => prometheus_integration.manual_configuration?, - 'token' => project_alerting_setting.token, - 'url' => "http://localhost/#{project.full_path}/prometheus/alerts/notify.json", - 'apiUrl' => prometheus_integration.api_url + :name, :active, :token, :url, type: 'PROMETHEUS', api_url: nil ) end end diff --git a/spec/services/integrations/test/project_service_spec.rb b/spec/services/integrations/test/project_service_spec.rb index 4f8f932fb45..cc88a815f65 100644 --- a/spec/services/integrations/test/project_service_spec.rb +++ b/spec/services/integrations/test/project_service_spec.rb @@ -41,6 +41,24 @@ RSpec.describe Integrations::Test::ProjectService, feature_category: :integratio end end + context 'milestone' do + let(:event) { 'milestone' } + + before do + # Mock the integration to support milestone events for testing + allow(integration).to receive(:supported_events).and_return(integration.supported_events + ['milestone']) + end + + it 'executes integration' do + milestone = create(:milestone, project: project) + allow(Gitlab::DataBuilder::Milestone).to receive(:build).and_return(sample_data) + allow_next(MilestonesFinder).to receive(:execute).and_return([milestone]) + + expect(integration).to receive(:test).with(sample_data).and_return(success_result) + expect(subject).to eq(success_result) + end + end + context 'push' do let(:event) { 'push' } diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb index f362c8da642..2c0caa38bb6 100644 --- a/spec/services/milestones/close_service_spec.rb +++ b/spec/services/milestones/close_service_spec.rb @@ -12,19 +12,71 @@ RSpec.describe Milestones::CloseService, feature_category: :team_planning do end describe '#execute' do - before do - described_class.new(project, user, {}).execute(milestone) + let(:service) { described_class.new(project, user, {}) } + + context 'when service is called before test suite' do + before do + service.execute(milestone) + end + + it { expect(milestone).to be_valid } + it { expect(milestone).to be_closed } + + describe 'event' do + let(:event) { Event.recent.first } + + it { expect(event.milestone).to be_truthy } + it { expect(event.target).to eq(milestone) } + it { expect(event.action_name).to eq('closed') } + end end - it { expect(milestone).to be_valid } - it { expect(milestone).to be_closed } + shared_examples 'closes the milestone' do |with_project_hooks:| + it 'executes hooks with close action and creates new event' do + expect(service).to receive(:execute_hooks).with(milestone, 'close').and_call_original + expect(project).to receive(:execute_hooks).with(kind_of(Hash), :milestone_hooks) if with_project_hooks - describe 'event' do - let(:event) { Event.recent.first } + expect { service.execute(milestone) }.to change { Event.count }.by(1) + end + end - it { expect(event.milestone).to be_truthy } - it { expect(event.target).to eq(milestone) } - it { expect(event.action_name).to eq('closed') } + shared_examples 'does not close the milestone' do + it 'does not execute hooks and does not create new event' do + expect(service).not_to receive(:execute_hooks) + + expect { service.execute(milestone) }.not_to change { Event.count } + end + end + + context 'when milestone is successfully closed' do + context 'when project has active milestone hooks' do + let(:project) do + create(:project).tap do |project| + create(:project_hook, project: project, milestone_events: true) + end + end + + it_behaves_like 'closes the milestone', with_project_hooks: true + end + + context 'when project has no active milestone hooks' do + it_behaves_like 'closes the milestone', with_project_hooks: false + end + end + + context 'when milestone fails to close' do + context 'when milestone is already closed' do + let(:milestone) { create(:milestone, :closed, project: project) } + + it_behaves_like 'does not close the milestone' + end + + context 'when milestone is a group milestone' do + let(:group) { create(:group) } + let(:milestone) { create(:milestone, group: group) } + + it_behaves_like 'does not close the milestone' + end end end end diff --git a/spec/services/milestones/create_service_spec.rb b/spec/services/milestones/create_service_spec.rb index 70010d88fbd..1999b1d4039 100644 --- a/spec/services/milestones/create_service_spec.rb +++ b/spec/services/milestones/create_service_spec.rb @@ -29,6 +29,29 @@ RSpec.describe Milestones::CreateService, feature_category: :team_planning do expect(milestone.title).to eq('New Milestone') expect(milestone.description).to eq('Description') end + + shared_examples 'creates the milestone' do |with_project_hooks:| + it 'executes hooks with create action and creates new event' do + expect(create_milestone).to receive(:execute_hooks).with(kind_of(Milestone), 'create').and_call_original + expect(project).to receive(:execute_hooks).with(kind_of(Hash), :milestone_hooks) if with_project_hooks + + expect { create_milestone.execute }.to change { Event.count }.by(1) + end + end + + context 'when project has active milestone hooks' do + let(:project) do + create(:project).tap do |project| + create(:project_hook, project: project, milestone_events: true) + end + end + + it_behaves_like 'creates the milestone', with_project_hooks: true + end + + context 'when project has no active milestone hooks' do + it_behaves_like 'creates the milestone', with_project_hooks: false + end end context 'when milestone fails to save' do @@ -48,6 +71,12 @@ RSpec.describe Milestones::CreateService, feature_category: :team_planning do create_milestone.execute end + it 'does not execute hooks and does not create new event' do + expect(create_milestone).not_to receive(:execute_hooks) + + expect { create_milestone.execute }.not_to change { Event.count } + end + it 'returns the unsaved milestone' do milestone = create_milestone.execute expect(milestone).to be_a(Milestone) @@ -56,6 +85,21 @@ RSpec.describe Milestones::CreateService, feature_category: :team_planning do end end + context 'when milestone is a group milestone' do + let(:group) { create(:group) } + let(:group_service) { described_class.new(group, user, params) } + + it 'does not execute hooks for group milestones' do + milestone = build(:milestone, group: group) + allow(milestone).to receive(:save).and_return(true) + allow(group_service).to receive(:build_milestone).and_return(milestone) + + expect(group).not_to receive(:execute_hooks) + + group_service.execute + end + end + it 'calls before_create method' do expect(create_milestone).to receive(:before_create) create_milestone.execute diff --git a/spec/services/milestones/destroy_service_spec.rb b/spec/services/milestones/destroy_service_spec.rb index fd276a54e10..c76edd2648c 100644 --- a/spec/services/milestones/destroy_service_spec.rb +++ b/spec/services/milestones/destroy_service_spec.rb @@ -48,7 +48,9 @@ RSpec.describe Milestones::DestroyService, feature_category: :team_planning do it_behaves_like 'deletes milestone id from issuables' - it 'logs destroy event' do + it 'logs destroy event and runs on-delete webhook' do + expect(service).to receive(:execute_hooks).with(milestone, 'delete') + service.execute(milestone) event = Event.where(project_id: milestone.project_id, target_type: 'Milestone') @@ -84,7 +86,9 @@ RSpec.describe Milestones::DestroyService, feature_category: :team_planning do it_behaves_like 'deletes milestone id from issuables' - it 'does not log destroy event' do + it 'does not log destroy event and does not run on-delete webhook' do + expect(service).not_to receive(:execute_hooks).with(milestone, 'delete') + expect { service.execute(milestone) }.not_to change { Event.count } end end diff --git a/spec/services/milestones/reopen_service_spec.rb b/spec/services/milestones/reopen_service_spec.rb new file mode 100644 index 00000000000..fc7874cbc84 --- /dev/null +++ b/spec/services/milestones/reopen_service_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Milestones::ReopenService, feature_category: :team_planning do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:milestone) { create(:milestone, :closed, title: "Milestone v1.2", project: project) } + + before_all do + project.add_maintainer(user) + end + + describe '#execute' do + let(:service) { described_class.new(project, user, {}) } + + context 'when service is called before test suite' do + before do + service.execute(milestone) + end + + it { expect(milestone).to be_valid } + it { expect(milestone).to be_active } + + describe 'event' do + let(:event) { Event.recent.first } + + it { expect(event.milestone).to be_truthy } + it { expect(event.target).to eq(milestone) } + it { expect(event.action_name).to eq('opened') } + end + end + + shared_examples 'reopens the milestone' do |with_project_hooks:| + it 'executes hooks with reopen action and creates new event' do + expect(service).to receive(:execute_hooks).with(milestone, 'reopen').and_call_original + expect(project).to receive(:execute_hooks).with(kind_of(Hash), :milestone_hooks) if with_project_hooks + + expect { service.execute(milestone) }.to change { Event.count }.by(1) + end + end + + shared_examples 'does not reopen the milestone' do + it 'does not execute hooks and does not create new event' do + expect(service).not_to receive(:execute_hooks) + + expect { service.execute(milestone) }.not_to change { Event.count } + end + end + + context 'when milestone is successfully reopened' do + let(:milestone) { create(:milestone, :closed, project: project) } + + context 'when project has active milestone hooks' do + let(:project) do + create(:project).tap do |project| + create(:project_hook, project: project, milestone_events: true) + end + end + + it_behaves_like 'reopens the milestone', with_project_hooks: true + end + + context 'when project has no active milestone hooks' do + it_behaves_like 'reopens the milestone', with_project_hooks: false + end + end + + context 'when milestone fails to reopen' do + context 'when milestone is already active' do + let(:milestone) { create(:milestone, project: project) } + + it_behaves_like 'does not reopen the milestone' + end + + context 'when milestone is a group milestone' do + let(:group) { create(:group) } + let(:milestone) { create(:milestone, :closed, group: group) } + + it_behaves_like 'does not reopen the milestone' + end + end + end +end diff --git a/spec/services/test_hooks/project_service_spec.rb b/spec/services/test_hooks/project_service_spec.rb index 1e06911f8f7..cd2bc14c0ed 100644 --- a/spec/services/test_hooks/project_service_spec.rb +++ b/spec/services/test_hooks/project_service_spec.rb @@ -211,6 +211,18 @@ RSpec.describe TestHooks::ProjectService, feature_category: :code_testing do end end + context 'milestone_events' do + let(:trigger) { 'milestone_events' } + let(:trigger_key) { :milestone_hooks } + + it 'executes hook' do + allow(Gitlab::DataBuilder::Milestone).to receive(:build_sample).and_return(sample_data) + + expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result) + expect(service.execute).to include(success_result) + end + end + context 'emoji' do let(:trigger) { 'emoji_events' } let(:trigger_key) { :emoji_hooks } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5a9fa87a223..1075e20e5ef 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -223,6 +223,8 @@ RSpec.configure do |config| config.include_context 'when rendered has no HTML escapes', type: :view config.include_context 'with STI disabled', type: :model + # Validate JSONB columns only in EE to avoid false positives in FOSS. + config.include_context 'with JSONB validated columns', type: :model if Gitlab.ee? include StubCurrentOrganization include StubFeatureFlags diff --git a/spec/support/shared_examples/models/jsonb_column_validation_shared_example.rb b/spec/support/shared_examples/models/jsonb_column_validation_shared_example.rb new file mode 100644 index 00000000000..f6c5ac1c26b --- /dev/null +++ b/spec/support/shared_examples/models/jsonb_column_validation_shared_example.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'yaml' + +module Support + module JsonbColumnValidation + TODO_YAML = File.join(__dir__, 'jsonb_column_validation_todo.yml') + + module_function + + def todo?(model, column) + @todo ||= YAML.load_file(TODO_YAML).to_set # rubocop:disable Gitlab/PredicateMemoization -- @todo is never `nil` or `false`. + @todo.include?("#{model.name}##{column}") + end + end +end + +# Checks whether JSONB columns are validated via JsonSchemaValidator. +# +# These checks are skipped in FOSS specs because they produce false positives, +# as the models are missing their EE extensions, including their validations. +# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/195456#note_2601197778 +# +# See https://docs.gitlab.com/development/migration_style_guide/#storing-json-in-database +# +# Parameter: +# - model: Model class +# - jsonb_column: List of JSONB columns +RSpec.shared_examples 'Model validates JSONB columns' do |model, jsonb_columns| + jsonb_columns.each do |column| + context "with JSONB column #{column}" do + let(:json_schema_validator) do + model.validators_on(column).find { |validator| validator.is_a?(JsonSchemaValidator) } + end + + it 'validates via JsonSchemaValidator' do + pending 'Still a TODO' if Support::JsonbColumnValidation.todo?(model, column) + + docs_reference = 'See https://docs.gitlab.com/development/migration_style_guide/#storing-json-in-database.' + expect(json_schema_validator).to be_present, + "This JSONB column is missing schema validation. #{docs_reference}" + end + end + end +end + +RSpec.shared_context 'with JSONB validated columns' do # rubocop:disable RSpec/SharedContext -- We cannot include `shared_examples` conditionally based on `type: :model` + model = described_class + jsonb_columns = \ + model && + model < ApplicationRecord && + model.name && # skip unnamed/anonymous models + !model.abstract_class? && + !model.table_name&.start_with?('_test') && # skip test models that define the tables in specs + model.columns.select { |c| c.type == :jsonb }.map(&:name).map(&:to_sym) + + if jsonb_columns && jsonb_columns.any? + include_examples 'Model validates JSONB columns', described_class, jsonb_columns + end +end diff --git a/spec/support/shared_examples/models/jsonb_column_validation_todo.yml b/spec/support/shared_examples/models/jsonb_column_validation_todo.yml new file mode 100644 index 00000000000..ad9facc2562 --- /dev/null +++ b/spec/support/shared_examples/models/jsonb_column_validation_todo.yml @@ -0,0 +1,47 @@ +# TODO list for `spec/support/shared_examples/models/jsonb_column_validation_shared_example.rb` +# Ideally, this list should only shrink and never grow. +- Ai::ActiveContext::Connection#options +- Ai::ActiveContext::Migration#metadata +- Ai::Conversation::Message#error_details +- Ai::DuoWorkflows::Checkpoint#checkpoint +- Ai::DuoWorkflows::Checkpoint#metadata +- AlertManagement::Alert#payload +- AlertManagement::HttpIntegration#payload_example +- ApplicationSetting#ci_cd_settings +- ApplicationSetting#oauth_provider +- ApplicationSetting#rate_limits_unauthenticated_git_http +- ApplicationSetting#repository_storages_weighted +- ApplicationSetting#tmp_asset_proxy_secret_key +- ApplicationSetting#token_prefixes +- Ci::BuildMetadata#config_options +- Ci::BuildMetadata#config_variables +- Ci::PipelineScheduleInput#value +- CloudConnector::Keys#secret_key +- DependencyProxy::GroupSetting#identity +- DependencyProxy::GroupSetting#secret +- GeoNodeStatus#status +- Geo::Event#payload +- Geo::SecondaryUsageData#payload +- GitlabSubscriptions::UserAddOnAssignmentVersion#object +- MergeRequest::CommitsMetadata#trailers +- Operations::FeatureFlags::Strategy#parameters +- Packages::Composer::Metadatum#composer_json +- RawUsageData#payload +- Releases::Evidence#summary +- RemoteDevelopment::WorkspaceAgentkState#desired_config +- RemoteDevelopment::WorkspacesAgentConfig#annotations +- RemoteDevelopment::WorkspacesAgentConfig#labels +- RemoteDevelopment::WorkspacesAgentConfigVersion#object +- RemoteDevelopment::WorkspacesAgentConfigVersion#object_changes +- Sbom::Occurrence#ancestors +- Security::ApprovalPolicyRule#content +- Security::Policy#metadata +- Security::ScanExecutionPolicyRule#content +- Security::VulnerabilityManagementPolicyRule#content +- ServicePing::NonSqlServicePing#metadata +- ServicePing::NonSqlServicePing#payload +- ServicePing::QueriesServicePing#payload +- VirtualRegistries::Packages::Maven::Upstream#password +- VirtualRegistries::Packages::Maven::Upstream#username +- Vulnerabilities::Finding::Evidence#data +- Vulnerabilities::Finding#location