Commit a1805cbc authored by Sean McGivern's avatar Sean McGivern

Quiet pipeline emails

1. Never send a pipeline email to anyone other than the user who created
   the pipeline.
2. Only send pipeline success emails to people with the custom
   notification setting for enabled. Watchers and participants will
   never receive this.
3. When custom settings are unset (for new settings and legacy ones),
   act as if failed_pipeline is set.
parent 2faf955c
...@@ -164,11 +164,6 @@ module Ci ...@@ -164,11 +164,6 @@ module Ci
builds.latest.with_artifacts_not_expired.includes(project: [:namespace]) builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
end 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 def valid_commit_sha
if self.sha == Gitlab::Git::BLANK_SHA if self.sha == Gitlab::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)") self.errors.add(:sha, " cant be 00000000 (branch removal)")
......
...@@ -60,16 +60,25 @@ class NotificationSetting < ActiveRecord::Base ...@@ -60,16 +60,25 @@ class NotificationSetting < ActiveRecord::Base
def set_events def set_events
return if custom? return if custom?
EMAIL_EVENTS.each do |event| self.events = {}
events[event] = false
end
end end
# Validates store accessors values as boolean # Validates store accessors values as boolean
# It is a text field so it does not cast correct boolean values in JSON # It is a text field so it does not cast correct boolean values in JSON
def events_to_boolean def events_to_boolean
EMAIL_EVENTS.each do |event| 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
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 end
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
# #
class NotificationRecipientService class NotificationRecipientService
attr_reader :project attr_reader :project
def initialize(project) def initialize(project)
@project = project @project = project
end end
...@@ -12,11 +12,7 @@ class NotificationRecipientService ...@@ -12,11 +12,7 @@ class NotificationRecipientService
custom_action = build_custom_key(action, target) custom_action = build_custom_key(action, target)
recipients = target.participants(current_user) recipients = target.participants(current_user)
recipients = add_project_watchers(recipients)
unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
recipients = add_project_watchers(recipients)
end
recipients = add_custom_notifications(recipients, custom_action) recipients = add_custom_notifications(recipients, custom_action)
recipients = reject_mention_users(recipients) recipients = reject_mention_users(recipients)
...@@ -43,6 +39,28 @@ class NotificationRecipientService ...@@ -43,6 +39,28 @@ class NotificationRecipientService
recipients.uniq recipients.uniq
end 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:) def build_relabeled_recipients(target, current_user, labels:)
recipients = add_labels_subscribers([], target, labels: labels) recipients = add_labels_subscribers([], target, labels: labels)
recipients = reject_unsubscribed_users(recipients, target) recipients = reject_unsubscribed_users(recipients, target)
...@@ -290,4 +308,16 @@ class NotificationRecipientService ...@@ -290,4 +308,16 @@ class NotificationRecipientService
def build_custom_key(action, object) def build_custom_key(action, object)
"#{action}_#{object.class.model_name.name.underscore}".to_sym "#{action}_#{object.class.model_name.name.underscore}".to_sym
end 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 end
...@@ -278,11 +278,11 @@ class NotificationService ...@@ -278,11 +278,11 @@ class NotificationService
return unless mailer.respond_to?(email_template) return unless mailer.respond_to?(email_template)
recipients ||= NotificationRecipientService.new(pipeline.project).build_recipients( recipients ||= NotificationRecipientService.new(pipeline.project).build_pipeline_recipients(
pipeline, pipeline,
pipeline.user, pipeline.user,
action: pipeline.status, action: pipeline.status,
skip_current_user: false).map(&:notification_email) ).map(&:notification_email)
if recipients.any? if recipients.any?
mailer.public_send(email_template, pipeline, recipients).deliver_later mailer.public_send(email_template, pipeline, recipients).deliver_later
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
.form-group .form-group
.checkbox{ class: ("prepend-top-0" if index == 0) } .checkbox{ class: ("prepend-top-0" if index == 0) }
%label{ for: field_id } %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 %strong
= notification_event_name(event) = notification_event_name(event)
= icon("spinner spin", class: "custom-notification-event-loading") = icon("spinner spin", class: "custom-notification-event-loading")
---
title: Only email pipeline creators; only email for successful pipelines with custom
settings
merge_request:
author:
...@@ -66,14 +66,13 @@ Below is the table of events users can be notified of: ...@@ -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: In all of the below cases, the notification will be sent to:
- Participants: - Participants:
- the author and assignee of the issue/merge request - the author and assignee of the issue/merge request
- the author of the pipeline
- authors of comments on the issue/merge request - 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 the issue/merge request title or description
- anyone mentioned by `@username` in any of the comments on the issue/merge request - anyone mentioned by `@username` in any of the comments on the issue/merge request
...with notification level "Participating" or higher ...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 - 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 - 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: ...@@ -89,8 +88,8 @@ In all of the below cases, the notification will be sent to:
| Reopen merge request | | | Reopen merge request | |
| Merge merge request | | | Merge merge request | |
| New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher | | 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 | | Failed pipeline | The author of the pipeline |
| Successful pipeline | The above, plus 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 In addition, if the title or description of an Issue or Merge Request is
......
...@@ -48,6 +48,10 @@ FactoryGirl.define do ...@@ -48,6 +48,10 @@ FactoryGirl.define do
trait :success do trait :success do
status :success status :success
end end
trait :failed do
status :failed
end
end end
end end
end end
...@@ -1055,10 +1055,13 @@ describe Ci::Pipeline, models: true do ...@@ -1055,10 +1055,13 @@ describe Ci::Pipeline, models: true do
end end
before do before do
reset_delivered_emails!
project.team << [pipeline.user, Gitlab::Access::DEVELOPER] 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 perform_enqueued_jobs do
pipeline.enqueue pipeline.enqueue
pipeline.run pipeline.run
......
...@@ -113,7 +113,7 @@ describe NotificationService, services: true do ...@@ -113,7 +113,7 @@ describe NotificationService, services: true do
project.add_master(issue.assignee) project.add_master(issue.assignee)
project.add_master(note.author) project.add_master(note.author)
create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@subscribed_participant cc this guy') 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) update_custom_notification(:new_note, @u_custom_global)
end end
...@@ -379,7 +379,7 @@ describe NotificationService, services: true do ...@@ -379,7 +379,7 @@ describe NotificationService, services: true do
build_team(note.project) build_team(note.project)
reset_delivered_emails! reset_delivered_emails!
allow_any_instance_of(Commit).to receive(:author).and_return(@u_committer) 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) update_custom_notification(:new_note, @u_custom_global)
end end
...@@ -457,7 +457,7 @@ describe NotificationService, services: true do ...@@ -457,7 +457,7 @@ describe NotificationService, services: true do
add_users_with_subscription(issue.project, issue) add_users_with_subscription(issue.project, issue)
reset_delivered_emails! 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) update_custom_notification(:new_issue, @u_custom_global)
end end
...@@ -567,7 +567,7 @@ describe NotificationService, services: true do ...@@ -567,7 +567,7 @@ describe NotificationService, services: true do
describe '#reassigned_issue' do describe '#reassigned_issue' do
before 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) update_custom_notification(:reassign_issue, @u_custom_global)
end end
...@@ -760,7 +760,7 @@ describe NotificationService, services: true do ...@@ -760,7 +760,7 @@ describe NotificationService, services: true do
describe '#close_issue' do describe '#close_issue' do
before 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) update_custom_notification(:close_issue, @u_custom_global)
end end
...@@ -791,7 +791,7 @@ describe NotificationService, services: true do ...@@ -791,7 +791,7 @@ describe NotificationService, services: true do
describe '#reopen_issue' do describe '#reopen_issue' do
before 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) update_custom_notification(:reopen_issue, @u_custom_global)
end end
...@@ -856,14 +856,14 @@ describe NotificationService, services: true do ...@@ -856,14 +856,14 @@ describe NotificationService, services: true do
before do before do
build_team(merge_request.target_project) build_team(merge_request.target_project)
add_users_with_subscription(merge_request.target_project, merge_request) 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) update_custom_notification(:new_merge_request, @u_custom_global)
reset_delivered_emails! reset_delivered_emails!
end end
describe '#new_merge_request' do describe '#new_merge_request' do
before 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) update_custom_notification(:new_merge_request, @u_custom_global)
end end
...@@ -952,7 +952,7 @@ describe NotificationService, services: true do ...@@ -952,7 +952,7 @@ describe NotificationService, services: true do
describe '#reassigned_merge_request' do describe '#reassigned_merge_request' do
before 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) update_custom_notification(:reassign_merge_request, @u_custom_global)
end end
...@@ -1026,7 +1026,7 @@ describe NotificationService, services: true do ...@@ -1026,7 +1026,7 @@ describe NotificationService, services: true do
describe '#closed_merge_request' do describe '#closed_merge_request' do
before 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) update_custom_notification(:close_merge_request, @u_custom_global)
end end
...@@ -1056,7 +1056,7 @@ describe NotificationService, services: true do ...@@ -1056,7 +1056,7 @@ describe NotificationService, services: true do
describe '#merged_merge_request' do describe '#merged_merge_request' do
before 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) update_custom_notification(:merge_merge_request, @u_custom_global)
end end
...@@ -1108,7 +1108,7 @@ describe NotificationService, services: true do ...@@ -1108,7 +1108,7 @@ describe NotificationService, services: true do
describe '#reopen_merge_request' do describe '#reopen_merge_request' do
before 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) update_custom_notification(:reopen_merge_request, @u_custom_global)
end end
...@@ -1281,40 +1281,172 @@ describe NotificationService, services: true do ...@@ -1281,40 +1281,172 @@ describe NotificationService, services: true do
describe 'Pipelines' do describe 'Pipelines' do
describe '#pipeline_finished' do describe '#pipeline_finished' do
let(:project) { create(:project, :public, :repository) } let(:project) { create(:project, :public, :repository) }
let(:current_user) { create(:user) }
let(:u_member) { 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(:commit) { project.commit }
let(:pipeline) do
create(:ci_pipeline, :success, def create_pipeline(user, status)
create(:ci_pipeline, status,
project: project, project: project,
user: current_user, user: user,
ref: 'refs/heads/master', ref: 'refs/heads/master',
sha: commit.id, sha: commit.id,
before_sha: '00000000') before_sha: '00000000')
end end
before do before do
project.add_master(current_user)
project.add_master(u_member) 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! reset_delivered_emails!
end end
context 'without custom recipients' do context 'with a successful pipeline' do
it 'notifies the pipeline user' do context 'when the creator has default settings' do
notification.pipeline_finished(pipeline) 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
end end
context 'with custom recipients' do context 'with a failed pipeline' do
it 'notifies the custom recipients' do context 'when the creator has no custom notification set' do
users = [u_member, u_other] before do
notification.pipeline_finished(pipeline, users.map(&:notification_email)) 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 end
end end
...@@ -1385,9 +1517,9 @@ describe NotificationService, services: true do ...@@ -1385,9 +1517,9 @@ describe NotificationService, services: true do
# Create custom notifications # Create custom notifications
# When resource is nil it means global notification # 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 = user.notification_settings_for(resource)
setting.events[event] = true setting.events[event] = value
setting.save setting.save
end end
......
...@@ -3,131 +3,19 @@ require 'spec_helper' ...@@ -3,131 +3,19 @@ require 'spec_helper'
describe PipelineNotificationWorker do describe PipelineNotificationWorker do
include EmailHelpers include EmailHelpers
let(:pipeline) do let(:pipeline) { create(:ci_pipeline) }
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 }
describe '#execute' do describe '#execute' do
before do it 'calls NotificationService#pipeline_finished when the pipeline exists' do
reset_delivered_emails! expect(NotificationService).to receive_message_chain(:new, :pipeline_finished)
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] }
before do subject.perform(pipeline.id)
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
end end
context 'when watcher has no read_build access' do it 'does nothing when the pipeline does not exist' do
let(:status) { 'failed' } expect(NotificationService).not_to receive(:new)
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 not send emails' do subject.perform(Ci::Pipeline.maximum(:id).to_i.succ)
should_only_email(pusher, kind: :bcc)
end
end end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment