Skip to content

Poc #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Jun 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ The linter is powered by `rubocop` with its config file located at `.rubocop.yml
- The use of whitespace (newlines) over compactness of files.
- Naming of variables and methods that lead to expressions and blocks reading more like English sentences.
- Less lines of code over more. Keep changes minimal and focused.
- The `docs/design.md` file is the main design document for the project. It might be out-of-date but it should still contain a general high-level overview of the project.

## Pull Request Requirements

Expand Down
29 changes: 29 additions & 0 deletions .github/workflows/acceptance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: acceptance

on:
push:
branches:
- main
pull_request:

permissions:
contents: read

jobs:
acceptance:
name: acceptance
runs-on: ubuntu-latest

steps:
- name: checkout
uses: actions/checkout@v4

- uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # [email protected]
with:
bundler-cache: true

- name: bootstrap
run: script/bootstrap

- name: acceptance
run: script/acceptance
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
bin/
# Ignore binstubs but do commit the one specific for this code.
bin/*
!bin/hooks

coverage/
logs/
tmp/
spec/integration/tmp/
tarballs/
vendor/gems/
.idea
Expand Down
43 changes: 0 additions & 43 deletions Dockerfile

This file was deleted.

181 changes: 181 additions & 0 deletions bin/hooks
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

# Development CLI script for the hooks framework

# Add lib directory to load path so we can require our code
lib_dir = File.expand_path("../lib", __dir__)
$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)

# Set bundle gemfile
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)

require "bundler/setup"
require "optparse"
require "hooks"
require "yaml"

# CLI implementation
class HooksCLI
def initialize
@options = {
config_file: "hooks.yaml",
port: 4567,
host: "0.0.0.0",
environment: "development",
threads: "5:5"
}
end

def run(args = ARGV)
# Handle version and help flags before parsing other options
if args.include?("--version") || args.include?("-v")
puts Hooks::VERSION
exit
end

if args.include?("--help") || args.include?("-h") || args.include?("help")
show_help
exit
end

parse_options(args)

case args.first
when "start", nil
start_server
when "version"
puts Hooks::VERSION
else
puts "Unknown command: #{args.first}"
show_help
exit 1
end
end

private

def parse_options(args)
OptionParser.new do |opts|
opts.banner = "Usage: hooks [command] [options]"

opts.on("-c", "--config FILE", "Configuration file (default: hooks.yaml)") do |file|
@options[:config_file] = file
end

opts.on("-p", "--port PORT", Integer, "Port to listen on (default: 4567)") do |port|
@options[:port] = port
end

opts.on("-H", "--host HOST", "Host to bind to (default: 0.0.0.0)") do |host|
@options[:host] = host
end

opts.on("-e", "--environment ENV", "Environment (default: development)") do |env|
@options[:environment] = env
end

opts.on("-t", "--threads THREADS", "Puma thread pool size (default: 5:5)") do |threads|
@options[:threads] = threads
end

opts.on("-h", "--help", "Show this help message") do
show_help
exit
end

opts.on("-v", "--version", "Show version") do
puts Hooks::VERSION
exit
end
end.parse!(args)
end

def start_server
puts "Starting Hooks webhook server..."
puts "Config file: #{@options[:config_file]}"
puts "Host: #{@options[:host]}"
puts "Port: #{@options[:port]}"
puts "Environment: #{@options[:environment]}"
puts "Threads: #{@options[:threads]}"
puts

# parse the configuration file
if File.exist?(@options[:config_file])
begin
config = YAML.load_file(@options[:config_file])
rescue Psych::SyntaxError => e
puts "Error parsing configuration file: #{e.message}"
exit 1
end
else
puts "Configuration file #{@options[:config_file]} not found. Using defaults."
config = {}
end

# Merge CLI options into config
config.merge!({
"host" => @options[:host],
"port" => @options[:port],
"environment" => @options[:environment],
"threads" => @options[:threads]
})

# Build the application with framework-level config
app = Hooks.build(config:)

# Start the server with CLI options
require "rack"
require "rack/handler/puma"
require "puma"

Rack::Handler::Puma.run(
app,
Host: @options[:host],
Port: @options[:port],
Threads: @options[:threads],
environment: @options[:environment]
)
rescue Interrupt
puts "\nShutting down gracefully..."
exit 0
rescue => e
puts "Error starting server: #{e.message}"
puts e.backtrace if @options[:environment] == "development"
exit 1
end

def show_help
puts <<~HELP
Hooks - A Pluggable Webhook Server Framework

Usage:
hooks [start] Start the webhook server (default)
hooks version Show version information
hooks help Show this help message

Options:
-c, --config FILE Configuration file (default: hooks.yaml)
-p, --port PORT Port to listen on (default: 4567)
-H, --host HOST Host to bind to (default: 0.0.0.0)
-e, --environment ENV Environment (default: development)
-t, --threads THREADS Puma thread pool size (default: 5:5)
-h, --help Show this help message
-v, --version Show version

Examples:
hooks Start server with default settings
hooks start -p 8080 Start server on port 8080
hooks -c custom.yaml -e production Start with custom config in production mode
hooks -t 10:10 Start with 10 threads
hooks version Show version information

For more information, see the README.md file.
HELP
end
end

# Run the CLI if this file is executed directly
if __FILE__ == $0
HooksCLI.new.run
end
6 changes: 6 additions & 0 deletions config.ru
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

require_relative "lib/hooks"

app = Hooks.build(config: "./spec/acceptance/config/hooks.yaml")
run app
4 changes: 2 additions & 2 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ path: /team1 # Mounted at <root_path>/team1
handler: Team1Handler # Class in handler_dir

# Signature validation
verify_signature:
request_validator:
type: default # 'default' uses HMACSHA256, or a custom class name
secret_env_key: TEAM1_SECRET
header: X-Hub-Signature
Expand Down Expand Up @@ -613,7 +613,7 @@ path: string # Endpoint path (mounted under root_path)
handler: string # Handler class name

# Optional signature validation
verify_signature:
request_validator:
type: string # 'default' or custom validator class name
secret_env_key: string # ENV key containing secret
header: string # Header containing signature (default: X-Hub-Signature)
Expand Down
5 changes: 4 additions & 1 deletion hooks.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ Gem::Specification.new do |spec|

spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")

spec.files = %w[LICENSE README.md hooks.gemspec]
spec.files = %w[LICENSE README.md hooks.gemspec config.ru]
spec.files += Dir.glob("lib/**/*.rb")
spec.files += Dir.glob("bin/*")
spec.bindir = "bin"
spec.executables = ["hooks"]
spec.require_paths = ["lib"]
end
30 changes: 30 additions & 0 deletions lib/hooks.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

require_relative "hooks/version"
require_relative "hooks/core/builder"
require_relative "hooks/handlers/base"

# Load all plugins (request validators, lifecycle hooks, etc.)
Dir[File.join(__dir__, "hooks/plugins/**/*.rb")].sort.each do |file|
require file
end

# Load all utils
Dir[File.join(__dir__, "hooks/utils/**/*.rb")].sort.each do |file|
require file
end

# Main module for the Hooks webhook server framework
module Hooks
# Build a Rack-compatible webhook server application
#
# @param config [String, Hash] Path to config file or config hash
# @param log [Logger] Custom logger instance (optional)
# @return [Object] Rack-compatible application
def self.build(config: nil, log: nil)
Core::Builder.new(
config:,
log:,
).build
end
end
Loading
Loading