diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index 7e8369d0a051b2d272981f597215981f69eceedb..03cc8f2b6bd9e3bd7df12c20299f5441c4b8227e 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -74,4 +74,13 @@ module NotificationsHelper
     return unless notification_setting.source_type
     hidden_field_tag "#{notification_setting.source_type.downcase}_id", notification_setting.source_id
   end
+
+  def notification_event_name(event)
+    case event
+    when :success_pipeline
+      'Successful pipeline'
+    else
+      event.to_s.humanize
+    end
+  end
 end
diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb
index 601c8b5cd621843f44867888dd04b6d262910ba2..9460a6cd2be9bc2edf15e180d2316313380303b8 100644
--- a/app/mailers/emails/pipelines.rb
+++ b/app/mailers/emails/pipelines.rb
@@ -1,22 +1,27 @@
 module Emails
   module Pipelines
-    def pipeline_success_email(pipeline, to)
-      pipeline_mail(pipeline, to, 'succeeded')
+    def pipeline_success_email(pipeline, recipients)
+      pipeline_mail(pipeline, recipients, 'succeeded')
     end
 
-    def pipeline_failed_email(pipeline, to)
-      pipeline_mail(pipeline, to, 'failed')
+    def pipeline_failed_email(pipeline, recipients)
+      pipeline_mail(pipeline, recipients, 'failed')
     end
 
     private
 
-    def pipeline_mail(pipeline, to, status)
+    def pipeline_mail(pipeline, recipients, status)
       @project = pipeline.project
       @pipeline = pipeline
       @merge_request = pipeline.merge_requests.first
       add_headers
 
-      mail(to: to, subject: pipeline_subject(status), skip_premailer: true) do |format|
+      # We use bcc here because we don't want to generate this emails for a
+      # thousand times. This could be potentially expensive in a loop, and
+      # recipients would contain all project watchers so it could be a lot.
+      mail(bcc: recipients,
+           subject: pipeline_subject(status),
+           skip_premailer: true) do |format|
         format.html { render layout: false }
         format.text
       end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index d343263289901afeba9e5d620d7db5002e9b54e2..3fee6c187700ee57e17cefdcccf830170d161d2b 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -81,6 +81,12 @@ module Ci
           PipelineHooksWorker.perform_async(id)
         end
       end
+
+      after_transition any => [:success, :failed] do |pipeline|
+        pipeline.run_after_commit do
+          PipelineNotificationWorker.perform_async(pipeline.id)
+        end
+      end
     end
 
     # ref can't be HEAD or SHA, can only be branch/tag name
@@ -109,6 +115,11 @@ module Ci
       project.id
     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 121b598b8f3839f67a9963bc31dfb3ad9b79f597..43fc218de2b5d458ab85add171d173ab8d2c0880 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -32,7 +32,9 @@ class NotificationSetting < ActiveRecord::Base
     :reopen_merge_request,
     :close_merge_request,
     :reassign_merge_request,
-    :merge_merge_request
+    :merge_merge_request,
+    :failed_pipeline,
+    :success_pipeline
   ]
 
   store :events, accessors: EMAIL_EVENTS, coder: JSON
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
index ec3c1bc85ee3f092f073c1482e282923125cfa0c..745f9bd1b43f492f9d1c3fb999e901ba7dc73d84 100644
--- a/app/models/project_services/pipelines_email_service.rb
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -1,10 +1,7 @@
 class PipelinesEmailService < Service
   prop_accessor :recipients
-  boolean_accessor :add_pusher
   boolean_accessor :notify_only_broken_pipelines
-  validates :recipients,
-    presence: true,
-    if: ->(s) { s.activated? && !s.add_pusher? }
+  validates :recipients, presence: true, if: :activated?
 
   def initialize_properties
     self.properties ||= { notify_only_broken_pipelines: true }
@@ -34,8 +31,8 @@ class PipelinesEmailService < Service
 
     return unless all_recipients.any?
 
-    pipeline = Ci::Pipeline.find(data[:object_attributes][:id])
-    Ci::SendPipelineNotificationService.new(pipeline).execute(all_recipients)
+    pipeline_id = data[:object_attributes][:id]
+    PipelineNotificationWorker.new.perform(pipeline_id, all_recipients)
   end
 
   def can_test?
@@ -57,9 +54,6 @@ class PipelinesEmailService < Service
       { type: 'textarea',
         name: 'recipients',
         placeholder: 'Emails separated by comma' },
