rodbot 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
 - checksums.yaml.gz.sig +1 -0
 - data/CHANGELOG.md +14 -0
 - data/LICENSE.txt +22 -0
 - data/README.md +653 -0
 - data/exe/rodbot +7 -0
 - data/lib/roda/plugins/rodbot.rb +36 -0
 - data/lib/rodbot/async.rb +45 -0
 - data/lib/rodbot/cli/command.rb +25 -0
 - data/lib/rodbot/cli/commands/console.rb +23 -0
 - data/lib/rodbot/cli/commands/credentials.rb +19 -0
 - data/lib/rodbot/cli/commands/deploy.rb +20 -0
 - data/lib/rodbot/cli/commands/new.rb +21 -0
 - data/lib/rodbot/cli/commands/simulator.rb +17 -0
 - data/lib/rodbot/cli/commands/start.rb +26 -0
 - data/lib/rodbot/cli/commands/stop.rb +15 -0
 - data/lib/rodbot/cli/commands/version.rb +15 -0
 - data/lib/rodbot/cli/commands.rb +18 -0
 - data/lib/rodbot/cli.rb +9 -0
 - data/lib/rodbot/config.rb +157 -0
 - data/lib/rodbot/constants.rb +13 -0
 - data/lib/rodbot/db/hash.rb +71 -0
 - data/lib/rodbot/db/redis.rb +61 -0
 - data/lib/rodbot/db.rb +91 -0
 - data/lib/rodbot/dispatcher.rb +125 -0
 - data/lib/rodbot/env.rb +48 -0
 - data/lib/rodbot/error.rb +19 -0
 - data/lib/rodbot/generator.rb +108 -0
 - data/lib/rodbot/log.rb +67 -0
 - data/lib/rodbot/memoize.rb +86 -0
 - data/lib/rodbot/plugins/github_webhook/README.github_webhook.md +42 -0
 - data/lib/rodbot/plugins/github_webhook/app.rb +46 -0
 - data/lib/rodbot/plugins/gitlab_webhook/README.gitlab_webhook.md +40 -0
 - data/lib/rodbot/plugins/gitlab_webhook/app.rb +40 -0
 - data/lib/rodbot/plugins/hal/README.hal.md +15 -0
 - data/lib/rodbot/plugins/hal/app.rb +22 -0
 - data/lib/rodbot/plugins/matrix/README.matrix.md +42 -0
 - data/lib/rodbot/plugins/matrix/relay.rb +113 -0
 - data/lib/rodbot/plugins/otp/README.otp.md +82 -0
 - data/lib/rodbot/plugins/otp/app.rb +47 -0
 - data/lib/rodbot/plugins/word_of_the_day/README.word_of_the_day.md +13 -0
 - data/lib/rodbot/plugins/word_of_the_day/schedule.rb +51 -0
 - data/lib/rodbot/plugins.rb +81 -0
 - data/lib/rodbot/rack.rb +50 -0
 - data/lib/rodbot/refinements.rb +118 -0
 - data/lib/rodbot/relay.rb +104 -0
 - data/lib/rodbot/services/app.rb +72 -0
 - data/lib/rodbot/services/relay.rb +37 -0
 - data/lib/rodbot/services/schedule.rb +29 -0
 - data/lib/rodbot/services.rb +32 -0
 - data/lib/rodbot/simulator.rb +60 -0
 - data/lib/rodbot/version.rb +5 -0
 - data/lib/rodbot.rb +60 -0
 - data/lib/templates/deploy/docker/compose.yaml.gerb +37 -0
 - data/lib/templates/deploy/docker-split/compose.yaml.gerb +52 -0
 - data/lib/templates/deploy/procfile/Procfile.gerb +1 -0
 - data/lib/templates/deploy/procfile-split/Procfile.gerb +5 -0
 - data/lib/templates/deploy/render/render.yaml.gerb +0 -0
 - data/lib/templates/deploy/render-split/render.yaml.gerb +0 -0
 - data/lib/templates/new/LICENSE.txt +22 -0
 - data/lib/templates/new/README.md +4 -0
 - data/lib/templates/new/app/app.rb +11 -0
 - data/lib/templates/new/app/routes/help.rb +19 -0
 - data/lib/templates/new/app/views/layout.erb +12 -0
 - data/lib/templates/new/app/views/root.erb +5 -0
 - data/lib/templates/new/config/rodbot.rb +8 -0
 - data/lib/templates/new/config/schedule.rb +5 -0
 - data/lib/templates/new/config.ru +3 -0
 - data/lib/templates/new/gems.locked +104 -0
 - data/lib/templates/new/gems.rb +15 -0
 - data/lib/templates/new/guardfile.rb +9 -0
 - data/lib/templates/new/public/assets/images/rodbot.avif +0 -0
 - data/lib/templates/new/public/assets/stylesheets/base.css +18 -0
 - data/lib/templates/new/rakefile.rb +28 -0
 - data.tar.gz.sig +0 -0
 - metadata +510 -0
 - metadata.gz.sig +3 -0
 
| 
         @@ -0,0 +1,125 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen-string-literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Rodbot
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
              # Dispatcher infrastructure to run and supervise tasks
         
     | 
| 
      
 6 
     | 
    
         
            +
              class Dispatcher
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                # Which signals detached processes trap in order to exit
         
     | 
| 
      
 9 
     | 
    
         
            +
                TRAPS = %w(INT TERM).freeze
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                # @return [String] name of the group of tasks
         
     | 
| 
      
 12 
     | 
    
         
            +
                attr_reader :group
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                # @return [String] registered tasks
         
     | 
| 
      
 15 
     | 
    
         
            +
                attr_reader :tasks
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                # @param group [String] name of the group of tasks
         
     | 
| 
      
 18 
     | 
    
         
            +
                # @param refork_delay [Integer] seconds to wait before re-forking dead tasks
         
     | 
| 
      
 19 
     | 
    
         
            +
                def initialize(group, refork_delay: 5)
         
     | 
| 
      
 20 
     | 
    
         
            +
                  @group, @refork_delay = group, refork_delay
         
     | 
| 
      
 21 
     | 
    
         
            +
                  @tasks = {}
         
     | 
| 
      
 22 
     | 
    
         
            +
                end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                # Register a task
         
     | 
