diff options
Diffstat (limited to 'app/models/project_services')
44 files changed, 148 insertions, 1919 deletions
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb deleted file mode 100644 index f31bf931a41..00000000000 --- a/app/models/project_services/asana_service.rb +++ /dev/null @@ -1,107 +0,0 @@ -# frozen_string_literal: true - -require 'asana' - -class AsanaService < Service - include ActionView::Helpers::UrlHelper - - prop_accessor :api_key, :restrict_to_branch - validates :api_key, presence: true, if: :activated? - - def title - 'Asana' - end - - def description - s_('AsanaService|Add commit messages as comments to Asana tasks') - end - - def help - docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/asana'), target: '_blank', rel: 'noopener noreferrer' - s_('Add commit messages as comments to Asana tasks. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } - end - - def self.to_param - 'asana' - end - - def fields - [ - { - type: 'text', - name: 'api_key', - title: 'API key', - help: s_('AsanaService|User Personal Access Token. User must have access to the task. All comments are attributed to this user.'), - # Example Personal Access Token from Asana docs - placeholder: '0/68a9e79b868c6789e79a124c30b0', - required: true - }, - { - type: 'text', - name: 'restrict_to_branch', - title: 'Restrict to branch (optional)', - help: s_('AsanaService|Comma-separated list of branches to be automatically inspected. Leave blank to include all branches.') - } - ] - end - - def self.supported_events - %w(push) - end - - def client - @_client ||= begin - Asana::Client.new do |c| - c.authentication :access_token, api_key - end - end - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - - # check the branch restriction is poplulated and branch is not included - branch = Gitlab::Git.ref_name(data[:ref]) - branch_restriction = restrict_to_branch.to_s - if branch_restriction.present? && branch_restriction.index(branch).nil? - return - end - - user = data[:user_name] - project_name = project.full_name - - data[:commits].each do |commit| - push_msg = s_("AsanaService|%{user} pushed to branch %{branch} of %{project_name} ( %{commit_url} ):") % { user: user, branch: branch, project_name: project_name, commit_url: commit[:url] } - check_commit(commit[:message], push_msg) - end - end - - def check_commit(message, push_msg) - # matches either: - # - #1234 - # - https://app.asana.com/0/{project_gid}/{task_gid} - # optionally preceded with: - # - fix/ed/es/ing - # - close/s/d - # - closing - issue_finder = %r{(fix\w*|clos[ei]\w*+)?\W*(?:https://app\.asana\.com/\d+/\w+/(\w+)|#(\w+))}i - - message.scan(issue_finder).each do |tuple| - # tuple will be - # [ 'fix', 'id_from_url', 'id_from_pound' ] - taskid = tuple[2] || tuple[1] - - begin - task = Asana::Resources::Task.find_by_id(client, taskid) - task.add_comment(text: "#{push_msg} #{message}") - - if tuple[0] - task.update(completed: true) - end - rescue => e - log_error(e.message) - next - end - end - end -end diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb deleted file mode 100644 index 8845fb99605..00000000000 --- a/app/models/project_services/assembla_service.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -class AssemblaService < Service - prop_accessor :token, :subdomain - validates :token, presence: true, if: :activated? - - def title - 'Assembla' - end - - def description - _('Manage projects.') - end - - def self.to_param - 'assembla' - end - - def fields - [ - { type: 'text', name: 'token', placeholder: '', required: true }, - { type: 'text', name: 'subdomain', placeholder: '' } - ] - end - - def self.supported_events - %w(push) - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - - url = "https://atlas.assembla.com/spaces/#{subdomain}/github_tool?secret_key=#{token}" - Gitlab::HTTP.post(url, body: { payload: data }.to_json, headers: { 'Content-Type' => 'application/json' }) - end -end diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb deleted file mode 100644 index a892d1a4314..00000000000 --- a/app/models/project_services/bamboo_service.rb +++ /dev/null @@ -1,181 +0,0 @@ -# frozen_string_literal: true - -class BambooService < CiService - include ActionView::Helpers::UrlHelper - include ReactiveService - - prop_accessor :bamboo_url, :build_key, :username, :password - - validates :bamboo_url, presence: true, public_url: true, if: :activated? - validates :build_key, presence: true, if: :activated? - validates :username, - presence: true, - if: ->(service) { service.activated? && service.password } - validates :password, - presence: true, - if: ->(service) { service.activated? && service.username } - - attr_accessor :response - - after_save :compose_service_hook, if: :activated? - before_update :reset_password - - def compose_service_hook - hook = service_hook || build_service_hook - hook.save - end - - def reset_password - if bamboo_url_changed? && !password_touched? - self.password = nil - end - end - - def title - s_('BambooService|Atlassian Bamboo') - end - - def description - s_('BambooService|Use the Atlassian Bamboo CI/CD server with GitLab.') - end - - def help - docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bamboo'), target: '_blank', rel: 'noopener noreferrer' - s_('BambooService|Use Atlassian Bamboo to run CI/CD pipelines. You must set up automatic revision labeling and a repository trigger in Bamboo. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } - end - - def self.to_param - 'bamboo' - end - - def fields - [ - { - type: 'text', - name: 'bamboo_url', - title: s_('BambooService|Bamboo URL'), - placeholder: s_('https://bamboo.example.com'), - help: s_('BambooService|Bamboo service root URL.'), - required: true - }, - { - type: 'text', - name: 'build_key', - placeholder: s_('KEY'), - help: s_('BambooService|Bamboo build plan key.'), - required: true - }, - { - type: 'text', - name: 'username', - help: s_('BambooService|The user with API access to the Bamboo server.') - }, - { - type: 'password', - name: 'password', - non_empty_password_title: s_('ProjectService|Enter new password'), - non_empty_password_help: s_('ProjectService|Leave blank to use your current password') - } - ] - end - - def build_page(sha, ref) - with_reactive_cache(sha, ref) {|cached| cached[:build_page] } - end - - def commit_status(sha, ref) - with_reactive_cache(sha, ref) {|cached| cached[:commit_status] } - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - - get_path("updateAndBuild.action", { buildKey: build_key }) - end - - def calculate_reactive_cache(sha, ref) - response = try_get_path("rest/api/latest/result/byChangeset/#{sha}") - - { build_page: read_build_page(response), commit_status: read_commit_status(response) } - end - - private - - def get_build_result(response) - return if response&.code != 200 - - # May be nil if no result, a single result hash, or an array if multiple results for a given changeset. - result = response.dig('results', 'results', 'result') - - # In case of multiple results, arbitrarily assume the last one is the most relevant. - return result.last if result.is_a?(Array) - - result - end - - def read_build_page(response) - result = get_build_result(response) - key = - if result.blank? - # If actual build link can't be determined, send user to build summary page. - build_key - else - # If actual build link is available, go to build result page. - result.dig('planResultKey', 'key') - end - - build_url("browse/#{key}") - end - - def read_commit_status(response) - return :error unless response && (response.code == 200 || response.code == 404) - - result = get_build_result(response) - status = - if result.blank? - 'Pending' - else - result.dig('buildState') - end - - return :error unless status.present? - - if status.include?('Success') - 'success' - elsif status.include?('Failed') - 'failed' - elsif status.include?('Pending') - 'pending' - else - :error - end - end - - def try_get_path(path, query_params = {}) - params = build_get_params(query_params) - params[:extra_log_info] = { project_id: project_id } - - Gitlab::HTTP.try_get(build_url(path), params) - end - - def get_path(path, query_params = {}) - Gitlab::HTTP.get(build_url(path), build_get_params(query_params)) - end - - def build_url(path) - Gitlab::Utils.append_path(bamboo_url, path) - end - - def build_get_params(query_params) - params = { verify: false, query: query_params } - return params if username.blank? && password.blank? - - query_params[:os_authType] = 'basic' - params[:basic_auth] = basic_auth - params - end - - def basic_auth - { username: username, password: password } - end -end diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb index 4332db3e961..d1c56d2a4d5 100644 --- a/app/models/project_services/bugzilla_service.rb +++ b/app/models/project_services/bugzilla_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class BugzillaService < IssueTrackerService + include ActionView::Helpers::UrlHelper + validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? def title @@ -8,7 +10,12 @@ class BugzillaService < IssueTrackerService end def description - s_('IssueTracker|Bugzilla issue tracker') + s_("IssueTracker|Use Bugzilla as this project's issue tracker.") + end + + def help + docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bugzilla'), target: '_blank', rel: 'noopener noreferrer' + s_("IssueTracker|Use Bugzilla as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe } end def self.to_param diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index 53bb7b47b41..f2ea5066e37 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -68,7 +68,7 @@ class BuildkiteService < CiService end def description - 'Buildkite is a platform for running fast, secure, and scalable continuous integration pipelines on your own infrastructure' + 'Run CI/CD pipelines with Buildkite.' end def self.to_param diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb deleted file mode 100644 index f2295a95b60..00000000000 --- a/app/models/project_services/builds_email_service.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -# This class is to be removed with 9.1 -# We should also by then remove BuildsEmailService from database -class BuildsEmailService < Service - def self.to_param - 'builds_email' - end - - def self.supported_events - %w[] - end -end diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb deleted file mode 100644 index ad26e42a21b..00000000000 --- a/app/models/project_services/campfire_service.rb +++ /dev/null @@ -1,102 +0,0 @@ -# frozen_string_literal: true - -class CampfireService < Service - prop_accessor :token, :subdomain, :room - validates :token, presence: true, if: :activated? - - def title - 'Campfire' - end - - def description - 'Simple web-based real-time group chat' - end - - def self.to_param - 'campfire' - end - - def fields - [ - { type: 'text', name: 'token', placeholder: '', required: true }, - { type: 'text', name: 'subdomain', placeholder: '' }, - { type: 'text', name: 'room', placeholder: '' } - ] - end - - def self.supported_events - %w(push) - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - - message = build_message(data) - speak(self.room, message, auth) - end - - private - - def base_uri - @base_uri ||= "https://#{subdomain}.campfirenow.com" - end - - def auth - # use a dummy password, as explained in the Campfire API doc: - # https://github.com/basecamp/campfire-api#authentication - @auth ||= { - basic_auth: { - username: token, - password: 'X' - } - } - end - - # Post a message into a room, returns the message Hash in case of success. - # Returns nil otherwise. - # https://github.com/basecamp/campfire-api/blob/master/sections/messages.md#create-message - def speak(room_name, message, auth) - room = rooms(auth).find { |r| r["name"] == room_name } - return unless room - - path = "/room/#{room["id"]}/speak.json" - body = { - body: { - message: { - type: 'TextMessage', - body: message - } - } - } - res = Gitlab::HTTP.post(path, base_uri: base_uri, **auth.merge(body)) - res.code == 201 ? res : nil - end - - # Returns a list of rooms, or []. - # https://github.com/basecamp/campfire-api/blob/master/sections/rooms.md#get-rooms - def rooms(auth) - res = Gitlab::HTTP.get("/rooms.json", base_uri: base_uri, **auth) - res.code == 200 ? res["rooms"] : [] - end - - def build_message(push) - ref = Gitlab::Git.ref_name(push[:ref]) - before = push[:before] - after = push[:after] - - message = [] - message << "[#{project.full_name}] " - message << "#{push[:user_name]} " - - if Gitlab::Git.blank_ref?(before) - message << "pushed new branch #{ref} \n" - elsif Gitlab::Git.blank_ref?(after) - message << "removed branch #{ref} \n" - else - message << "pushed #{push[:total_commits_count]} commits to #{ref}. " - message << "#{project.web_url}/compare/#{before}...#{after}" - end - - message.join - end -end 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 4a99842b4d5..2f841bf903e 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -2,7 +2,7 @@ # Base class for Chat notifications services # This class is not meant to be used directly, but only to inherit from. -class ChatNotificationService < Service +class ChatNotificationService < Integration include ChatMessage include NotificationBranchSelection @@ -15,9 +15,14 @@ class ChatNotificationService < Service EVENT_CHANNEL = proc { |event| "#{event}_channel" } + LABEL_NOTIFICATION_BEHAVIOURS = [ + MATCH_ANY_LABEL = 'match_any', + MATCH_ALL_LABELS = 'match_all' + ].freeze + default_value_for :category, 'chat' - prop_accessor :webhook, :username, :channel, :branches_to_be_notified, :labels_to_be_notified + prop_accessor :webhook, :username, :channel, :branches_to_be_notified, :labels_to_be_notified, :labels_to_be_notified_behavior # Custom serialized properties initialization prop_accessor(*SUPPORTED_EVENTS.map { |event| EVENT_CHANNEL[event] }) @@ -25,12 +30,14 @@ class ChatNotificationService < Service boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch validates :webhook, presence: true, public_url: true, if: :activated? + validates :labels_to_be_notified_behavior, inclusion: { in: LABEL_NOTIFICATION_BEHAVIOURS }, allow_blank: true def initialize_properties if properties.nil? self.properties = {} self.notify_only_broken_pipelines = true self.branches_to_be_notified = "default" + self.labels_to_be_notified_behavior = MATCH_ANY_LABEL elsif !self.notify_only_default_branch.nil? # In older versions, there was only a boolean property named # `notify_only_default_branch`. Now we have a string property named @@ -65,7 +72,20 @@ class ChatNotificationService < Service { type: 'text', name: 'username', placeholder: 'GitLab-integration' }.freeze, { type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'Do not send notifications for successful pipelines.' }.freeze, { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }.freeze, - { type: 'text', name: 'labels_to_be_notified', placeholder: '~backend,~frontend', help: 'Send notifications for issue, merge request, and comment events with the listed labels only. Leave blank to receive notifications for all events.' }.freeze + { + type: 'text', + name: 'labels_to_be_notified', + placeholder: '~backend,~frontend', + help: 'Send notifications for issue, merge request, and comment events with the listed labels only. Leave blank to receive notifications for all events.' + }.freeze, + { + type: 'select', + name: 'labels_to_be_notified_behavior', + choices: [ + ['Match any of the labels', MATCH_ANY_LABEL], + ['Match all of the labels', MATCH_ALL_LABELS] + ] + }.freeze ].freeze end @@ -136,11 +156,17 @@ class ChatNotificationService < Service def notify_label?(data) return true unless SUPPORTED_EVENTS_FOR_LABEL_FILTER.include?(data[:object_kind]) && labels_to_be_notified.present? - issue_labels = data.dig(:issue, :labels) || [] - merge_request_labels = data.dig(:merge_request, :labels) || [] - label_titles = (issue_labels + merge_request_labels).pluck(:title) + labels = data.dig(:issue, :labels) || data.dig(:merge_request, :labels) + + return false if labels.nil? - (labels_to_be_notified_list & label_titles).any? + matching_labels = labels_to_be_notified_list & labels.pluck(:title) + + if labels_to_be_notified_behavior == MATCH_ALL_LABELS + labels_to_be_notified_list.difference(matching_labels).empty? + else + matching_labels.any? + end end def user_id_from_hook_data(data) @@ -159,19 +185,19 @@ class ChatNotificationService < Service 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/ci_service.rb b/app/models/project_services/ci_service.rb index 29edb9ec16f..0733da761d5 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -3,7 +3,7 @@ # Base class for CI services # List methods you need to implement to get your CI service # working with GitLab merge requests -class CiService < Service +class CiService < Integration default_value_for :category, 'ci' def valid_token?(token) diff --git a/app/models/project_services/confluence_service.rb b/app/models/project_services/confluence_service.rb deleted file mode 100644 index 8a6f4de540c..00000000000 --- a/app/models/project_services/confluence_service.rb +++ /dev/null @@ -1,91 +0,0 @@ -# frozen_string_literal: true - -class ConfluenceService < Service - include ActionView::Helpers::UrlHelper - - VALID_SCHEME_MATCH = %r{\Ahttps?\Z}.freeze - VALID_HOST_MATCH = %r{\A.+\.atlassian\.net\Z}.freeze - VALID_PATH_MATCH = %r{\A/wiki(/|\Z)}.freeze - - prop_accessor :confluence_url - - validates :confluence_url, presence: true, if: :activated? - validate :validate_confluence_url_is_cloud, if: :activated? - - after_commit :cache_project_has_confluence - - def self.to_param - 'confluence' - end - - def self.supported_events - %w() - end - - def title - s_('ConfluenceService|Confluence Workspace') - end - - def description - s_('ConfluenceService|Connect a Confluence Cloud Workspace to GitLab') - end - - def help - return unless project&.wiki_enabled? - - if activated? - wiki_url = project.wiki.web_url - - s_( - 'ConfluenceService|Your GitLab Wiki can be accessed here: %{wiki_link}. To re-enable your GitLab Wiki, disable this integration' % - { wiki_link: link_to(wiki_url, wiki_url) } - ).html_safe - else - s_('ConfluenceService|Enabling the Confluence Workspace will disable the default GitLab Wiki. Your GitLab Wiki data will be saved and you can always re-enable it later by turning off this integration').html_safe - end - end - - def fields - [ - { - type: 'text', - name: 'confluence_url', - title: 'Confluence Cloud Workspace URL', - placeholder: s_('ConfluenceService|The URL of the Confluence Workspace'), - required: true - } - ] - end - - def can_test? - false - end - - private - - def validate_confluence_url_is_cloud - unless confluence_uri_valid? - errors.add(:confluence_url, 'URL must be to a Confluence Cloud Workspace hosted on atlassian.net') - end - end - - def confluence_uri_valid? - return false unless confluence_url - - uri = URI.parse(confluence_url) - - (uri.scheme&.match(VALID_SCHEME_MATCH) && - uri.host&.match(VALID_HOST_MATCH) && - uri.path&.match(VALID_PATH_MATCH)).present? - - rescue URI::InvalidURIError - false - end - - def cache_project_has_confluence - return unless project && !project.destroyed? - - project.project_setting.save! unless project.project_setting.persisted? - project.project_setting.update_column(:has_confluence, active?) - end -end diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb index aab8661ec55..6f99d104904 100644 --- a/app/models/project_services/custom_issue_tracker_service.rb +++ b/app/models/project_services/custom_issue_tracker_service.rb @@ -1,25 +1,23 @@ # frozen_string_literal: true class CustomIssueTrackerService < IssueTrackerService + include ActionView::Helpers::UrlHelper validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? def title - 'Custom Issue Tracker' + s_('IssueTracker|Custom issue tracker') end def description - s_('IssueTracker|Custom issue tracker') + s_("IssueTracker|Use a custom issue tracker as this project's issue tracker.") end - def self.to_param - 'custom_issue_tracker' + def help + docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/custom_issue_tracker'), target: '_blank', rel: 'noopener noreferrer' + s_('IssueTracker|Use a custom issue tracker that is not in the integration list. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end - def fields - [ - { type: 'text', name: 'project_url', title: _('Project URL'), required: true }, - { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), required: true }, - { type: 'text', name: 'new_issue_url', title: s_('ProjectService|New issue URL'), required: true } - ] + def self.to_param + 'custom_issue_tracker' end end diff --git a/app/models/project_services/data_fields.rb b/app/models/project_services/data_fields.rb index 12ebf260e08..ca4dc0375fb 100644 --- a/app/models/project_services/data_fields.rb +++ b/app/models/project_services/data_fields.rb @@ -42,9 +42,9 @@ module DataFields end included do - has_one :issue_tracker_data, autosave: true - has_one :jira_tracker_data, autosave: true - has_one :open_project_tracker_data, autosave: true + has_one :issue_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id + has_one :jira_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id + has_one :open_project_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id def data_fields raise NotImplementedError diff --git a/app/models/project_services/datadog_service.rb b/app/models/project_services/datadog_service.rb deleted file mode 100644 index 9a2d99c46c9..00000000000 --- a/app/models/project_services/datadog_service.rb +++ /dev/null @@ -1,144 +0,0 @@ -# frozen_string_literal: true - -class DatadogService < Service - DEFAULT_SITE = 'datadoghq.com' - URL_TEMPLATE = 'https://webhooks-http-intake.logs.%{datadog_site}/v1/input/' - URL_TEMPLATE_API_KEYS = 'https://app.%{datadog_site}/account/settings#api' - URL_API_KEYS_DOCS = "https://docs.#{DEFAULT_SITE}/account_management/api-app-keys/" - - SUPPORTED_EVENTS = %w[ - pipeline job - ].freeze - - prop_accessor :datadog_site, :api_url, :api_key, :datadog_service, :datadog_env - - with_options if: :activated? do - validates :api_key, presence: true, format: { with: /\A\w+\z/ } - validates :datadog_site, format: { with: /\A[\w\.]+\z/, allow_blank: true } - validates :api_url, public_url: { allow_blank: true } - validates :datadog_site, presence: true, unless: -> (obj) { obj.api_url.present? } - validates :api_url, presence: true, unless: -> (obj) { obj.datadog_site.present? } - end - - after_save :compose_service_hook, if: :activated? - - def initialize_properties - super - - self.datadog_site ||= DEFAULT_SITE - end - - def self.supported_events - SUPPORTED_EVENTS - end - - def self.default_test_event - 'pipeline' - end - - def configurable_events - [] # do not allow to opt out of required hooks - end - - def title - 'Datadog' - end - - def description - 'Trace your GitLab pipelines with Datadog' - end - - def help - nil - # Maybe adding something in the future - # We could link to static help pages as well - # [More information](#{Gitlab::Routing.url_helpers.help_page_url('integration/datadog')})" - end - - def self.to_param - 'datadog' - end - - def fields - [ - { - type: 'text', - name: 'datadog_site', - placeholder: DEFAULT_SITE, - help: 'Choose the Datadog site to send data to. Set to "datadoghq.eu" to send data to the EU site', - required: false - }, - { - type: 'text', - name: 'api_url', - title: 'API URL', - help: '(Advanced) Define the full URL for your Datadog site directly', - required: false - }, - { - type: 'password', - name: 'api_key', - title: _('API key'), - non_empty_password_title: s_('ProjectService|Enter new API key'), - non_empty_password_help: s_('ProjectService|Leave blank to use your current API key'), - help: "<a href=\"#{api_keys_url}\" target=\"_blank\">API key</a> used for authentication with Datadog", - required: true - }, - { - type: 'text', - name: 'datadog_service', - title: 'Service', - placeholder: 'gitlab-ci', - help: 'Name of this GitLab instance that all data will be tagged with' - }, - { - type: 'text', - name: 'datadog_env', - title: 'Env', - help: 'The environment tag that traces will be tagged with' - } - ] - end - - def compose_service_hook - hook = service_hook || build_service_hook - hook.url = hook_url - hook.save - end - - def hook_url - url = api_url.presence || sprintf(URL_TEMPLATE, datadog_site: datadog_site) - url = URI.parse(url) - url.path = File.join(url.path || '/', api_key) - query = { service: datadog_service.presence, env: datadog_env.presence }.compact - url.query = query.to_query unless query.empty? - url.to_s - end - - def api_keys_url - return URL_API_KEYS_DOCS unless datadog_site.presence - - sprintf(URL_TEMPLATE_API_KEYS, datadog_site: datadog_site) - end - - def execute(data) - return if project.disabled_services.include?(to_param) - - object_kind = data[:object_kind] - object_kind = 'job' if object_kind == 'build' - return unless supported_events.include?(object_kind) - - service_hook.execute(data, "#{object_kind} hook") - end - - def test(data) - begin - result = execute(data) - return { success: false, result: result[:message] } if result[:http_status] != 200 - rescue StandardError => error - return { success: false, result: error } - end - - { success: true, result: result[:message] } - end -end diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb deleted file mode 100644 index cdb69684d16..00000000000 --- a/app/models/project_services/emails_on_push_service.rb +++ /dev/null @@ -1,97 +0,0 @@ -# frozen_string_literal: true - -class EmailsOnPushService < Service - include NotificationBranchSelection - - RECIPIENTS_LIMIT = 750 - - boolean_accessor :send_from_committer_email - boolean_accessor :disable_diffs - prop_accessor :recipients, :branches_to_be_notified - validates :recipients, presence: true, if: :validate_recipients? - validate :number_of_recipients_within_limit, if: :validate_recipients? - - def self.valid_recipients(recipients) - recipients.split.select do |recipient| - recipient.include?('@') - end.uniq(&:downcase) - end - - def title - s_('EmailsOnPushService|Emails on push') - end - - def description - s_('EmailsOnPushService|Email the commits and diff of each push to a list of recipients.') - end - - def self.to_param - 'emails_on_push' - end - - def self.supported_events - %w(push tag_push) - end - - def initialize_properties - super - - self.branches_to_be_notified = 'all' if branches_to_be_notified.nil? - end - - def execute(push_data) - return unless supported_events.include?(push_data[:object_kind]) - return if project.emails_disabled? - return unless notify_for_ref?(push_data) - - EmailsOnPushWorker.perform_async( - project_id, - recipients, - push_data, - send_from_committer_email: send_from_committer_email?, - disable_diffs: disable_diffs? - ) - end - - def notify_for_ref?(push_data) - return true if push_data[:object_kind] == 'tag_push' - return true if push_data.dig(:object_attributes, :tag) - - notify_for_branch?(push_data) - end - - def send_from_committer_email? - Gitlab::Utils.to_boolean(self.send_from_committer_email) - end - - def disable_diffs? - Gitlab::Utils.to_boolean(self.disable_diffs) - end - - def fields - domains = Notify.allowed_email_domains.map { |domain| "user@#{domain}" }.join(", ") - [ - { type: 'checkbox', name: 'send_from_committer_email', title: s_("EmailsOnPushService|Send from committer"), - help: s_("EmailsOnPushService|Send notifications from the committer's email address if the domain matches the domain used by your GitLab instance (such as %{domains}).") % { domains: domains } }, - { type: 'checkbox', name: 'disable_diffs', title: s_("EmailsOnPushService|Disable code diffs"), - help: s_("EmailsOnPushService|Don't include possibly sensitive code diffs in notification body.") }, - { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }, - { - type: 'textarea', - name: 'recipients', - placeholder: s_('EmailsOnPushService|tanuki@example.com gitlab@example.com'), - help: s_('EmailsOnPushService|Emails separated by whitespace.') - } - ] - end - - private - - def number_of_recipients_within_limit - return if recipients.blank? - - if self.class.valid_recipients(recipients).size > RECIPIENTS_LIMIT - errors.add(:recipients, s_("EmailsOnPushService|can't exceed %{recipients_limit}") % { recipients_limit: RECIPIENTS_LIMIT }) - end - end -end diff --git a/app/models/project_services/ewm_service.rb b/app/models/project_services/ewm_service.rb index af402e50292..90fcbb10d2b 100644 --- a/app/models/project_services/ewm_service.rb +++ b/app/models/project_services/ewm_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class EwmService < IssueTrackerService + include ActionView::Helpers::UrlHelper + validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? def self.reference_pattern(only_long: true) @@ -12,7 +14,12 @@ class EwmService < IssueTrackerService end def description - s_('IssueTracker|EWM work items tracker') + s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker.") + end + + def help + docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/ewm'), target: '_blank', rel: 'noopener noreferrer' + s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe } end def self.to_param diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb index c41783d1af4..f49b008533d 100644 --- a/app/models/project_services/external_wiki_service.rb +++ b/app/models/project_services/external_wiki_service.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true -class ExternalWikiService < Service +class ExternalWikiService < Integration include ActionView::Helpers::UrlHelper + prop_accessor :external_wiki_url validates :external_wiki_url, presence: true, public_url: true, if: :activated? @@ -39,7 +40,7 @@ class ExternalWikiService < Service def execute(_data) response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true) response.body if response.code == 200 - rescue + rescue StandardError nil end diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index e721fded1d9..7aae5af7454 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true -class FlowdockService < Service +class FlowdockService < Integration + include ActionView::Helpers::UrlHelper + prop_accessor :token validates :token, presence: true, if: :activated? @@ -9,7 +11,12 @@ class FlowdockService < Service end def description - s_('FlowdockService|Flowdock is a collaboration web app for technical teams.') + s_('FlowdockService|Send event notifications from GitLab to Flowdock flows.') + end + + def help + docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('api/services', anchor: 'flowdock'), target: '_blank', rel: 'noopener noreferrer' + s_('FlowdockService|Send event notifications from GitLab to Flowdock flows. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end def self.to_param @@ -18,7 +25,7 @@ class FlowdockService < Service def fields [ - { type: 'text', name: 'token', placeholder: s_('FlowdockService|Flowdock Git source token'), required: true } + { type: 'text', name: 'token', placeholder: s_('FlowdockService|1b609b52537...'), required: true, help: 'Enter your Flowdock token.' } ] end diff --git a/app/models/project_services/hangouts_chat_service.rb b/app/models/project_services/hangouts_chat_service.rb index 299a306add7..6e7708a169f 100644 --- a/app/models/project_services/hangouts_chat_service.rb +++ b/app/models/project_services/hangouts_chat_service.rb @@ -3,12 +3,14 @@ require 'hangouts_chat' class HangoutsChatService < ChatNotificationService + include ActionView::Helpers::UrlHelper + def title - 'Hangouts Chat' + 'Google Chat' end def description - 'Receive event notifications in Google Hangouts Chat' + 'Send notifications from GitLab to a room in Google Chat.' end def self.to_param @@ -16,13 +18,8 @@ class HangoutsChatService < ChatNotificationService end def help - 'This service sends notifications about projects events to Google Hangouts Chat room.<br /> - To set up this service: - <ol> - <li><a href="https://developers.google.com/hangouts/chat/how-tos/webhooks">Set up an incoming webhook for your room</a>. All notifications will come to this room.</li> - <li>Paste the <strong>Webhook URL</strong> into the field below.</li> - <li>Select events below to enable notifications.</li> - </ol>' + docs_link = link_to _('How do I set up a Google Chat webhook?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/hangouts_chat'), target: '_blank', rel: 'noopener noreferrer' + s_('Before enabling this integration, create a webhook for the room in Google Chat where you want to receive notifications from this project. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end def event_field(event) @@ -42,7 +39,7 @@ class HangoutsChatService < ChatNotificationService def default_fields [ - { type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" }, + { type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}" }, { type: 'checkbox', name: 'notify_only_broken_pipelines' }, { type: 'select', name: 'branches_to_be_notified', choices: branch_choices } ] diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index cd49c6d253d..71d8e7bfac4 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -1,54 +1,17 @@ # frozen_string_literal: true -class HipchatService < Service - include ActionView::Helpers::SanitizeHelper - - MAX_COMMITS = 3 - HIPCHAT_ALLOWED_TAGS = %w[ - a b i strong em br img pre code - table th tr td caption colgroup col thead tbody tfoot - ul ol li dl dt dd - ].freeze - - prop_accessor :token, :room, :server, :color, :api_version - boolean_accessor :notify_only_broken_pipelines, :notify - validates :token, presence: true, if: :activated? - - def initialize_properties - if properties.nil? - self.properties = {} - self.notify_only_broken_pipelines = true - end - end - - def title - 'HipChat' - end - - def description - 'Private group chat and IM' - end +# This service is scheduled for removal. All records must +# be deleted before the class can be removed. +# https://gitlab.com/gitlab-org/gitlab/-/issues/27954 +class HipchatService < Integration + before_save :prevent_save def self.to_param 'hipchat' end - def fields - [ - { type: 'text', name: 'token', placeholder: 'Room token', required: true }, - { type: 'text', name: 'room', placeholder: 'Room name or ID' }, - { type: 'checkbox', name: 'notify' }, - { type: 'select', name: 'color', choices: %w(yellow red green purple gray random) }, - { type: 'text', name: 'api_version', title: _('API version'), - placeholder: 'Leave blank for default (v2)' }, - { type: 'text', name: 'server', - placeholder: 'Leave blank for default. https://hipchat.example.com' }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' } - ] - end - def self.supported_events - %w(push issue confidential_issue merge_request note confidential_note tag_push pipeline) + [] end def execute(data) @@ -56,96 +19,14 @@ class HipchatService < Service # HipChat is unusable anyway, so do nothing in this method end - def test(data) - begin - result = execute(data) - rescue StandardError => error - return { success: false, result: error } - end - - { success: true, result: result } - end - private - def message_options(data = nil) - { notify: notify.present? && Gitlab::Utils.to_boolean(notify), color: message_color(data) } - end - - def render_line(text) - markdown(text.lines.first.chomp, pipeline: :single_line) if text - end - - def markdown(text, options = {}) - return "" unless text - - context = { - project: project, - pipeline: :email - } - - Banzai.render(text, context) - - context.merge!(options) - - html = Banzai.render_and_post_process(text, context) - sanitized_html = sanitize(html, tags: HIPCHAT_ALLOWED_TAGS, attributes: %w[href title alt]) - - sanitized_html.truncate(200, separator: ' ', omission: '...') - end - - def format_title(title) - "<b>#{render_line(title)}</b>" - end - - def message_color(data) - pipeline_status_color(data) || color || 'yellow' - end - - def pipeline_status_color(data) - return unless data && data[:object_kind] == 'pipeline' - - case data[:object_attributes][:status] - when 'success' - 'green' - else - 'red' - end - end - - def project_name - project.full_name.gsub(/\s/, '') - end - - def project_url - project.web_url - end - - def project_link - "<a href=\"#{project_url}\">#{project_name}</a>" - end - - def update?(data) - data[:object_attributes][:action] == 'update' - end - - def humanized_status(status) - case status - when 'success' - 'passed' - else - status - end - end + def prevent_save + errors.add(:base, _('HipChat endpoint is deprecated and should not be created or modified.')) - def should_pipeline_be_notified?(data) - case data[:object_attributes][:status] - when 'success' - !notify_only_broken_pipelines? - when 'failed' - true - else - false - end + # Stops execution of callbacks and database operation while + # preserving expectations of #save (will not raise) & #save! (raises) + # https://guides.rubyonrails.org/active_record_callbacks.html#halting-execution + throw :abort # rubocop:disable Cop/BanCatchThrow end end diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index 4f1ce16ebb2..5cca620c659 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -2,7 +2,7 @@ require 'uri' -class IrkerService < Service +class IrkerService < Integration prop_accessor :server_host, :server_port, :default_irc_uri prop_accessor :recipients, :channels boolean_accessor :colorize_messages @@ -15,8 +15,7 @@ class IrkerService < Service end def description - 'Send IRC messages, on update, to a list of recipients through an Irker '\ - 'gateway.' + 'Send IRC messages.' end def self.to_param @@ -103,7 +102,7 @@ class IrkerService < Service begin new_recipient = URI.join(default_irc_uri, '/', recipient).to_s uri = consider_uri(URI.parse(new_recipient)) - rescue + rescue StandardError log_error("Unable to create a valid URL", default_irc_uri: default_irc_uri, recipient: recipient) end end diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 19a5b4a74bb..099e3c336dd 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class IssueTrackerService < Service +class IssueTrackerService < Integration validate :one_issue_tracker, if: :activated?, on: :manual_change # TODO: we can probably just delegate as part of @@ -73,9 +73,9 @@ class IssueTrackerService < Service def fields [ - { type: 'text', name: 'project_url', title: _('Project URL'), required: true }, - { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), required: true }, - { type: 'text', name: 'new_issue_url', title: s_('ProjectService|New issue URL'), required: true } + { type: 'text', name: 'project_url', title: _('Project URL'), help: s_('IssueTracker|The URL to the project in the external issue tracker.'), required: true }, + { type: 'text', name: 'issues_url', title: s_('IssueTracker|Issue URL'), help: s_('IssueTracker|The URL to view an issue in the external issue tracker. Must contain %{colon_id}.') % { colon_id: '<code>:id</code>'.html_safe }, required: true }, + { type: 'text', name: 'new_issue_url', title: s_('IssueTracker|New issue URL'), help: s_('IssueTracker|The URL to create an issue in the external issue tracker.'), required: true } ] end @@ -143,10 +143,10 @@ class IssueTrackerService < Service return if template? || instance? return if project.blank? - if project.services.external_issue_trackers.where.not(id: id).any? + if project.integrations.external_issue_trackers.where.not(id: id).any? errors.add(:base, _('Another issue tracker is already in use. Only one issue tracker service can be active at a time')) end end end -IssueTrackerService.prepend_if_ee('EE::IssueTrackerService') +IssueTrackerService.prepend_mod_with('IssueTrackerService') diff --git a/app/models/project_services/jenkins_service.rb b/app/models/project_services/jenkins_service.rb index 6a123517b84..990a35cd617 100644 --- a/app/models/project_services/jenkins_service.rb +++ b/app/models/project_services/jenkins_service.rb @@ -64,12 +64,12 @@ class JenkinsService < CiService end def description - s_('An extendable open source CI/CD server.') + s_('Run CI/CD pipelines with Jenkins.') end def help docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('integration/jenkins'), target: '_blank', rel: 'noopener noreferrer' - s_('Trigger Jenkins builds when you push to a repository, or when a merge request is created, updated, or merged. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } + s_('Run CI/CD pipelines with Jenkins when you push to a repository, or when a merge request is created, updated, or merged. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end def self.to_param diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 3e14bf44c12..5cd6e79eb1d 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -106,9 +106,8 @@ class JiraService < IssueTrackerService end def help - "You need to configure Jira before enabling this service. For more details - read the - [Jira service documentation](#{help_page_url('user/project/integrations/jira')})." + jira_doc_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_url('integration/jira/index.html') } + s_("JiraService|You need to configure Jira before enabling this integration. For more details, read the %{jira_doc_link_start}Jira integration documentation%{link_end}.") % { jira_doc_link_start: jira_doc_link_start, link_end: '</a>'.html_safe } end def title @@ -116,7 +115,7 @@ class JiraService < IssueTrackerService end def description - s_('JiraService|Track issues in Jira') + s_("JiraService|Use Jira as this project's issue tracker.") end def self.to_param @@ -305,7 +304,7 @@ class JiraService < IssueTrackerService ) true - rescue => error + rescue StandardError => error log_error( "Issue transition failed", error: { @@ -490,7 +489,7 @@ class JiraService < IssueTrackerService # Handle errors when doing Jira API calls def jira_request yield - rescue => error + rescue StandardError => error @error = error log_error("Error sending message", client_url: client_url, error: @error.message) nil @@ -539,4 +538,4 @@ class JiraService < IssueTrackerService end end -JiraService.prepend_if_ee('EE::JiraService') +JiraService.prepend_mod_with('JiraService') diff --git a/app/models/project_services/microsoft_teams_service.rb b/app/models/project_services/microsoft_teams_service.rb index 803c1255195..1d2067067da 100644 --- a/app/models/project_services/microsoft_teams_service.rb +++ b/app/models/project_services/microsoft_teams_service.rb @@ -6,7 +6,7 @@ class MicrosoftTeamsService < ChatNotificationService end def description - 'Receive event notifications in Microsoft Teams' + 'Send notifications about project events to Microsoft Teams.' end def self.to_param diff --git a/app/models/project_services/monitoring_service.rb b/app/models/project_services/monitoring_service.rb index 1b530a8247b..ea65a200027 100644 --- a/app/models/project_services/monitoring_service.rb +++ b/app/models/project_services/monitoring_service.rb @@ -4,7 +4,7 @@ # # These services integrate with a deployment solution like Prometheus # to provide additional features for environments. -class MonitoringService < Service +class MonitoringService < Integration default_value_for :category, 'monitoring' def self.supported_events diff --git a/app/models/project_services/packagist_service.rb b/app/models/project_services/packagist_service.rb index 21f0a2b2463..f3ea8c64302 100644 --- a/app/models/project_services/packagist_service.rb +++ b/app/models/project_services/packagist_service.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class PackagistService < Service +class PackagistService < Integration prop_accessor :username, :token, :server validates :username, presence: true, if: :activated? @@ -16,7 +16,7 @@ class PackagistService < Service end def description - s_('Integrations|Update your projects on Packagist, the main Composer repository') + s_('Integrations|Update your Packagist projects.') end def self.to_param diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb index 0a0a41c525c..4603193ac8e 100644 --- a/app/models/project_services/pipelines_email_service.rb +++ b/app/models/project_services/pipelines_email_service.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class PipelinesEmailService < Service +class PipelinesEmailService < Integration include NotificationBranchSelection prop_accessor :recipients, :branches_to_be_notified diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index d3fff100964..6e67984591d 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class PivotaltrackerService < Service +class PivotaltrackerService < Integration API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits' prop_accessor :token, :restrict_to_branch @@ -11,7 +11,7 @@ class PivotaltrackerService < Service end def description - s_('PivotalTrackerService|Project Management Software (Source Commits Endpoint)') + s_('PivotalTrackerService|Add commit messages as comments to PivotalTracker stories.') end def self.to_param diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index 1781ec7456d..89765fbdf41 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class PushoverService < Service +class PushoverService < Integration BASE_URI = 'https://api.pushover.net/1' prop_accessor :api_key, :user_key, :device, :priority, :sound @@ -11,7 +11,7 @@ class PushoverService < Service end def description - s_('PushoverService|Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop.') + s_('PushoverService|Get real-time notifications on your device.') end def self.to_param diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index 26a6cf86bf4..7a0f500209c 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -9,7 +9,7 @@ class RedmineService < IssueTrackerService end def description - s_('IssueTracker|Use Redmine as the issue tracker.') + s_("IssueTracker|Use Redmine as this project's issue tracker.") end def help 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 diff --git a/app/models/project_services/slash_commands_service.rb b/app/models/project_services/slash_commands_service.rb index d436176a52c..37d16737052 100644 --- a/app/models/project_services/slash_commands_service.rb +++ b/app/models/project_services/slash_commands_service.rb @@ -2,7 +2,7 @@ # Base class for Chat services # This class is not meant to be used directly, but only to inherrit from. -class SlashCommandsService < Service +class SlashCommandsService < Integration default_value_for :category, 'chat' prop_accessor :token diff --git a/app/models/project_services/unify_circuit_service.rb b/app/models/project_services/unify_circuit_service.rb index 1a0eebe7d64..5f43388e1c9 100644 --- a/app/models/project_services/unify_circuit_service.rb +++ b/app/models/project_services/unify_circuit_service.rb @@ -6,7 +6,7 @@ class UnifyCircuitService < ChatNotificationService end def description - 'Receive event notifications in Unify Circuit' + s_('Integrations|Send notifications about project events to Unify Circuit.') end def self.to_param diff --git a/app/models/project_services/webex_teams_service.rb b/app/models/project_services/webex_teams_service.rb index 4e8281f4e81..3d92d3bb85e 100644 --- a/app/models/project_services/webex_teams_service.rb +++ b/app/models/project_services/webex_teams_service.rb @@ -1,12 +1,14 @@ # frozen_string_literal: true class WebexTeamsService < ChatNotificationService + include ActionView::Helpers::UrlHelper + def title - 'Webex Teams' + s_("WebexTeamsService|Webex Teams") end def description - 'Receive event notifications in Webex Teams' + s_("WebexTeamsService|Send notifications about project events to Webex Teams.") end def self.to_param @@ -14,13 +16,8 @@ class WebexTeamsService < ChatNotificationService end def help - 'This service sends notifications about projects events to a Webex Teams conversation.<br /> - To set up this service: - <ol> - <li><a href="https://apphub.webex.com/teams/applications/incoming-webhooks-cisco-systems">Set up an incoming webhook for your conversation</a>. All notifications will come to this conversation.</li> - <li>Paste the <strong>Webhook URL</strong> into the field below.</li> - <li>Select events below to enable notifications.</li> - </ol>' + docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/webex_teams'), target: '_blank', rel: 'noopener noreferrer' + s_("WebexTeamsService|Send notifications about project events to a Webex Teams conversation. %{docs_link}") % { docs_link: docs_link.html_safe } end def event_field(event) @@ -36,7 +33,7 @@ class WebexTeamsService < ChatNotificationService def default_fields [ - { type: 'text', name: 'webhook', placeholder: "e.g. https://api.ciscospark.com/v1/webhooks/incoming/…", required: true }, + { type: 'text', name: 'webhook', placeholder: "https://api.ciscospark.com/v1/webhooks/incoming/...", required: true }, { type: 'checkbox', name: 'notify_only_broken_pipelines' }, { type: 'select', name: 'branches_to_be_notified', choices: branch_choices } ] diff --git a/app/models/project_services/youtrack_service.rb b/app/models/project_services/youtrack_service.rb index 30abd0159b3..9760a22a872 100644 --- a/app/models/project_services/youtrack_service.rb +++ b/app/models/project_services/youtrack_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class YoutrackService < IssueTrackerService + include ActionView::Helpers::UrlHelper + validates :project_url, :issues_url, presence: true, public_url: true, if: :activated? # {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1, gl-030 @@ -17,7 +19,12 @@ class YoutrackService < IssueTrackerService end def description - s_('IssueTracker|YouTrack issue tracker') + s_("IssueTracker|Use YouTrack as this project's issue tracker.") + end + + def help + docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/youtrack'), target: '_blank', rel: 'noopener noreferrer' + s_("IssueTracker|Use YouTrack as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe } end def self.to_param @@ -26,8 +33,8 @@ class YoutrackService < IssueTrackerService def fields [ - { type: 'text', name: 'project_url', title: _('Project URL'), required: true }, - { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), required: true } + { type: 'text', name: 'project_url', title: _('Project URL'), help: s_('IssueTracker|The URL to the project in YouTrack.'), required: true }, + { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), help: s_('IssueTracker|The URL to view an issue in the YouTrack project. Must contain %{colon_id}.') % { colon_id: '<code>:id</code>'.html_safe }, required: true } ] end end |