Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKamil Trzciński <ayufan@ayufan.eu>2016-10-18 19:56:13 +0300
committerKamil Trzciński <ayufan@ayufan.eu>2016-10-18 19:56:13 +0300
commit2f7e1c0ead6b6bca102c674707ea8ee55ba55fa1 (patch)
tree3849a8cfd4e0cc8dcf9d8967f140c996e8e0fa6b
parentd25a1f305ac504662a74987b329e55e34ee8cd31 (diff)
parent88d988a2edb5c56e9cb475a1db51bf8bb399f437 (diff)
Merge branch 'pipeline-emails' into 'master'
Add a new pipeline email service ## What does this MR do? Add a new pipeline email service ## What are the relevant issue numbers? Closes #3976 ## Remaining tasks * [x] Preserve `&middot;` and `&nbsp;` * [x] Use XHTML 1.0 * [ ] Use the same layout (`app/views/layouts/notify.html.haml`) * [ ] Digest or not (assets or public) * [x] A similar email for succeeded pipeline * [x] Plain text versions for both emails ## Screenshots (if relevant) https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6019#note_16594345 ## Does this MR meet the acceptance criteria? - [x] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG) entry added - [ ] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md) - [ ] API support added - Tests - [x] `PipelinesEmailService` - [x] `SendPipelineNotificationService` See merge request !6019
-rw-r--r--CHANGELOG.md1
-rw-r--r--app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gifbin0 -> 3654 bytes
-rw-r--r--app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gifbin0 -> 3040 bytes
-rw-r--r--app/assets/images/mailers/ci_pipeline_notif_v1/icon-branch-gray.gifbin0 -> 663 bytes
-rw-r--r--app/assets/images/mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gifbin0 -> 369 bytes
-rw-r--r--app/assets/images/mailers/ci_pipeline_notif_v1/icon-commit-gray.gifbin0 -> 278 bytes
-rw-r--r--app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gifbin0 -> 1013 bytes
-rw-r--r--app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red.gifbin0 -> 660 bytes
-rw-r--r--app/controllers/admin/services_controller.rb8
-rw-r--r--app/controllers/projects/builds_controller.rb4
-rw-r--r--app/helpers/gitlab_routing_helper.rb16
-rw-r--r--app/mailers/.gitkeep0
-rw-r--r--app/mailers/emails/pipelines.rb43
-rw-r--r--app/mailers/notify.rb1
-rw-r--r--app/models/ci/build.rb23
-rw-r--r--app/models/project.rb3
-rw-r--r--app/models/project_services/builds_email_service.rb2
-rw-r--r--app/models/project_services/pipelines_email_service.rb96
-rw-r--r--app/models/service.rb5
-rw-r--r--app/services/ci/send_pipeline_notification_service.rb19
-rw-r--r--app/views/notify/pipeline_failed_email.html.haml177
-rw-r--r--app/views/notify/pipeline_failed_email.text.erb31
-rw-r--r--app/views/notify/pipeline_success_email.html.haml154
-rw-r--r--app/views/notify/pipeline_success_email.text.erb24
-rw-r--r--lib/gitlab/ci/trace_reader.rb49
-rw-r--r--lib/tasks/.gitkeep0
-rw-r--r--lib/tasks/ci/.gitkeep0
-rw-r--r--spec/lib/gitlab/ci/trace_reader_spec.rb40
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml3
-rw-r--r--spec/models/merge_request_spec.rb2
-rw-r--r--spec/models/project_services/pipeline_email_service_spec.rb182
-rw-r--r--spec/services/ci/send_pipeline_notification_service_spec.rb48
32 files changed, 909 insertions, 22 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3c75ad325f4..1bf24f5fa4d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -250,6 +250,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Changed MR widget build status to pipeline status !6335
- Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
- Enable pipeline events by default !6278
+ - Add pipeline email service !6019
- Move parsing of sidekiq ps into helper !6245 (pascalbetz)
- Added go to issue boards keyboard shortcut
- Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel)
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif
new file mode 100644
index 00000000000..3f4ef31947b
--- /dev/null
+++ b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif
Binary files differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gif
new file mode 100644
index 00000000000..387628f831c
--- /dev/null
+++ b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gif
Binary files differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-branch-gray.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-branch-gray.gif
new file mode 100644
index 00000000000..5f8f8ca143c
--- /dev/null
+++ b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-branch-gray.gif
Binary files differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif
new file mode 100644
index 00000000000..27a55b1d61f
--- /dev/null
+++ b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif
Binary files differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-commit-gray.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-commit-gray.gif
new file mode 100644
index 00000000000..8fe3281d2f6
--- /dev/null
+++ b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-commit-gray.gif
Binary files differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif
new file mode 100644
index 00000000000..4260e312929
--- /dev/null
+++ b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif
Binary files differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red.gif
new file mode 100644
index 00000000000..6de166ce0a2
--- /dev/null
+++ b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red.gif
Binary files differ
diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb
index 7c37f3155da..37a1a23178e 100644
--- a/app/controllers/admin/services_controller.rb
+++ b/app/controllers/admin/services_controller.rb
@@ -26,14 +26,10 @@ class Admin::ServicesController < Admin::ApplicationController
private
def services_templates
- templates = []
-
- Service.available_services_names.each do |service_name|
+ Service.available_services_names.map do |service_name|
service_template = service_name.concat("_service").camelize.constantize
- templates << service_template.where(template: true).first_or_create
+ service_template.where(template: true).first_or_create
end
-
- templates
end
def service
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 3b2e35a7a05..fbe391fc58c 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -47,7 +47,9 @@ class Projects::BuildsController < Projects::ApplicationController
def trace
respond_to do |format|
format.json do
- render json: @build.trace_with_state(params[:state].presence).merge!(id: @build.id, status: @build.status)
+ state = params[:state].presence
+ render json: @build.trace_with_state(state: state).
+ merge!(id: @build.id, status: @build.status)
end
end
end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 670a7ca36f4..bccf64d1aac 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -94,6 +94,22 @@ module GitlabRoutingHelper
namespace_project_merge_request_url(entity.project.namespace, entity.project, entity, *args)
end
+ def pipeline_url(pipeline, *args)
+ namespace_project_pipeline_url(pipeline.project.namespace, pipeline.project, pipeline.id, *args)
+ end
+
+ def pipeline_build_url(pipeline, build, *args)
+ namespace_project_build_url(pipeline.project.namespace, pipeline.project, build.id, *args)
+ end
+
+ def commits_url(entity, *args)
+ namespace_project_commits_url(entity.project.namespace, entity.project, entity.ref, *args)
+ end
+
+ def commit_url(entity, *args)
+ namespace_project_commit_url(entity.project.namespace, entity.project, entity.sha, *args)
+ end
+
def project_snippet_url(entity, *args)
namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args)
end
diff --git a/app/mailers/.gitkeep b/app/mailers/.gitkeep
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/app/mailers/.gitkeep
+++ /dev/null
diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb
new file mode 100644
index 00000000000..601c8b5cd62
--- /dev/null
+++ b/app/mailers/emails/pipelines.rb
@@ -0,0 +1,43 @@
+module Emails
+ module Pipelines
+ def pipeline_success_email(pipeline, to)
+ pipeline_mail(pipeline, to, 'succeeded')
+ end
+
+ def pipeline_failed_email(pipeline, to)
+ pipeline_mail(pipeline, to, 'failed')
+ end
+
+ private
+
+ def pipeline_mail(pipeline, to, 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|
+ format.html { render layout: false }
+ format.text
+ end
+ end
+
+ def add_headers
+ add_project_headers
+ add_pipeline_headers
+ end
+
+ def add_pipeline_headers
+ headers['X-GitLab-Pipeline-Id'] = @pipeline.id
+ headers['X-GitLab-Pipeline-Ref'] = @pipeline.ref
+ headers['X-GitLab-Pipeline-Status'] = @pipeline.status
+ end
+
+ def pipeline_subject(status)
+ commit = @pipeline.short_sha
+ commit << " in #{@merge_request.to_reference}" if @merge_request
+
+ subject("Pipeline ##{@pipeline.id} has #{status} for #{@pipeline.ref}", commit)
+ end
+ end
+end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 2444702104e..eca6ec29767 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -7,6 +7,7 @@ class Notify < BaseMailer
include Emails::Projects
include Emails::Profile
include Emails::Builds
+ include Emails::Pipelines
include Emails::Members
add_template_helper MergeRequestsHelper
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 87475119b23..a6b606d13de 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -133,13 +133,17 @@ module Ci
latest_builds.where('stage_idx < ?', stage_idx)
end
- def trace_html
- trace_with_state[:html] || ''
+ def trace_html(**args)
+ trace_with_state(**args)[:html] || ''
end
- def trace_with_state(state = nil)
- trace_with_state = Ci::Ansi2html::convert(trace, state) if trace.present?
- trace_with_state || {}
+ def trace_with_state(state: nil, last_lines: nil)
+ trace_ansi = trace(last_lines: last_lines)
+ if trace_ansi.present?
+ Ci::Ansi2html.convert(trace_ansi, state)
+ else
+ {}
+ end
end
def timeout
@@ -222,9 +226,10 @@ module Ci
raw_trace.present?
end
- def raw_trace
+ def raw_trace(last_lines: nil)
if File.exist?(trace_file_path)
- File.read(trace_file_path)
+ Gitlab::Ci::TraceReader.new(trace_file_path).
+ read(last_lines: last_lines)
else
# backward compatibility
read_attribute :trace
@@ -239,8 +244,8 @@ module Ci
project.ci_id && File.exist?(old_path_to_trace)
end
- def trace
- hide_secrets(raw_trace)
+ def trace(last_lines: nil)
+ hide_secrets(raw_trace(last_lines: last_lines))
end
def trace_length
diff --git a/app/models/project.rb b/app/models/project.rb
index db3088677d8..aee74c3dba1 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -76,6 +76,7 @@ class Project < ActiveRecord::Base
has_one :drone_ci_service, dependent: :destroy
has_one :emails_on_push_service, dependent: :destroy
has_one :builds_email_service, dependent: :destroy
+ has_one :pipelines_email_service, dependent: :destroy
has_one :irker_service, dependent: :destroy
has_one :pivotaltracker_service, dependent: :destroy
has_one :hipchat_service, dependent: :destroy
@@ -718,7 +719,7 @@ class Project < ActiveRecord::Base
if template.nil?
# If no template, we should create an instance. Ex `create_gitlab_ci_service`
- self.send :"create_#{service_name}_service"
+ public_send("create_#{service_name}_service")
else
Service.create_from_template(self.id, template)
end
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index fa66e5864b8..201b94b065b 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -43,7 +43,7 @@ class BuildsEmailService < Service
end
def can_test?
- project.builds.count > 0
+ project.builds.any?
end
def disabled_title
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
new file mode 100644
index 00000000000..ec3c1bc85ee
--- /dev/null
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -0,0 +1,96 @@
+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? }
+
+ def initialize_properties
+ self.properties ||= { notify_only_broken_pipelines: true }
+ end
+
+ def title
+ 'Pipelines emails'
+ end
+
+ def description
+ 'Email the pipelines status to a list of recipients.'
+ end
+
+ def to_param
+ 'pipelines_email'
+ end
+
+ def supported_events
+ %w[pipeline]
+ end
+
+ def execute(data, force: false)
+ return unless supported_events.include?(data[:object_kind])
+ return unless force || should_pipeline_be_notified?(data)
+
+ all_recipients = retrieve_recipients(data)
+
+ return unless all_recipients.any?
+
+ pipeline = Ci::Pipeline.find(data[:object_attributes][:id])
+ Ci::SendPipelineNotificationService.new(pipeline).execute(all_recipients)
+ end
+
+ def can_test?
+ project.pipelines.any?
+ end
+
+ def disabled_title
+ 'Please setup a pipeline on your repository.'
+ end
+
+ def test_data(project, user)
+ data = Gitlab::DataBuilder::Pipeline.build(project.pipelines.last)
+ data[:user] = user.hook_attrs
+ data
+ end
+
+ def fields
+ [
+ { 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' },
+ ]
+ end
+
+ def test(data)
+ result = execute(data, force: true)
+
+ { success: true, result: result }
+ rescue StandardError => error
+ { success: false, result: error }
+ end
+
+ def should_pipeline_be_notified?(data)
+ case data[:object_attributes][:status]
+ when 'success'
+ !notify_only_broken_pipelines?
+ when 'failed'
+ true
+ else
+ false
+ end
+ 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
+ end
+end
diff --git a/app/models/service.rb b/app/models/service.rb
index 66c804f2b06..625fbc48302 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -196,12 +196,13 @@ class Service < ActiveRecord::Base
end
def self.available_services_names
- %w(
+ %w[
asana
assembla
bamboo
buildkite
builds_email
+ pipelines_email
bugzilla
campfire
custom_issue_tracker
@@ -218,7 +219,7 @@ class Service < ActiveRecord::Base
redmine
slack
teamcity
- )
+ ]
end
def self.create_from_template(project_id, template)
diff --git a/app/services/ci/send_pipeline_notification_service.rb b/app/services/ci/send_pipeline_notification_service.rb
new file mode 100644
index 00000000000..ceb182801f7
--- /dev/null
+++ b/app/services/ci/send_pipeline_notification_service.rb
@@ -0,0 +1,19 @@
+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/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml
new file mode 100644
index 00000000000..ec02c2e2d90
--- /dev/null
+++ b/app/views/notify/pipeline_failed_email.html.haml
@@ -0,0 +1,177 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+%html{lang: "en"}
+ %head
+ %meta{content: "text/html; charset=UTF-8", "http-equiv": "Content-Type"}/
+ %meta{content: "width=device-width, initial-scale=1", name: "viewport"}/
+ %meta{content: "IE=edge", "http-equiv": "X-UA-Compatible"}/
+ %title= message.subject
+ :css
+ /* CLIENT-SPECIFIC STYLES */
+ body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
+ table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
+ img { -ms-interpolation-mode: bicubic; }
+
+ /* iOS BLUE LINKS */
+ a[x-apple-data-detectors] {
+ color: inherit !important;
+ text-decoration: none !important;
+ font-size: inherit !important;
+ font-family: inherit !important;
+ font-weight: inherit !important;
+ line-height: inherit !important;
+ }
+
+ /* ANDROID MARGIN HACK */
+ body { margin:0 !important; }
+ div[style*="margin: 16px 0"] { margin:0 !important; }
+
+ @media only screen and (max-width: 639px) {
+ body, #body {
+ min-width: 320px !important;
+ }
+ table.wrapper {
+ width: 100% !important;
+ min-width: 320px !important;
+ }
+ table.wrapper > tbody > tr > td {
+ border-left: 0 !important;
+ border-right: 0 !important;
+ border-radius: 0 !important;
+ padding-left: 10px !important;
+ padding-right: 10px !important;
+ }
+ }
+ %body{style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"}
+ %table#body{border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;"}
+ %tbody
+ %tr.line
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;"}  
+ %tr.header
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
+ %img{alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55"}/
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"}
+ %table.wrapper{border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"}
+ %table.content{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;"}
+ %tbody
+ %tr.alert
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;"}
+ %img{alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;"}
+ Your pipeline has failed.
+ %tr.spacer
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"}
+ &nbsp;
+ %tr.section
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"}
+ %table.info{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;"} Project
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;"}
+ - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
+ - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
+ %a.muted{href: namespace_url, style: "color:#333333;text-decoration:none;"}
+ = namespace_name
+ \/
+ %a.muted{href: project_url(@project), style: "color:#333333;text-decoration:none;"}
+ = @project.name
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Branch
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
+ %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
+ %a.muted{href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;"}
+ = @pipeline.ref
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Commit
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
+ %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
+ %a{href: commit_url(@pipeline), style: "color:#3084bb;text-decoration:none;"}
+ = @pipeline.short_sha
+ - if @merge_request
+ in
+ %a{href: merge_request_url(@merge_request), style: "color:#3084bb;text-decoration:none;"}
+ = @merge_request.to_reference
+ .commit{style: "color:#5c5c5c;font-weight:300;"}
+ = @pipeline.git_commit_message.truncate(50)
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Author
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %tbody
+ %tr
+ - commit = @pipeline.commit
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
+ %img.avatar{height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
+ - if commit.author
+ %a.muted{href: user_url(commit.author), style: "color:#333333;text-decoration:none;"}
+ = commit.author.name
+ - else
+ %span
+ = commit.author_name
+ %tr.spacer
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"}
+ &nbsp;
+ - failed = @pipeline.statuses.latest.failed
+ %tr.pre-section
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;"}
+ Pipeline
+ %a{href: pipeline_url(@pipeline), style: "color:#3084bb;text-decoration:none;"}
+ = "\##{@pipeline.id}"
+ had
+ = failed.size
+ failed
+ = "#{'build'.pluralize(failed.size)}."
+ %tr.warning
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;"}
+ Logs may contain sensitive data. Please consider before forwarding this email.
+ %tr.section
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;"}
+ %table.builds{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;"}
+ %tbody
+ - failed.each do |build|
+ %tr.build-state
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;"}
+ %img{alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;"}
+ = build.stage
+ %td{align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;"}
+ %a{href: pipeline_build_url(@pipeline, build), style: "color:#3084bb;text-decoration:none;"}
+ = build.name
+ %tr.build-log
+ %td{colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;"}
+ %pre{style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;"}
+ = build.trace_html(last_lines: 10).html_safe
+ %tr.footer
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
+ %img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/
+ %div
+ %a{href: profile_notifications_url, style: "color:#3084bb;text-decoration:none;"} Manage all notifications
+ &middot;
+ %a{href: help_url, style: "color:#3084bb;text-decoration:none;"} Help
+ %div
+ You're receiving this email because of your account on
+ = succeed "." do
+ %a{href: root_url, style: "color:#3084bb;text-decoration:none;"}= Gitlab.config.gitlab.host
diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb
new file mode 100644
index 00000000000..8f8084b58e1
--- /dev/null
+++ b/app/views/notify/pipeline_failed_email.text.erb
@@ -0,0 +1,31 @@
+Your pipeline has failed.
+
+Project: <%= @project.name %> ( <%= project_url(@project) %> )
+Branch: <%= @pipeline.ref %> ( <%= commits_url(@pipeline) %> )
+<% if @merge_request -%>
+Merge Request: <%= @merge_request.to_reference %> ( <%= merge_request_url(@merge_request) %> )
+<% end -%>
+
+Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> )
+Commit Message: <%= @pipeline.git_commit_message.truncate(50) %>
+<% commit = @pipeline.commit -%>
+<% if commit.author -%>
+Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> )
+<% else -%>
+Commit Author: <%= commit.author_name %>
+<% end -%>
+
+<% failed = @pipeline.statuses.latest.failed -%>
+Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
+
+<% failed.each do |build| -%>
+Build #<%= build.id %> ( <%= pipeline_build_url(@pipeline, build) %> )
+Stage: <%= build.stage %>
+Name: <%= build.name %>
+Trace: <%= build.trace_with_state(last_lines: 10)[:text] %>
+
+<% end -%>
+
+You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
+Manage all notifications: <%= profile_notifications_url %>
+Help: <%= help_url %>
diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml
new file mode 100644
index 00000000000..0fdf118c9bc
--- /dev/null
+++ b/app/views/notify/pipeline_success_email.html.haml
@@ -0,0 +1,154 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+%html{lang: "en"}
+ %head
+ %meta{content: "text/html; charset=UTF-8", "http-equiv": "Content-Type"}/
+ %meta{content: "width=device-width, initial-scale=1", name: "viewport"}/
+ %meta{content: "IE=edge", "http-equiv": "X-UA-Compatible"}/
+ %title= message.subject
+ :css
+ /* CLIENT-SPECIFIC STYLES */
+ body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
+ table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
+ img { -ms-interpolation-mode: bicubic; }
+
+ /* iOS BLUE LINKS */
+ a[x-apple-data-detectors] {
+ color: inherit !important;
+ text-decoration: none !important;
+ font-size: inherit !important;
+ font-family: inherit !important;
+ font-weight: inherit !important;
+ line-height: inherit !important;
+ }
+
+ /* ANDROID MARGIN HACK */
+ body { margin:0 !important; }
+ div[style*="margin: 16px 0"] { margin:0 !important; }
+
+ @media only screen and (max-width: 639px) {
+ body, #body {
+ min-width: 320px !important;
+ }
+ table.wrapper {
+ width: 100% !important;
+ min-width: 320px !important;
+ }
+ table.wrapper > tbody > tr > td {
+ border-left: 0 !important;
+ border-right: 0 !important;
+ border-radius: 0 !important;
+ padding-left: 10px !important;
+ padding-right: 10px !important;
+ }
+ }
+ %body{style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"}
+ %table#body{border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;"}
+ %tbody
+ %tr.line
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;"}  
+ %tr.header
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
+ %img{alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55"}/
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"}
+ %table.wrapper{border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"}
+ %table.content{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;"}
+ %tbody
+ %tr.success
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;"}
+ %img{alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;"}
+ Your pipeline has passed.
+ %tr.spacer
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"}
+ &nbsp;
+ %tr.section
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"}
+ %table.info{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;"} Project
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;"}
+ - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
+ - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
+ %a.muted{href: namespace_url, style: "color:#333333;text-decoration:none;"}
+ = namespace_name
+ \/
+ %a.muted{href: project_url(@project), style: "color:#333333;text-decoration:none;"}
+ = @project.name
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Branch
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
+ %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
+ %a.muted{href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;"}
+ = @pipeline.ref
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Commit
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
+ %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
+ %a{href: commit_url(@pipeline), style: "color:#3084bb;text-decoration:none;"}
+ = @pipeline.short_sha
+ - if @merge_request
+ in
+ %a{href: merge_request_url(@merge_request), style: "color:#3084bb;text-decoration:none;"}
+ = @merge_request.to_reference
+ .commit{style: "color:#5c5c5c;font-weight:300;"}
+ = @pipeline.git_commit_message.truncate(50)
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Author
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %tbody
+ %tr
+ - commit = @pipeline.commit
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
+ %img.avatar{height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
+ - if commit.author
+ %a.muted{href: user_url(commit.author), style: "color:#333333;text-decoration:none;"}
+ = commit.author.name
+ - else
+ %span
+ = commit.author_name
+ %tr.spacer
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"}
+ &nbsp;
+ %tr.success-message
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;"}
+ - build_count = @pipeline.statuses.latest.size
+ - stage_count = @pipeline.stages.size
+ Pipeline
+ %a{href: pipeline_url(@pipeline), style: "color:#3084bb;text-decoration:none;"}
+ = "\##{@pipeline.id}"
+ successfully completed
+ = "#{build_count} #{'build'.pluralize(build_count)}"
+ in
+ = "#{stage_count} #{'stage'.pluralize(stage_count)}."
+ %tr.footer
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
+ %img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/
+ %div
+ %a{href: profile_notifications_url, style: "color:#3084bb;text-decoration:none;"} Manage all notifications
+ &middot;
+ %a{href: help_url, style: "color:#3084bb;text-decoration:none;"} Help
+ %div
+ You're receiving this email because of your account on
+ = succeed "." do
+ %a{href: root_url, style: "color:#3084bb;text-decoration:none;"}= Gitlab.config.gitlab.host
diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb
new file mode 100644
index 00000000000..ae22d474f2c
--- /dev/null
+++ b/app/views/notify/pipeline_success_email.text.erb
@@ -0,0 +1,24 @@
+Your pipeline has passed.
+
+Project: <%= @project.name %> ( <%= project_url(@project) %> )
+Branch: <%= @pipeline.ref %> ( <%= commits_url(@pipeline) %> )
+<% if @merge_request -%>
+Merge Request: <%= @merge_request.to_reference %> ( <%= merge_request_url(@merge_request) %> )
+<% end -%>
+
+Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> )
+Commit Message: <%= @pipeline.git_commit_message.truncate(50) %>
+<% commit = @pipeline.commit -%>
+<% if commit.author -%>
+Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> )
+<% else -%>
+Commit Author: <%= commit.author_name %>
+<% end -%>
+
+<% build_count = @pipeline.statuses.latest.size -%>
+<% stage_count = @pipeline.stages.size -%>
+Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>.
+
+You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
+Manage all notifications: <%= profile_notifications_url %>
+Help: <%= help_url %>
diff --git a/lib/gitlab/ci/trace_reader.rb b/lib/gitlab/ci/trace_reader.rb
new file mode 100644
index 00000000000..37e51536e8f
--- /dev/null
+++ b/lib/gitlab/ci/trace_reader.rb
@@ -0,0 +1,49 @@
+module Gitlab
+ module Ci
+ # This was inspired from: http://stackoverflow.com/a/10219411/1520132
+ class TraceReader
+ BUFFER_SIZE = 4096
+
+ attr_accessor :path, :buffer_size
+
+ def initialize(new_path, buffer_size: BUFFER_SIZE)
+ self.path = new_path
+ self.buffer_size = Integer(buffer_size)
+ end
+
+ def read(last_lines: nil)
+ if last_lines
+ read_last_lines(last_lines)
+ else
+ File.read(path)
+ end
+ end
+
+ def read_last_lines(max_lines)
+ File.open(path) do |file|
+ chunks = []
+ pos = lines = 0
+ max = file.size
+
+ # We want an extra line to make sure fist line has full contents
+ while lines <= max_lines && pos < max
+ pos += buffer_size
+
+ buf = if pos <= max
+ file.seek(-pos, IO::SEEK_END)
+ file.read(buffer_size)
+ else # Reached the head, read only left
+ file.seek(0)
+ file.read(buffer_size - (pos - max))
+ end
+
+ lines += buf.count("\n")
+ chunks.unshift(buf)
+ end
+
+ chunks.join.lines.last(max_lines).join
+ end
+ end
+ end
+ end
+end
diff --git a/lib/tasks/.gitkeep b/lib/tasks/.gitkeep
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/lib/tasks/.gitkeep
+++ /dev/null
diff --git a/lib/tasks/ci/.gitkeep b/lib/tasks/ci/.gitkeep
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/lib/tasks/ci/.gitkeep
+++ /dev/null
diff --git a/spec/lib/gitlab/ci/trace_reader_spec.rb b/spec/lib/gitlab/ci/trace_reader_spec.rb
new file mode 100644
index 00000000000..f06d78694d6
--- /dev/null
+++ b/spec/lib/gitlab/ci/trace_reader_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::TraceReader do
+ let(:path) { __FILE__ }
+ let(:lines) { File.readlines(path) }
+ let(:bytesize) { lines.sum(&:bytesize) }
+
+ it 'returns last few lines' do
+ 10.times do
+ subject = build_subject
+ last_lines = random_lines
+
+ expected = lines.last(last_lines).join
+
+ expect(subject.read(last_lines: last_lines)).to eq(expected)
+ end
+ end
+
+ it 'returns everything if trying to get too many lines' do
+ expect(build_subject.read(last_lines: lines.size * 2)).to eq(lines.join)
+ end
+
+ it 'raises an error if not passing an integer for last_lines' do
+ expect do
+ build_subject.read(last_lines: lines)
+ end.to raise_error(ArgumentError)
+ end
+
+ def random_lines
+ Random.rand(lines.size) + 1
+ end
+
+ def random_buffer
+ Random.rand(bytesize) + 1
+ end
+
+ def build_subject
+ described_class.new(__FILE__, buffer_size: random_buffer)
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 5d5836e9bee..8fcbf12eab8 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -125,6 +125,7 @@ project:
- drone_ci_service
- emails_on_push_service
- builds_email_service
+- pipelines_email_service
- irker_service
- pivotaltracker_service
- hipchat_service
@@ -184,4 +185,4 @@ project:
- project_feature
award_emoji:
- awardable
-- user \ No newline at end of file
+- user
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 91a423b670c..1acc8d748af 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -334,7 +334,7 @@ describe MergeRequest, models: true do
wip_title = "WIP: #{subject.title}"
expect(subject.wip_title).to eq wip_title
- end
+ end
it "does not add the WIP: prefix multiple times" do
wip_title = "WIP: #{subject.title}"
diff --git a/spec/models/project_services/pipeline_email_service_spec.rb b/spec/models/project_services/pipeline_email_service_spec.rb
new file mode 100644
index 00000000000..1368a2925e8
--- /dev/null
+++ b/spec/models/project_services/pipeline_email_service_spec.rb
@@ -0,0 +1,182 @@
+require 'spec_helper'
+
+describe PipelinesEmailService do
+ let(:pipeline) do
+ create(:ci_pipeline, project: project, sha: project.commit('master').sha)
+ end
+
+ let(:project) { create(:project) }
+ let(:recipient) { 'test@gitlab.com' }
+
+ let(:data) do
+ Gitlab::DataBuilder::Pipeline.build(pipeline)
+ end
+
+ before do
+ ActionMailer::Base.deliveries.clear
+ end
+
+ describe 'Validations' do
+ context 'when service is active' do
+ before do
+ subject.active = true
+ 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
+ before do
+ subject.active = false
+ end
+
+ it { is_expected.not_to validate_presence_of(:recipients) }
+ end
+ end
+
+ describe '#test_data' do
+ let(:build) { create(:ci_build) }
+ let(:project) { build.project }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'builds test data' do
+ data = subject.test_data(project, user)
+
+ expect(data[:object_kind]).to eq('pipeline')
+ end
+ end
+
+ shared_examples 'sending email' do
+ before do
+ perform_enqueued_jobs do
+ run
+ end
+ end
+
+ it 'sends email' do
+ sent_to = ActionMailer::Base.deliveries.flat_map(&:to)
+ expect(sent_to).to contain_exactly(recipient)
+ end
+ end
+
+ shared_examples 'not sending email' do
+ before do
+ perform_enqueued_jobs do
+ run
+ end
+ end
+
+ it 'does not send email' do
+ expect(ActionMailer::Base.deliveries).to be_empty
+ end
+ end
+
+ describe '#test' do
+ def run
+ subject.test(data)
+ end
+
+ before do
+ subject.recipients = recipient
+ end
+
+ context 'when pipeline is failed' do
+ before do
+ data[:object_attributes][:status] = 'failed'
+ pipeline.update(status: 'failed')
+ end
+
+ it_behaves_like 'sending email'
+ end
+
+ context 'when pipeline is succeeded' do
+ before do
+ data[:object_attributes][:status] = 'success'
+ pipeline.update(status: 'success')
+ end
+
+ it_behaves_like 'sending email'
+ end
+ end
+
+ describe '#execute' do
+ def run
+ subject.execute(data)
+ end
+
+ context 'with recipients' do
+ before do
+ subject.recipients = recipient
+ end
+
+ context 'with failed pipeline' do
+ before do
+ data[:object_attributes][:status] = 'failed'
+ pipeline.update(status: 'failed')
+ end
+
+ it_behaves_like 'sending email'
+ end
+
+ context 'with succeeded pipeline' do
+ before do
+ data[:object_attributes][:status] = 'success'
+ pipeline.update(status: 'success')
+ end
+
+ it_behaves_like 'not sending email'
+ end
+
+ context 'with notify_only_broken_pipelines on' do
+ before do
+ subject.notify_only_broken_pipelines = true
+ end
+
+ context 'with failed pipeline' do
+ before do
+ data[:object_attributes][:status] = 'failed'
+ pipeline.update(status: 'failed')
+ end
+
+ it_behaves_like 'sending email'
+ end
+
+ context 'with succeeded pipeline' do
+ before do
+ data[:object_attributes][:status] = 'success'
+ pipeline.update(status: 'success')
+ end
+
+ it_behaves_like 'not sending email'
+ end
+ end
+ end
+
+ context 'with empty recipients list' do
+ before do
+ subject.recipients = ' ,, '
+ end
+
+ context 'with failed pipeline' do
+ before do
+ data[:object_attributes][:status] = 'failed'
+ pipeline.update(status: 'failed')
+ end
+
+ it_behaves_like 'not sending email'
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/send_pipeline_notification_service_spec.rb b/spec/services/ci/send_pipeline_notification_service_spec.rb
new file mode 100644
index 00000000000..288302cc94f
--- /dev/null
+++ b/spec/services/ci/send_pipeline_notification_service_spec.rb
@@ -0,0 +1,48 @@
+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