| 
      
 25 
     | 
    
         
            +
                #
         
     | 
| 
      
 26 
     | 
    
         
            +
                # @param task [String] task name
         
     | 
| 
      
 27 
     | 
    
         
            +
                # @yield block for the task to run
         
     | 
| 
      
 28 
     | 
    
         
            +
                # @return self
         
     | 
| 
      
 29 
     | 
    
         
            +
                def register(task)
         
     | 
| 
      
 30 
     | 
    
         
            +
                  tasks[task] = Proc.new do
         
     | 
| 
      
 31 
     | 
    
         
            +
                    detach task
         
     | 
| 
      
 32 
     | 
    
         
            +
                    unless Rodbot::Log.std?
         
     | 
| 
      
 33 
     | 
    
         
            +
                      logger = Rodbot::Log.logger("dispatcher #{group}.#{task}]")
         
     | 
| 
      
 34 
     | 
    
         
            +
                      $stdout = Rodbot::Log::LoggerIO.new(logger, Logger::INFO)
         
     | 
| 
      
 35 
     | 
    
         
            +
                      $stderr = Rodbot::Log::LoggerIO.new(logger, Logger::WARN)
         
     | 
| 
      
 36 
     | 
    
         
            +
                      $stdin.reopen(File::NULL)
         
     | 
| 
      
 37 
     | 
    
         
            +
                    end
         
     | 
| 
      
 38 
     | 
    
         
            +
                    yield
         
     | 
| 
      
 39 
     | 
    
         
            +
                  end
         
     | 
| 
      
 40 
     | 
    
         
            +
                  self
         
     | 
| 
      
 41 
     | 
    
         
            +
                end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                # Run the registered tasks
         
     | 
| 
      
 44 
     | 
    
         
            +
                #
         
     | 
| 
      
 45 
     | 
    
         
            +
                # @param daemonize [Boolean] whether to run and supervise the tasks in
         
     | 
| 
      
 46 
     | 
    
         
            +
                #   the background
         
     | 
| 
      
 47 
     | 
    
         
            +
                def run(daemonize: false)
         
     | 
| 
      
 48 
     | 
    
         
            +
                  if daemonize
         
     | 
| 
      
 49 
     | 
    
         
            +
                    Process.daemon(false, true)
         
     | 
| 
      
 50 
     | 
    
         
            +
                    detach 'supervisor'
         
     | 
| 
      
 51 
     | 
    
         
            +
                    dispatch
         
     | 
| 
      
 52 
     | 
    
         
            +
                    supervise
         
     | 
| 
      
 53 
     | 
    
         
            +
                  else
         
     | 
| 
      
 54 
     | 
    
         
            +
                    Process.setproctitle("#{group}.supervisor")
         
     | 
| 
      
 55 
     | 
    
         
            +
                    dispatch
         
     | 
| 
      
 56 
     | 
    
         
            +
                    sleep
         
     | 
| 
      
 57 
     | 
    
         
            +
                  end
         
     | 
| 
      
 58 
     | 
    
         
            +
                ensure
         
     | 
| 
      
 59 
     | 
    
         
            +
                  cleanup
         
     | 
| 
      
 60 
     | 
    
         
            +
                end
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                # Interrupt the registered tasks
         
     | 
| 
      
 63 
     | 
    
         
            +
                def interrupt
         
     | 
| 
      
 64 
     | 
    
         
            +
                  Process.kill('INT', pid_file('supervisor').read.to_i)
         
     | 
| 
      
 65 
     | 
    
         
            +
                rescue Errno::ESRCH
         
     | 
| 
      
 66 
     | 
    
         
            +
                end
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                private
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
                # Dispatch all registered tasks
         
     | 
| 
      
 71 
     | 
    
         
            +
                def dispatch
         
     | 
| 
      
 72 
     | 
    
         
            +
                  tasks.each_value { fork &_1 }
         
     | 
| 
      
 73 
     | 
    
         
            +
                end
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                # Supervise all dispatched tasks
         
     | 
| 
      
 76 
     | 
    
         
            +
                def supervise
         
     | 
| 
      
 77 
     | 
    
         
            +
                  loop do
         
     | 
| 
      
 78 
     | 
    
         
            +
                    pid = Process.wait
         
     | 
| 
      
 79 
     | 
    
         
            +
                    sleep @refork_delay
         
     | 
| 
      
 80 
     | 
    
         
            +
                    fork &tasks[task(pid)]
         
     | 
| 
      
 81 
     | 
    
         
            +
                  end
         
     | 
| 
      
 82 
     | 
    
         
            +
                end
         
     | 
| 
      
 83 
     | 
    
         
            +
             
     | 
| 
      
 84 
     | 
    
         
            +
                # Remove all artefacts
         
     | 
| 
      
 85 
     | 
    
         
            +
                def cleanup
         
     | 
| 
      
 86 
     | 
    
         
            +
                  Rodbot.env.tmp.glob("#{group}.*.pid").each do |pid_file|
         
     | 
| 
      
 87 
     | 
    
         
            +
                    pid = pid_file.read.to_i
         
     | 
| 
      
 88 
     | 
    
         
            +
                    Process.kill('INT', pid) unless pid == Process.pid
         
     | 
| 
      
 89 
     | 
    
         
            +
                  rescue Errno::ESRCH
         
     | 
| 
      
 90 
     | 
    
         
            +
                  ensure
         
     | 
| 
      
 91 
     | 
    
         
            +
                    pid_file.delete
         
     | 
| 
      
 92 
     | 
    
         
            +
                  end
         
     | 
| 
      
 93 
     | 
    
         
            +
                end
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
                # Perform operations to properly detach the task
         
     | 
| 
      
 96 
     | 
    
         
            +
                #
         
     | 
| 
      
 97 
     | 
    
         
            +
                # @param task [String] task name
         
     | 
| 
      
 98 
     | 
    
         
            +
                def detach(task)
         
     | 
| 
      
 99 
     | 
    
         
            +
                  pid_file(task).write Process.pid
         
     | 
| 
      
 100 
     | 
    
         
            +
                  Process.setproctitle("#{group}.#{task}")
         
     | 
| 
      
 101 
     | 
    
         
            +
                  TRAPS.each { trap(_1) { exit } }
         
     | 
