summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Friend <nathan@gitlab.com>2019-07-25 11:26:29 -0300
committerNathan Friend <nathan@gitlab.com>2019-07-25 11:26:29 -0300
commit7b5fb754f8b74e5b356e7e7ef882bada1acc20e7 (patch)
tree350a31246cbf891ef970a313553b7480cf2f6438
parent6dcde68b6dbce7190ef78e7fef280b634b549450 (diff)
downloadgitlab-ce-7b5fb754f8b74e5b356e7e7ef882bada1acc20e7.tar.gz
Improve pipeline status Slack notifications32495-improve-slack-notification-on-pipeline-status
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.
-rw-r--r--app/models/project_services/chat_message/pipeline_message.rb225
-rw-r--r--changelogs/unreleased/32495-improve-slack-notification-on-pipeline-status.yml5
-rw-r--r--locale/gitlab.pot51
-rw-r--r--spec/models/project_services/chat_message/pipeline_message_spec.rb484
-rw-r--r--spec/models/project_services/microsoft_teams_service_spec.rb3
-rw-r--r--spec/support/shared_examples/models/chat_service_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb4
7 files changed, 671 insertions, 104 deletions
diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb
index 62aec4351db..4edf263433f 100644
--- a/app/models/project_services/chat_message/pipeline_message.rb
+++ b/app/models/project_services/chat_message/pipeline_message.rb
@@ -1,24 +1,47 @@
# 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,38 +51,145 @@ 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
- case status
- when 'success'
- 'passed'
+ if fancy_notifications?
+ case status
+ when 'success'
+ 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
- status
+ case status
+ when 'success'
+ s_("ChatMessage|passed")
+ when 'failed'
+ s_("ChatMessage|failed")
+ else
+ status
+ end
end
end
def attachment_color
- if status == 'success'
- 'good'
+ if fancy_notifications?
+ case status
+ when 'success'
+ detailed_status == 'passed with warnings' ? 'warning' : 'good'
+ else
+ 'danger'
+ end
else
- 'danger'
+ case status
+ when 'success'
+ 'good'
+ else
+ 'danger'
+ end
end
end
@@ -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
- "#{project_url}/pipelines/#{pipeline_id}"
+ 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
diff --git a/changelogs/unreleased/32495-improve-slack-notification-on-pipeline-status.yml b/changelogs/unreleased/32495-improve-slack-notification-on-pipeline-status.yml
new file mode 100644
index 00000000000..b7b39303c2e
--- /dev/null
+++ b/changelogs/unreleased/32495-improve-slack-notification-on-pipeline-status.yml
@@ -0,0 +1,5 @@
+---
+title: Improve pipeline status Slack notifications
+merge_request: 27683
+author:
+type: added
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index cbab027d4a3..bc644925281 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2011,6 +2011,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 ""
diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb
index 8f9fa310ad4..619ab96af94 100644
--- a/spec/models/project_services/chat_message/pipeline_message_spec.rb
+++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb
@@ -1,12 +1,9 @@
# frozen_string_literal: true
-
require 'spec_helper'
describe ChatMessage::PipelineMessage do
subject { described_class.new(args) }
- let(:user) { { name: "The Hacker", username: 'hacker' } }
- let(:duration) { 7210 }
let(:args) do
{
object_attributes: {
@@ -14,122 +11,437 @@ describe ChatMessage::PipelineMessage do
sha: '97de212e80737a608d939f648d959671fb0a0142',
tag: false,
ref: 'develop',
- status: status,
- duration: duration
+ status: 'success',
+ detailed_status: nil,
+ duration: 7210,
+ finished_at: "2019-05-27 11:56:36 -0300"
},
project: {
- path_with_namespace: 'project_name',
- web_url: 'http://example.gitlab.com'
+ id: 234,
+ name: "project_name",
+ path_with_namespace: 'group/project_name',
+ web_url: 'http://example.gitlab.com',
+ avatar_url: 'http://example.com/project_avatar'
+ },
+ user: {
+ id: 345,
+ name: "The Hacker",
+ username: "hacker",
+ email: "hacker@example.gitlab.com",
+ avatar_url: "http://example.com/avatar"
+ },
+ commit: {
+ id: "abcdef"
},
- user: user
+ builds: nil,
+ markdown: false
}
end
- let(:combined_name) { "The Hacker (hacker)" }
- context 'without markdown' do
- context 'pipeline succeeded' do
- let(:status) { 'success' }
- let(:color) { 'good' }
- let(:message) { build_message('passed', combined_name) }
+ let(:has_yaml_errors) { false }
+
+ before do
+ test_commit = double("A test commit", committer: args[:user], title: "A test commit message")
+ test_project = double("A test project",
+ commit_by: test_commit, name: args[:project][:name],
+ web_url: args[:project][:web_url], avatar_url: args[:project][:avatar_url])
+ allow(Project).to receive(:find) { test_project }
+
+ test_pipeline = double("A test pipeline", has_yaml_errors?: has_yaml_errors,
+ yaml_errors: "yaml error description here")
+ allow(Ci::Pipeline).to receive(:find) { test_pipeline }
+
+ allow(Gitlab::UrlBuilder).to receive(:build).with(test_commit).and_return("http://example.com/commit")
+ allow(Gitlab::UrlBuilder).to receive(:build).with(args[:user]).and_return("http://example.gitlab.com/hacker")
+ end
+
+ context 'when the fancy_pipeline_slack_notifications feature flag is disabled' do
+ before do
+ stub_feature_flags(fancy_pipeline_slack_notifications: false)
+ end
+
+ it 'returns an empty pretext' do
+ expect(subject.pretext).to be_empty
+ end
+
+ it "returns the pipeline summary in the activity's title" do
+ expect(subject.activity[:title]).to eq(
+ "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
+ " of branch [develop](http://example.gitlab.com/commits/develop)" \
+ " by The Hacker (hacker) passed"
+ )
+ end
- it 'returns a message with information about succeeded build' do
- expect(subject.pretext).to be_empty
- expect(subject.fallback).to eq(message)
- expect(subject.attachments).to eq([text: message, color: color])
+ context "when the pipeline failed" do
+ before do
+ args[:object_attributes][:status] = 'failed'
+ end
+
+ it "returns the summary with a 'failed' status" do
+ expect(subject.activity[:title]).to eq(
+ "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
+ " of branch [develop](http://example.gitlab.com/commits/develop)" \
+ " by The Hacker (hacker) failed"
+ )
end
end
- context 'pipeline failed' do
- let(:status) { 'failed' }
- let(:color) { 'danger' }
- let(:message) { build_message(status, combined_name) }
+ context 'when no user is provided because the pipeline was triggered by the API' do
+ before do
+ args[:user] = nil
+ end
- it 'returns a message with information about failed build' do
- expect(subject.pretext).to be_empty
- expect(subject.fallback).to eq(message)
- expect(subject.attachments).to eq([text: message, color: color])
+ it "returns the summary with 'API' as the username" do
+ expect(subject.activity[:title]).to eq(
+ "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
+ " of branch [develop](http://example.gitlab.com/commits/develop)" \
+ " by API passed"
+ )
end
+ end
- context 'when triggered by API therefore lacking user' do
- let(:user) { nil }
- let(:message) { build_message(status, 'API') }
+ it "returns a link to the project in the activity's subtitle" do
+ expect(subject.activity[:subtitle]).to eq("in [project_name](http://example.gitlab.com)")
+ end
- it 'returns a message stating it is by API' do
- expect(subject.pretext).to be_empty
- expect(subject.fallback).to eq(message)
- expect(subject.attachments).to eq([text: message, color: color])
- end
+ it "returns the build duration in the activity's text property" do
+ expect(subject.activity[:text]).to eq("in 02:00:10")
+ end
+
+ it "returns the user's avatar image URL in the activity's image property" do
+ expect(subject.activity[:image]).to eq("http://example.com/avatar")
+ end
+
+ context 'when the user does not have an avatar' do
+ before do
+ args[:user][:avatar_url] = nil
+ end
+
+ it "returns an empty string in the activity's image property" do
+ expect(subject.activity[:image]).to be_empty
+ end
+ end
+
+ it "returns the pipeline summary as the attachment's text property" do
+ expect(subject.attachments.first[:text]).to eq(
+ "<http://example.gitlab.com|project_name>:" \
+ " Pipeline <http://example.gitlab.com/pipelines/123|#123>" \
+ " of branch <http://example.gitlab.com/commits/develop|develop>" \
+ " by The Hacker (hacker) passed in 02:00:10"
+ )
+ end
+
+ it "returns 'good' as the attachment's color property" do
+ expect(subject.attachments.first[:color]).to eq('good')
+ end
+
+ context "when the pipeline failed" do
+ before do
+ args[:object_attributes][:status] = 'failed'
+ end
+
+ it "returns 'danger' as the attachment's color property" do
+ expect(subject.attachments.first[:color]).to eq('danger')
end
end
- def build_message(status_text = status, name = user[:name])
- "<http://example.gitlab.com|project_name>:" \
- " Pipeline <http://example.gitlab.com/pipelines/123|#123>" \
- " of branch <http://example.gitlab.com/commits/develop|develop>" \
- " by #{name} #{status_text} in 02:00:10"
+ context 'when rendering markdown' do
+ before do
+ args[:markdown] = true
+ end
+
+ it 'returns the pipeline summary as the attachments in markdown format' do
+ expect(subject.attachments).to eq(
+ "[project_name](http://example.gitlab.com):" \
+ " Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
+ " of branch [develop](http://example.gitlab.com/commits/develop)" \
+ " by The Hacker (hacker) passed in 02:00:10"
+ )
+ end
end
end
- context 'with markdown' do
+ context 'when the fancy_pipeline_slack_notifications feature flag is enabled' do
before do
- args[:markdown] = true
- end
-
- context 'pipeline succeeded' do
- let(:status) { 'success' }
- let(:color) { 'good' }
- let(:message) { build_markdown_message('passed', combined_name) }
-
- it 'returns a message with information about succeeded build' do
- expect(subject.pretext).to be_empty
- expect(subject.attachments).to eq(message)
- expect(subject.activity).to eq({
- title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by The Hacker (hacker) passed',
- subtitle: 'in [project_name](http://example.gitlab.com)',
- text: 'in 02:00:10',
- image: ''
- })
+ stub_feature_flags(fancy_pipeline_slack_notifications: true)
+ end
+
+ it 'returns an empty pretext' do
+ expect(subject.pretext).to be_empty
+ end
+
+ it "returns the pipeline summary in the activity's title" do
+ expect(subject.activity[:title]).to eq(
+ "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
+ " of branch [develop](http://example.gitlab.com/commits/develop)" \
+ " by The Hacker (hacker) has passed"
+ )
+ end
+
+ context "when the pipeline failed" do
+ before do
+ args[:object_attributes][:status] = 'failed'
+ end
+
+ it "returns the summary with a 'failed' status" do
+ expect(subject.activity[:title]).to eq(
+ "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
+ " of branch [develop](http://example.gitlab.com/commits/develop)" \
+ " by The Hacker (hacker) has failed"
+ )
+ end
+ end
+
+ context "when the pipeline passed with warnings" do
+ before do
+ args[:object_attributes][:detailed_status] = 'passed with warnings'
+ end
+
+ it "returns the summary with a 'passed with warnings' status" do
+ expect(subject.activity[:title]).to eq(
+ "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
+ " of branch [develop](http://example.gitlab.com/commits/develop)" \
+ " by The Hacker (hacker) has passed with warnings"
+ )
+ end
+ end
+
+ context 'when no user is provided because the pipeline was triggered by the API' do
+ before do
+ args[:user] = nil
+ end
+
+ it "returns the summary with 'API' as the username" do
+ expect(subject.activity[:title]).to eq(
+ "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
+ " of branch [develop](http://example.gitlab.com/commits/develop)" \
+ " by API has passed"
+ )
+ end
+ end
+
+ it "returns a link to the project in the activity's subtitle" do
+ expect(subject.activity[:subtitle]).to eq("in [project_name](http://example.gitlab.com)")
+ end
+
+ it "returns the build duration in the activity's text property" do
+ expect(subject.activity[:text]).to eq("in 02:00:10")
+ end
+
+ it "returns the user's avatar image URL in the activity's image property" do
+ expect(subject.activity[:image]).to eq("http://example.com/avatar")
+ end
+
+ context 'when the user does not have an avatar' do
+ before do
+ args[:user][:avatar_url] = nil
+ end
+
+ it "returns an empty string in the activity's image property" do
+ expect(subject.activity[:image]).to be_empty
+ end
+ end
+
+ it "returns the pipeline summary as the attachment's fallback property" do
+ expect(subject.attachments.first[:fallback]).to eq(
+ "<http://example.gitlab.com|project_name>:" \
+ " Pipeline <http://example.gitlab.com/pipelines/123|#123>" \
+ " of branch <http://example.gitlab.com/commits/develop|develop>" \
+ " by The Hacker (hacker) has passed in 02:00:10"
+ )
+ end
+
+ it "returns 'good' as the attachment's color property" do
+ expect(subject.attachments.first[:color]).to eq('good')
+ end
+
+ context "when the pipeline failed" do
+ before do
+ args[:object_attributes][:status] = 'failed'
+ end
+
+ it "returns 'danger' as the attachment's color property" do
+ expect(subject.attachments.first[:color]).to eq('danger')
+ end
+ end
+
+ context "when the pipeline passed with warnings" do
+ before do
+ args[:object_attributes][:detailed_status] = 'passed with warnings'
+ end
+
+ it "returns 'warning' as the attachment's color property" do
+ expect(subject.attachments.first[:color]).to eq('warning')
end
end
- context 'pipeline failed' do
- let(:status) { 'failed' }
- let(:color) { 'danger' }
- let(:message) { build_markdown_message(status, combined_name) }
+ it "returns the committer's name and username as the attachment's author_name property" do
+ expect(subject.attachments.first[:author_name]).to eq('The Hacker (hacker)')
+ end
+
+ it "returns the committer's avatar URL as the attachment's author_icon property" do
+ expect(subject.attachments.first[:author_icon]).to eq('http://example.com/avatar')
+ end
+
+ it "returns the committer's GitLab profile URL as the attachment's author_link property" do
+ expect(subject.attachments.first[:author_link]).to eq('http://example.gitlab.com/hacker')
+ end
+
+ context 'when no user is provided because the pipeline was triggered by the API' do
+ before do
+ args[:user] = nil
+ end
+
+ it "returns the committer's name and username as the attachment's author_name property" do
+ expect(subject.attachments.first[:author_name]).to eq('API')
+ end
+
+ it "returns nil as the attachment's author_icon property" do
+ expect(subject.attachments.first[:author_icon]).to be_nil
+ end
+
+ it "returns nil as the attachment's author_link property" do
+ expect(subject.attachments.first[:author_link]).to be_nil
+ end
+ end
+
+ it "returns the pipeline ID, status, and duration as the attachment's title property" do
+ expect(subject.attachments.first[:title]).to eq("Pipeline #123 has passed in 02:00:10")
+ end
+
+ it "returns the pipeline URL as the attachment's title_link property" do
+ expect(subject.attachments.first[:title_link]).to eq("http://example.gitlab.com/pipelines/123")
+ end
+
+ it "returns two attachment fields" do
+ expect(subject.attachments.first[:fields].count).to eq(2)
+ end
+
+ it "returns the commit message as the attachment's second field property" do
+ expect(subject.attachments.first[:fields][0]).to eq({
+ title: "Branch",
+ value: "<http://example.gitlab.com/commits/develop|develop>",
+ short: true
+ })
+ end
+
+ it "returns the ref name and link as the attachment's second field property" do
+ expect(subject.attachments.first[:fields][1]).to eq({
+ title: "Commit",
+ value: "<http://example.com/commit|A test commit message>",
+ short: true
+ })
+ end
+
+ context "when a job in the pipeline fails" do
+ before do
+ args[:builds] = [
+ { id: 1, name: "rspec", status: "failed", stage: "test" },
+ { id: 2, name: "karma", status: "success", stage: "test" }
+ ]
+ end
+
+ it "returns four attachment fields" do
+ expect(subject.attachments.first[:fields].count).to eq(4)
+ end
- it 'returns a message with information about failed build' do
- expect(subject.pretext).to be_empty
- expect(subject.attachments).to eq(message)
- expect(subject.activity).to eq({
- title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by The Hacker (hacker) failed',
- subtitle: 'in [project_name](http://example.gitlab.com)',
- text: 'in 02:00:10',
- image: ''
+ it "returns the stage name and link to the 'Failed jobs' tab on the pipeline's page as the attachment's third field property" do
+ expect(subject.attachments.first[:fields][2]).to eq({
+ title: "Failed stage",
+ value: "<http://example.gitlab.com/pipelines/123/failures|test>",
+ short: true
})
end
- context 'when triggered by API therefore lacking user' do
- let(:user) { nil }
- let(:message) { build_markdown_message(status, 'API') }
+ it "returns the job name and link as the attachment's fourth field property" do
+ expect(subject.attachments.first[:fields][3]).to eq({
+ title: "Failed job",
+ value: "<http://example.gitlab.com/-/jobs/1|rspec>",
+ short: true
+ })
+ end
+ end
+
+ context "when lots of jobs across multiple stages fail" do
+ before do
+ args[:builds] = (1..25).map do |i|
+ { id: i, name: "job-#{i}", status: "failed", stage: "stage-" + ((i % 3) + 1).to_s }
+ end
+ end
+
+ it "returns the stage names and links to the 'Failed jobs' tab on the pipeline's page as the attachment's third field property" do
+ expect(subject.attachments.first[:fields][2]).to eq({
+ title: "Failed stages",
+ value: "<http://example.gitlab.com/pipelines/123/failures|stage-2>, <http://example.gitlab.com/pipelines/123/failures|stage-1>, <http://example.gitlab.com/pipelines/123/failures|stage-3>",
+ short: true
+ })
+ end
- it 'returns a message stating it is by API' do
- expect(subject.pretext).to be_empty
- expect(subject.attachments).to eq(message)
- expect(subject.activity).to eq({
- title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by API failed',
- subtitle: 'in [project_name](http://example.gitlab.com)',
- text: 'in 02:00:10',
- image: ''
- })
+ it "returns the job names and links as the attachment's fourth field property" do
+ expected_jobs = 25.downto(16).map do |i|
+ "<http://example.gitlab.com/-/jobs/#{i}|job-#{i}>"
end
+
+ expected_jobs << "and <http://example.gitlab.com/pipelines/123/failures|15 more>"
+
+ expect(subject.attachments.first[:fields][3]).to eq({
+ title: "Failed jobs",
+ value: expected_jobs.join(", "),
+ short: true
+ })
end
end
- def build_markdown_message(status_text = status, name = user[:name])
- "[project_name](http://example.gitlab.com):" \
- " Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
- " of branch [develop](http://example.gitlab.com/commits/develop)" \
- " by #{name} #{status_text} in 02:00:10"
+ context "when the CI config file contains a YAML error" do
+ let(:has_yaml_errors) { true }
+
+ it "returns three attachment fields" do
+ expect(subject.attachments.first[:fields].count).to eq(3)
+ end
+
+ it "returns the YAML error deatils as the attachment's third field property" do
+ expect(subject.attachments.first[:fields][2]).to eq({
+ title: "Invalid CI config YAML file",
+ value: "yaml error description here",
+ short: false
+ })
+ end
+ end
+
+ it "returns the stage name and link as the attachment's second field property" do
+ expect(subject.attachments.first[:fields][1]).to eq({
+ title: "Commit",
+ value: "<http://example.com/commit|A test commit message>",
+ short: true
+ })
+ end
+
+ it "returns the project's name as the attachment's footer property" do
+ expect(subject.attachments.first[:footer]).to eq("project_name")
+ end
+
+ it "returns the project's avatar URL as the attachment's footer_icon property" do
+ expect(subject.attachments.first[:footer_icon]).to eq("http://example.com/project_avatar")
+ end
+
+ it "returns the pipeline's timestamp as the attachment's ts property" do
+ expected_ts = Time.parse(args[:object_attributes][:finished_at]).to_i
+ expect(subject.attachments.first[:ts]).to eq(expected_ts)
+ end
+
+ context 'when rendering markdown' do
+ before do
+ args[:markdown] = true
+ end
+
+ it 'returns the pipeline summary as the attachments in markdown format' do
+ expect(subject.attachments).to eq(
+ "[project_name](http://example.gitlab.com):" \
+ " Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
+ " of branch [develop](http://example.gitlab.com/commits/develop)" \
+ " by The Hacker (hacker) has passed in 02:00:10"
+ )
+ end
end
end
end
diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb
index 3ffe633868f..73c20359091 100644
--- a/spec/models/project_services/microsoft_teams_service_spec.rb
+++ b/spec/models/project_services/microsoft_teams_service_spec.rb
@@ -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
diff --git a/spec/support/shared_examples/models/chat_service_shared_examples.rb b/spec/support/shared_examples/models/chat_service_shared_examples.rb
index 0a302e7d030..8d70c06c468 100644
--- a/spec/support/shared_examples/models/chat_service_shared_examples.rb
+++ b/spec/support/shared_examples/models/chat_service_shared_examples.rb
@@ -220,7 +220,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
diff --git a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
index 36c486dbdd6..bbed3b1f6a9 100644
--- a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
+++ b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
@@ -452,7 +452,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
@@ -470,7 +470,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