Commit c712340f authored by Robert Speicher's avatar Robert Speicher

Merge branch '32495-improve-slack-notification-on-pipeline-status' into 'master'

Make pipeline failure Slack notifications prettier and more informative

Closes #32495

See merge request gitlab-org/gitlab-ce!27683
parents 88c8f0ba 7b5fb754
# frozen_string_literal: true
require 'slack-notifier'
module ChatMessage
class PipelineMessage < BaseMessage
MAX_VISIBLE_JOBS = 10
attr_reader :user
attr_reader :ref_type
attr_reader :ref
attr_reader :status
attr_reader :detailed_status
attr_reader :duration
attr_reader :finished_at
attr_reader :pipeline_id
attr_reader :failed_stages
attr_reader :failed_jobs
attr_reader :project
attr_reader :commit
attr_reader :committer
attr_reader :pipeline
def initialize(data)
super
@user = data[:user]
@user_name = data.dig(:user, :username) || 'API'
pipeline_attributes = data[:object_attributes]
@ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
@ref = pipeline_attributes[:ref]
@status = pipeline_attributes[:status]
@detailed_status = pipeline_attributes[:detailed_status]
@duration = pipeline_attributes[:duration].to_i
@finished_at = pipeline_attributes[:finished_at] ? Time.parse(pipeline_attributes[:finished_at]).to_i : nil
@pipeline_id = pipeline_attributes[:id]
@failed_jobs = Array(data[:builds]).select { |b| b[:status] == 'failed' }.reverse # Show failed jobs from oldest to newest
@failed_stages = @failed_jobs.map { |j| j[:stage] }.uniq
@project = Project.find(data[:project][:id])
@commit = project.commit_by(oid: data[:commit][:id])
@committer = commit.committer
@pipeline = Ci::Pipeline.find(pipeline_id)
end
def pretext
......@@ -28,40 +51,147 @@ module ChatMessage
def attachments
return message if markdown
[{ text: format(message), color: attachment_color }]
return [{ text: format(message), color: attachment_color }] unless fancy_notifications?
[{
fallback: format(message),
color: attachment_color,
author_name: user_combined_name,
author_icon: user_avatar,
author_link: author_url,
title: s_("ChatMessage|Pipeline #%{pipeline_id} %{humanized_status} in %{duration}") %
{
pipeline_id: pipeline_id,
humanized_status: humanized_status,
duration: pretty_duration(duration)
},
title_link: pipeline_url,
fields: attachments_fields,
footer: project.name,
footer_icon: project.avatar_url,
ts: finished_at
}]
end
def activity
{
title: "Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_combined_name} #{humanized_status}",
subtitle: "in #{project_link}",
text: "in #{pretty_duration(duration)}",
title: s_("ChatMessage|Pipeline %{pipeline_link} of %{ref_type} %{branch_link} by %{user_combined_name} %{humanized_status}") %
{
pipeline_link: pipeline_link,
ref_type: ref_type,
branch_link: branch_link,
user_combined_name: user_combined_name,
humanized_status: humanized_status
},
subtitle: s_("ChatMessage|in %{project_link}") % { project_link: project_link },
text: s_("ChatMessage|in %{duration}") % { duration: pretty_duration(duration) },
image: user_avatar || ''
}
end
private
def fancy_notifications?
Feature.enabled?(:fancy_pipeline_slack_notifications, default_enabled: true)
end
def failed_stages_field
{
title: s_("ChatMessage|Failed stage").pluralize(failed_stages.length),
value: Slack::Notifier::LinkFormatter.format(failed_stages_links),
short: true
}
end
def failed_jobs_field
{
title: s_("ChatMessage|Failed job").pluralize(failed_jobs.length),
value: Slack::Notifier::LinkFormatter.format(failed_jobs_links),
short: true
}
end
def yaml_error_field
{
title: s_("ChatMessage|Invalid CI config YAML file"),
value: pipeline.yaml_errors,
short: false
}
end
def attachments_fields
fields = [
{
title: ref_type == "tag" ? s_("ChatMessage|Tag") : s_("ChatMessage|Branch"),
value: Slack::Notifier::LinkFormatter.format(ref_name_link),
short: true
},
{
title: s_("ChatMessage|Commit"),
value: Slack::Notifier::LinkFormatter.format(commit_link),
short: true
}
]
fields << failed_stages_field if failed_stages.any?
fields << failed_jobs_field if failed_jobs.any?
fields << yaml_error_field if pipeline.has_yaml_errors?
fields
end
def message
"#{project_link}: Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_combined_name} #{humanized_status} in #{pretty_duration(duration)}"
s_("ChatMessage|%{project_link}: Pipeline %{pipeline_link} of %{ref_type} %{branch_link} by %{user_combined_name} %{humanized_status} in %{duration}") %
{
project_link: project_link,
pipeline_link: pipeline_link,
ref_type: ref_type,
branch_link: branch_link,
user_combined_name: user_combined_name,
humanized_status: humanized_status,
duration: pretty_duration(duration)
}
end
def humanized_status
if fancy_notifications?
case status
when 'success'
'passed'
detailed_status == "passed with warnings" ? s_("ChatMessage|has passed with warnings") : s_("ChatMessage|has passed")
when 'failed'
s_("ChatMessage|has failed")
else
status
end
else
case status
when 'success'
s_("ChatMessage|passed")
when 'failed'
s_("ChatMessage|failed")
else
status
end
end
end
def attachment_color
if status == 'success'
if fancy_notifications?
case status
when 'success'
detailed_status == 'passed with warnings' ? 'warning' : 'good'
else
'danger'
end
else
case status
when 'success'
'good'
else
'danger'
end
end
end
def branch_url
"#{project_url}/commits/#{ref}"
......@@ -71,16 +201,83 @@ module ChatMessage
"[#{ref}](#{branch_url})"
end
def project_url
project.web_url
end
def project_link
"[#{project_name}](#{project_url})"
"[#{project.name}](#{project_url})"
end
def pipeline_failed_jobs_url
"#{project_url}/pipelines/#{pipeline_id}/failures"
end
def pipeline_url
if fancy_notifications? && failed_jobs.any?
pipeline_failed_jobs_url
else
"#{project_url}/pipelines/#{pipeline_id}"
end
end
def pipeline_link
"[##{pipeline_id}](#{pipeline_url})"
end
def job_url(job)
"#{project_url}/-/jobs/#{job[:id]}"
end
def job_link(job)
"[#{job[:name]}](#{job_url(job)})"
end
def failed_jobs_links
failed = failed_jobs.slice(0, MAX_VISIBLE_JOBS)
truncated = failed_jobs.slice(MAX_VISIBLE_JOBS, failed_jobs.size)
failed_links = failed.map { |job| job_link(job) }
unless truncated.blank?
failed_links << s_("ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})") % {
count: truncated.size,
pipeline_failed_jobs_url: pipeline_failed_jobs_url
}
end
failed_links.join(I18n.translate(:'support.array.words_connector'))
end
def stage_link(stage)
# All stages link to the pipeline page
"[#{stage}](#{pipeline_url})"
end
def failed_stages_links
failed_stages.map { |s| stage_link(s) }.join(I18n.translate(:'support.array.words_connector'))
end
def commit_url
Gitlab::UrlBuilder.build(commit)
end
def commit_link
"[#{commit.title}](#{commit_url})"
end
def commits_page_url
"#{project_url}/commits/#{ref}"
end
def ref_name_link
"[#{ref}](#{commits_page_url})"
end
def author_url
return unless user && committer
Gitlab::UrlBuilder.build(committer)
end
end
end
---
title: Improve pipeline status Slack notifications
merge_request: 27683
author:
type: added
......@@ -2020,6 +2020,57 @@ msgstr ""
msgid "Chat"
msgstr ""
msgid "ChatMessage|%{project_link}: Pipeline %{pipeline_link} of %{ref_type} %{branch_link} by %{user_combined_name} %{humanized_status} in %{duration}"
msgstr ""
msgid "ChatMessage|Branch"
msgstr ""
msgid "ChatMessage|Commit"
msgstr ""
msgid "ChatMessage|Failed job"
msgstr ""
msgid "ChatMessage|Failed stage"
msgstr ""
msgid "ChatMessage|Invalid CI config YAML file"
msgstr ""
msgid "ChatMessage|Pipeline #%{pipeline_id} %{humanized_status} in %{duration}"
msgstr ""
msgid "ChatMessage|Pipeline %{pipeline_link} of %{ref_type} %{branch_link} by %{user_combined_name} %{humanized_status}"
msgstr ""
msgid "ChatMessage|Tag"
msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
msgid "ChatMessage|failed"
msgstr ""
msgid "ChatMessage|has failed"
msgstr ""
msgid "ChatMessage|has passed"
msgstr ""
msgid "ChatMessage|has passed with warnings"
msgstr ""
msgid "ChatMessage|in %{duration}"
msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
msgid "ChatMessage|passed"
msgstr ""
msgid "Check again"
msgstr ""
......
......@@ -292,7 +292,8 @@ describe MicrosoftTeamsService do
context 'when disabled' do
let(:pipeline) do
create(:ci_pipeline, :failed, project: project, ref: 'not-the-default-branch')
create(:ci_pipeline, :failed, project: project,
sha: project.commit.sha, ref: 'not-the-default-branch')
end
before do
......
......@@ -222,7 +222,8 @@ shared_examples_for "chat service" do |service_name|
context "with not default branch" do
let(:pipeline) do
create(:ci_pipeline, project: project, status: "failed", ref: "not-the-default-branch")
create(:ci_pipeline, :failed, project: project,
sha: project.commit.sha, ref: "not-the-default-branch")
end
context "when notify_only_default_branch enabled" do
......
......@@ -454,7 +454,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
context 'only notify for the default branch' do
context 'when enabled' do
let(:pipeline) do
create(:ci_pipeline, :failed, project: project, ref: 'not-the-default-branch')
create(:ci_pipeline, :failed, project: project, sha: project.commit.sha, ref: 'not-the-default-branch')
end
before do
......@@ -472,7 +472,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
context 'when disabled' do
let(:pipeline) do
create(:ci_pipeline, :failed, project: project, ref: 'not-the-default-branch')
create(:ci_pipeline, :failed, project: project, sha: project.commit.sha, ref: 'not-the-default-branch')
end
before do
......
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