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
|