From 8cbeb98b595d819f47fa4afc81e69597b2316dc5 Mon Sep 17 00:00:00 2001 From: Jason Volk Date: Sat, 1 Apr 2023 11:23:01 -0700 Subject: [PATCH] modules/web_hook: Add github workflow handlers. --- modules/web_hook.cc | 552 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 551 insertions(+), 1 deletion(-) diff --git a/modules/web_hook.cc b/modules/web_hook.cc index 47f94d644..d198a17d3 100644 --- a/modules/web_hook.cc +++ b/modules/web_hook.cc @@ -131,6 +131,23 @@ static bool github_handle__dependabot_alert(std::ostream &, const json::object &content); +static bool +github_handle__check_suite(std::ostream &, + const json::object &content); + +static bool +github_handle__check_run(std::ostream &, + const json::object &content); + +static bool +github_handle__workflow_job(std::ostream &, + const json::object &content); + +static bool +github_handle__workflow_run(std::ostream &, + const json::object &content); + +static bool github_handle__milestone(std::ostream &, const json::object &content); @@ -281,6 +298,14 @@ github_handle(client &client, github_handle__milestone(out, request.content): type == "dependabot_alert"? github_handle__dependabot_alert(out, request.content): + type == "workflow_run"? + github_handle__workflow_run(out, request.content): + type == "workflow_job"? + github_handle__workflow_job(out, request.content): + type == "check_run"? + github_handle__check_run(out, request.content): + type == "check_suite"? + github_handle__check_suite(out, request.content): true // unhandled will just show heading }; @@ -374,8 +399,23 @@ github_heading(std::ostream &out, github_find_issue_number(content) }; + const json::object workflow + { + content.has("workflow_run")? + content["workflow_run"]: + content["workflow_job"] + }; + + const json::string + workflow_name{workflow["workflow_name"]}, + job_name{workflow["name"]}; + if(issue_number) out << " #" << issue_number << ""; + else if(workflow_name && job_name) + out << " job " << workflow_name << ""; + else if(job_name) + out << " job " << job_name << ""; else out << " " << type; @@ -463,6 +503,500 @@ github_handle__dependabot_alert(std::ostream &out, return true; } +// Find the message resulting from the push and react with the status. +static ircd::m::event::id::buf +github_find_push_reaction_id(const m::room &room, + const m::user::id &user_id, + const m::event::id &push_event_id, + const string_view &label) +{ + const auto type_match + { + [](const string_view &type) noexcept + { + return type == "m.reaction"; + } + }; + + const auto user_match + { + [&user_id](const string_view &sender) noexcept + { + return sender && sender == user_id; + } + }; + + const auto content_match + { + [&push_event_id, &label](const json::object &content) + { + const json::object relates_to + { + content["m.relates_to"] + }; + + const json::string event_id + { + relates_to["event_id"] + }; + + const json::string key + { + relates_to["key"] + }; + + return event_id == push_event_id && endswith(key, label); + } + }; + + // Limit the search to a maximum of recent messages from the + // webhook user and total messages so we don't run out of control + // and scan the whole room history. + int lim[2] { 768, 384 }; + m::room::events it{room}; + for(; it && lim[0] > 0 && lim[1] > 0; --it, --lim[0]) + { + if(!m::query(std::nothrow, it.event_idx(), "sender", user_match)) + continue; + + --lim[1]; + if(!m::query(std::nothrow, it.event_idx(), "type", type_match)) + continue; + + if(!m::query(std::nothrow, it.event_idx(), "content", content_match)) + continue; + + return m::event_id(std::nothrow, it.event_idx()); + } + + return {}; +} + +// Find the message resulting from the push and react with the status. +static ircd::m::event::id::buf +github_find_job_table(const m::room &room, + const m::user::id &user_id, + const string_view &str) +{ + const auto type_match + { + [](const string_view &type) noexcept + { + return type == "m.room.message"; + } + }; + + const auto user_match + { + [&user_id](const string_view &sender) noexcept + { + return sender && sender == user_id; + } + }; + + const auto content_match + { + [&str](const json::object &content) + { + const json::string &body + { + content["body"] + }; + + return has(body, str); + } + }; + + // Limit the search to a maximum of recent messages from the + // webhook user and total messages so we don't run out of control + // and scan the whole room history. + int lim[2] { 768, 384 }; + m::room::events it{room}; + for(; it && lim[0] > 0 && lim[1] > 0; --it, --lim[0]) + { + if(!m::query(std::nothrow, it.event_idx(), "sender", user_match)) + continue; + + --lim[1]; + if(!m::query(std::nothrow, it.event_idx(), "type", type_match)) + continue; + + if(!m::query(std::nothrow, it.event_idx(), "content", content_match)) + continue; + + return m::event_id(std::nothrow, it.event_idx()); + } + + return {}; +} + +bool +github_handle__workflow_run(std::ostream &out, + const json::object &content) +{ + const json::object + workflow{content["workflow"]}, + workflow_run{content["workflow_run"]}; + + const json::string + action{content["action"]}, + title{workflow_run["display_title"]}, + status{workflow_run["status"]}, + conclusion{workflow_run["conclusion"]}, + url{workflow_run["html_url"]}, + name{workflow_run["name"]}, + head_sha{workflow_run["head_sha"]}, + created_at{workflow_run["created_at"]}, + updated_at{workflow_run["updated_at"]}, + run_started_at{workflow_run["run_started_at"]}, + run_id{workflow_run["id"]}; + + const auto _webhook_room_id + { + m::room_id(string_view(webhook_room)) + }; + + const m::user::id::buf _webhook_user + { + string_view{webhook_user}, my_host() + }; + + const m::room _webhook_room + { + _webhook_room_id + }; + + const auto push_event_id + { + github_find_push_event_id(_webhook_room, _webhook_user, head_sha) + }; + + const auto &stage + { + workflow_run["conclusion"] == json::literal_null? + status: conclusion + }; + + string_view annote; + switch(hash(stage)) + { + case "queued"_: annote = "🔵"_sv; break; + case "in_progress"_: annote = "🟡"_sv; break; + case "success"_: annote = "🟢"_sv; break; + case "failure"_: annote = "🔴"_sv; break; + case "skipped"_: annote = "⭕"_sv; break; + case "cancelled"_: annote = "⭕"_sv; break; + default: annote = "❓️"_sv; break; + } + + char buf[64] {0}; + annote = ircd::strlcpy(buf, annote); + annote = ircd::strlcat(buf, " "_sv); + annote = ircd::strlcat(buf, name); + + const auto reaction_id + { + push_event_id && action != "requested"? // skip search on first action + github_find_push_reaction_id(_webhook_room, _webhook_user, push_event_id, name): + m::event::id::buf{} + }; + + if(reaction_id) + m::redact(_webhook_room, _webhook_user, reaction_id, "status change"); + + m::annotate(_webhook_room, _webhook_user, push_event_id, annote); + + bool outputs{false}; + if(action == "requested" && conclusion == "failure") + { + outputs = true; + out + << "
" + << "" + << " " + << " " + << "" + << name + << "" + << " " + << " " + << "" + << " failed " + << "" + << "" + ; + } + + return outputs; +} + +bool +github_handle__workflow_job(std::ostream &out, + const json::object &content) +{ + const json::object workflow_job + { + content["workflow_job"] + }; + + const json::string + action{content["action"]}, + flow_name{workflow_job["workflow_name"]}, + job_name{workflow_job["name"]}, + url{workflow_job["html_url"]}, + status{workflow_job["status"]}, + conclusion{workflow_job["conclusion"]}, + head_sha{workflow_job["head_sha"]}, + started_at{workflow_job["started_at"]}, + completed_at{workflow_job["completed_at"]}, + run_id{workflow_job["run_id"]}, + job_id{workflow_job["id"]}; + + const json::array steps + { + workflow_job["steps"] + }; + + const auto _webhook_room_id + { + m::room_id(string_view(webhook_room)) + }; + + const m::user::id::buf _webhook_user + { + string_view{webhook_user}, my_host() + }; + + const m::room _webhook_room + { + _webhook_room_id + }; + + const auto &stage + { + workflow_job["conclusion"] == json::literal_null? + status: conclusion + }; + + string_view annote; + switch(hash(stage)) + { + case "queued"_: annote = "🟦"_sv; break; + case "in_progress"_: annote = "🟨"_sv; break; + case "success"_: annote = "🟩"_sv; break; + case "failure"_: annote = "🟥"_sv; break; + case "skipped"_: annote = "⬜️"_sv; break; + case "cancelled"_: annote = "⬛️"_sv; break; + default: annote = "❓️"_sv; break; + } + + const fmt::bsprintf<96> alt + { + "job %s status table", run_id + }; + + const fmt::bsprintf<96> alt_up + { + "job %s status update", run_id + }; + + // slow this bird down + static ctx::mutex mutex; + const std::unique_lock lock + { + mutex + }; + + const auto orig_table_id + { + github_find_job_table(_webhook_room, _webhook_user, alt) + }; + + const auto last_table_id + { + github_find_job_table(_webhook_room, _webhook_user, alt_up) + }; + + const unique_mutable_buffer buf + { + 32_KiB + }; + + if(orig_table_id) + { + const auto old_content + { + m::get(last_table_id?: orig_table_id, "content") + }; + + const json::string old_tab + { + json::object(old_content)["formatted_body"] + }; + + const string_view td + { + between(old_tab, "", "") + }; + + char headbuf[512] {0}; + std::stringstream heading; + pubsetbuf(heading, headbuf); + github_heading(heading, "push", content); + + string_view tab; + tab = ircd::strlcpy(buf, view(heading, headbuf)); + tab = ircd::strlcat(buf, "
"); + + const fmt::bsprintf<512> expect + { + "", url + }; + + bool estab(false); + tokens(td, "​"_sv, [&] + (const string_view &cell) + { + if(!startswith(cell, expect)) + { + tab = ircd::strlcat(buf, cell); + tab = ircd::strlcat(buf, "​"_sv); + return; + } + + tab = ircd::strlcat(buf, ""); + tab = ircd::strlcat(buf, annote); + tab = ircd::strlcat(buf, ""); + tab = ircd::strlcat(buf, "​"_sv); + estab = true; + }); + + if(!estab) + { + tab = ircd::strlcat(buf, ""); + tab = ircd::strlcat(buf, annote); + tab = ircd::strlcat(buf, ""); + tab = ircd::strlcat(buf, "​"_sv); + } + + tab = ircd::strlcat(buf, "
"); + + m::message(_webhook_room, _webhook_user, json::members + { + { "body", alt_up }, + { "msgtype", "m.notice" }, + { "format", "org.matrix.custom.html" }, + { "formatted_body", tab }, + { "m.new_content", json::members + { + { "body", alt_up }, + { "msgtype", "m.notice" }, + { "format", "org.matrix.custom.html" }, + { "formatted_body", tab }, + }}, + { "m.relates_to", json::members + { + { "event_id", orig_table_id }, + { "rel_type", "m.replace" }, + }} + }); + } else { + + char headbuf[512] {0}; + std::stringstream heading; + pubsetbuf(heading, headbuf); + github_heading(heading, "push", content); + + string_view tab; + tab = ircd::strlcpy(buf, view(heading, headbuf)); + tab = ircd::strlcat(buf, "
"); + + tab = ircd::strlcat(buf, ""); + tab = ircd::strlcat(buf, annote); + tab = ircd::strlcat(buf, ""); + tab = ircd::strlcat(buf, "​"_sv); + + tab = ircd::strlcat(buf, "
"); + + m::msghtml(_webhook_room, _webhook_user, tab, alt); + } + + bool outputs{false}; + if(conclusion == "failure") + { + outputs = true; + out + << "
" + << "" + << " " + << " " + << "" + << flow_name + << "" + << " " + << " " + << "" + << " failed " + << "" + << "" + << job_name + << "" + << "" + ; + } + + return outputs; +} + +bool +github_handle__check_run(std::ostream &out, + const json::object &content) +{ + const json::string action + { + content["action"] + }; + + const json::object check_run + { + content["check_run"] + }; + + const json::object check_suite + { + check_run["check_suite"] + }; + + return false; +} + +bool +github_handle__check_suite(std::ostream &out, + const json::object &content) +{ + const json::string action + { + content["action"] + }; + + const json::object check_suite + { + content["check_suite"] + }; + + return false; +} + bool github_handle__gollum(std::ostream &out, const json::object &content) @@ -1726,7 +2260,7 @@ github_find_push_event_id(const m::room &room, // Limit the search to a maximum of recent messages from the // webhook user and total messages so we don't run out of control // and scan the whole room history. - int lim[2] { 512, 32 }; + int lim[2] { 768, 384 }; m::room::events it{room}; for(; it && lim[0] > 0 && lim[1] > 0; --it, --lim[0]) { @@ -1847,6 +2381,22 @@ github_find_commit_hash(const json::object &content) if(prhead["sha"]) return prhead["sha"]; + const json::object workflow_run{content["workflow_run"]}; + if(workflow_run["head_sha"]) + return workflow_run["head_sha"]; + + const json::object workflow_job{content["workflow_job"]}; + if(workflow_job["head_sha"]) + return workflow_job["head_sha"]; + + const json::object check_run{content["check_run"]}; + if(check_run["head_sha"]) + return check_run["head_sha"]; + + const json::object check_suite{content["check_suite"]}; + if(check_suite["head_sha"]) + return check_suite["head_sha"]; + return {}; }