| 
      
 102 
     | 
    
         
            +
                end
         
     | 
| 
      
 103 
     | 
    
         
            +
             
     | 
| 
      
 104 
     | 
    
         
            +
                # PID file of the given task should be
         
     | 
| 
      
 105 
     | 
    
         
            +
                #
         
     | 
| 
      
 106 
     | 
    
         
            +
                # @param task [String] task name
         
     | 
| 
      
 107 
     | 
    
         
            +
                # @return [Pathname] PID file
         
     | 
| 
      
 108 
     | 
    
         
            +
                def pid_file(task)
         
     | 
| 
      
 109 
     | 
    
         
            +
                  Rodbot.env.tmp.join("#{group}.#{task}.pid")
         
     | 
| 
      
 110 
     | 
    
         
            +
                end
         
     | 
| 
      
 111 
     | 
    
         
            +
             
     | 
| 
      
 112 
     | 
    
         
            +
                # Fetch a task name for a process ID from the PID files
         
     | 
| 
      
 113 
     | 
    
         
            +
                #
         
     | 
| 
      
 114 
     | 
    
         
            +
                # @param pid [Integer] process ID
         
     | 
| 
      
 115 
     | 
    
         
            +
                # @return [String] task name
         
     | 
| 
      
 116 
     | 
    
         
            +
                def task(pid)
         
     | 
| 
      
 117 
     | 
    
         
            +
                  Rodbot.env.tmp.glob("#{group}.*.pid").find do |pid_file|
         
     | 
| 
      
 118 
     | 
    
         
            +
                    pid_file.read.to_i == pid
         
     | 
| 
      
 119 
     | 
    
         
            +
                  end.then do |pid_file|
         
     | 
| 
      
 120 
     | 
    
         
            +
                    pid_file.basename.to_s.split('.')[1] if pid_file
         
     | 
| 
      
 121 
     | 
    
         
            +
                  end
         
     | 
| 
      
 122 
     | 
    
         
            +
                end
         
     | 
| 
      
 123 
     | 
    
         
            +
             
     | 
| 
      
 124 
     | 
    
         
            +
              end
         
     | 
| 
      
 125 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/rodbot/env.rb
    ADDED
    
    | 
         @@ -0,0 +1,48 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen-string-literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Rodbot
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
              # Environment the bot is currently living in
         
     | 
| 
      
 6 
     | 
    
         
            +
              #
         
     | 
| 
      
 7 
     | 
    
         
            +
              # @note Use the +Rodbot.env+ shortcut to access these methods!
         
     | 
| 
      
 8 
     | 
    
         
            +
              class Env
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                # Supported environments
         
     | 
| 
      
 11 
     | 
    
         
            +
                ENVS = %w(production development test).freeze
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                # @return [Pathname] root directory
         
     | 
| 
      
 14 
     | 
    
         
            +
                attr_reader :root
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                # @return [Pathname] root directory
         
     | 
| 
      
 17 
     | 
    
         
            +
                attr_reader :tmp
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                # @return [Pathname] gem root directory
         
     | 
| 
      
 20 
     | 
    
         
            +
                attr_reader :gem
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                # @return [String] current environment - any of {ENVS}
         
     | 
| 
      
 23 
     | 
    
         
            +
                attr_reader :current
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                # @param root [Pathname, String] root path (default: current directory)
         
     | 
| 
      
 26 
     | 
    
         
            +
                def initialize(root: nil)
         
     | 
| 
      
 27 
     | 
    
         
            +
                  @root = root ? Pathname(root).realpath : Pathname.pwd
         
     | 
| 
      
 28 
     | 
    
         
            +
                  @tmp = @root.join('tmp')
         
     | 
| 
      
 29 
     | 
    
         
            +
                  @gem = Pathname(__dir__).join('..', '..').realpath
         
     | 
| 
      
 30 
     | 
    
         
            +
                  @current = ENV['RODBOT_ENV']
         
     | 
| 
      
 31 
     | 
    
         
            +
                  @current = 'development' unless ENVS.include? @current
         
     | 
| 
      
 32 
     | 
    
         
            +
                end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                # @!method production?
         
     | 
| 
      
 35 
     | 
    
         
            +
                # @!method development?
         
     | 
| 
      
 36 
     | 
    
         
            +
                # @!method test?
         
     | 
| 
      
 37 
     | 
    
         
            +
                #
         
     | 
| 
      
 38 
     | 
    
         
            +
                # Inquire the env based on RODBOT_ENV
         
     | 
| 
      
 39 
     | 
    
         
            +
                #
         
     | 
| 
      
 40 
     | 
    
         
            +
                # @return [Boolean]
         
     | 
| 
      
 41 
     | 
    
         
            +
                ENVS.each do |env|
         
     | 
| 
      
 42 
     | 
    
         
            +
                  define_method "#{env}?" do
         
     | 
| 
      
 43 
     | 
    
         
            +
                    env == current
         
     | 
| 
      
 44 
     | 
    
         
            +
                  end
         
     | 
| 
      
 45 
     | 
    
         
            +
                end
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
              end
         
     | 
| 
      
 48 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/rodbot/error.rb
    ADDED
    
    | 
         @@ -0,0 +1,19 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen-string-literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Rodbot
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Error < StandardError
         
     | 
| 
      
 5 
     | 
    
         
            +
                def initialize(message, details=nil)
         
     | 
| 
      
 6 
     | 
    
         
            +
                  @details = details
         
     | 
| 
      
 7 
     | 
    
         
            +
                  super(message)
         
     | 
| 
      
 8 
     | 
    
         
            +
                end
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                def detailed_message
         
     | 
| 
      
 11 
     | 
    
         
            +
                  [message, @details].compact.join(': ')
         
     | 
| 
      
 12 
     | 
    
         
            +
                end
         
     | 
| 
      
 13 
     | 
    
         
            +
              end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
              GeneratorError = Class.new(Error)
         
     | 
| 
      
 16 
     | 
    
         
            +
              PluginError = Class.new(Error)
         
     | 
| 
      
 17 
     | 
    
         
            +
              ServiceError = Class.new(Error)
         
     | 
| 
      
 18 
     | 
    
         
            +
              RelayError = Class.new(Error)
         
     | 
