diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index ad7e0b23ff4103247b9879c2945d3d2cb8876808..49dec770096ee422c5e3b5eab8de5479bb5e97e6 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -164,11 +164,6 @@ module Ci builds.latest.with_artifacts_not_expired.includes(project: [:namespace]) end - # For now the only user who participates is the user who triggered - def participants(_current_user = nil) - Array(user) - end - def valid_commit_sha if self.sha == Gitlab::Git::BLANK_SHA self.errors.add(:sha, " cant be 00000000 (branch removal)") diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 52577bd52ea3043c1dd40b9a0578dac72223757b..e4726e62e935846018c4f1cc488481dacad92c5f 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -60,16 +60,25 @@ class NotificationSetting < ActiveRecord::Base def set_events return if custom? - EMAIL_EVENTS.each do |event| - events[event] = false - end + self.events = {} end # Validates store accessors values as boolean # It is a text field so it does not cast correct boolean values in JSON def events_to_boolean EMAIL_EVENTS.each do |event| - events[event] = ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(events[event]) + bool = ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(public_send(event)) + + events[event] = bool end end + + # Allow people to receive failed pipeline notifications if they already have + # custom notifications enabled, as these are more like mentions than the other + # custom settings. + def failed_pipeline + bool = super + + bool.nil? || bool + end end diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 940e850600f8ab803a6d3cf90bbf3d8eb59ff75f..8bb995158de7e5baf8de86b5ffff098d5067c70d 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -3,7 +3,7 @@ # class NotificationRecipientService attr_reader :project - + def initialize(project) @project = project end @@ -12,11 +12,7 @@ class NotificationRecipientService custom_action = build_custom_key(action, target) recipients = target.participants(current_user) - - unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action) - recipients = add_project_watchers(recipients) - end - + recipients = add_project_watchers(recipients) recipients = add_custom_notifications(recipients, custom_action) recipients = reject_mention_users(recipients) @@ -43,6 +39,28 @@ class NotificationRecipientService recipients.uniq end + def build_pipeline_recipients(target, current_user, action:) + return [] unless current_user + + custom_action = + case action.to_s + when 'failed' + :failed_pipeline + when 'success' + :success_pipeline + end + + notification_setting = notification_setting_for_user_project(current_user, target.project) + + return [] if notification_setting.mention? || notification_setting.disabled? + + return [] if notification_setting.custom? && !notification_setting.public_send(custom_action) + + return [] if (notification_setting.watch? || notification_setting.participating?) && NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action) + + reject_users_without_access([current_user], target) + end + def build_relabeled_recipients(target, current_user, labels:) recipients = add_labels_subscribers([], target, labels: labels) recipients = reject_unsubscribed_users(recipients, target) @@ -290,4 +308,16 @@ class NotificationRecipientService def build_custom_key(action, object) "#{action}_#{object.class.model_name.name.underscore}".to_sym end + + def notification_setting_for_user_project(user, project) + project_setting = user.notification_settings_for(project) + + return project_setting unless project_setting.global? + + group_setting = user.notification_settings_for(project.group) + + return group_setting unless group_setting.global? + + user.global_notification_setting + end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 2c6f849259e418543f84503a79400b6e2c15a66d..6b186263bd103c106c572837ef707779e31811ac 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -278,11 +278,11 @@ class NotificationService return unless mailer.respond_to?(email_template) - recipients ||= NotificationRecipientService.new(pipeline.project).build_recipients( + recipients ||= NotificationRecipientService.new(pipeline.project).build_pipeline_recipients( pipeline, pipeline.user, action: pipeline.status, - skip_current_user: false).map(&:notification_email) + ).map(&:notification_email) if recipients.any? mailer.public_send(email_template, pipeline, recipients).deliver_later diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml index a736bfd91e2876d44a88b55b363ea5dbba5d2da8..708adbc38f13054263807cdb3ef0e49a1fc8c949 100644 --- a/app/views/shared/notifications/_custom_notifications.html.haml +++ b/app/views/shared/notifications/_custom_notifications.html.haml @@ -25,7 +25,7 @@ .form-group .checkbox{ class: ("prepend-top-0" if index == 0) } %label{ for: field_id } - = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.events[event]) + = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.public_send(event)) %strong = notification_event_name(event) = icon("spinner spin", class: "custom-notification-event-loading") diff --git a/changelogs/unreleased/quiet-pipelines.yml b/changelogs/unreleased/quiet-pipelines.yml new file mode 100644 index 0000000000000000000000000000000000000000..c02eb59b82472bd6693869f00d1e191d7d3cba87 --- /dev/null +++ b/changelogs/unreleased/quiet-pipelines.yml @@ -0,0 +1,5 @@ +--- +title: Only email pipeline creators; only email for successful pipelines with custom + settings +merge_request: +author: diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md index 4c52974e1038447b3de0f725b9e33b2c247fe765..e91d36987a93efc00e5c645a7cc653668b265a05 100644 --- a/doc/workflow/notifications.md +++ b/doc/workflow/notifications.md @@ -66,14 +66,13 @@ Below is the table of events users can be notified of: In all of the below cases, the notification will be sent to: - Participants: - the author and assignee of the issue/merge request - - the author of the pipeline - authors of comments on the issue/merge request - anyone mentioned by `@username` in the issue/merge request title or description - anyone mentioned by `@username` in any of the comments on the issue/merge request ...with notification level "Participating" or higher -- Watchers: users with notification level "Watch" (however successful pipeline would be off for watchers) +- Watchers: users with notification level "Watch" - Subscribers: anyone who manually subscribed to the issue/merge request - Custom: Users with notification level "custom" who turned on notifications for any of the events present in the table below @@ -89,8 +88,8 @@ In all of the below cases, the notification will be sent to: | Reopen merge request | | | Merge merge request | | | New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher | -| Failed pipeline | The above, plus the author of the pipeline | -| Successful pipeline | The above, plus the author of the pipeline | +| Failed pipeline | The author of the pipeline | +| Successful pipeline | The author of the pipeline, if they have the custom notification setting for successful pipelines set | In addition, if the title or description of an Issue or Merge Request is diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb index b67c96bc00d0bf5fccbd12e29720c738663471f1..561fbc8e247a53655f62ea62ebd141033ee79664 100644 --- a/spec/factories/ci/pipelines.rb +++ b/spec/factories/ci/pipelines.rb @@ -48,6 +48,10 @@ FactoryGirl.define do trait :success do status :success end + + trait :failed do + status :failed + end end end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 53282b999dc604e45f17b00992017b47cf62256d..e4a24fd63c22555e3fc84ab545e1bdf0b77be551 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1055,10 +1055,13 @@ describe Ci::Pipeline, models: true do end before do - reset_delivered_emails! - project.team << [pipeline.user, Gitlab::Access::DEVELOPER] + pipeline.user.global_notification_setting. + update(level: 'custom', failed_pipeline: true, success_pipeline: true) + + reset_delivered_emails! + perform_enqueued_jobs do pipeline.enqueue pipeline.run diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 5c841843b40639b8b4f9a21c6ae48610bbd119cd..e3146a56495c5ad545906d3be25ceef40c0a2562 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -113,7 +113,7 @@ describe NotificationService, services: true do project.add_master(issue.assignee) project.add_master(note.author) create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@subscribed_participant cc this guy') - update_custom_notification(:new_note, @u_guest_custom, project) + update_custom_notification(:new_note, @u_guest_custom, resource: project) update_custom_notification(:new_note, @u_custom_global) end @@ -379,7 +379,7 @@ describe NotificationService, services: true do build_team(note.project) reset_delivered_emails! allow_any_instance_of(Commit).to receive(:author).and_return(@u_committer) - update_custom_notification(:new_note, @u_guest_custom, project) + update_custom_notification(:new_note, @u_guest_custom, resource: project) update_custom_notification(:new_note, @u_custom_global) end @@ -457,7 +457,7 @@ describe NotificationService, services: true do add_users_with_subscription(issue.project, issue) reset_delivered_emails! - update_custom_notification(:new_issue, @u_guest_custom, project) + update_custom_notification(:new_issue, @u_guest_custom, resource: project) update_custom_notification(:new_issue, @u_custom_global) end @@ -567,7 +567,7 @@ describe NotificationService, services: true do describe '#reassigned_issue' do before do - update_custom_notification(:reassign_issue, @u_guest_custom, project) + update_custom_notification(:reassign_issue, @u_guest_custom, resource: project) update_custom_notification(:reassign_issue, @u_custom_global) end @@ -760,7 +760,7 @@ describe NotificationService, services: true do describe '#close_issue' do before do - update_custom_notification(:close_issue, @u_guest_custom, project) + update_custom_notification(:close_issue, @u_guest_custom, resource: project) update_custom_notification(:close_issue, @u_custom_global) end @@ -791,7 +791,7 @@ describe NotificationService, services: true do describe '#reopen_issue' do before do - update_custom_notification(:reopen_issue, @u_guest_custom, project) + update_custom_notification(:reopen_issue, @u_guest_custom, resource: project) update_custom_notification(:reopen_issue, @u_custom_global) end @@ -856,14 +856,14 @@ describe NotificationService, services: true do before do build_team(merge_request.target_project) add_users_with_subscription(merge_request.target_project, merge_request) - update_custom_notification(:new_merge_request, @u_guest_custom, project) + update_custom_notification(:new_merge_request, @u_guest_custom, resource: project) update_custom_notification(:new_merge_request, @u_custom_global) reset_delivered_emails! end describe '#new_merge_request' do before do - update_custom_notification(:new_merge_request, @u_guest_custom, project) + update_custom_notification(:new_merge_request, @u_guest_custom, resource: project) update_custom_notification(:new_merge_request, @u_custom_global) end @@ -952,7 +952,7 @@ describe NotificationService, services: true do describe '#reassigned_merge_request' do before do - update_custom_notification(:reassign_merge_request, @u_guest_custom, project) + update_custom_notification(:reassign_merge_request, @u_guest_custom, resource: project) update_custom_notification(:reassign_merge_request, @u_custom_global) end @@ -1026,7 +1026,7 @@ describe NotificationService, services: true do describe '#closed_merge_request' do before do - update_custom_notification(:close_merge_request, @u_guest_custom, project) + update_custom_notification(:close_merge_request, @u_guest_custom, resource: project) update_custom_notification(:close_merge_request, @u_custom_global) end @@ -1056,7 +1056,7 @@ describe NotificationService, services: true do describe '#merged_merge_request' do before do - update_custom_notification(:merge_merge_request, @u_guest_custom, project) + update_custom_notification(:merge_merge_request, @u_guest_custom, resource: project) update_custom_notification(:merge_merge_request, @u_custom_global) end @@ -1108,7 +1108,7 @@ describe NotificationService, services: true do describe '#reopen_merge_request' do before do - update_custom_notification(:reopen_merge_request, @u_guest_custom, project) + update_custom_notification(:reopen_merge_request, @u_guest_custom, resource: project) update_custom_notification(:reopen_merge_request, @u_custom_global) end @@ -1281,40 +1281,172 @@ describe NotificationService, services: true do describe 'Pipelines' do describe '#pipeline_finished' do let(:project) { create(:project, :public, :repository) } - let(:current_user) { create(:user) } let(:u_member) { create(:user) } - let(:u_other) { create(:user) } + let(:u_watcher) { create_user_with_notification(:watch, 'watcher') } + + let(:u_custom_notification_unset) do + create_user_with_notification(:custom, 'custom_unset') + end + + let(:u_custom_notification_enabled) do + user = create_user_with_notification(:custom, 'custom_enabled') + update_custom_notification(:success_pipeline, user, resource: project) + update_custom_notification(:failed_pipeline, user, resource: project) + user + end + + let(:u_custom_notification_disabled) do + user = create_user_with_notification(:custom, 'custom_disabled') + update_custom_notification(:success_pipeline, user, resource: project, value: false) + update_custom_notification(:failed_pipeline, user, resource: project, value: false) + user + end let(:commit) { project.commit } - let(:pipeline) do - create(:ci_pipeline, :success, + + def create_pipeline(user, status) + create(:ci_pipeline, status, project: project, - user: current_user, + user: user, ref: 'refs/heads/master', sha: commit.id, before_sha: '00000000') end before do - project.add_master(current_user) project.add_master(u_member) + project.add_master(u_watcher) + project.add_master(u_custom_notification_unset) + project.add_master(u_custom_notification_enabled) + project.add_master(u_custom_notification_disabled) + reset_delivered_emails! end - context 'without custom recipients' do - it 'notifies the pipeline user' do - notification.pipeline_finished(pipeline) + context 'with a successful pipeline' do + context 'when the creator has default settings' do + before do + pipeline = create_pipeline(u_member, :success) + notification.pipeline_finished(pipeline) + end + + it 'notifies nobody' do + should_not_email_anyone + end + end + + context 'when the creator has watch set' do + before do + pipeline = create_pipeline(u_watcher, :success) + notification.pipeline_finished(pipeline) + end + + it 'notifies nobody' do + should_not_email_anyone + end + end + + context 'when the creator has custom notifications, but without any set' do + before do + pipeline = create_pipeline(u_custom_notification_unset, :success) + notification.pipeline_finished(pipeline) + end + + it 'notifies nobody' do + should_not_email_anyone + end + end + + context 'when the creator has custom notifications disabled' do + before do + pipeline = create_pipeline(u_custom_notification_disabled, :success) + notification.pipeline_finished(pipeline) + end + + it 'notifies nobody' do + should_not_email_anyone + end + end + + context 'when the creator has custom notifications enabled' do + before do + pipeline = create_pipeline(u_custom_notification_enabled, :success) + notification.pipeline_finished(pipeline) + end - should_only_email(current_user, kind: :bcc) + it 'emails only the creator' do + should_only_email(u_custom_notification_enabled, kind: :bcc) + end end end - context 'with custom recipients' do - it 'notifies the custom recipients' do - users = [u_member, u_other] - notification.pipeline_finished(pipeline, users.map(&:notification_email)) + context 'with a failed pipeline' do + context 'when the creator has no custom notification set' do + before do + pipeline = create_pipeline(u_member, :failed) + notification.pipeline_finished(pipeline) + end + + it 'emails only the creator' do + should_only_email(u_member, kind: :bcc) + end + end + + context 'when the creator has watch set' do + before do + pipeline = create_pipeline(u_watcher, :failed) + notification.pipeline_finished(pipeline) + end + + it 'emails only the creator' do + should_only_email(u_watcher, kind: :bcc) + end + end + + context 'when the creator has custom notifications, but without any set' do + before do + pipeline = create_pipeline(u_custom_notification_unset, :failed) + notification.pipeline_finished(pipeline) + end + + it 'emails only the creator' do + should_only_email(u_custom_notification_unset, kind: :bcc) + end + end + + context 'when the creator has custom notifications disabled' do + before do + pipeline = create_pipeline(u_custom_notification_disabled, :failed) + notification.pipeline_finished(pipeline) + end - should_only_email(*users, kind: :bcc) + it 'notifies nobody' do + should_not_email_anyone + end + end + + context 'when the creator has custom notifications set' do + before do + pipeline = create_pipeline(u_custom_notification_enabled, :failed) + notification.pipeline_finished(pipeline) + end + + it 'emails only the creator' do + should_only_email(u_custom_notification_enabled, kind: :bcc) + end + end + + context 'when the creator has no read_build access' do + before do + pipeline = create_pipeline(u_member, :failed) + project.update(public_builds: false) + project.team.truncate + notification.pipeline_finished(pipeline) + end + + it 'does not send emails' do + should_not_email_anyone + end end end end @@ -1385,9 +1517,9 @@ describe NotificationService, services: true do # Create custom notifications # When resource is nil it means global notification - def update_custom_notification(event, user, resource = nil) + def update_custom_notification(event, user, resource: nil, value: true) setting = user.notification_settings_for(resource) - setting.events[event] = true + setting.events[event] = value setting.save end diff --git a/spec/workers/pipeline_notification_worker_spec.rb b/spec/workers/pipeline_notification_worker_spec.rb index 5a7ce2e08c482fc9be21530a0c7fb68ed6bbdc02..139032d77bd49365910b3e223c39277486929024 100644 --- a/spec/workers/pipeline_notification_worker_spec.rb +++ b/spec/workers/pipeline_notification_worker_spec.rb @@ -3,131 +3,19 @@ require 'spec_helper' describe PipelineNotificationWorker do include EmailHelpers - let(:pipeline) do - create(:ci_pipeline, - project: project, - sha: project.commit('master').sha, - user: pusher, - status: status) - end - - let(:project) { create(:project, :repository, public_builds: false) } - let(:user) { create(:user) } - let(:pusher) { user } - let(:watcher) { pusher } + let(:pipeline) { create(:ci_pipeline) } describe '#execute' do - before do - reset_delivered_emails! - pipeline.project.team << [pusher, Gitlab::Access::DEVELOPER] - end - - context 'when watcher has developer access' do - before do - pipeline.project.team << [watcher, Gitlab::Access::DEVELOPER] - end - - shared_examples 'sending emails' do - it 'sends emails' do - perform_enqueued_jobs do - subject.perform(pipeline.id) - end - - emails = ActionMailer::Base.deliveries - actual = emails.flat_map(&:bcc).sort - expected_receivers = receivers.map(&:email).uniq.sort - - expect(actual).to eq(expected_receivers) - expect(emails.size).to eq(1) - expect(emails.last.subject).to include(email_subject) - end - end - - context 'with success pipeline' do - let(:status) { 'success' } - let(:email_subject) { "Pipeline ##{pipeline.id} has succeeded" } - let(:receivers) { [pusher, watcher] } - - it_behaves_like 'sending emails' - - context 'with pipeline from someone else' do - let(:pusher) { create(:user) } - let(:watcher) { user } - - context 'with success pipeline notification on' do - before do - watcher.global_notification_setting. - update(level: 'custom', success_pipeline: true) - end - - it_behaves_like 'sending emails' - end - - context 'with success pipeline notification off' do - let(:receivers) { [pusher] } + it 'calls NotificationService#pipeline_finished when the pipeline exists' do + expect(NotificationService).to receive_message_chain(:new, :pipeline_finished) - before do - watcher.global_notification_setting. - update(level: 'custom', success_pipeline: false) - end - - it_behaves_like 'sending emails' - end - end - - context 'with failed pipeline' do - let(:status) { 'failed' } - let(:email_subject) { "Pipeline ##{pipeline.id} has failed" } - - it_behaves_like 'sending emails' - - context 'with pipeline from someone else' do - let(:pusher) { create(:user) } - let(:watcher) { user } - - context 'with failed pipeline notification on' do - before do - watcher.global_notification_setting. - update(level: 'custom', failed_pipeline: true) - end - - it_behaves_like 'sending emails' - end - - context 'with failed pipeline notification off' do - let(:receivers) { [pusher] } - - before do - watcher.global_notification_setting. - update(level: 'custom', failed_pipeline: false) - end - - it_behaves_like 'sending emails' - end - end - end - end + subject.perform(pipeline.id) end - context 'when watcher has no read_build access' do - let(:status) { 'failed' } - let(:email_subject) { "Pipeline ##{pipeline.id} has failed" } - let(:watcher) { create(:user) } - - before do - pipeline.project.team << [watcher, Gitlab::Access::GUEST] - - watcher.global_notification_setting. - update(level: 'custom', failed_pipeline: true) - - perform_enqueued_jobs do - subject.perform(pipeline.id) - end - end + it 'does nothing when the pipeline does not exist' do + expect(NotificationService).not_to receive(:new) - it 'does not send emails' do - should_only_email(pusher, kind: :bcc) - end + subject.perform(Ci::Pipeline.maximum(:id).to_i.succ) end end end