-      { type: 'checkbox',
-        name: 'add_pusher',
-        label: 'Add pusher to recipients list' },
       { type: 'checkbox',
         name: 'notify_only_broken_pipelines' },
     ]
@@ -85,12 +79,6 @@ class PipelinesEmailService < Service
   end
 
   def retrieve_recipients(data)
-    all_recipients = recipients.to_s.split(',').reject(&:blank?)
-
-    if add_pusher? && data[:user].try(:[], :email)
-      all_recipients << data[:user][:email]
-    end
-
-    all_recipients
+    recipients.to_s.split(',').reject(&:blank?)
   end
 end
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 2232e231cf84f0b9c37505c3a522185db3a3040e..8b25332b73ceeeb2635ae78d1d0665b36dca8017 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -5,7 +5,7 @@ module Ci
 
       # If we can't read build we should also not have that
       # ability when looking at this in context of commit_status
-      %w(read create update admin).each do |rule|
+      %w[read create update admin].each do |rule|
         cannot! :"#{rule}_commit_status" unless can? :"#{rule}_build"
       end
     end
diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3d2eef1c50cf95b13a188870ec98a38355ceb1a3
--- /dev/null
+++ b/app/policies/ci/pipeline_policy.rb
@@ -0,0 +1,4 @@
+module Ci
+  class PipelinePolicy < BuildPolicy
+  end
+end
diff --git a/app/services/ci/send_pipeline_notification_service.rb b/app/services/ci/send_pipeline_notification_service.rb
deleted file mode 100644
index ceb182801f7f5a788acb0ac475172897578b0a62..0000000000000000000000000000000000000000
--- a/app/services/ci/send_pipeline_notification_service.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module Ci
-  class SendPipelineNotificationService
-    attr_reader :pipeline
-
-    def initialize(new_pipeline)
-      @pipeline = new_pipeline
-    end
-
-    def execute(recipients)
-      email_template = "pipeline_#{pipeline.status}_email"
-
-      return unless Notify.respond_to?(email_template)
-
-      recipients.each do |to|
-        Notify.public_send(email_template, pipeline, to).deliver_later
-      end
-    end
-  end
-end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 72712afc07e529cc7190c5abc247e1a70ac3438b..6697840cc26e6f9db64c4ef5d74a15c89ba2d669 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -312,6 +312,22 @@ class NotificationService
     mailer.project_was_not_exported_email(current_user, project, errors).deliver_later
   end
 
+  def pipeline_finished(pipeline, recipients = nil)
+    email_template = "pipeline_#{pipeline.status}_email"
+
+    return unless mailer.respond_to?(email_template)
+
+    recipients ||= build_recipients(
+      pipeline,
+      pipeline.project,
+      nil, # The acting user, who won't be added to recipients
+      action: pipeline.status).map(&:notification_email)
+
+    if recipients.any?
+      mailer.public_send(email_template, pipeline, recipients).deliver_later
+    end
+  end
+
   protected
 
   # Get project/group users with CUSTOM notification level
@@ -475,9 +491,14 @@ class NotificationService
   end
 
   def reject_users_without_access(recipients, target)
-    return recipients unless target.is_a?(Issuable)
+    ability = case target
+              when Issuable
+                :"read_#{target.to_ability_name}"
+              when Ci::Pipeline
+                :read_build # We have build trace in pipeline emails
+              end
 
-    ability = :"read_#{target.to_ability_name}"
+    return recipients unless ability
 
     recipients.select do |user|
       user.can?(ability, target)
@@ -624,6 +645,6 @@ class NotificationService
   # Build event key to search on custom notification level
   # Check NotificationSetting::EMAIL_EVENTS
   def build_custom_key(action, object)
-    "#{action}_#{object.class.name.underscore}".to_sym
+    "#{action}_#{object.class.model_name.name.underscore}".to_sym
   end
 end
diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml
index b704981e3dbfdcabec388392611647077a7e53c9..a82fc95df84a92582f1cc1afb7549d5048c66ec5 100644
--- a/app/views/shared/notifications/_custom_notifications.html.haml
+++ b/app/views/shared/notifications/_custom_notifications.html.haml
@@ -27,5 +27,5 @@
                       %label{ for: field_id }
                         = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.events[event])
                         %strong
-                          = event.to_s.humanize
+                          = notification_event_name(event)
                           = icon("spinner spin", class: "custom-notification-event-loading")