| 
      
 19 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,108 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen-string-literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'fileutils'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'erb'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'pastel'
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            module Rodbot
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
              # Generators for new bots, deployments and such
         
     | 
| 
      
 10 
     | 
    
         
            +
              #
         
     | 
| 
      
 11 
     | 
    
         
            +
              # All files inside the +templates_path+ are honoured provided they match the
         
     | 
| 
      
 12 
     | 
    
         
            +
              # {GLOB}. Files with the extension ".gerb" are parsed like ERB files, however,
         
     | 
| 
      
 13 
     | 
    
         
            +
              # GERB tags must use square brackets.
         
     | 
| 
      
 14 
     | 
    
         
            +
              #
         
     | 
| 
      
 15 
     | 
    
         
            +
              #   ERB:  <%= 'foobar' %>
         
     | 
| 
      
 16 
     | 
    
         
            +
              #   GERB: [%= 'foobar' %]
         
     | 
| 
      
 17 
     | 
    
         
            +
              #
         
     | 
| 
      
 18 
     | 
    
         
            +
              # It's therefore possible to generate ERB files such as +index.erb.gerb+.
         
     | 
| 
      
 19 
     | 
    
         
            +
              #
         
     | 
| 
      
 20 
     | 
    
         
            +
              # Helpers available in GERB templates have to be defined in
         
     | 
| 
      
 21 
     | 
    
         
            +
              # {Rodbot::Generator::Helpers}.
         
     | 
| 
      
 22 
     | 
    
         
            +
              class Generator
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                # Glob to filter relevant template files
         
     | 
| 
      
 25 
     | 
    
         
            +
                GLOB = "**/{*,.ruby-version,.gitignore,.keep}"
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                # Colors used by +info+ to color part of the output
         
     | 
| 
      
 28 
     | 
    
         
            +
                TAG_COLORS = {
         
     | 
| 
      
 29 
     | 
    
         
            +
                  create: :green
         
     | 
| 
      
 30 
     | 
    
         
            +
                }.freeze
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                # @param templates_path [Pathname] root path of the templates to generate from
         
     | 
| 
      
 33 
     | 
    
         
            +
                def initialize(templates_path)
         
     | 
| 
      
 34 
     | 
    
         
            +
                  @templates_path = templates_path
         
     | 
| 
      
 35 
     | 
    
         
            +
                  @helpers_binding = Helpers.new.instance_eval('binding')
         
     | 
| 
      
 36 
     | 
    
         
            +
                  @pastel = Pastel.new
         
     | 
| 
      
 37 
     | 
    
         
            +
                end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                # Print the interpolated template to STDOUT
         
     | 
| 
      
 40 
     | 
    
         
            +
                def display
         
     | 
| 
      
 41 
     | 
    
         
            +
                  each_template_path do |template_path, target_path, content|
         
     | 
| 
      
 42 
     | 
    
         
            +
                    puts "# #{target_path}", (content || template_path.read)
         
     | 
| 
      
 43 
     | 
    
         
            +
                  end
         
     | 
| 
      
 44 
     | 
    
         
            +
                end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                # Write the interpolated template to directory
         
     | 
| 
      
 47 
     | 
    
         
            +
                #
         
     | 
| 
      
 48 
     | 
    
         
            +
                # @param directory [Pathname] where to write the files to
         
     | 
| 
      
 49 
     | 
    
         
            +
                def write(directory)
         
     | 
| 
      
 50 
     | 
    
         
            +
                  fail(Rodbot::GeneratorError, "cowardly refusing to write to existing #{directory}") if directory.exist?
         
     | 
| 
      
 51 
     | 
    
         
            +
                  each_template_path do |template_path, target_path, content|
         
     | 
| 
      
 52 
     | 
    
         
            +
                    absolute_target_path = directory.join(target_path)
         
     | 
| 
      
 53 
     | 
    
         
            +
                    absolute_target_path.dirname.mkpath
         
     | 
| 
      
 54 
     | 
    
         
            +
                    puts tag(:create, target_path)
         
     | 
| 
      
 55 
     | 
    
         
            +
                    if content
         
     | 
| 
      
 56 
     | 
    
         
            +
                      absolute_target_path.write(content)
         
     | 
| 
      
 57 
     | 
    
         
            +
                    else
         
     | 
| 
      
 58 
     | 
    
         
            +
                      FileUtils.copy(template_path, absolute_target_path)
         
     | 
| 
      
 59 
     | 
    
         
            +
                    end
         
     | 
| 
      
 60 
     | 
    
         
            +
                  end
         
     | 
| 
      
 61 
     | 
    
         
            +
                end
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
                private
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                def each_template_path
         
     | 
| 
      
 66 
     | 
    
         
            +
                  @templates_path.glob(GLOB).each do |template_path|
         
     | 
| 
      
 67 
     | 
    
         
            +
                    next unless template_path.file?
         
     | 
| 
      
 68 
     | 
    
         
            +
                    target_path = template_path.relative_path_from(@templates_path)
         
     | 
| 
      
 69 
     | 
    
         
            +
                    content = if template_path.extname == '.gerb'
         
     | 
| 
      
 70 
     | 
    
         
            +
                      target_path = target_path.dirname.join(target_path.basename('.gerb'))
         
     | 
| 
      
 71 
     | 
    
         
            +
                      eval_gerb(template_path.read)
         
     | 
| 
      
 72 
     | 
    
         
            +
                    end
         
     | 
| 
      
 73 
     | 
    
         
            +
                    yield(template_path, target_path, content)
         
     | 
| 
      
 74 
     | 
    
         
            +
                  end
         
     | 
| 
      
 75 
     | 
    
         
            +
                end
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                def eval_gerb(string)
         
     | 
| 
      
 78 
     | 
    
         
            +
                  ungerbify(ERB.new(gerbify(string), trim_mode: '-').result(@helpers_binding))
         
     | 
| 
      
 79 
     | 
    
         
            +
                end
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
                def gerbify(string)
         
     | 
| 
      
 82 
     | 
    
         
            +
                  string.gsub('<%', '{%').gsub('%>', '%}').gsub('[%', '<%').gsub('%]', '%>')
         
     | 
