diff --git a/ansible/files/home/deployer/webhook/lib/deployer/app.rb b/ansible/files/home/deployer/webhook/lib/deployer/app.rb
index cf6f8c1..b9559aa 100644
--- a/ansible/files/home/deployer/webhook/lib/deployer/app.rb
+++ b/ansible/files/home/deployer/webhook/lib/deployer/app.rb
@@ -14,7 +14,9 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
+require "json"
require "openssl"
+require_relative "payload"
require_relative "response"
module Deployer
@@ -29,23 +31,70 @@ def call(env)
private
def process(request, response)
- unless request.post?
- response.set(:method_not_allowed, "must POST")
- return
- end
-
- unless valid_signature?(request)
- response.set(:unauthorized, "Authorization failed")
- return
+ begin
+ unless request.post?
+ raise RequestError.new(:method_not_allowed, "must POST")
+ end
+ verify_signature!(request)
+ payload = parse_body!(request)
+ process_payload!(payload)
+ rescue RequestError => request_error
+ response.set(request_error.status, request_error.message)
+ rescue => e
+ response.set(:internal_server_error, e.message)
end
end
- def valid_signature?(request)
+ def verify_signature!(request)
hmac_sha256 = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"),
ENV["SECRET_TOKEN"],
request.body.read)
signature = "sha256=#{hmac_sha256}"
- Rack::Utils.secure_compare(signature, request.env["HTTP_X_HUB_SIGNATURE_256"])
+ unless Rack::Utils.secure_compare(signature, request.env["HTTP_X_HUB_SIGNATURE_256"])
+ raise RequestError.new(:unauthorized, "Authorization failed")
+ end
+ end
+
+ def parse_body!(request)
+ unless request.media_type == "application/json"
+ raise RequestError.new(:bad_request, "invalid payload format")
+ end
+
+ body = request.body.read
+ if body.nil?
+ raise RequestError.new(:bad_request, "request body is missing")
+ end
+
+ begin
+ raw_payload = JSON.parse(body)
+ rescue JSON::ParserError
+ raise RequestError.new(:bad_request, "invalid JSON format: <#{$!.message}>")
+ end
+
+ metadata = {
+ "x-github-event" => request.env["HTTP_X_GITHUB_EVENT"]
+ }
+ Payload.new(raw_payload, metadata)
+ end
+
+ def process_payload!(payload)
+ case payload.event_name
+ when "ping"
+ # Do nothing because this is a kind of healthcheck.
+ nil
+ when "workflow_run"
+ return unless payload.released?
+ deploy(payload)
+ else
+ raise RequestError.new(:bad_request, "Unsupported event: <#{payload.event_name}>")
+ end
+ end
+
+ def deploy(payload)
+ Thread.new do
+ # TODO: call rake tasks for sign packages.
+ # TODO: write down the errors into log files.
+ end
end
end
end
diff --git a/ansible/files/home/deployer/webhook/lib/deployer/error.rb b/ansible/files/home/deployer/webhook/lib/deployer/error.rb
new file mode 100644
index 0000000..80c708b
--- /dev/null
+++ b/ansible/files/home/deployer/webhook/lib/deployer/error.rb
@@ -0,0 +1,29 @@
+# Copyright (C) 2024 Horimoto Yasuhiro
+# Copyright (C) 2024 Takuya Kodama
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+module Deployer
+ class Error < StandardError
+ end
+
+ class RequestError < Error
+ attr_reader :status, :message
+
+ def initialize(status, message)
+ @status = status
+ @message = message
+ end
+ end
+end
diff --git a/ansible/files/home/deployer/webhook/lib/deployer/payload.rb b/ansible/files/home/deployer/webhook/lib/deployer/payload.rb
new file mode 100644
index 0000000..ee940af
--- /dev/null
+++ b/ansible/files/home/deployer/webhook/lib/deployer/payload.rb
@@ -0,0 +1,73 @@
+# Copyright (C) 2010-2019 Sutou Kouhei
+# Copyright (C) 2015 Kenji Okimoto
+# Copyright (C) 2024 Takuya Kodama
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+module Deployer
+ class Payload
+ RELEASE_WORKFLOWS = ["Package", "CMake"].freeze
+
+ def initialize(data, metadata={})
+ @data = data
+ @metadata = metadata
+ end
+
+ def [](key)
+ @data.dig(*key.split("."))
+ end
+
+ def event_name
+ @metadata["x-github-event"]
+ end
+
+ def workflow_name
+ self["workflow_run.name"]
+ end
+
+ def workflow_succeeded?
+ self["workflow_run.conclusion"] == "success"
+ end
+
+ def branch
+ self["workflow_run.head_branch"]
+ end
+
+ def version
+ return unless workflow_tag?
+ branch.delete_prefix("v")
+ end
+
+ def released?
+ RELEASE_WORKFLOWS.include?(workflow_name) &&
+ workflow_tag? &&
+ workflow_succeeded?
+ end
+
+ def repository_owner
+ self["repository.owner.login"]
+ end
+
+ def repository_name
+ self["repository.name"]
+ end
+
+ private
+
+ def workflow_tag?
+ return false unless branch
+ branch.match?(/\Av\d+(\.\d+){1,2}\z/)
+ end
+ end
+end