diff --git a/app/workers/pipeline_notification_worker.rb b/app/workers/pipeline_notification_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cdb860b66758056253cff38cfb2883cbfe620d41
--- /dev/null
+++ b/app/workers/pipeline_notification_worker.rb
@@ -0,0 +1,12 @@
+class PipelineNotificationWorker
+  include Sidekiq::Worker
+  include PipelineQueue
+
+  def perform(pipeline_id, recipients = nil)
+    pipeline = Ci::Pipeline.find_by(id: pipeline_id)
+
+    return unless pipeline
+
+    NotificationService.new.pipeline_finished(pipeline, recipients)
+  end
+end
diff --git a/changelogs/unreleased/pipeline-notifications.yml b/changelogs/unreleased/pipeline-notifications.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b43060674b2f903ed056df1d9375dc1e124ce403
--- /dev/null
+++ b/changelogs/unreleased/pipeline-notifications.yml
@@ -0,0 +1,6 @@
+---
+title: Add CI notifications. Who triggered a pipeline would receive an email after
+  the pipeline is succeeded or failed. Users could also update notification settings
+  accordingly
+merge_request: 6342
+author:
diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md
index ff6c9e4931c747722f18feebf7b88b438ff482ad..aea1c12a3927678fe6313376753ad870921499c8 100644
--- a/doc/api/notification_settings.md
+++ b/doc/api/notification_settings.md
@@ -4,7 +4,7 @@
 
 **Valid notification levels**
 
-The notification levels are defined in the `NotificationSetting::level` model enumeration. Currently, these levels are recognized:
+The notification levels are defined in the `NotificationSetting.level` model enumeration. Currently, these levels are recognized:
 
 ```
 disabled
@@ -28,6 +28,8 @@ reopen_merge_request
 close_merge_request
 reassign_merge_request
 merge_merge_request
+failed_pipeline
+success_pipeline
 ```
 
 ## Global notification settings
@@ -77,6 +79,8 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
 | `close_merge_request` | boolean | no | Enable/disable this notification |
 | `reassign_merge_request` | boolean | no | Enable/disable this notification |
 | `merge_merge_request` | boolean | no | Enable/disable this notification |
+| `failed_pipeline` | boolean | no | Enable/disable this notification |
+| `success_pipeline` | boolean | no | Enable/disable this notification |
 
 Example response:
 
@@ -141,6 +145,8 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
 | `close_merge_request` | boolean | no | Enable/disable this notification |
 | `reassign_merge_request` | boolean | no | Enable/disable this notification |
 | `merge_merge_request` | boolean | no | Enable/disable this notification |
+| `failed_pipeline` | boolean | no | Enable/disable this notification |
+| `success_pipeline` | boolean | no | Enable/disable this notification |
 
 Example responses:
 
@@ -161,7 +167,9 @@ Example responses:
     "reopen_merge_request": false,
     "close_merge_request": false,
     "reassign_merge_request": false,
