diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-17 18:10:15 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-17 18:10:15 +0300 |
commit | 68c476dbd8a2c670aeeebffce8b63b554a3ac7f0 (patch) | |
tree | c46b90a5c131d8e8d7fb530f1b8f9390b8eb2613 /app/models/integrations/chat_message | |
parent | a62238de7302e54edafa3407a2dc8ba1a5f96e4d (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/models/integrations/chat_message')
9 files changed, 944 insertions, 0 deletions
diff --git a/app/models/integrations/chat_message/alert_message.rb b/app/models/integrations/chat_message/alert_message.rb new file mode 100644 index 00000000000..ef0579124fe --- /dev/null +++ b/app/models/integrations/chat_message/alert_message.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module Integrations + module ChatMessage + class AlertMessage < BaseMessage + attr_reader :title + attr_reader :alert_url + attr_reader :severity + attr_reader :events + attr_reader :status + attr_reader :started_at + + def initialize(params) + @project_name = params[:project_name] || params.dig(:project, :path_with_namespace) + @project_url = params.dig(:project, :web_url) || params[:project_url] + @title = params.dig(:object_attributes, :title) + @alert_url = params.dig(:object_attributes, :url) + @severity = params.dig(:object_attributes, :severity) + @events = params.dig(:object_attributes, :events) + @status = params.dig(:object_attributes, :status) + @started_at = params.dig(:object_attributes, :started_at) + end + + def attachments + [{ + title: title, + title_link: alert_url, + color: attachment_color, + fields: attachment_fields + }] + end + + def message + "Alert firing in #{project_name}" + end + + private + + def attachment_color + "#C95823" + end + + def attachment_fields + [ + { + title: "Severity", + value: severity.to_s.humanize, + short: true + }, + { + title: "Events", + value: events, + short: true + }, + { + title: "Status", + value: status.to_s.humanize, + short: true + }, + { + title: "Start time", + value: format_time(started_at), + short: true + } + ] + end + + # This formats time into the following format + # April 23rd, 2020 1:06AM UTC + def format_time(time) + time = Time.zone.parse(time.to_s) + time.strftime("%B #{time.day.ordinalize}, %Y %l:%M%p %Z") + end + end + end +end diff --git a/app/models/integrations/chat_message/base_message.rb b/app/models/integrations/chat_message/base_message.rb new file mode 100644 index 00000000000..2f70384d3b9 --- /dev/null +++ b/app/models/integrations/chat_message/base_message.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +module Integrations + module ChatMessage + class BaseMessage + RELATIVE_LINK_REGEX = %r{!\[[^\]]*\]\((/uploads/[^\)]*)\)}.freeze + + attr_reader :markdown + attr_reader :user_full_name + attr_reader :user_name + attr_reader :user_avatar + attr_reader :project_name + attr_reader :project_url + + def initialize(params) + @markdown = params[:markdown] || false + @project_name = params[:project_name] || params.dig(:project, :path_with_namespace) + @project_url = params.dig(:project, :web_url) || params[:project_url] + @user_full_name = params.dig(:user, :name) || params[:user_full_name] + @user_name = params.dig(:user, :username) || params[:user_name] + @user_avatar = params.dig(:user, :avatar_url) || params[:user_avatar] + end + + def user_combined_name + if user_full_name.present? + "#{user_full_name} (#{user_name})" + else + user_name + end + end + + def summary + return message if markdown + + format(message) + end + + def pretext + summary + end + + def fallback + format(message) + end + + def attachments + raise NotImplementedError + end + + def activity + raise NotImplementedError + end + + private + + def message + raise NotImplementedError + end + + def format(string) + Slack::Messenger::Util::LinkFormatter.format(format_relative_links(string)) + end + + def format_relative_links(string) + string.gsub(RELATIVE_LINK_REGEX, "#{project_url}\\1") + end + + def attachment_color + '#345' + end + + def link(text, url) + "[#{text}](#{url})" + end + + def pretty_duration(seconds) + parse_string = + if duration < 1.hour + '%M:%S' + else + '%H:%M:%S' + end + + Time.at(seconds).utc.strftime(parse_string) + end + end + end +end diff --git a/app/models/integrations/chat_message/deployment_message.rb b/app/models/integrations/chat_message/deployment_message.rb new file mode 100644 index 00000000000..c4f3bf9610d --- /dev/null +++ b/app/models/integrations/chat_message/deployment_message.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module Integrations + module ChatMessage + class DeploymentMessage < BaseMessage + attr_reader :commit_title + attr_reader :commit_url + attr_reader :deployable_id + attr_reader :deployable_url + attr_reader :environment + attr_reader :short_sha + attr_reader :status + attr_reader :user_url + + def initialize(data) + super + + @commit_title = data[:commit_title] + @commit_url = data[:commit_url] + @deployable_id = data[:deployable_id] + @deployable_url = data[:deployable_url] + @environment = data[:environment] + @short_sha = data[:short_sha] + @status = data[:status] + @user_url = data[:user_url] + end + + def attachments + [{ + text: "#{project_link} with job #{deployment_link} by #{user_link}\n#{commit_link}: #{commit_title}", + color: color + }] + end + + def activity + {} + end + + private + + def message + if running? + "Starting deploy to #{environment}" + else + "Deploy to #{environment} #{humanized_status}" + end + end + + def color + case status + when 'success' + 'good' + when 'canceled' + 'warning' + when 'failed' + 'danger' + else + '#334455' + end + end + + def project_link + link(project_name, project_url) + end + + def deployment_link + link("##{deployable_id}", deployable_url) + end + + def user_link + link(user_combined_name, user_url) + end + + def commit_link + link(short_sha, commit_url) + end + + def humanized_status + status == 'success' ? 'succeeded' : status + end + + def running? + status == 'running' + end + end + end +end diff --git a/app/models/integrations/chat_message/issue_message.rb b/app/models/integrations/chat_message/issue_message.rb new file mode 100644 index 00000000000..5fa6bd4090f --- /dev/null +++ b/app/models/integrations/chat_message/issue_message.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module Integrations + module ChatMessage + class IssueMessage < BaseMessage + attr_reader :title + attr_reader :issue_iid + attr_reader :issue_url + attr_reader :action + attr_reader :state + attr_reader :description + + def initialize(params) + super + + obj_attr = params[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + @title = obj_attr[:title] + @issue_iid = obj_attr[:iid] + @issue_url = obj_attr[:url] + @action = obj_attr[:action] + @state = obj_attr[:state] + @description = obj_attr[:description] || '' + end + + def attachments + return [] unless opened_issue? + return description if markdown + + description_message + end + + def activity + { + title: "Issue #{state} by #{user_combined_name}", + subtitle: "in #{project_link}", + text: issue_link, + image: user_avatar + } + end + + private + + def message + "[#{project_link}] Issue #{issue_link} #{state} by #{user_combined_name}" + end + + def opened_issue? + action == 'open' + end + + def description_message + [{ + title: issue_title, + title_link: issue_url, + text: format(description), + color: '#C95823' + }] + end + + def project_link + link(project_name, project_url) + end + + def issue_link + link(issue_title, issue_url) + end + + def issue_title + "#{Issue.reference_prefix}#{issue_iid} #{title}" + end + end + end +end diff --git a/app/models/integrations/chat_message/merge_message.rb b/app/models/integrations/chat_message/merge_message.rb new file mode 100644 index 00000000000..d2f48699f50 --- /dev/null +++ b/app/models/integrations/chat_message/merge_message.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +module Integrations + module ChatMessage + class MergeMessage < BaseMessage + attr_reader :merge_request_iid + attr_reader :source_branch + attr_reader :target_branch + attr_reader :action + attr_reader :state + attr_reader :title + + def initialize(params) + super + + obj_attr = params[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + @merge_request_iid = obj_attr[:iid] + @source_branch = obj_attr[:source_branch] + @target_branch = obj_attr[:target_branch] + @action = obj_attr[:action] + @state = obj_attr[:state] + @title = format_title(obj_attr[:title]) + end + + def attachments + [] + end + + def activity + { + title: "Merge request #{state_or_action_text} by #{user_combined_name}", + subtitle: "in #{project_link}", + text: merge_request_link, + image: user_avatar + } + end + + private + + def format_title(title) + '*' + title.lines.first.chomp + '*' + end + + def message + merge_request_message + end + + def project_link + link(project_name, project_url) + end + + def merge_request_message + "#{user_combined_name} #{state_or_action_text} merge request #{merge_request_link} in #{project_link}" + end + + def merge_request_link + link(merge_request_title, merge_request_url) + end + + def merge_request_title + "#{MergeRequest.reference_prefix}#{merge_request_iid} #{title}" + end + + def merge_request_url + "#{project_url}/-/merge_requests/#{merge_request_iid}" + end + + def state_or_action_text + case action + when 'approved', 'unapproved' + action + when 'approval' + 'added their approval to' + when 'unapproval' + 'removed their approval from' + else + state + end + end + end + end +end diff --git a/app/models/integrations/chat_message/note_message.rb b/app/models/integrations/chat_message/note_message.rb new file mode 100644 index 00000000000..96675d2b27c --- /dev/null +++ b/app/models/integrations/chat_message/note_message.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +module Integrations + module ChatMessage + class NoteMessage < BaseMessage + attr_reader :note + attr_reader :note_url + attr_reader :title + attr_reader :target + + def initialize(params) + super + + params = HashWithIndifferentAccess.new(params) + obj_attr = params[:object_attributes] + @note = obj_attr[:note] + @note_url = obj_attr[:url] + @target, @title = case obj_attr[:noteable_type] + when "Commit" + create_commit_note(params[:commit]) + when "Issue" + create_issue_note(params[:issue]) + when "MergeRequest" + create_merge_note(params[:merge_request]) + when "Snippet" + create_snippet_note(params[:snippet]) + end + end + + def attachments + return note if markdown + + description_message + end + + def activity + { + title: "#{user_combined_name} #{link('commented on ' + target, note_url)}", + subtitle: "in #{project_link}", + text: formatted_title, + image: user_avatar + } + end + + private + + def message + "#{user_combined_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{formatted_title}*" + end + + def format_title(title) + title.lines.first.chomp + end + + def formatted_title + format_title(title) + end + + def create_issue_note(issue) + ["issue #{Issue.reference_prefix}#{issue[:iid]}", issue[:title]] + end + + def create_commit_note(commit) + commit_sha = Commit.truncate_sha(commit[:id]) + + ["commit #{commit_sha}", commit[:message]] + end + + def create_merge_note(merge_request) + ["merge request #{MergeRequest.reference_prefix}#{merge_request[:iid]}", merge_request[:title]] + end + + def create_snippet_note(snippet) + ["snippet #{Snippet.reference_prefix}#{snippet[:id]}", snippet[:title]] + end + + def description_message + [{ text: format(note), color: attachment_color }] + end + + def project_link + link(project_name, project_url) + end + end + end +end diff --git a/app/models/integrations/chat_message/pipeline_message.rb b/app/models/integrations/chat_message/pipeline_message.rb new file mode 100644 index 00000000000..a0f6f582e4c --- /dev/null +++ b/app/models/integrations/chat_message/pipeline_message.rb @@ -0,0 +1,267 @@ +# frozen_string_literal: true + +module Integrations + 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] + + # Get list of jobs that have actually failed (after exhausting all retries) + @failed_jobs = actually_failed_jobs(Array(data[:builds])) + @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 + '' + end + + def attachments + return message if markdown + + [{ + 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(only_path: false), + ts: finished_at + }] + end + + def activity + { + title: s_("ChatMessage|Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status}") % + { + pipeline_link: pipeline_link, + ref_type: ref_type, + ref_link: ref_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 actually_failed_jobs(builds) + succeeded_job_names = builds.map { |b| b[:name] if b[:status] == 'success' }.compact.uniq + + failed_jobs = builds.select do |build| + # Select jobs which doesn't have a successful retry + build[:status] == 'failed' && !succeeded_job_names.include?(build[:name]) + end + + failed_jobs.uniq { |job| job[:name] }.reverse + end + + def failed_stages_field + { + title: s_("ChatMessage|Failed stage").pluralize(failed_stages.length), + value: Slack::Messenger::Util::LinkFormatter.format(failed_stages_links), + short: true + } + end + + def failed_jobs_field + { + title: s_("ChatMessage|Failed job").pluralize(failed_jobs.length), + value: Slack::Messenger::Util::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::Messenger::Util::LinkFormatter.format(ref_link), + short: true + }, + { + title: s_("ChatMessage|Commit"), + value: Slack::Messenger::Util::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 + s_("ChatMessage|%{project_link}: Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status} in %{duration}") % + { + project_link: project_link, + pipeline_link: pipeline_link, + ref_type: ref_type, + ref_link: ref_link, + user_combined_name: user_combined_name, + humanized_status: humanized_status, + duration: pretty_duration(duration) + } + end + + def humanized_status + 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 + end + + def attachment_color + case status + when 'success' + detailed_status == 'passed with warnings' ? 'warning' : 'good' + else + 'danger' + end + end + + def ref_url + if ref_type == 'tag' + "#{project_url}/-/tags/#{ref}" + else + "#{project_url}/-/commits/#{ref}" + end + end + + def ref_link + "[#{ref}](#{ref_url})" + end + + def project_url + project.web_url + end + + def project_link + "[#{project.name}](#{project_url})" + end + + def pipeline_failed_jobs_url + "#{project_url}/-/pipelines/#{pipeline_id}/failures" + end + + def pipeline_url + if 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.t(:'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.t(:'support.array.words_connector')) + end + + def commit_url + Gitlab::UrlBuilder.build(commit) + end + + def commit_link + "[#{commit.title}](#{commit_url})" + end + + def author_url + return unless user && committer + + Gitlab::UrlBuilder.build(committer) + end + end + end +end diff --git a/app/models/integrations/chat_message/push_message.rb b/app/models/integrations/chat_message/push_message.rb new file mode 100644 index 00000000000..0952986e923 --- /dev/null +++ b/app/models/integrations/chat_message/push_message.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +module Integrations + module ChatMessage + class PushMessage < BaseMessage + attr_reader :after + attr_reader :before + attr_reader :commits + attr_reader :ref + attr_reader :ref_type + + def initialize(params) + super + + @after = params[:after] + @before = params[:before] + @commits = params.fetch(:commits, []) + @ref_type = Gitlab::Git.tag_ref?(params[:ref]) ? 'tag' : 'branch' + @ref = Gitlab::Git.ref_name(params[:ref]) + end + + def attachments + return [] if new_branch? || removed_branch? + return commit_messages if markdown + + commit_message_attachments + end + + def activity + { + title: humanized_action(short: true), + subtitle: "in #{project_link}", + text: compare_link, + image: user_avatar + } + end + + private + + def humanized_action(short: false) + action, ref_link, target_link = compose_action_details + text = [user_combined_name, action, ref_type, ref_link] + text << target_link unless short + text.join(' ') + end + + def message + humanized_action + end + + def format(string) + Slack::Messenger::Util::LinkFormatter.format(string) + end + + def commit_messages + commits.map { |commit| compose_commit_message(commit) }.join("\n\n") + end + + def commit_message_attachments + [{ text: format(commit_messages), color: attachment_color }] + end + + def compose_commit_message(commit) + author = commit[:author][:name] + id = Commit.truncate_sha(commit[:id]) + title = commit[:title] + + url = commit[:url] + + "[#{id}](#{url}): #{title} - #{author}" + end + + def new_branch? + Gitlab::Git.blank_ref?(before) + end + + def removed_branch? + Gitlab::Git.blank_ref?(after) + end + + def ref_url + if ref_type == 'tag' + "#{project_url}/-/tags/#{ref}" + else + "#{project_url}/commits/#{ref}" + end + end + + def compare_url + "#{project_url}/compare/#{before}...#{after}" + end + + def ref_link + "[#{ref}](#{ref_url})" + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def compare_link + "[Compare changes](#{compare_url})" + end + + def compose_action_details + if new_branch? + ['pushed new', ref_link, "to #{project_link}"] + elsif removed_branch? + ['removed', ref, "from #{project_link}"] + else + ['pushed to', ref_link, "of #{project_link} (#{compare_link})"] + end + end + + def attachment_color + '#345' + end + end + end +end diff --git a/app/models/integrations/chat_message/wiki_page_message.rb b/app/models/integrations/chat_message/wiki_page_message.rb new file mode 100644 index 00000000000..9b5275b8c03 --- /dev/null +++ b/app/models/integrations/chat_message/wiki_page_message.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Integrations + module ChatMessage + class WikiPageMessage < BaseMessage + attr_reader :title + attr_reader :wiki_page_url + attr_reader :action + attr_reader :description + + def initialize(params) + super + + obj_attr = params[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + @title = obj_attr[:title] + @wiki_page_url = obj_attr[:url] + @description = obj_attr[:message] + + @action = + case obj_attr[:action] + when "create" + "created" + when "update" + "edited" + end + end + + def attachments + return description if markdown + + description_message + end + + def activity + { + title: "#{user_combined_name} #{action} #{wiki_page_link}", + subtitle: "in #{project_link}", + text: title, + image: user_avatar + } + end + + private + + def message + "#{user_combined_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*" + end + + def description_message + [{ text: format(@description), color: attachment_color }] + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def wiki_page_link + "[wiki page](#{wiki_page_url})" + end + end + end +end |