| 
      
 83 
     | 
    
         
            +
                end
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
                def ungerbify(string)
         
     | 
| 
      
 86 
     | 
    
         
            +
                  string.gsub('{%', '<%').gsub('%}', '%>')
         
     | 
| 
      
 87 
     | 
    
         
            +
                end
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
                def tag(tag, string)
         
     | 
| 
      
 90 
     | 
    
         
            +
                  padded_tag = '[' + tag.to_s.ljust(6, ' ') + '] '
         
     | 
| 
      
 91 
     | 
    
         
            +
                  @pastel.decorate(padded_tag, TAG_COLORS[tag]) + string.to_s.strip
         
     | 
| 
      
 92 
     | 
    
         
            +
                end
         
     | 
| 
      
 93 
     | 
    
         
            +
             
     | 
| 
      
 94 
     | 
    
         
            +
                class Helpers
         
     | 
| 
      
 95 
     | 
    
         
            +
                  def timezone
         
     | 
| 
      
 96 
     | 
    
         
            +
                    Rodbot.config(:timezone)
         
     | 
| 
      
 97 
     | 
    
         
            +
                  end
         
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
      
 99 
     | 
    
         
            +
                  def relay_extensions
         
     | 
| 
      
 100 
     | 
    
         
            +
                    Rodbot.plugins.extend_relay
         
     | 
| 
      
 101 
     | 
    
         
            +
                    Rodbot.plugins.extensions[:relay].to_h do |name, _|
         
     | 
| 
      
 102 
     | 
    
         
            +
                      [name, URI(Rodbot::Services::Relay.url(name)).port]
         
     | 
| 
      
 103 
     | 
    
         
            +
                    end
         
     | 
| 
      
 104 
     | 
    
         
            +
                  end
         
     | 
| 
      
 105 
     | 
    
         
            +
                end
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
              end
         
     | 
| 
      
 108 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/rodbot/log.rb
    ADDED
    
    | 
         @@ -0,0 +1,67 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen-string-literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Rodbot
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
              # Log facilities
         
     | 
| 
      
 6 
     | 
    
         
            +
              class Log
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                # Default logger
         
     | 
| 
      
 9 
     | 
    
         
            +
                attr_reader :default_logger
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                # Black hole logger
         
     | 
| 
      
 12 
     | 
    
         
            +
                attr_reader :null_logger
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                def initialize
         
     | 
| 
      
 15 
     | 
    
         
            +
                  @default_logger = self.class.logger('rodbot')
         
     | 
| 
      
 16 
     | 
    
         
            +
                  @null_logger = Logger.new(File::NULL)
         
     | 
| 
      
 17 
     | 
    
         
            +
                end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                # Add a log entry to the default log
         
     | 
| 
      
 20 
     | 
    
         
            +
                #
         
     | 
| 
      
 21 
     | 
    
         
            +
                # @note Use the +Rodbot.log+ shortcut to access this method!
         
     | 
| 
      
 22 
     | 
    
         
            +
                #
         
     | 
| 
      
 23 
     | 
    
         
            +
                # @param message [String] log message
         
     | 
| 
      
 24 
     | 
    
         
            +
                # @param level [Integer] any log level from {Logger}
         
     | 
| 
      
 25 
     | 
    
         
            +
                def log(message, level: Logger::INFO)
         
     | 
| 
      
 26 
     | 
    
         
            +
                  @default_logger.log(level, message)
         
     | 
| 
      
 27 
     | 
    
         
            +
                end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                # Create a logger instance for the given scope
         
     | 
| 
      
 30 
     | 
    
         
            +
                #
         
     | 
| 
      
 31 
     | 
    
         
            +
                # @param progname [String] progname used as default scope
         
     | 
| 
      
 32 
     | 
    
         
            +
                def self.logger(progname)
         
     | 
| 
      
 33 
     | 
    
         
            +
                  Logger.new(Rodbot.config(:log, :to), progname: progname).tap do |logger|
         
     | 
| 
      
 34 
     | 
    
         
            +
                    logger.level = Rodbot.config(:log, :level)
         
     | 
| 
      
 35 
     | 
    
         
            +
                  end
         
     | 
| 
      
 36 
     | 
    
         
            +
                end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                # Whether currently configured to log to a std device (+STDOUT+ or +STDERR+)
         
     | 
| 
      
 39 
     | 
    
         
            +
                #
         
     | 
| 
      
 40 
     | 
    
         
            +
                # @return [Boolean]
         
     | 
| 
      
 41 
     | 
    
         
            +
                def self.std?
         
     | 
| 
      
 42 
     | 
    
         
            +
                  [STDOUT, STDERR].include? Rodbot.config(:log, :to)
         
     | 
| 
      
 43 
     | 
    
         
            +
                end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                # Simple wrapper to decorate a logger for use with $stdout and $stderr
         
     | 
| 
      
 46 
     | 
    
         
            +
                class LoggerIO
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                  # @ param logger [Logger] logger instance
         
     | 
| 
      
 49 
     | 
    
         
            +
                  # @ param level [Integer] any log level from +Logger+
         
     | 
| 
      
 50 
     | 
    
         
            +
                  def initialize(logger, level)
         
     | 
| 
      
 51 
     | 
    
         
            +
                    @logger, @level = logger, level
         
     | 
| 
      
 52 
     | 
    
         
            +
                  end
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                  # Write to the log
         
     | 
| 
      
 55 
     | 
    
         
            +
                  #
         
     | 
| 
      
 56 
     | 
    
         
            +
                  # @param message [String] log entry to add
         
     | 
| 
      
 57 
     | 
    
         
            +
                  def write(message)
         
     | 
| 
      
 58 
     | 
    
         
            +
                    @logger.log(@level, message.strip)
         
     | 
| 
      
 59 
     | 
    
         
            +
                  end
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                  # Swallow any other method such as +sync+ or +flush+
         
     | 
| 
      
 62 
     | 
    
         
            +
                  def method_missing(*)
         
     | 
| 
      
 63 
     | 
    
         
            +
                  end
         
     | 
| 
      
 64 
     | 
    
         
            +
                end
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
              end
         
     | 
| 
      
 67 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,86 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen-string-literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Rodbot
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
              # Memoize the return value of a specific method
         
     | 
