ubicloud/routes/web/webhook/github.rb
Enes Cakir 4385c4a6cb Add a prog to destroy GitHub installation
GithubInstallation has repositories and runners that must be destroyed
when the installation is destroyed. Given that they have strands, their
removal is asynchronous. However, the installation must be removed
synchronously when we receive a webhook from GitHub or when the user
attempts to delete the project. Currently, it fails if the installation
has any repositories or runners.

Since the installation doesn't have a strand, I've created an
operational strand to facilitate its destruction. In this prog, we
initially delete the installation from the GitHub API. This ensures that
GitHub will no longer send us new webhook events. Then it destroys the
repositories and runners, then proceed to destroy the installation. It
bypasses deregistration of the runner to ensure destroy it immediately,
even if it's currently running a job.

We may also need to manually clean up the installation due to fraudulent
activities. This prog will assist with these operations.
2024-10-03 17:37:42 +03:00

107 lines
3.1 KiB
Ruby

# frozen_string_literal: true
class CloverWeb
hash_branch(:webhook_prefix, "github") do |r|
r.post true do
body = r.body.read
unless check_signature(r.headers["x-hub-signature-256"], body)
response.status = 401
r.halt
end
response.headers["Content-Type"] = "application/json"
data = JSON.parse(body)
case r.headers["x-github-event"]
when "installation"
return handle_installation(data)
when "workflow_job"
return handle_workflow_job(data)
end
return error("Unhandled event")
end
end
def error(msg)
{error: {message: msg}}.to_json
end
def success(msg)
{message: msg}.to_json
end
def check_signature(signature, body)
return false unless signature
method, actual_digest = signature.split("=")
expected_digest = OpenSSL::HMAC.hexdigest(method, Config.github_app_webhook_secret, body)
Rack::Utils.secure_compare(actual_digest, expected_digest)
end
def handle_installation(data)
installation = GithubInstallation[installation_id: data["installation"]["id"]]
case data["action"]
when "deleted"
unless installation
return error("Unregistered installation")
end
Prog::Github::DestroyGithubInstallation.assemble(installation)
return success("GithubInstallation[#{installation.ubid}] deleted")
end
error("Unhandled installation action")
end
def handle_workflow_job(data)
unless (installation = GithubInstallation[installation_id: data["installation"]["id"]])
return error("Unregistered installation")
end
unless (job = data["workflow_job"])
Clog.emit("No workflow_job in the payload") { {workflow_job_missing: {installation_id: installation.id, action: data["action"]}} }
return error("No workflow_job in the payload")
end
unless (label = job.fetch("labels").find { Github.runner_labels.key?(_1) })
return error("Unmatched label")
end
if data["action"] == "queued"
st = Prog::Vm::GithubRunner.assemble(
installation,
repository_name: data["repository"]["full_name"],
label: label,
default_branch: data["repository"]["default_branch"]
)
runner = GithubRunner[st.id]
return success("GithubRunner[#{runner.ubid}] created")
end
unless (runner_id = job.fetch("runner_id"))
return error("A workflow_job without runner_id")
end
runner = GithubRunner.first(
installation_id: installation.id,
repository_name: data["repository"]["full_name"],
runner_id: runner_id
)
return error("Unregistered runner") unless runner
runner.update(workflow_job: job.except("steps"))
case data["action"]
when "in_progress"
runner.log_duration("runner_started", Time.parse(job["started_at"]) - Time.parse(job["created_at"]))
success("GithubRunner[#{runner.ubid}] picked job #{job.fetch("id")}")
when "completed"
runner.incr_destroy
success("GithubRunner[#{runner.ubid}] deleted")
else
error("Unhandled workflow_job action")
end
end
end