diff --git a/.env.sample b/.env.sample index 9f45506..6cc9a18 100644 --- a/.env.sample +++ b/.env.sample @@ -14,3 +14,6 @@ AWS_REGION= # CDN for image uploads IMAGES_CDN_HOST= + +# Build notifications as part of CI +SLACK_WEBHOOK_URL= diff --git a/Dockerfile b/Dockerfile index 7eaee2e..410bd67 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,6 +40,3 @@ RUN bundle install --jobs 20 --retry 5 # Copy the main application. COPY . ./ - -# Default command -ENTRYPOINT ['/app/robles/bin/robles'] diff --git a/Gemfile b/Gemfile index 3465ac4..0e83ad9 100644 --- a/Gemfile +++ b/Gemfile @@ -33,6 +33,9 @@ gem 'concurrent-ruby', '~> 1.1' # Interacting with github gem 'octokit', '~> 4.18' +# Sending notifications to slack +gem 'slack-notifier', '~> 2.3', '>= 2.3.2' + group :development do # For integration with VSCode gem 'debase', '~> 0.2.4.1' diff --git a/Gemfile.lock b/Gemfile.lock index 9e1496f..eafef47 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,20 +13,20 @@ GEM public_suffix (>= 2.0.2, < 5.0) ast (2.4.0) aws-eventstream (1.1.0) - aws-partitions (1.313.0) - aws-sdk-core (3.95.0) + aws-partitions (1.325.0) + aws-sdk-core (3.97.1) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.31.0) + aws-sdk-kms (1.33.0) aws-sdk-core (~> 3, >= 3.71.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.64.0) - aws-sdk-core (~> 3, >= 3.83.0) + aws-sdk-s3 (1.67.1) + aws-sdk-core (~> 3, >= 3.96.1) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) - aws-sigv4 (1.1.3) + aws-sigv4 (1.1.4) aws-eventstream (~> 1.0, >= 1.0.2) backport (1.1.2) benchmark (0.1.0) @@ -40,7 +40,7 @@ GEM multipart-post (>= 1.2, < 3) git (1.7.0) rchardet (~> 1.8) - i18n (1.8.2) + i18n (1.8.3) concurrent-ruby (~> 1.0) jaro_winkler (1.5.4) jmespath (1.4.0) @@ -55,31 +55,36 @@ GEM faraday (>= 0.9) sawyer (~> 0.8.0, >= 0.5.3) parallel (1.19.1) - parser (2.7.1.2) + parser (2.7.1.3) ast (~> 2.4.0) public_suffix (4.0.5) rainbow (3.0.0) rake (13.0.1) rchardet (1.8.0) redcarpet (3.5.0) - reverse_markdown (1.4.0) + regexp_parser (1.7.0) + reverse_markdown (2.0.0) nokogiri rexml (3.2.4) - rubocop (0.81.0) - jaro_winkler (~> 1.5.1) + rubocop (0.85.0) parallel (~> 1.10) parser (>= 2.7.0.1) rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.7) rexml + rubocop-ast (>= 0.0.3) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 2.0) + rubocop-ast (0.0.3) + parser (>= 2.7.0.1) ruby-debug-ide (0.7.2) rake (>= 0.8.1) ruby-progressbar (1.10.1) sawyer (0.8.2) addressable (>= 2.3.5) faraday (> 0.8, < 2.0) - solargraph (0.39.7) + slack-notifier (2.3.2) + solargraph (0.39.8) backport (~> 1.1) benchmark bundler (>= 1.17.2) @@ -88,7 +93,7 @@ GEM maruku (~> 0.7, >= 0.7.3) nokogiri (~> 1.9, >= 1.9.1) parser (~> 2.3) - reverse_markdown (~> 1.0, >= 1.0.5) + reverse_markdown (>= 1.0.5, < 3) rubocop (~> 0.52) thor (~> 1.0) tilt (~> 2.0) @@ -119,6 +124,7 @@ DEPENDENCIES redcarpet (~> 3.5) rubocop (~> 0.81) ruby-debug-ide (~> 0.7.2) + slack-notifier (~> 2.3, >= 2.3.2) solargraph (~> 0.39) thor (~> 1.0, >= 1.0.1) zeitwerk (~> 2.3) diff --git a/app/commands/robles_cli.rb b/app/commands/robles_cli.rb index fdf5bd3..b6893fd 100644 --- a/app/commands/robles_cli.rb +++ b/app/commands/robles_cli.rb @@ -2,6 +2,11 @@ # Overall CLI app for robles class RoblesCli < Thor + # Ensures that invalid arguments etc result in a failure response to the shell + def self.exit_on_failure? + true + end + desc 'render', 'renders book' option :'publish-file', type: :string, desc: 'Location of the publish.yaml file' option :local, type: :boolean diff --git a/app/lib/api/alexandria/book_uploader.rb b/app/lib/api/alexandria/book_uploader.rb index 6280c7a..8d6a3cc 100644 --- a/app/lib/api/alexandria/book_uploader.rb +++ b/app/lib/api/alexandria/book_uploader.rb @@ -41,6 +41,7 @@ def conn faraday.response(:logger, logger) do |logger| logger.filter(/(Token token=\\\")(\w+)/, '\1[REMOVED]') end + faraday.response(:raise_error) faraday.token_auth(ALEXANDRIA_SERVICE_API_TOKEN) faraday.adapter(Faraday.default_adapter) end diff --git a/app/lib/renderer/image_attributes.rb b/app/lib/renderer/image_attributes.rb index dc5add1..871255b 100644 --- a/app/lib/renderer/image_attributes.rb +++ b/app/lib/renderer/image_attributes.rb @@ -35,6 +35,7 @@ def width_class(alt_text) width_match = /width=(\d+)%/.match(alt_text) return if width_match.blank? + # Convert width request to a class that's a multiple of 10 width = width_match[1].to_i.round(-1).clamp(0, 100) "l-image-#{width}" end diff --git a/app/lib/runner/base.rb b/app/lib/runner/base.rb index 34d8aee..5b8a3bd 100644 --- a/app/lib/runner/base.rb +++ b/app/lib/runner/base.rb @@ -4,6 +4,7 @@ module Runner # Base class with shared functionality class Base include Util::Logging + include Util::SlackNotifiable def self.runner return Runner::Ci.new if CI @@ -23,16 +24,18 @@ def render(publish_file:, local: false) book end - def publish(publish_file:) + def publish(publish_file:) # rubocop:disable Metrics/MethodLength publish_file ||= default_publish_file - parser = Parser::Publish.new(file: publish_file) book = parser.parse image_provider = ImageProvider::Provider.new(book: book) image_provider.process - renderer = Renderer::Book.new(book: book, image_provider: image_provider) - renderer.render + Renderer::Book.new(book: book, image_provider: image_provider).render Api::Alexandria::BookUploader.upload(book) + notify_success(book: book) + rescue StandardError => e + notify_failure(book: defined?(book) ? book : nil, details: e.full_message) + raise e end def lint(publish_file:, options: {}) diff --git a/app/lib/util/slack_notifiable.rb b/app/lib/util/slack_notifiable.rb new file mode 100644 index 0000000..b372bce --- /dev/null +++ b/app/lib/util/slack_notifiable.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +module Util + # Adds methods that allow slack notifications + module SlackNotifiable # rubocop:disable Metrics/ModuleLength + extend ActiveSupport::Concern + + SUCCESS_IMAGE_URL = 'https://wolverine.raywenderlich.com/v3-resources/razebot/images/object_textkit-book.png' + FAILURE_IMAGE_URL = 'https://wolverine.raywenderlich.com/v3-resources/razebot/images/object_errors.png' + ROBLES_CONTEXT_IMAGE_URL = 'https://wolverine.raywenderlich.com/v3-resources/razebot/images/object_box-of-books.png' + + def notify_success(book:) + return unless notifiable? + + notifier.post(blocks: success_blocks(book: book)) + end + + def notify_failure(book:, details: nil) + return unless notifiable? + + notifier.post(blocks: failure_blocks(book: book, details: details || 'N/A')) + end + + def notifiable? + SLACK_WEBHOOK_URL.present? + end + + def notifier + @notifier ||= Slack::Notifier.new(SLACK_WEBHOOK_URL, channel: SLACK_CHANNEL, username: SLACK_USERNAME) + end + + def success_blocks(book:) + [ + intro_section(book: book, + message: ':white_check_mark: Book publication successful!', + image_url: SUCCESS_IMAGE_URL, + alt_text: 'Publication successful'), + { + type: 'divider' + }, + context + ] + end + + def failure_blocks(book:, details:) # rubocop:disable Metrics/MethodLength + [ + intro_section(book: book, + message: ':x: Book publication failed!', + image_url: FAILURE_IMAGE_URL, + alt_text: 'Publication failed'), + { + type: 'section', + text: { + type: 'mrkdwn', + text: "```#{details}```" + } + }, + { + type: 'divider' + }, + context + ] + end + + def standard_fields(book:) # rubocop:disable Metrics/MethodLength + [ + { + type: 'mrkdwn', + text: "*Book*\n#{book&.title || '_unknown_'}" + }, + { + type: 'mrkdwn', + text: "*SKU*\n`#{book&.sku || 'unknown'}`" + }, + { + type: 'mrkdwn', + text: "*Edition*\n#{book&.edition || '_unknown_'}" + }, + { + type: 'mrkdwn', + text: "*Environment*\n`#{ENVIRONMENT}`" + } + ] + end + + def intro_section(book:, message:, image_url:, alt_text:) # rubocop:disable Metrics/MethodLength + { + type: 'section', + text: { + type: 'mrkdwn', + text: message + }, + fields: standard_fields(book: book), + accessory: { + type: 'image', + image_url: image_url, + alt_text: alt_text + } + } + end + + def context # rubocop:disable Metrics/MethodLength + { + type: 'context', + elements: [ + { + type: 'image', + image_url: ROBLES_CONTEXT_IMAGE_URL, + alt_text: 'robles via razebot' + }, + { + type: 'mrkdwn', + text: 'This is a message sent from *robles* via *razebot*.' + } + ] + } + end + end +end diff --git a/config/initialisers/environment.rb b/config/initialisers/environment.rb new file mode 100644 index 0000000..d4efdc2 --- /dev/null +++ b/config/initialisers/environment.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +ENVIRONMENT = ENV['ENV'] || 'development' diff --git a/config/initialisers/slack.rb b/config/initialisers/slack.rb new file mode 100644 index 0000000..11e8233 --- /dev/null +++ b/config/initialisers/slack.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +SLACK_WEBHOOK_URL = ENV['SLACK_WEBHOOK_URL'] +SLACK_CHANNEL = ENV['SLACK_CHANNEL'] || '#robles' +SLACK_USERNAME = ENV['SLACK_USERNAME'] || 'razebot'