Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-07-04 00:07:30 +00:00
parent 191fe0b178
commit f01ede3183
71 changed files with 989 additions and 345 deletions

View File

@ -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'

View File

@ -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'

View File

@ -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

View File

@ -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"},

View File

@ -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)

View File

@ -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"},

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -15,6 +15,7 @@ class ProjectHook < WebHook
:issue_hooks,
:job_hooks,
:merge_request_hooks,
:milestone_hooks,
:note_hooks,
:pipeline_hooks,
:push_hooks,

View File

@ -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

View File

@ -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?

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -14,6 +14,7 @@ module Milestones
return unless milestone.destroyed?
execute_hooks(milestone, 'delete') if milestone.project_milestone?
milestone
end

View File

@ -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

View File

@ -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'

View File

@ -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,

View File

@ -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
default_enabled: true

View File

@ -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

View File

@ -0,0 +1 @@
ecd7624a83f30d66eb361cdfa56d857162c5ff00a2f9ae3ce256013f003f4d1b

View File

@ -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))

View File

@ -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.

View File

@ -2614,6 +2614,7 @@ Input type: `AuditEventsAmazonS3ConfigurationUpdateInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationauditeventsamazons3configurationupdateaccesskeyxid"></a>`accessKeyXid` | [`String`](#string) | Access key ID of the Amazon S3 account. |
| <a id="mutationauditeventsamazons3configurationupdateactive"></a>`active` | [`Boolean`](#boolean) | Active status of the destination. |
| <a id="mutationauditeventsamazons3configurationupdateawsregion"></a>`awsRegion` | [`String`](#string) | AWS region where the bucket is created. |
| <a id="mutationauditeventsamazons3configurationupdatebucketname"></a>`bucketName` | [`String`](#string) | Name of the bucket where the audit events would be logged. |
| <a id="mutationauditeventsamazons3configurationupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
@ -2776,6 +2777,7 @@ Input type: `AuditEventsInstanceAmazonS3ConfigurationUpdateInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationauditeventsinstanceamazons3configurationupdateaccesskeyxid"></a>`accessKeyXid` | [`String`](#string) | Access key ID of the Amazon S3 account. |
| <a id="mutationauditeventsinstanceamazons3configurationupdateactive"></a>`active` | [`Boolean`](#boolean) | Active status of the destination. |
| <a id="mutationauditeventsinstanceamazons3configurationupdateawsregion"></a>`awsRegion` | [`String`](#string) | AWS region where the bucket is created. |
| <a id="mutationauditeventsinstanceamazons3configurationupdatebucketname"></a>`bucketName` | [`String`](#string) | Name of the bucket where the audit events would be logged. |
| <a id="mutationauditeventsinstanceamazons3configurationupdateclientmutationid"></a>`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 |
| ---- | ---- | ----------- |
| <a id="mutationprometheusintegrationcreateactive"></a>`active` | [`Boolean!`](#boolean) | Whether the integration is receiving alerts. |
| <a id="mutationprometheusintegrationcreateapiurl"></a>`apiUrl` | [`String`](#string) | Endpoint at which Prometheus can be queried. |
| <a id="mutationprometheusintegrationcreateapiurl"></a>`apiUrl` {{< icon name="warning-solid" >}} | [`String`](#string) | **Deprecated**: Feature removed in 16.0. Deprecated in GitLab 18.2. |
| <a id="mutationprometheusintegrationcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationprometheusintegrationcreatename"></a>`name` | [`String`](#string) | Name of the integration. |
| <a id="mutationprometheusintegrationcreatepayloadattributemappings"></a>`payloadAttributeMappings` | [`[AlertManagementPayloadAlertFieldInput!]`](#alertmanagementpayloadalertfieldinput) | Custom mapping of GitLab alert attributes to fields from the payload example. |
| <a id="mutationprometheusintegrationcreatepayloadexample"></a>`payloadExample` | [`JsonString`](#jsonstring) | Example of an alert payload. |
| <a id="mutationprometheusintegrationcreateprojectpath"></a>`projectPath` | [`ID!`](#id) | Project to create the integration in. |
| <a id="mutationprometheusintegrationcreatetype"></a>`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`
| ---- | ---- | ----------- |
| <a id="mutationprometheusintegrationresettokenclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationprometheusintegrationresettokenerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during the mutation. |
| <a id="mutationprometheusintegrationresettokenintegration"></a>`integration` | [`AlertManagementPrometheusIntegration`](#alertmanagementprometheusintegration) | Newly created integration. |
| <a id="mutationprometheusintegrationresettokenintegration"></a>`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 |
| ---- | ---- | ----------- |
| <a id="mutationprometheusintegrationupdateactive"></a>`active` | [`Boolean`](#boolean) | Whether the integration is receiving alerts. |
| <a id="mutationprometheusintegrationupdateapiurl"></a>`apiUrl` | [`String`](#string) | Endpoint at which Prometheus can be queried. |
| <a id="mutationprometheusintegrationupdateapiurl"></a>`apiUrl` {{< icon name="warning-solid" >}} | [`String`](#string) | **Deprecated**: Feature removed in 16.0. Deprecated in GitLab 18.2. |
| <a id="mutationprometheusintegrationupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationprometheusintegrationupdateid"></a>`id` | [`IntegrationsPrometheusID!`](#integrationsprometheusid) | ID of the integration to mutate. |
| <a id="mutationprometheusintegrationupdatename"></a>`name` | [`String`](#string) | Name of the integration. |
| <a id="mutationprometheusintegrationupdatepayloadattributemappings"></a>`payloadAttributeMappings` | [`[AlertManagementPayloadAlertFieldInput!]`](#alertmanagementpayloadalertfieldinput) | Custom mapping of GitLab alert attributes to fields from the payload example. |
| <a id="mutationprometheusintegrationupdatepayloadexample"></a>`payloadExample` | [`JsonString`](#jsonstring) | Example of an alert payload. |
#### Fields
@ -10071,7 +10095,7 @@ Input type: `PrometheusIntegrationUpdateInput`
| ---- | ---- | ----------- |
| <a id="mutationprometheusintegrationupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationprometheusintegrationupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during the mutation. |
| <a id="mutationprometheusintegrationupdateintegration"></a>`integration` | [`AlertManagementPrometheusIntegration`](#alertmanagementprometheusintegration) | Newly created integration. |
| <a id="mutationprometheusintegrationupdateintegration"></a>`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 |
| ---- | ---- | ----------- |
| <a id="alertmanagementhttpintegrationactive"></a>`active` | [`Boolean`](#boolean) | Whether the endpoint is currently accepting alerts. |
| <a id="alertmanagementhttpintegrationapiurl"></a>`apiUrl` | [`String`](#string) | URL at which Prometheus metrics can be queried to populate the metrics dashboard. |
| <a id="alertmanagementhttpintegrationapiurl"></a>`apiUrl` {{< icon name="warning-solid" >}} | [`String`](#string) | **Deprecated** in GitLab 18.2. Feature removed in 16.0. |
| <a id="alertmanagementhttpintegrationid"></a>`id` | [`ID!`](#id) | ID of the integration. |
| <a id="alertmanagementhttpintegrationname"></a>`name` | [`String`](#string) | Name of the integration. |
| <a id="alertmanagementhttpintegrationpayloadalertfields"></a>`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 |
| ---- | ---- | ----------- |
| <a id="alertmanagementprometheusintegrationactive"></a>`active` | [`Boolean`](#boolean) | Whether the endpoint is currently accepting alerts. |
| <a id="alertmanagementprometheusintegrationapiurl"></a>`apiUrl` | [`String`](#string) | URL at which Prometheus metrics can be queried to populate the metrics dashboard. |
| <a id="alertmanagementprometheusintegrationapiurl"></a>`apiUrl` {{< icon name="warning-solid" >}} | [`String`](#string) | **Deprecated** in GitLab 18.2. Feature removed in 16.0. |
| <a id="alertmanagementprometheusintegrationid"></a>`id` | [`ID!`](#id) | ID of the integration. |
| <a id="alertmanagementprometheusintegrationname"></a>`name` | [`String`](#string) | Name of the integration. |
| <a id="alertmanagementprometheusintegrationtoken"></a>`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 |
| ---- | ---- | ----------- |
| <a id="amazons3configurationtypeaccesskeyxid"></a>`accessKeyXid` | [`String!`](#string) | Access key ID of the Amazon S3 account. |
| <a id="amazons3configurationtypeactive"></a>`active` | [`Boolean!`](#boolean) | Active status of the destination. |
| <a id="amazons3configurationtypeawsregion"></a>`awsRegion` | [`String!`](#string) | AWS region where the bucket is created. |
| <a id="amazons3configurationtypebucketname"></a>`bucketName` | [`String!`](#string) | Name of the bucket where the audit events would be logged. |
| <a id="amazons3configurationtypegroup"></a>`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 |
| ---- | ---- | ----------- |
| <a id="instanceamazons3configurationtypeaccesskeyxid"></a>`accessKeyXid` | [`String!`](#string) | Access key ID of the Amazon S3 account. |
| <a id="instanceamazons3configurationtypeactive"></a>`active` | [`Boolean!`](#boolean) | Active status of the destination. |
| <a id="instanceamazons3configurationtypeawsregion"></a>`awsRegion` | [`String!`](#string) | AWS region where the bucket is created. |
| <a id="instanceamazons3configurationtypebucketname"></a>`bucketName` | [`String!`](#string) | Name of the bucket where the audit events would be logged. |
| <a id="instanceamazons3configurationtypeid"></a>`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 |
| ---- | ---- | ----------- |
| <a id="alertmanagementintegrationactive"></a>`active` | [`Boolean`](#boolean) | Whether the endpoint is currently accepting alerts. |
| <a id="alertmanagementintegrationapiurl"></a>`apiUrl` | [`String`](#string) | URL at which Prometheus metrics can be queried to populate the metrics dashboard. |
| <a id="alertmanagementintegrationapiurl"></a>`apiUrl` {{< icon name="warning-solid" >}} | [`String`](#string) | **Deprecated** in GitLab 18.2. Feature removed in 16.0. |
| <a id="alertmanagementintegrationid"></a>`id` | [`ID!`](#id) | ID of the integration. |
| <a id="alertmanagementintegrationname"></a>`name` | [`String`](#string) | Name of the integration. |
| <a id="alertmanagementintegrationtoken"></a>`token` | [`String`](#string) | Token used to authenticate alert notification requests. |
@ -49586,6 +49617,7 @@ Implementations:
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="amazons3configurationinterfaceaccesskeyxid"></a>`accessKeyXid` | [`String!`](#string) | Access key ID of the Amazon S3 account. |
| <a id="amazons3configurationinterfaceactive"></a>`active` | [`Boolean!`](#boolean) | Active status of the destination. |
| <a id="amazons3configurationinterfaceawsregion"></a>`awsRegion` | [`String!`](#string) | AWS region where the bucket is created. |
| <a id="amazons3configurationinterfacebucketname"></a>`bucketName` | [`String!`](#string) | Name of the bucket where the audit events would be logged. |
| <a id="amazons3configurationinterfaceid"></a>`id` | [`ID!`](#id) | ID of the configuration. |

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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 >}}

View File

@ -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' }

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -117,6 +117,9 @@
"releases_events": {
"type": "boolean"
},
"milestone_events": {
"type": "boolean"
},
"emoji_events": {
"type": "boolean"
},

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -619,6 +619,7 @@ ProjectHook:
- confidential_note_events
- repository_update_events
- releases_events
- milestone_events
- emoji_events
- resource_access_token_events
ProtectedBranch:

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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' }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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