| 
      
 6 
     | 
    
         
            +
              #
         
     | 
| 
      
 7 
     | 
    
         
            +
              # The method signature is taken into account, therefore calls of the same
         
     | 
| 
      
 8 
     | 
    
         
            +
              # method with different positional and/or keyword arguments are cached
         
     | 
| 
      
 9 
     | 
    
         
            +
              # independently. On the other hand, when calling the method with a block,
         
     | 
| 
      
 10 
     | 
    
         
            +
              # no memoization is performed at all.
         
     | 
| 
      
 11 
     | 
    
         
            +
              #
         
     | 
| 
      
 12 
     | 
    
         
            +
              # @example Explicit declaration
         
     | 
| 
      
 13 
     | 
    
         
            +
              #   class Either
         
     | 
| 
      
 14 
     | 
    
         
            +
              #     include Rodbot::Memoize
         
     | 
| 
      
 15 
     | 
    
         
            +
              #
         
     | 
| 
      
 16 
     | 
    
         
            +
              #     def either(argument=nil, keyword: nil, &block)
         
     | 
| 
      
 17 
     | 
    
         
            +
              #       $entropy || argument || keyword || (block.call if block)
         
     | 
| 
      
 18 
     | 
    
         
            +
              #     end
         
     | 
| 
      
 19 
     | 
    
         
            +
              #     memoize :either
         
     | 
| 
      
 20 
     | 
    
         
            +
              #   end
         
     | 
| 
      
 21 
     | 
    
         
            +
              #
         
     | 
| 
      
 22 
     | 
    
         
            +
              # @example Prefixed declaration
         
     | 
| 
      
 23 
     | 
    
         
            +
              #   class Either
         
     | 
| 
      
 24 
     | 
    
         
            +
              #     include Rodbot::Memoize
         
     | 
| 
      
 25 
     | 
    
         
            +
              #
         
     | 
| 
      
 26 
     | 
    
         
            +
              #     memoize def either(argument=nil, keyword: nil, &block)
         
     | 
| 
      
 27 
     | 
    
         
            +
              #       $entropy || argument || keyword || (block.call if block)
         
     | 
| 
      
 28 
     | 
    
         
            +
              #     end
         
     | 
| 
      
 29 
     | 
    
         
            +
              #   end
         
     | 
| 
      
 30 
     | 
    
         
            +
              #
         
     | 
| 
      
 31 
     | 
    
         
            +
              # @example Behaviour of either of the above
         
     | 
| 
      
 32 
     | 
    
         
            +
              #   e = Either.new
         
     | 
| 
      
 33 
     | 
    
         
            +
              #   $entropy = nil
         
     | 
| 
      
 34 
     | 
    
         
            +
              #   e.either(1)                 # => 1
         
     | 
| 
      
 35 
     | 
    
         
            +
              #   e.either(keyword: 2)        # => 2
         
     | 
| 
      
 36 
     | 
    
         
            +
              #   e.either { 3 }              # => 3
         
     | 
| 
      
 37 
     | 
    
         
            +
              #   $entropy = :not_nil
         
     | 
| 
      
 38 
     | 
    
         
            +
              #   e.either(1)                 # => 1          (memoized)
         
     | 
| 
      
 39 
     | 
    
         
            +
              #   e.either(keyword: 2)        # => 2          (memoized)
         
     | 
| 
      
 40 
     | 
    
         
            +
              #   e.either { 3 }              # => :not_nil   (cannot be memoized)
         
     | 
| 
      
 41 
     | 
    
         
            +
              #   Rodbot::Memoize.suspend do
         
     | 
| 
      
 42 
     | 
    
         
            +
              #     e.either(1)               # => 1          (calculated, not memoized)
         
     | 
| 
      
 43 
     | 
    
         
            +
              #   end
         
     | 
| 
      
 44 
     | 
    
         
            +
              module Memoize
         
     | 
| 
      
 45 
     | 
    
         
            +
                module ClassMethods
         
     | 
| 
      
 46 
     | 
    
         
            +
                  def memoize(method)
         
     | 
| 
      
 47 
     | 
    
         
            +
                    unmemoized_method = :"_unmemoized_#{method}"
         
     | 
| 
      
 48 
     | 
    
         
            +
                    alias_method unmemoized_method, method
         
     | 
| 
      
 49 
     | 
    
         
            +
                    define_method method do |*args, **kargs, &block|
         
     | 
| 
      
 50 
     | 
    
         
            +
                      if Rodbot::Memoize.suspended? || block
         
     | 
| 
      
 51 
     | 
    
         
            +
                        send(unmemoized_method, *args, **kargs, &block)
         
     | 
| 
      
 52 
     | 
    
         
            +
                      else
         
     | 
| 
      
 53 
     | 
    
         
            +
                        id = method.hash ^ args.hash ^ kargs.hash
         
     | 
| 
      
 54 
     | 
    
         
            +
                        @_memoize_cache ||= {}
         
     | 
| 
      
 55 
     | 
    
         
            +
                        if @_memoize_cache.has_key? id
         
     | 
| 
      
 56 
     | 
    
         
            +
                          @_memoize_cache[id]
         
     | 
| 
      
 57 
     | 
    
         
            +
                        else
         
     | 
| 
      
 58 
     | 
    
         
            +
                          @_memoize_cache[id] = send(unmemoized_method, *args, **kargs)
         
     | 
| 
      
 59 
     | 
    
         
            +
                        end
         
     | 
| 
      
 60 
     | 
    
         
            +
                      end
         
     | 
| 
      
 61 
     | 
    
         
            +
                    end
         
     | 
| 
      
 62 
     | 
    
         
            +
                    method
         
     | 
| 
      
 63 
     | 
    
         
            +
                  end
         
     | 
| 
      
 64 
     | 
    
         
            +
                end
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                class << self
         
     | 
| 
      
 67 
     | 
    
         
            +
                  def suspend
         
     | 
| 
      
 68 
     | 
    
         
            +
                    @suspended = true
         
     | 
| 
      
 69 
     | 
    
         
            +
                    yield
         
     | 
| 
      
 70 
     | 
    
         
            +
                  ensure
         
     | 
| 
      
 71 
     | 
    
         
            +
                    @suspended = false
         
     | 