-    "merge_merge_request": false
+    "merge_merge_request": false,
+    "failed_pipeline": false,
+    "success_pipeline": false
   }
 }
 ```
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index 1b49a5c385fa8fba633bf1a2271329b80088c1bf..c936e8833c6e0770b377e465109235e41f685d10 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -66,6 +66,7 @@ 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
@@ -88,6 +89,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 |
 
 
 In addition, if the title or description of an Issue or Merge Request is
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 5eb14dc6bd2029282d4b668cfd33f62bb00c9385..71b7628ef10269fd95acc1e21b9c9c95de1bb91b 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -524,4 +524,78 @@ describe Ci::Pipeline, models: true do
       expect(pipeline.merge_requests).to be_empty
     end
   end
+
+  describe 'notifications when pipeline success or failed' do
+    let(:project) { create(:project) }
+
+    let(:pipeline) do
+      create(:ci_pipeline,
+             project: project,
+             sha: project.commit('master').sha,
+             user: create(:user))
+    end
+
+    before do
+      reset_delivered_emails!
+
+      project.team << [pipeline.user, Gitlab::Access::DEVELOPER]
+
+      perform_enqueued_jobs do
+        pipeline.enqueue
+        pipeline.run
+      end
+    end
+
+    shared_examples 'sending a notification' do
+      it 'sends an email' do
+        should_only_email(pipeline.user, kind: :bcc)
+      end
+    end
+
+    shared_examples 'not sending any notification' do
+      it 'does not send any email' do
+        should_not_email_anyone
+      end
+    end
+
+    context 'with success pipeline' do
+      before do
+        perform_enqueued_jobs do
+          pipeline.succeed
+        end
+      end
+
+      it_behaves_like 'sending a notification'
+    end
+
+    context 'with failed pipeline' do
+      before do
+        perform_enqueued_jobs do
+          pipeline.drop
+        end
+      end
+
+      it_behaves_like 'sending a notification'
+    end
+
+    context 'with skipped pipeline' do
+      before do
+        perform_enqueued_jobs do
+          pipeline.skip
+        end
+      end
+
+      it_behaves_like 'not sending any notification'
+    end
+
+    context 'with cancelled pipeline' do
+      before do
+        perform_enqueued_jobs do
+          pipeline.cancel
+        end
+      end
+
+      it_behaves_like 'not sending any notification'
+    end
+  end
 end
diff --git a/spec/models/project_services/pipeline_email_service_spec.rb b/spec/models/project_services/pipeline_email_service_spec.rb
index 1368a2925e88d9218ba03117106e9c6a1d70122a..4f56bceda44c9f2887a9e91dcf63f1a0cdaa0f41 100644
--- a/spec/models/project_services/pipeline_email_service_spec.rb
+++ b/spec/models/project_services/pipeline_email_service_spec.rb
@@ -13,7 +13,7 @@ describe PipelinesEmailService do
   end
 
   before do
-    ActionMailer::Base.deliveries.clear
+    reset_delivered_emails!
   end
 
   describe 'Validations' do
@@ -23,14 +23,6 @@ describe PipelinesEmailService do
       end
 
       it { is_expected.to validate_presence_of(:recipients) }
-
-      context 'when pusher is added' do
-        before do
-          subject.add_pusher = true
-        end
-
-        it { is_expected.not_to validate_presence_of(:recipients) }
-      end
     end
 
     context 'when service is inactive' do
@@ -66,8 +58,7 @@ describe PipelinesEmailService do
     end
 
     it 'sends email' do
-      sent_to = ActionMailer::Base.deliveries.flat_map(&:to)
-      expect(sent_to).to contain_exactly(recipient)
+      should_only_email(double(notification_email: recipient), kind: :bcc)
     end
   end
 
@@ -79,7 +70,7 @@ describe PipelinesEmailService do
     end
 
     it 'does not send email' do
-      expect(ActionMailer::Base.deliveries).to be_empty
+      should_not_email_anyone
     end
   end
 
diff --git a/spec/services/ci/send_pipeline_notification_service_spec.rb b/spec/services/ci/send_pipeline_notification_service_spec.rb
deleted file mode 100644
index 288302cc94f5cc6d38ae5d02ae876c470e60e811..0000000000000000000000000000000000000000
--- a/spec/services/ci/send_pipeline_notification_service_spec.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-require 'spec_helper'
-
-describe Ci::SendPipelineNotificationService, services: true do
-  let(:pipeline) do
-    create(:ci_pipeline,
-           project: project,
-           sha: project.commit('master').sha,
-           user: user,
-           status: status)
-  end
-
-  let(:project) { create(:project) }
-  let(:user) { create(:user) }
-
-  subject{ described_class.new(pipeline) }
-
-  describe '#execute' do
-    before do
-      reset_delivered_emails!
-    end
-
-    shared_examples 'sending emails' do
-      it 'sends an email to pipeline user' do
-        perform_enqueued_jobs do
-          subject.execute([user.email])
-        end
-
-        email = ActionMailer::Base.deliveries.last
-        expect(email.subject).to include(email_subject)
-        expect(email.to).to eq([user.email])
-      end
-    end
-
-    context 'with success pipeline' do
-      let(:status) { 'success' }
-      let(:email_subject) { "Pipeline ##{pipeline.id} has succeeded" }
-
-      it_behaves_like 'sending emails'
-    end
-
-    context 'with failed pipeline' do
-      let(:status) { 'failed' }
-      let(:email_subject) { "Pipeline ##{pipeline.id} has failed" }
-
-      it_behaves_like 'sending emails'
-    end
-  end
-end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 699b9925b4e571bf8590dfe4f0b491ac731baec6..8ce35354c22fe1e34fd76fab5ab25677f4229a4b 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -17,7 +17,7 @@ describe NotificationService, services: true do
 
     it 'sends no emails when no new mentions are present' do
       send_notifications
-      expect(ActionMailer::Base.deliveries).to be_empty
+      should_not_email_anyone
     end
 
     it 'emails new mentions with a watch level higher than participant' do
@@ -27,7 +27,7 @@ describe NotificationService, services: true do
 
     it 'does not email new mentions with a watch level equal to or less than participant' do
       send_notifications(@u_participating, @u_mentioned)
-      expect(ActionMailer::Base.deliveries).to be_empty
+      should_not_email_anyone
     end
   end
 
@@ -79,7 +79,7 @@ describe NotificationService, services: true do
           # Ensure create SentNotification by noteable = issue 6 times, not noteable = note
           expect(SentNotification).to receive(:record).with(issue, any_args).exactly(8).times
 
-          ActionMailer::Base.deliveries.clear
+          reset_delivered_emails!
 
           notification.new_note(note)
 
@@ -111,7 +111,7 @@ describe NotificationService, services: true do
         context 'participating' do
           context 'by note' do
             before do
-              ActionMailer::Base.deliveries.clear
+              reset_delivered_emails!
               note.author = @u_lazy_participant
               note.save
               notification.new_note(note)
@@ -134,7 +134,7 @@ describe NotificationService, services: true do
           @u_watcher.notification_settings_for(note.project).participating!
           @u_watcher.notification_settings_for(note.project.group).global!
           update_custom_notification(:new_note, @u_custom_global)
-          ActionMailer::Base.deliveries.clear
+          reset_delivered_emails!
         end
 
         it do
@@ -173,7 +173,7 @@ describe NotificationService, services: true do
 
         expect(SentNotification).to receive(:record).with(confidential_issue, any_args).exactly(4).times
 
-        ActionMailer::Base.deliveries.clear
+        reset_delivered_emails!
 
         notification.new_note(note)
 
@@ -196,7 +196,7 @@ describe NotificationService, services: true do
       before do
         build_team(note.project)
         note.project.team << [note.author, :master]
-        ActionMailer::Base.deliveries.clear
+        reset_delivered_emails!
       end
 
       describe '#new_note' do
@@ -238,7 +238,7 @@ describe NotificationService, services: true do
       before do
         build_team(note.project)
         note.project.team << [note.author, :master]
-        ActionMailer::Base.deliveries.clear
+        reset_delivered_emails!
       end
 
       describe '#new_note' do
@@ -273,7 +273,7 @@ describe NotificationService, services: true do
 
       before do
         build_team(note.project)
-        ActionMailer::Base.deliveries.clear
+        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_custom_global)
@@ -348,7 +348,7 @@ describe NotificationService, services: true do
     before do
       build_team(issue.project)
       add_users_with_subscription(issue.project, issue)
-      ActionMailer::Base.deliveries.clear
+      reset_delivered_emails!
       update_custom_notification(:new_issue, @u_guest_custom, project)
       update_custom_notification(:new_issue, @u_custom_global)
     end
@@ -408,7 +408,7 @@ describe NotificationService, services: true do
           label.toggle_subscription(guest)
           label.toggle_subscription(admin)
 
-          ActionMailer::Base.deliveries.clear
+          reset_delivered_emails!
 
           notification.new_issue(confidential_issue, @u_disabled)
 
@@ -604,7 +604,7 @@ describe NotificationService, services: true do
           label_2.toggle_subscription(guest)
           label_2.toggle_subscription(admin)
 
-          ActionMailer::Base.deliveries.clear
+          reset_delivered_emails!
 
           notification.relabeled_issue(confidential_issue, [label_2], @u_disabled)
 
@@ -733,7 +733,7 @@ describe NotificationService, services: true do
       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_custom_global)
-      ActionMailer::Base.deliveries.clear
+      reset_delivered_emails!
     end
 
     describe '#new_merge_request' do
@@ -1111,7 +1111,7 @@ describe NotificationService, services: true do
 
     before do
       build_team(project)
-      ActionMailer::Base.deliveries.clear
+      reset_delivered_emails!
     end
 
     describe '#project_was_moved' do
diff --git a/spec/support/email_helpers.rb b/spec/support/email_helpers.rb
index 0bfc4685532f576c184eb0393b26ee94a41b494e..3e979f2f470c99a2f7cc4ff90e2d40a5f585425e 100644
--- a/spec/support/email_helpers.rb
+++ b/spec/support/email_helpers.rb
@@ -1,23 +1,33 @@
 module EmailHelpers
-  def sent_to_user?(user)
-    ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1
+  def sent_to_user?(user, recipients = email_recipients)
+    recipients.include?(user.notification_email)
   end
 
   def reset_delivered_emails!
     ActionMailer::Base.deliveries.clear
   end
 
-  def should_only_email(*users)
-    users.each {|user| should_email(user) }
-    recipients = ActionMailer::Base.deliveries.flat_map(&:to)
+  def should_only_email(*users, kind: :to)
+    recipients = email_recipients(kind: kind)
+
+    users.each { |user| should_email(user, recipients) }
+
     expect(recipients.count).to eq(users.count)
   end
 
-  def should_email(user)
-    expect(sent_to_user?(user)).to be_truthy
+  def should_email(user, recipients = email_recipients)
+    expect(sent_to_user?(user, recipients)).to be_truthy
+  end
+
+  def should_not_email(user, recipients = email_recipients)
+    expect(sent_to_user?(user, recipients)).to be_falsey
+  end
+
+  def should_not_email_anyone
+    expect(ActionMailer::Base.deliveries).to be_empty
   end
 
-  def should_not_email(user)
-    expect(sent_to_user?(user)).to be_falsey
+  def email_recipients(kind: :to)
+    ActionMailer::Base.deliveries.flat_map(&kind)
   end
 end
diff --git a/spec/support/notify_shared_examples.rb b/spec/support/notify_shared_examples.rb
index 3956d05060b1e320408574fb6126a48cd633c5d4..49867aa5cc4dbdd0512d9b1d77c10928ee6d6d7e 100644
--- a/spec/support/notify_shared_examples.rb
+++ b/spec/support/notify_shared_examples.rb
@@ -7,7 +7,7 @@ shared_context 'gitlab email notification' do
   let(:new_user_address) { 'newguy@example.com' }
 
   before do
-    ActionMailer::Base.deliveries.clear
+    reset_delivered_emails!
     email = recipient.emails.create(email: "notifications@example.com")
     recipient.update_attribute(:notification_email, email.email)
     stub_incoming_email_setting(enabled: true, address: "reply+%{key}@#{Gitlab.config.gitlab.host}")
diff --git a/spec/workers/build_email_worker_spec.rb b/spec/workers/build_email_worker_spec.rb
index 788b92c1b84e6e36a71e3416fb21797706713297..a1aa336361a3166fbb9424146b185086e897d757 100644
--- a/spec/workers/build_email_worker_spec.rb
+++ b/spec/workers/build_email_worker_spec.rb
@@ -24,7 +24,7 @@ describe BuildEmailWorker do
     end
 
     it "gracefully handles an input SMTP error" do
-      ActionMailer::Base.deliveries.clear
+      reset_delivered_emails!
       allow(Notify).to receive(:build_success_email).and_raise(Net::SMTPFatalError)
 
       subject.perform(build.id, [user.email], data.stringify_keys)
diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb
index 036d037f3f99fd8ea574c584a6deefca56335b1a..fc652f6f4c36cb4180eb6ce69e4e837bbb24fc30 100644
--- a/spec/workers/emails_on_push_worker_spec.rb
+++ b/spec/workers/emails_on_push_worker_spec.rb
@@ -87,7 +87,7 @@ describe EmailsOnPushWorker do
 
     context "when there is an SMTP error" do
       before do
-        ActionMailer::Base.deliveries.clear
+        reset_delivered_emails!
         allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError)
         allow(subject).to receive_message_chain(:logger, :info)
         perform
@@ -112,7 +112,7 @@ describe EmailsOnPushWorker do
           original.call(Mail.new(mail.encoded))
         end
 
-        ActionMailer::Base.deliveries.clear
+        reset_delivered_emails!
       end
 
       it "sends the mail to each of the recipients" do
diff --git a/spec/workers/pipeline_notification_worker_spec.rb b/spec/workers/pipeline_notification_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d487a7196800fc7298fb547fd03f6bd4a42aeb43
--- /dev/null
+++ b/spec/workers/pipeline_notification_worker_spec.rb
@@ -0,0 +1,131 @@
+require 'spec_helper'
+
+describe PipelineNotificationWorker do
+  let(:pipeline) do
+    create(:ci_pipeline,
+           project: project,
+           sha: project.commit('master').sha,
+           user: pusher,
+           status: status)
+  end
+
+  let(:project) { create(:project) }
+  let(:user) { create(:user) }
+  let(:pusher) { user }
+  let(:watcher) { pusher }
+
+  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] }
+
+            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
+    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 not send emails' do
+        should_only_email(pusher, kind: :bcc)
+      end
+    end
+  end
+end