Commit 2f7e1c0e authored by Kamil Trzciński's avatar Kamil Trzciński

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 `·` and ` `
* [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
parents d25a1f30 88d988a2
...@@ -250,6 +250,7 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -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 - Changed MR widget build status to pipeline status !6335
- Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel) - Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
- Enable pipeline events by default !6278 - Enable pipeline events by default !6278
- Add pipeline email service !6019
- Move parsing of sidekiq ps into helper !6245 (pascalbetz) - Move parsing of sidekiq ps into helper !6245 (pascalbetz)
- Added go to issue boards keyboard shortcut - Added go to issue boards keyboard shortcut
- Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel) - Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel)
......
...@@ -26,14 +26,10 @@ class Admin::ServicesController < Admin::ApplicationController ...@@ -26,14 +26,10 @@ class Admin::ServicesController < Admin::ApplicationController
private private
def services_templates def services_templates
templates = [] Service.available_services_names.map do |service_name|
Service.available_services_names.each do |service_name|
service_template = service_name.concat("_service").camelize.constantize 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 end
templates
end end
def service def service
......
...@@ -47,7 +47,9 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -47,7 +47,9 @@ class Projects::BuildsController < Projects::ApplicationController
def trace def trace
respond_to do |format| respond_to do |format|
format.json do 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 end
end end
......
...@@ -94,6 +94,22 @@ module GitlabRoutingHelper ...@@ -94,6 +94,22 @@ module GitlabRoutingHelper
namespace_project_merge_request_url(entity.project.namespace, entity.project, entity, *args) namespace_project_merge_request_url(entity.project.namespace, entity.project, entity, *args)
end 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) def project_snippet_url(entity, *args)
namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args) namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args)
end end
......
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
...@@ -7,6 +7,7 @@ class Notify < BaseMailer ...@@ -7,6 +7,7 @@ class Notify < BaseMailer
include Emails::Projects include Emails::Projects
include Emails::Profile include Emails::Profile
include Emails::Builds include Emails::Builds
include Emails::Pipelines
include Emails::Members include Emails::Members
add_template_helper MergeRequestsHelper add_template_helper MergeRequestsHelper
......
...@@ -133,13 +133,17 @@ module Ci ...@@ -133,13 +133,17 @@ module Ci
latest_builds.where('stage_idx < ?', stage_idx) latest_builds.where('stage_idx < ?', stage_idx)
end end
def trace_html def trace_html(**args)
trace_with_state[:html] || '' trace_with_state(**args)[:html] || ''
end end
def trace_with_state(state = nil) def trace_with_state(state: nil, last_lines: nil)
trace_with_state = Ci::Ansi2html::convert(trace, state) if trace.present? trace_ansi = trace(last_lines: last_lines)
trace_with_state || {} if trace_ansi.present?
Ci::Ansi2html.convert(trace_ansi, state)
else
{}
end
end end
def timeout def timeout
...@@ -222,9 +226,10 @@ module Ci ...@@ -222,9 +226,10 @@ module Ci
raw_trace.present? raw_trace.present?
end end
def raw_trace def raw_trace(last_lines: nil)
if File.exist?(trace_file_path) if File.exist?(trace_file_path)
File.read(trace_file_path) Gitlab::Ci::TraceReader.new(trace_file_path).
read(last_lines: last_lines)
else else
# backward compatibility # backward compatibility
read_attribute :trace read_attribute :trace
...@@ -239,8 +244,8 @@ module Ci ...@@ -239,8 +244,8 @@ module Ci
project.ci_id && File.exist?(old_path_to_trace) project.ci_id && File.exist?(old_path_to_trace)
end end
def trace def trace(last_lines: nil)
hide_secrets(raw_trace) hide_secrets(raw_trace(last_lines: last_lines))
end end
def trace_length def trace_length
......
...@@ -76,6 +76,7 @@ class Project < ActiveRecord::Base ...@@ -76,6 +76,7 @@ class Project < ActiveRecord::Base
has_one :drone_ci_service, dependent: :destroy has_one :drone_ci_service, dependent: :destroy
has_one :emails_on_push_service, dependent: :destroy has_one :emails_on_push_service, dependent: :destroy
has_one :builds_email_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 :irker_service, dependent: :destroy
has_one :pivotaltracker_service, dependent: :destroy has_one :pivotaltracker_service, dependent: :destroy
has_one :hipchat_service, dependent: :destroy has_one :hipchat_service, dependent: :destroy
...@@ -718,7 +719,7 @@ class Project < ActiveRecord::Base ...@@ -718,7 +719,7 @@ class Project < ActiveRecord::Base
if template.nil? if template.nil?
# If no template, we should create an instance. Ex `create_gitlab_ci_service` # 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 else
Service.create_from_template(self.id, template) Service.create_from_template(self.id, template)
end end
......
...@@ -43,7 +43,7 @@ class BuildsEmailService < Service ...@@ -43,7 +43,7 @@ class BuildsEmailService < Service
end end
def can_test? def can_test?
project.builds.count > 0 project.builds.any?
end end
def disabled_title def disabled_title
......
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
...@@ -196,12 +196,13 @@ class Service < ActiveRecord::Base ...@@ -196,12 +196,13 @@ class Service < ActiveRecord::Base
end end
def self.available_services_names def self.available_services_names
%w( %w[
asana asana
assembla assembla
bamboo bamboo
buildkite buildkite
builds_email builds_email
pipelines_email
bugzilla bugzilla
campfire campfire
custom_issue_tracker custom_issue_tracker
...@@ -218,7 +219,7 @@ class Service < ActiveRecord::Base ...@@ -218,7 +219,7 @@ class Service < ActiveRecord::Base
redmine redmine
slack slack
teamcity teamcity
) ]
end end
def self.create_from_template(project_id, template) def self.create_from_template(project_id, template)
......
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
<!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
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 %>
<!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
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 %>
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
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
...@@ -125,6 +125,7 @@ project: ...@@ -125,6 +125,7 @@ project:
- drone_ci_service - drone_ci_service
- emails_on_push_service - emails_on_push_service
- builds_email_service - builds_email_service
- pipelines_email_service
- irker_service - irker_service
- pivotaltracker_service - pivotaltracker_service
- hipchat_service - hipchat_service
......
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
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
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