diff options
Diffstat (limited to 'app/models')
23 files changed, 957 insertions, 947 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 diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb index fa7921afcbf..36edf646658 100644 --- a/app/models/packages/package.rb +++ b/app/models/packages/package.rb @@ -117,11 +117,6 @@ class Packages::Package < ApplicationRecord scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) } scope :has_version, -> { where.not(version: nil) } - scope :processed, -> do - where.not(package_type: :nuget).or( - where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) - ) - end scope :preload_files, -> { preload(:package_files) } scope :last_of_each_version, -> { where(id: all.select('MAX(id) AS id').group(:version)) } scope :limit_recent, ->(limit) { order_created_desc.limit(limit) } diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb index bd4b232849f..17131cd736d 100644 --- a/app/models/pages/lookup_path.rb +++ b/app/models/pages/lookup_path.rb @@ -62,17 +62,16 @@ module Pages } end + # TODO: remove support for legacy storage in 14.3 https://gitlab.com/gitlab-org/gitlab/-/issues/328712 + # we support this till 14.3 to allow people to still use legacy storage if something goes very wrong + # on self-hosted installations, and we'll need some time to fix it def legacy_source - raise LegacyStorageDisabledError unless Feature.enabled?(:pages_serve_from_legacy_storage, default_enabled: true) + return unless ::Settings.pages.local_store.enabled { type: 'file', path: File.join(project.full_path, 'public/') } - rescue LegacyStorageDisabledError => e - Gitlab::ErrorTracking.track_exception(e, project_id: project.id) - - nil end end end diff --git a/app/models/pages/virtual_domain.rb b/app/models/pages/virtual_domain.rb index 90cb8253b52..497f67993ae 100644 --- a/app/models/pages/virtual_domain.rb +++ b/app/models/pages/virtual_domain.rb @@ -21,9 +21,7 @@ module Pages project.pages_lookup_path(trim_prefix: trim_prefix, domain: domain) end - # TODO: remove in https://gitlab.com/gitlab-org/gitlab/-/issues/297524 - # source can only be nil if pages_serve_from_legacy_storage FF is disabled - # we can remove this filtering once we remove legacy storage + # TODO: remove in https://gitlab.com/gitlab-org/gitlab/-/issues/328715 paths = paths.select(&:source) paths.sort_by(&:prefix).reverse diff --git a/app/models/project_services/chat_message/alert_message.rb b/app/models/project_services/chat_message/alert_message.rb deleted file mode 100644 index c8913775843..00000000000 --- a/app/models/project_services/chat_message/alert_message.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -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 diff --git a/app/models/project_services/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb deleted file mode 100644 index bdd77a919e3..00000000000 --- a/app/models/project_services/chat_message/base_message.rb +++ /dev/null @@ -1,86 +0,0 @@ -# frozen_string_literal: true - -module ChatMessage - class BaseMessage - RELATIVE_LINK_REGEX = /!\[[^\]]*\]\((\/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 diff --git a/app/models/project_services/chat_message/deployment_message.rb b/app/models/project_services/chat_message/deployment_message.rb deleted file mode 100644 index 5deb757e60f..00000000000 --- a/app/models/project_services/chat_message/deployment_message.rb +++ /dev/null @@ -1,85 +0,0 @@ -# frozen_string_literal: true - -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 diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb deleted file mode 100644 index c8e90b66bae..00000000000 --- a/app/models/project_services/chat_message/issue_message.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -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 diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb deleted file mode 100644 index e45bb9b8ce1..00000000000 --- a/app/models/project_services/chat_message/merge_message.rb +++ /dev/null @@ -1,81 +0,0 @@ -# frozen_string_literal: true - -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 diff --git a/app/models/project_services/chat_message/note_message.rb b/app/models/project_services/chat_message/note_message.rb deleted file mode 100644 index 741474fb27b..00000000000 --- a/app/models/project_services/chat_message/note_message.rb +++ /dev/null @@ -1,84 +0,0 @@ -# frozen_string_literal: true - -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 diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb deleted file mode 100644 index f4c6938fa78..00000000000 --- a/app/models/project_services/chat_message/pipeline_message.rb +++ /dev/null @@ -1,265 +0,0 @@ -# frozen_string_literal: true - -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.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 author_url - return unless user && committer - - Gitlab::UrlBuilder.build(committer) - end - end -end diff --git a/app/models/project_services/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb deleted file mode 100644 index c8e70a69c88..00000000000 --- a/app/models/project_services/chat_message/push_message.rb +++ /dev/null @@ -1,118 +0,0 @@ -# frozen_string_literal: true - -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 diff --git a/app/models/project_services/chat_message/wiki_page_message.rb b/app/models/project_services/chat_message/wiki_page_message.rb deleted file mode 100644 index ebe7abb379f..00000000000 --- a/app/models/project_services/chat_message/wiki_page_message.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -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 diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index c95a5d4e6cb..2f841bf903e 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -185,19 +185,19 @@ class ChatNotificationService < Integration def get_message(object_kind, data) case object_kind when "push", "tag_push" - ChatMessage::PushMessage.new(data) if notify_for_ref?(data) + Integrations::ChatMessage::PushMessage.new(data) if notify_for_ref?(data) when "issue" - ChatMessage::IssueMessage.new(data) unless update?(data) + Integrations::ChatMessage::IssueMessage.new(data) unless update?(data) when "merge_request" - ChatMessage::MergeMessage.new(data) unless update?(data) + Integrations::ChatMessage::MergeMessage.new(data) unless update?(data) when "note" - ChatMessage::NoteMessage.new(data) + Integrations::ChatMessage::NoteMessage.new(data) when "pipeline" - ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data) + Integrations::ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data) when "wiki_page" - ChatMessage::WikiPageMessage.new(data) + Integrations::ChatMessage::WikiPageMessage.new(data) when "deployment" - ChatMessage::DeploymentMessage.new(data) + Integrations::ChatMessage::DeploymentMessage.new(data) end end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 7badcc24870..92a46f8d01f 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -39,7 +39,7 @@ class SlackService < ChatNotificationService end def get_message(object_kind, data) - return ChatMessage::AlertMessage.new(data) if object_kind == 'alert' + return Integrations::ChatMessage::AlertMessage.new(data) if object_kind == 'alert' super end |