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