| 
      
 72 
     | 
    
         
            +
                  end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                  def suspended?
         
     | 
| 
      
 75 
     | 
    
         
            +
                    @suspended = false if @suspended.nil?
         
     | 
| 
      
 76 
     | 
    
         
            +
                    @suspended
         
     | 
| 
      
 77 
     | 
    
         
            +
                  end
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
                  def included(base)
         
     | 
| 
      
 80 
     | 
    
         
            +
                    base.extend(ClassMethods)
         
     | 
| 
      
 81 
     | 
    
         
            +
                  end
         
     | 
| 
      
 82 
     | 
    
         
            +
                end
         
     | 
| 
      
 83 
     | 
    
         
            +
              end
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
              end
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
         @@ -0,0 +1,42 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # Rodbot Plugin – GitHub Webhook
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            Pipeline event announcements from GitHub
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            ## Preparation
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            The Rodbot app binds to `localhost` by default which cannot be reached from GitHub. Make sure this connection is possible by setting a different IP in `config/rodbot.rb`:
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 10 
     | 
    
         
            +
            app do
         
     | 
| 
      
 11 
     | 
    
         
            +
              host '0.0.0.0'
         
     | 
| 
      
 12 
     | 
    
         
            +
            end
         
     | 
| 
      
 13 
     | 
    
         
            +
            ```
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
            To authenticate the webhook calls from GitHub, create a new random secret token:
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
            ```
         
     | 
| 
      
 18 
     | 
    
         
            +
            ruby -r securerandom -e "puts SecureRandom.alphanumeric(20)"
         
     | 
| 
      
 19 
     | 
    
         
            +
            ```
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
            ## Activation
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
            Activate and configure this plugin in `config/rodbot.rb`:
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 26 
     | 
    
         
            +
            plugin :github_webhook do
         
     | 
| 
      
 27 
     | 
    
         
            +
              secret_tokens '<TOKEN>'
         
     | 
| 
      
 28 
     | 
    
         
            +
            end
         
     | 
| 
      
 29 
     | 
    
         
            +
            ```
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
            You can set any number of secure tokens here separated with colons.
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
            ## Add Repositories
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
            Add a webhook to every GitHub repository you'd like to see pipeline event announcements for. Go to `https://github.com/<USER>/<REPO>/settings/hooks` and create a new webhook with the following properties:
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
            * Payload URL: `https://<RODBOT-APP>/github_webhook`
         
     | 
| 
      
 38 
     | 
    
         
            +
            * Content type: `application/json`
         
     | 
| 
      
 39 
     | 
    
         
            +
            * Secret: `<TOKEN>`
         
     | 
| 
      
 40 
     | 
    
         
            +
            * SSL verification: (o) Enable SSL verification
         
     | 
| 
      
 41 
     | 
    
         
            +
            * Which events? (o) Let me select individual events: [x] Workflow runs
         
     | 
| 
      
 42 
     | 
    
         
            +
            * And... [x] Active
         
     | 
| 
         @@ -0,0 +1,46 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Rodbot
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Plugins
         
     | 
| 
      
 5 
     | 
    
         
            +
                class GithubWebhook
         
     | 
| 
      
 6 
     | 
    
         
            +
                  module App
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                    class Routes < ::App
         
     | 
| 
      
 9 
     | 
    
         
            +
                      route do |r|
         
     | 
| 
      
 10 
     | 
    
         
            +
                        r.post '' do
         
     | 
| 
      
 11 
     | 
    
         
            +
                          r.halt 200 if request.env['HTTP_X_GITHUB_EVENT'] == 'ping'
         
     | 
| 
      
 12 
     | 
    
         
            +
                          r.halt 400 unless request.env['HTTP_X_GITHUB_EVENT'] == 'workflow_run'
         
     | 
| 
      
 13 
     | 
    
         
            +
                          r.halt 401 unless authorized? request
         
     | 
| 
      
 14 
     | 
    
         
            +
                          json = JSON.parse(request.body.read)
         
     | 
| 
      
 15 
     | 
    
         
            +
                          project = json.dig('repository', 'full_name')
         
     | 
| 
      
 16 
     | 
    
         
            +
                          status = json.dig('workflow_run', 'status')
         
     | 
| 
      
 17 
     | 
    
         
            +
                          status = json.dig('workflow_run', 'conclusion') if status == 'completed'
         
     | 
| 
      
 18 
     | 
    
         
            +
                          Rodbot.say [emoji_for(status), project, status.gsub('_', ' ')].join(' ')
         
     | 
| 
      
 19 
     | 
    
         
            +
                          r.halt 200
         
     | 
| 
      
 20 
     | 
    
         
            +
                        end
         
     | 
| 
      
 21 
     | 
    
         
            +
                      end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                      private
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                      def authorized?(request)
         
     | 
| 
      
 26 
     | 
    
         
            +
                        Rodbot.config(:plugin, :github_webhook, :secret_tokens).to_s.split(':').any? do |secret|
         
     | 
| 
      
 27 
     | 
    
         
            +
                          signature = 'sha256=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret, request.body.read)
         
     | 
| 
      
 28 
     | 
    
         
            +
                          request.body.rewind
         
     | 
| 
      
 29 
     | 
    
         
            +
                          ::Rack::Utils.secure_compare(signature, request.env['HTTP_X_HUB_SIGNATURE_256'])
         
     | 
| 
      
 30 
     | 
    
         
            +
                        end
         
     | 
| 
      
 31 
     | 
    
         
            +
                      end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                      def emoji_for(status)
         
     | 
| 
      
 34 
     | 
    
         
            +
                        case status
         
     | 
| 
      
 35 
     | 
    
         
            +
                          when 'requested' then '🟡'
         
     | 
| 
      
 36 
     | 
    
         
            +
                          when 'success' then '🟢'
         
     | 
| 
      
 37 
     | 
    
         
            +
                          when 'failure' then '🔴'
         
     | 
| 
      
 38 
     | 
    
         
            +
                          else '⚪️'
         
     | 
| 
      
 39 
     | 
    
         
            +
                        end
         
     | 
| 
      
 40 
     | 
    
         
            +
                      end
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                    end
         
     | 
| 
      
 43 
     | 
    
         
            +
                  end
         
     | 
