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: user + user: { + id: 345, + name: "The Hacker", + username: "hacker", + email: "hacker@example.gitlab.com", + avatar_url: "http://example.com/avatar" + }, + commit: { + id: "abcdef" + }, + 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 } - 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]) + 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 + + 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) } - - 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]) + context 'when no user is provided because the pipeline was triggered by the API' do + before do + args[:user] = nil end - context 'when triggered by API therefore lacking user' do - let(:user) { nil } - let(:message) { build_message(status, 'API') } - - 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 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 - def build_message(status_text = status, name = user[:name]) - ":" \ - " Pipeline " \ - " of branch " \ - " by #{name} #{status_text} in 02:00:10" + 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 text property" do + expect(subject.attachments.first[:text]).to eq( + ":" \ + " Pipeline " \ + " of branch " \ + " 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 + + 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 + stub_feature_flags(fancy_pipeline_slack_notifications: true) end - context 'pipeline succeeded' do - let(:status) { 'success' } - let(:color) { 'good' } - let(:message) { build_markdown_message('passed', combined_name) } + it 'returns an empty pretext' do + expect(subject.pretext).to be_empty + end - 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: '' + 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( + ":" \ + " Pipeline " \ + " of branch " \ + " 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 + + 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: "", + 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: "", + 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 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: "", + short: true + }) + end + + 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: "", + short: true }) end end - context 'pipeline failed' do - let(:status) { 'failed' } - let(:color) { 'danger' } - let(:message) { build_markdown_message(status, combined_name) } - - 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: '' - }) - end - - context 'when triggered by API therefore lacking user' do - let(:user) { nil } - let(:message) { build_markdown_message(status, 'API') } - - 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: '' - }) + 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: ", , ", + short: true + }) + end + + it "returns the job names and links as the attachment's fourth field property" do + expected_jobs = 25.downto(16).map do |i| + "" + end + + expected_jobs << "and " + + 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: "", + 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