Commit 7b5fb754 authored by Nathan Friend's avatar Nathan Friend

Improve pipeline status Slack notifications

This commit adds some formatting to the Slack notifications for pipeline
statuses, as well as adds information about the stage and jobs
that failed in the case of pipeline failure.
parent 6dcde68b
# frozen_string_literal: true # frozen_string_literal: true
require 'slack-notifier'
module ChatMessage module ChatMessage
class PipelineMessage < BaseMessage class PipelineMessage < BaseMessage
MAX_VISIBLE_JOBS = 10
attr_reader :user
attr_reader :ref_type attr_reader :ref_type
attr_reader :ref attr_reader :ref
attr_reader :status attr_reader :status
attr_reader :detailed_status
attr_reader :duration attr_reader :duration
attr_reader :finished_at
attr_reader :pipeline_id 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) def initialize(data)
super super
@user = data[:user]
@user_name = data.dig(:user, :username) || 'API' @user_name = data.dig(:user, :username) || 'API'
pipeline_attributes = data[:object_attributes] pipeline_attributes = data[:object_attributes]
@ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
@ref = pipeline_attributes[:ref] @ref = pipeline_attributes[:ref]
@status = pipeline_attributes[:status] @status = pipeline_attributes[:status]
@detailed_status = pipeline_attributes[:detailed_status]
@duration = pipeline_attributes[:duration].to_i @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] @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 end
def pretext def pretext
...@@ -28,40 +51,147 @@ module ChatMessage ...@@ -28,40 +51,147 @@ module ChatMessage
def attachments def attachments
return message if markdown 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 end
def activity def activity
{ {
title: "Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_combined_name} #{humanized_status}", title: s_("ChatMessage|Pipeline %{pipeline_link} of %{ref_type} %{branch_link} by %{user_combined_name} %{humanized_status}") %
subtitle: "in #{project_link}", {
text: "in #{pretty_duration(duration)}", 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 || '' image: user_avatar || ''
} }
end end
private 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 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 end
def humanized_status def humanized_status
if fancy_notifications?
case status case status
when 'success' 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 else
status status
end end
end end
end
def attachment_color 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' 'good'
else else
'danger' 'danger'
end end
end end
end
def branch_url def branch_url
"#{project_url}/commits/#{ref}" "#{project_url}/commits/#{ref}"
...@@ -71,16 +201,83 @@ module ChatMessage ...@@ -71,16 +201,83 @@ module ChatMessage
"[#{ref}](#{branch_url})" "[#{ref}](#{branch_url})"
end end
def project_url
project.web_url
end
def project_link def project_link
"[#{project_name}](#{project_url})" "[#{project.name}](#{project_url})"
end
def pipeline_failed_jobs_url
"#{project_url}/pipelines/#{pipeline_id}/failures"
end end
def pipeline_url def pipeline_url
if fancy_notifications? && failed_jobs.any?
pipeline_failed_jobs_url
else
"#{project_url}/pipelines/#{pipeline_id}" "#{project_url}/pipelines/#{pipeline_id}"
end end
end
def pipeline_link def pipeline_link
"[##{pipeline_id}](#{pipeline_url})" "[##{pipeline_id}](#{pipeline_url})"
end 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
end end
---
title: Improve pipeline status Slack notifications
merge_request: 27683
author:
type: added
...@@ -2011,6 +2011,57 @@ msgstr "" ...@@ -2011,6 +2011,57 @@ msgstr ""
msgid "Chat" msgid "Chat"
msgstr "" 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" msgid "Check again"
msgstr "" msgstr ""
......
...@@ -292,7 +292,8 @@ describe MicrosoftTeamsService do ...@@ -292,7 +292,8 @@ describe MicrosoftTeamsService do
context 'when disabled' do context 'when disabled' do
let(:pipeline) 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 end
before do before do
......
...@@ -220,7 +220,8 @@ shared_examples_for "chat service" do |service_name| ...@@ -220,7 +220,8 @@ shared_examples_for "chat service" do |service_name|
context "with not default branch" do context "with not default branch" do
let(:pipeline) 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 end
context "when notify_only_default_branch enabled" do context "when notify_only_default_branch enabled" do
......
...@@ -452,7 +452,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do ...@@ -452,7 +452,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
context 'only notify for the default branch' do context 'only notify for the default branch' do
context 'when enabled' do context 'when enabled' do
let(:pipeline) 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 end
before do before do
...@@ -470,7 +470,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do ...@@ -470,7 +470,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
context 'when disabled' do context 'when disabled' do
let(:pipeline) 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 end
before do 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