diff options
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 Binary files differnew file mode 100644 index 00000000000..3f4ef31947b --- /dev/null +++ b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif 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 Binary files differnew file mode 100644 index 00000000000..387628f831c --- /dev/null +++ b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gif 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 Binary files differnew file mode 100644 index 00000000000..5f8f8ca143c --- /dev/null +++ b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-branch-gray.gif 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 Binary files differnew file mode 100644 index 00000000000..27a55b1d61f --- /dev/null +++ b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif 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 Binary files differnew file mode 100644 index 00000000000..8fe3281d2f6 --- /dev/null +++ b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-commit-gray.gif 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 Binary files differnew file mode 100644 index 00000000000..4260e312929 --- /dev/null +++ b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif 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 Binary files differnew file mode 100644 index 00000000000..6de166ce0a2 --- /dev/null +++ b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red.gif 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;"} + + %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;"} + + - 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 + · + %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;"} + + %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;"} + + %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 + · + %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 |