diff --git a/fastlane/lib/fastlane/actions/slack.rb b/fastlane/lib/fastlane/actions/slack.rb index eb6e8a49f4d..b2bef53695f 100644 --- a/fastlane/lib/fastlane/actions/slack.rb +++ b/fastlane/lib/fastlane/actions/slack.rb @@ -4,49 +4,40 @@ module Fastlane module Actions class SlackAction < Action - def self.is_supported?(platform) - true - end - - # As there is a text limit in the notifications, we are - # usually interested in the last part of the message - # e.g. for tests - def self.trim_message(message) - # We want the last 7000 characters, instead of the first 7000, as the error is at the bottom - start_index = [message.length - 7000, 0].max - message = message[start_index..-1] - # We want line breaks to be shown on slack output so we replace - # input non-interpreted line break with interpreted line break - message.gsub('\n', "\n") - end - - def self.run(options) - require 'slack-notifier' - - options[:message] = self.trim_message(options[:message].to_s || '') - options[:message] = Slack::Notifier::Util::LinkFormatter.format(options[:message]) - - options[:pretext] = options[:pretext].gsub('\n', "\n") unless options[:pretext].nil? - - if options[:channel].to_s.length > 0 - channel = options[:channel] - channel = ('#' + options[:channel]) unless ['#', '@'].include?(channel[0]) # send message to channel by default + class Runner + def initialize(slack_url) + @notifier = Slack::Notifier.new(slack_url) end - username = options[:use_webhook_configured_username_and_icon] ? nil : options[:username] + def run(options) + options[:message] = self.class.trim_message(options[:message].to_s || '') + options[:message] = Slack::Notifier::Util::LinkFormatter.format(options[:message]) - notifier = Slack::Notifier.new(options[:slack_url], channel: channel, username: username) + options[:pretext] = options[:pretext].gsub('\n', "\n") unless options[:pretext].nil? - link_names = options[:link_names] + if options[:channel].to_s.length > 0 + channel = options[:channel] + channel = ('#' + options[:channel]) unless ['#', '@'].include?(channel[0]) # send message to channel by default + end - icon_url = options[:use_webhook_configured_username_and_icon] ? nil : options[:icon_url] + username = options[:use_webhook_configured_username_and_icon] ? nil : options[:username] - slack_attachment = generate_slack_attachments(options) + slack_attachment = self.class.generate_slack_attachments(options) + link_names = options[:link_names] + icon_url = options[:use_webhook_configured_username_and_icon] ? nil : options[:icon_url] - return [notifier, slack_attachment] if Helper.test? # tests will verify the slack attachments and other properties + post_message( + channel: channel, + username: username, + attachments: [slack_attachment], + link_names: link_names, + icon_url: icon_url, + fail_on_error: options[:fail_on_error] + ) + end - begin - results = notifier.ping('', link_names: link_names, icon_url: icon_url, attachments: [slack_attachment]) + def post_message(channel:, username:, attachments:, link_names:, icon_url:, fail_on_error:) + results = @notifier.ping('', channel: channel, username: username, link_names: link_names, icon_url: icon_url, attachments: attachments) rescue => exception UI.error("Exception: #{exception}") ensure @@ -56,13 +47,129 @@ def self.run(options) else UI.verbose(result) unless result.nil? message = "Error pushing Slack message, maybe the integration has no permission to post on this channel? Try removing the channel parameter in your Fastfile, this is usually caused by a misspelled or changed group/channel name or an expired SLACK_URL" - if options[:fail_on_error] + if fail_on_error UI.user_error!(message) else UI.error(message) end end end + + # As there is a text limit in the notifications, we are + # usually interested in the last part of the message + # e.g. for tests + def self.trim_message(message) + # We want the last 7000 characters, instead of the first 7000, as the error is at the bottom + start_index = [message.length - 7000, 0].max + message = message[start_index..-1] + # We want line breaks to be shown on slack output so we replace + # input non-interpreted line break with interpreted line break + message.gsub('\n', "\n") + end + + def self.generate_slack_attachments(options) + color = (options[:success] ? 'good' : 'danger') + should_add_payload = ->(payload_name) { options[:default_payloads].map(&:to_sym).include?(payload_name.to_sym) } + + slack_attachment = { + fallback: options[:message], + text: options[:message], + pretext: options[:pretext], + color: color, + mrkdwn_in: ["pretext", "text", "fields", "message"], + fields: [] + } + + # custom user payloads + slack_attachment[:fields] += options[:payload].map do |k, v| + { + title: k.to_s, + value: Slack::Notifier::Util::LinkFormatter.format(v.to_s), + short: false + } + end + + # Add the lane to the Slack message + # This might be nil, if slack is called as "one-off" action + if should_add_payload[:lane] && Actions.lane_context[Actions::SharedValues::LANE_NAME] + slack_attachment[:fields] << { + title: 'Lane', + value: Actions.lane_context[Actions::SharedValues::LANE_NAME], + short: true + } + end + + # test_result + if should_add_payload[:test_result] + slack_attachment[:fields] << { + title: 'Result', + value: (options[:success] ? 'Success' : 'Error'), + short: true + } + end + + # git branch + if Actions.git_branch && should_add_payload[:git_branch] + slack_attachment[:fields] << { + title: 'Git Branch', + value: Actions.git_branch, + short: true + } + end + + # git_author + if Actions.git_author_email && should_add_payload[:git_author] + if FastlaneCore::Env.truthy?('FASTLANE_SLACK_HIDE_AUTHOR_ON_SUCCESS') && options[:success] + # We only show the git author if the build failed + else + slack_attachment[:fields] << { + title: 'Git Author', + value: Actions.git_author_email, + short: true + } + end + end + + # last_git_commit + if Actions.last_git_commit_message && should_add_payload[:last_git_commit] + slack_attachment[:fields] << { + title: 'Git Commit', + value: Actions.last_git_commit_message, + short: false + } + end + + # last_git_commit_hash + if Actions.last_git_commit_hash(true) && should_add_payload[:last_git_commit_hash] + slack_attachment[:fields] << { + title: 'Git Commit Hash', + value: Actions.last_git_commit_hash(short: true), + short: false + } + end + + # merge additional properties + deep_merge(slack_attachment, options[:attachment_properties]) + end + + # Adapted from https://stackoverflow.com/a/30225093/158525 + def self.deep_merge(a, b) + merger = proc do |key, v1, v2| + Hash === v1 && Hash === v2 ? + v1.merge(v2, &merger) : Array === v1 && Array === v2 ? + v1 | v2 : [:undefined, nil, :nil].include?(v2) ? v1 : v2 + end + a.merge(b, &merger) + end + end + + def self.is_supported?(platform) + true + end + + def self.run(options) + require 'slack-notifier' + Runner.new(options[:slack_url]).run(options) end def self.description @@ -185,99 +292,13 @@ def self.details # @!group Helper ##################################################### - def self.generate_slack_attachments(options) - color = (options[:success] ? 'good' : 'danger') - should_add_payload = ->(payload_name) { options[:default_payloads].map(&:to_sym).include?(payload_name.to_sym) } - - slack_attachment = { - fallback: options[:message], - text: options[:message], - pretext: options[:pretext], - color: color, - mrkdwn_in: ["pretext", "text", "fields", "message"], - fields: [] - } - - # custom user payloads - slack_attachment[:fields] += options[:payload].map do |k, v| - { - title: k.to_s, - value: Slack::Notifier::Util::LinkFormatter.format(v.to_s), - short: false - } - end - - # Add the lane to the Slack message - # This might be nil, if slack is called as "one-off" action - if should_add_payload[:lane] && Actions.lane_context[Actions::SharedValues::LANE_NAME] - slack_attachment[:fields] << { - title: 'Lane', - value: Actions.lane_context[Actions::SharedValues::LANE_NAME], - short: true - } - end - - # test_result - if should_add_payload[:test_result] - slack_attachment[:fields] << { - title: 'Result', - value: (options[:success] ? 'Success' : 'Error'), - short: true - } - end - - # git branch - if Actions.git_branch && should_add_payload[:git_branch] - slack_attachment[:fields] << { - title: 'Git Branch', - value: Actions.git_branch, - short: true - } - end - - # git_author - if Actions.git_author_email && should_add_payload[:git_author] - if FastlaneCore::Env.truthy?('FASTLANE_SLACK_HIDE_AUTHOR_ON_SUCCESS') && options[:success] - # We only show the git author if the build failed - else - slack_attachment[:fields] << { - title: 'Git Author', - value: Actions.git_author_email, - short: true - } - end - end - - # last_git_commit - if Actions.last_git_commit_message && should_add_payload[:last_git_commit] - slack_attachment[:fields] << { - title: 'Git Commit', - value: Actions.last_git_commit_message, - short: false - } - end - - # last_git_commit_hash - if Actions.last_git_commit_hash(true) && should_add_payload[:last_git_commit_hash] - slack_attachment[:fields] << { - title: 'Git Commit Hash', - value: Actions.last_git_commit_hash(short: true), - short: false - } - end - - # merge additional properties - deep_merge(slack_attachment, options[:attachment_properties]) + def self.trim_message(message) + Runner.trim_message(message) end - # Adapted from https://stackoverflow.com/a/30225093/158525 - def self.deep_merge(a, b) - merger = proc do |key, v1, v2| - Hash === v1 && Hash === v2 ? - v1.merge(v2, &merger) : Array === v1 && Array === v2 ? - v1 | v2 : [:undefined, nil, :nil].include?(v2) ? v1 : v2 - end - a.merge(b, &merger) + def self.generate_slack_attachments(options) + UI.deprecated('`Fastlane::Actions::Slack.generate_slack_attachments` is subject to be removed as Slack recommends migrating `attachments` to Block Kit. fastlane will also follow the same direction.') + Runner.generate_slack_attachments(options) end end end diff --git a/fastlane/spec/actions_specs/slack_spec.rb b/fastlane/spec/actions_specs/slack_spec.rb index 059f907cb29..c1811048500 100644 --- a/fastlane/spec/actions_specs/slack_spec.rb +++ b/fastlane/spec/actions_specs/slack_spec.rb @@ -1,13 +1,13 @@ -describe Fastlane do - describe Fastlane::FastFile do - describe "Slack Action" do - before :each do - ENV['SLACK_URL'] = 'https://127.0.0.1' - end +require 'slack-notifier' + +describe Fastlane::Actions do + describe Fastlane::Actions::SlackAction do + describe Fastlane::Actions::SlackAction::Runner do + subject { Fastlane::Actions::SlackAction::Runner.new('https://127.0.0.1') } it "trims long messages to show the bottom of the messages" do long_text = "a" * 10_000 - expect(Fastlane::Actions::SlackAction.trim_message(long_text).length).to eq(7000) + expect(described_class.trim_message(long_text).length).to eq(7000) end it "works so perfect, like Slack does" do @@ -17,7 +17,6 @@ Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::LANE_NAME] = lane_name - require 'fastlane/actions/slack' options = FastlaneCore::Configuration.create(Fastlane::Actions::SlackAction.available_options, { slack_url: 'https://127.0.0.1', message: message, @@ -30,24 +29,27 @@ default_payloads: [:lane, :test_result, :git_branch, :git_author, :last_git_commit_hash] }) - notifier, attachments = Fastlane::Actions::SlackAction.run(options) - - expect(notifier.config.defaults[:username]).to eq('fastlane') - expect(notifier.config.defaults[:channel]).to eq(channel) - - expect(attachments[:color]).to eq('danger') - expect(attachments[:text]).to eq(message) - expect(attachments[:pretext]).to eq(nil) - - fields = attachments[:fields] - expect(fields[1][:title]).to eq('Built by') - expect(fields[1][:value]).to eq('Jenkins') - - expect(fields[2][:title]).to eq('Lane') - expect(fields[2][:value]).to eq(lane_name) - - expect(fields[3][:title]).to eq('Result') - expect(fields[3][:value]).to eq('Error') + expected_args = { + channel: channel, + username: 'fastlane', + attachments: [ + hash_including( + color: 'danger', + pretext: nil, + text: message, + fields: array_including( + { title: 'Built by', value: 'Jenkins', short: false }, + { title: 'Lane', value: lane_name, short: true }, + { title: 'Result', value: 'Error', short: true } + ) + ) + ], + link_names: false, + icon_url: 'https://fastlane.tools/assets/img/fastlane_icon.png', + fail_on_error: true + } + expect(subject).to receive(:post_message).with(expected_args) + subject.run(options) end it "works so perfect, like Slack does with pretext" do @@ -58,7 +60,6 @@ Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::LANE_NAME] = lane_name - require 'fastlane/actions/slack' options = FastlaneCore::Configuration.create(Fastlane::Actions::SlackAction.available_options, { slack_url: 'https://127.0.0.1', message: message, @@ -67,14 +68,18 @@ channel: channel }) - notifier, attachments = Fastlane::Actions::SlackAction.run(options) - - expect(notifier.config.defaults[:username]).to eq('fastlane') - expect(notifier.config.defaults[:channel]).to eq(channel) - - expect(attachments[:color]).to eq('danger') - expect(attachments[:text]).to eq(message) - expect(attachments[:pretext]).to eq(pretext) + expected_args = hash_including( + attachments: [ + hash_including( + color: 'danger', + pretext: pretext, + text: message + ) + ] + ) + + expect(subject).to receive(:post_message).with(expected_args) + subject.run(options) end it "merges attachment_properties when specified" do @@ -101,18 +106,18 @@ } }) - notifier, attachments = Fastlane::Actions::SlackAction.run(options) - - fields = attachments[:fields] - - expect(fields[0][:title]).to eq('Lane') - expect(fields[0][:value]).to eq(lane_name) - - expect(fields[1][:title]).to eq('My Field') - expect(fields[1][:value]).to eq('My Value') - expect(fields[1][:short]).to eq(true) - - expect(attachments[:thumb_url]).to eq('https://example.com/path/to/thumb.png') + expected_args = hash_including( + attachments: [ + hash_including( + fields: array_including( + { title: 'Lane', value: lane_name, short: true }, + { title: 'My Field', value: 'My Value', short: true } + ) + ) + ] + ) + expect(subject).to receive(:post_message).with(expected_args) + subject.run(options) end it "parses default_payloads from a comma delimited string" do @@ -122,7 +127,6 @@ Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::LANE_NAME] = lane_name - require 'fastlane/actions/slack' options = FastlaneCore::Configuration.create(Fastlane::Actions::SlackAction.available_options, { slack_url: 'https://127.0.0.1', message: message, @@ -131,15 +135,18 @@ default_payloads: "lane,test_result" }) - notifier, attachments = Fastlane::Actions::SlackAction.run(options) - - fields = attachments[:fields] - - expect(fields[0][:title]).to eq('Lane') - expect(fields[0][:value]).to eq(lane_name) - - expect(fields[1][:title]).to eq('Result') - expect(fields[1][:value]).to eq('Error') + expected_args = hash_including( + attachments: [ + hash_including( + fields: [ + { title: 'Lane', value: lane_name, short: true }, + { title: 'Result', value: 'Error', short: true } + ] + ) + ] + ) + expect(subject).to receive(:post_message).with(expected_args) + subject.run(options) end # https://github.com/fastlane/fastlane/issues/14234 @@ -156,14 +163,18 @@ default_payloads: [:git_branch, :last_git_commit_hash] }) - notifier, attachments = Fastlane::Actions::SlackAction.run(options) - - fields = attachments[:fields] - - expect(fields.count).to eq(2) - - expect(fields[0][:title]).to eq('Git Branch') - expect(fields[1][:title]).to eq('Git Commit Hash') + expected_args = hash_including( + attachments: [ + hash_including( + fields: [ + { title: 'Git Branch', value: anything, short: true }, + { title: 'Git Commit Hash', value: anything, short: false } + ] + ) + ] + ) + expect(subject).to receive(:post_message).with(expected_args) + subject.run(options) end it "receives default_payloads as nil and falls back to its default value" do @@ -182,18 +193,23 @@ channel: channel, default_payloads: nil }) - notifier, attachments = Fastlane::Actions::SlackAction.run(options) - - fields = attachments[:fields] - expect(fields[0][:title]).to eq('Lane') - expect(fields[0][:value]).to eq(lane_name) - expect(fields[1][:title]).to eq('Result') - expect(fields[2][:title]).to eq('Git Branch') - expect(fields[3][:title]).to eq('Git Author') - expect(fields[4][:title]).to eq('Git Commit') - expect(fields[5][:title]).to eq('Git Commit Hash') - - expect(fields.count).to eq(6) + + expected_args = hash_including( + attachments: [ + hash_including( + fields: [ + { title: 'Lane', value: lane_name, short: true }, + { title: 'Result', value: anything, short: true }, + { title: 'Git Branch', value: anything, short: true }, + { title: 'Git Author', value: anything, short: true }, + { title: 'Git Commit', value: anything, short: false }, + { title: 'Git Commit Hash', value: anything, short: false } + ] + ) + ] + ) + expect(subject).to receive(:post_message).with(expected_args) + subject.run(options) end # https://github.com/fastlane/fastlane/issues/14141 @@ -207,7 +223,6 @@ Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::LANE_NAME] = lane_name - require 'fastlane/actions/slack' options = FastlaneCore::Configuration.create(Fastlane::Actions::SlackAction.available_options, { slack_url: 'https://127.0.0.1', message: input_message, @@ -215,13 +230,16 @@ channel: channel }) - notifier, attachments = Fastlane::Actions::SlackAction.run(options) - - expect(notifier.config.defaults[:username]).to eq('fastlane') - expect(notifier.config.defaults[:channel]).to eq(channel) - - expect(attachments[:color]).to eq('danger') - expect(attachments[:text]).to eq(expected_message) + expected_args = hash_including( + attachments: [ + hash_including( + color: 'danger', + text: expected_message + ) + ] + ) + expect(subject).to receive(:post_message).with(expected_args) + subject.run(options) end # https://github.com/fastlane/fastlane/issues/14141 @@ -235,7 +253,6 @@ Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::LANE_NAME] = lane_name - require 'fastlane/actions/slack' options = FastlaneCore::Configuration.create(Fastlane::Actions::SlackAction.available_options, { slack_url: 'https://127.0.0.1', pretext: input_pretext, @@ -243,13 +260,16 @@ channel: channel }) - notifier, attachments = Fastlane::Actions::SlackAction.run(options) - - expect(notifier.config.defaults[:username]).to eq('fastlane') - expect(notifier.config.defaults[:channel]).to eq(channel) - - expect(attachments[:color]).to eq('danger') - expect(attachments[:pretext]).to eq(expected_pretext) + expected_args = hash_including( + attachments: [ + hash_including( + color: 'danger', + pretext: expected_pretext + ) + ] + ) + expect(subject).to receive(:post_message).with(expected_args) + subject.run(options) end end end diff --git a/scan/spec/slack_poster_spec.rb b/scan/spec/slack_poster_spec.rb index b6a5109fb70..30f7a77bf6f 100644 --- a/scan/spec/slack_poster_spec.rb +++ b/scan/spec/slack_poster_spec.rb @@ -2,6 +2,11 @@ require 'slack-notifier' describe Scan::SlackPoster do + before do + # mock the network request part + allow_any_instance_of(Fastlane::Actions::SlackAction::Runner).to receive(:post_message).with(any_args) + end + describe "slack_url handling" do describe "without a slack_url set" do it "skips Slack posting", requires_xcodebuild: true do