FarmBot/Farmbot-Web-App

View on GitHub
lib/tasks/releases.rake

Summary

Maintainability
Test Coverage
require "open-uri"

NEW_RELEASE_TEMPLATE = "
NEW RELEASE:
=============
Image URL: %{image_url}
Version:   %{version}
Platform:  %{platform}
Channel:   %{channel}
"

OLD_RELEASE_TEMPLATE = "
NOT SAVING %{platform} %{version} (%{channel})
"

namespace :releases do
  module ReleaseTask
    def self.download_metadata(tag_name)
      real_url = "https://api.github.com/repos/farmbot/farmbot_os/releases/tags/#{tag_name}"
      JSON.parse(URI.parse(real_url).open.read, symbolize_names: true)
    end

    def self.get_choice(choices)
      input = STDIN.gets.chomp
      if input.empty?
        puts "Input empty. Exiting."
        exit 0
      end
      index = input.to_i
      if index >= choices.length
        puts "Invalid choice. Exiting."
        exit 0
      end
      if index == 0 and input != "0"
        puts "Invalid choice. Exiting."
        exit 0
      end
      choice = choices.fetch(index)
      puts "\n#{choice}\n\n"
      choice
    end

    def self.select_version(choices)
      puts "\n=== AVAILABLE RELEASES ==="
      choices.each_with_index do |version, index|
        puts "#{index}) #{version}"
      end
      print "Select a release to publish: "
      get_choice(choices)
    end

    def self.get_release_list
      uri = "https://api.github.com/repos/farmbot/farmbot_os/releases"
      file = URI.parse(uri).open
      raw_json = file.read
      json = JSON.parse(raw_json, symbolize_names: true).pluck(:tag_name)
      json.first(9).sort.reverse
    end

    def self.get_channel
      puts "\n=== AVAILABLE CHANNELS ==="
      Release::CHANNEL.each_with_index do |chan, inx|
        puts "#{inx}) #{chan}"
      end
      print "Select a channel to publish to: "
      get_choice(Release::CHANNEL)
    end

    def self.print_release(release)
      is_new = release.saved_change_to_attribute?(:id)
      tpl = is_new ? NEW_RELEASE_TEMPLATE : OLD_RELEASE_TEMPLATE
      params = release.as_json.symbolize_keys
      puts tpl % params
      release
    end

    def self.get_brief_release_info
      info = []
      Release.all.map do |r|
        if r.platform == "rpi"
          info.push("#{r.channel.ljust(8)} #{r.version.ljust(14)}" +
                    "#{r.created_at.to_s.slice(0, 10)}")
        end
      end
      info.join("\n")
    end

    def self.print_all_existing_releases
      puts ""
      Release.all.map do |r|
        puts "#{r.id.to_s.ljust(6)} #{r.channel.ljust(8)}" +
             "#{r.platform.ljust(6)} #{r.version.ljust(14)} #{r.created_at}"
      end
      puts ""
      puts get_brief_release_info
      puts ""
    end

    def self.create_releases(metadata, channel)
      output = Releases::Parse.run!(metadata)
        .map { |params| Releases::Create.run!(params.merge(channel: channel)) }
        .map { |release| print_release(release) }
      if channel == "stable"
        # QA cycles are expected to be short.
        # Do not allow devices to stay on unstable channels
        # when a QA cycle ends.
        puts "=== Moving all devices to `stable`"
        FbosConfig
          .where
          .not(update_channel: "stable")
          .update_all(update_channel: "stable")
      end
      output
    end

    def self.prevent_disaster(version:, chan:)
      if version.include?("rc") && chan == Release::STABLE
        puts "Refusing to publish unstable release candidate to stable channel."
        exit 1
      end
    end

    def self.select_summary_type
      puts "\n=== SUMMARY OPTIONS ==="
      choices = ["none", "message", "link", "body"]
      choices.each_with_index do |version, index|
        puts "#{index}) #{version}"
      end
      print "Select a summary type (cumulative): "
      get_choice(choices)
    end

    def self.prepare_summary(server, metadata)
      title = "current releases: #{server}"
      tag_name = metadata.fetch(:tag_name)
      notification_text = title + " (new: #{tag_name})"
      puts "\n=== notification ===\n"
      puts notification_text
      puts "\n=== message ===\n"
      info = title
      info += "\n```#{get_brief_release_info}```"
      puts info
      puts "\n=== link ==="
      link = "\n\n<#{metadata.fetch(:html_url)}|#{tag_name}>"
      puts link
      puts "\n=== body ==="
      body = "\n#{metadata.fetch(:body)}"
      puts body
      [notification_text, info, link, body]
    end

    def self.post_summary(metadata)
      webhook_url = ENV["RELEASE_WEBHOOK_URL"]
      if webhook_url
        server = Release.first.image_url.split("/")[3].split("-")[1]
        notification_text, info, link, body = prepare_summary(server, metadata)
        summary_type = select_summary_type
        return if summary_type == "none"
        if ["link", "body"].include?(summary_type)
          info += link
        end
        if ["body"].include?(summary_type)
          info += body
        end
        payload = {
          "mrkdwn": true,
          "text": notification_text,
          "blocks": [
            {
              "type": "section",
              "text": {
                "type": "mrkdwn",
                "text": info,
              }
            }
          ],
        }.to_json
        Faraday.post(webhook_url,
                     payload,
                     "Content-Type" => "application/json")
      end
    end
  end

  desc "Send upgrade notification to devices that are online"
  task notify: :environment do
    Devices::UnattendedUpgrade.delay.run!()
  end

  desc "Publish the latest release found on farmbot/farmbot_os github org"
  task publish: :environment do
    ReleaseTask.print_all_existing_releases
    choices = ReleaseTask.get_release_list
    version = ReleaseTask.select_version(choices)
    chan = ReleaseTask.get_channel
    ReleaseTask.prevent_disaster(version: version, chan: chan)
    json = ReleaseTask.download_metadata(version)
    releases = ReleaseTask.create_releases(json, chan)
    # Clean out old releases for $CHANNEL
    Release
      .where(channel: chan)
      .where.not(id: releases.pluck(:id))
      .map do |release|
      puts "Destroying old release ##{release.id}"
      release.destroy!
    end
    ReleaseTask.print_all_existing_releases
    ReleaseTask.post_summary(json)
  end
end