| 
      
 44 
     | 
    
         
            +
                end
         
     | 
| 
      
 45 
     | 
    
         
            +
              end
         
     | 
| 
      
 46 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,40 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # Rodbot Plugin – GitLab Webhook
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            Pipeline event announcements from GitLab
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            ## Preparation
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            The Rodbot app binds to `localhost` by default which cannot be reached from GitLab. Make sure this connection is possible by setting a different IP in `config/rodbot.rb`:
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 10 
     | 
    
         
            +
            app do
         
     | 
| 
      
 11 
     | 
    
         
            +
              host '0.0.0.0'
         
     | 
| 
      
 12 
     | 
    
         
            +
            end
         
     | 
| 
      
 13 
     | 
    
         
            +
            ```
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
            To authenticate the webhook calls from GitLab, create a new random secret token:
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
            ```
         
     | 
| 
      
 18 
     | 
    
         
            +
            ruby -r securerandom -e "puts SecureRandom.alphanumeric(20)"
         
     | 
| 
      
 19 
     | 
    
         
            +
            ```
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
            ## Activation
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
            Activate and configure this plugin in `config/rodbot.rb`:
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 26 
     | 
    
         
            +
            plugin :gitlab_webhook do
         
     | 
| 
      
 27 
     | 
    
         
            +
              secret_tokens '<TOKEN>'
         
     | 
| 
      
 28 
     | 
    
         
            +
            end
         
     | 
| 
      
 29 
     | 
    
         
            +
            ```
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
            You can set any number of secure tokens here separated with colons.
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
            ## Add Repositories
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
            Add a webhook to every GitLab repository you'd like to see pipeline event announcements for. Go to `https://gitlab.com/<USER>/<REPO>/-/hooks` and create a new webhook with the following properties:
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
            * URL: `https://<RODBOT-APP>/gitlab_webhook`
         
     | 
| 
      
 38 
     | 
    
         
            +
            * Secret token: `<TOKEN>`
         
     | 
| 
      
 39 
     | 
    
         
            +
            * Trigger: [x] Pipeline events
         
     | 
| 
      
 40 
     | 
    
         
            +
            * SSL verification: [x] Enable SSL verification
         
     | 
| 
         @@ -0,0 +1,40 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Rodbot
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Plugins
         
     | 
| 
      
 5 
     | 
    
         
            +
                class GitlabWebhook
         
     | 
| 
      
 6 
     | 
    
         
            +
                  module App
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                    class Routes < ::App
         
     | 
| 
      
 9 
     | 
    
         
            +
                      route do |r|
         
     | 
| 
      
 10 
     | 
    
         
            +
                        r.post '' do
         
     | 
| 
      
 11 
     | 
    
         
            +
                          r.halt 401 unless authorized? request
         
     | 
| 
      
 12 
     | 
    
         
            +
                          json = JSON.parse(request.body.read)
         
     | 
| 
      
 13 
     | 
    
         
            +
                          r.halt 400 unless json['object_kind'] == 'pipeline'
         
     | 
| 
      
 14 
     | 
    
         
            +
                          project = json.dig('project', 'path_with_namespace')
         
     | 
| 
      
 15 
     | 
    
         
            +
                          status = json.dig('object_attributes', 'detailed_status')
         
     | 
| 
      
 16 
     | 
    
         
            +
                          Rodbot.say [emoji_for(status), project, status.gsub('_', ' ')].join(' ')
         
     | 
| 
      
 17 
     | 
    
         
            +
                          r.halt 200
         
     | 
| 
      
 18 
     | 
    
         
            +
                        end
         
     | 
| 
      
 19 
     | 
    
         
            +
                      end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                      private
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                      def authorized?(request)
         
     | 
| 
      
 24 
     | 
    
         
            +
                        Rodbot.config(:plugin, :gitlab_webhook, :secret_tokens).to_s.split(':').include?(request.env['HTTP_X_GITLAB_TOKEN'])
         
     | 
| 
      
 25 
     | 
    
         
            +
                      end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                      def emoji_for(status)
         
     | 
| 
      
 28 
     | 
    
         
            +
                        case status
         
     | 
| 
      
 29 
     | 
    
         
            +
                          when 'running' then '🟡'
         
     | 
| 
      
 30 
     | 
    
         
            +
                          when 'passed' then '🟢'
         
     | 
| 
      
 31 
     | 
    
         
            +
                          when 'failed' then '🔴'
         
     | 
| 
      
 32 
     | 
    
         
            +
                          else '⚪️'
         
     | 
| 
      
 33 
     | 
    
         
            +
                        end
         
     | 
| 
      
 34 
     | 
    
         
            +
                      end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                    end
         
     | 
| 
      
 37 
     | 
    
         
            +
                  end
         
     | 
| 
      
 38 
     | 
    
         
            +
                end
         
     | 
| 
      
 39 
     | 
    
         
            +
              end
         
     | 
| 
      
 40 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,22 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Rodbot
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Plugins
         
     | 
| 
      
 5 
     | 
    
         
            +
                class Hal
         
     | 
| 
      
 6 
     | 
    
         
            +
                  module App
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                    class Routes < ::App
         
     | 
| 
      
 9 
     | 
    
         
            +
                      route do |r|
         
     | 
| 
      
 10 
     | 
    
         
            +
                        r.root do
         
     | 
| 
      
 11 
     | 
    
         
            +
                          response['Content-Type'] = 'text/markdown; charset=utf-8'
         
     | 
| 
      
 12 
     | 
    
         
            +
                          <<~END
         
     | 
| 
      
 13 
     | 
    
         
            +
                            [🔴](https://www.youtube.com/watch?v=ARJ8cAGm6JE) I'm sorry [[SENDER]], I'm afraid I can't do that.
         
     | 
| 
      
 14 
     | 
    
         
            +
                          END
         
     | 
| 
      
 15 
     | 
    
         
            +
                        end
         
     | 
| 
      
 16 
     | 
    
         
            +
                      end
         
     | 
| 
      
 17 
     | 
    
         
            +
                    end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                  end
         
     | 
| 
      
 20 
     | 
    
         
            +
                end
         
     | 
| 
      
 21 
     | 
    
         
            +
              end
         
     | 
| 
      
 22 
     | 
    
         
            +
            end
         
     |