diff options
author | Francois Kritzinger <francois@codesynthesis.com> | 2024-12-10 16:19:52 +0200 |
---|---|---|
committer | Francois Kritzinger <francois@codesynthesis.com> | 2024-12-10 16:32:40 +0200 |
commit | d716c64e4501a741ac40b0c2fd0303534a7d7f05 (patch) | |
tree | 076dfa192c65a84b2272579a40fb8a564f74e583 /mod | |
parent | c7cb981910fd97128ca5e3f7aa77355a18a56738 (diff) |
Handle built notification
Diffstat (limited to 'mod')
-rw-r--r-- | mod/mod-ci-github-gh.cxx | 34 | ||||
-rw-r--r-- | mod/mod-ci-github-gh.hxx | 6 | ||||
-rw-r--r-- | mod/mod-ci-github-gq.cxx | 184 | ||||
-rw-r--r-- | mod/mod-ci-github-gq.hxx | 40 | ||||
-rw-r--r-- | mod/mod-ci-github-service-data.cxx | 15 | ||||
-rw-r--r-- | mod/mod-ci-github-service-data.hxx | 8 | ||||
-rw-r--r-- | mod/mod-ci-github.cxx | 382 | ||||
-rw-r--r-- | mod/mod-ci-github.hxx | 10 |
8 files changed, 586 insertions, 93 deletions
diff --git a/mod/mod-ci-github-gh.cxx b/mod/mod-ci-github-gh.cxx index 0bc6595..7007db8 100644 --- a/mod/mod-ci-github-gh.cxx +++ b/mod/mod-ci-github-gh.cxx @@ -20,10 +20,9 @@ namespace brep case build_state::queued: return "QUEUED"; case build_state::building: return "IN_PROGRESS"; case build_state::built: return "COMPLETED"; - default: - throw invalid_argument ("invalid build_state value: " + - to_string (static_cast<int> (st))); } + + return ""; // Should never reach. } // Return the build_state corresponding to a GitHub check run status @@ -36,11 +35,38 @@ namespace brep else if (s == "IN_PROGRESS") return build_state::building; else if (s == "COMPLETED") return build_state::built; else - throw invalid_argument ("invalid GitHub check run status: '" + s + + throw invalid_argument ("unexpected GitHub check run status: '" + s + '\''); } string + gh_to_conclusion (result_status rs, bool warning_success) + { + switch (rs) + { + case result_status::success: + return "SUCCESS"; + + case result_status::warning: + return warning_success ? "SUCCESS" : "FAILURE"; + + case result_status::error: + case result_status::abort: + case result_status::abnormal: + return "FAILURE"; + + // Valid values we should never encounter. + // + case result_status::skip: + case result_status::interrupt: + throw invalid_argument ("unexpected result_status value: " + + to_string (rs)); + } + + return ""; // Should never reach. + } + + string gh_check_run_name (const build& b, const build_queued_hints* bh) { string r; diff --git a/mod/mod-ci-github-gh.hxx b/mod/mod-ci-github-gh.hxx index 6fa8590..b3da197 100644 --- a/mod/mod-ci-github-gh.hxx +++ b/mod/mod-ci-github-gh.hxx @@ -77,6 +77,12 @@ namespace brep build_state gh_from_status (const string&); + // If warning_success is true, then map result_status::warning to SUCCESS + // and to FAILURE otherwise. + // + string + gh_to_conclusion (result_status, bool warning_success); + // Create a check_run name from a build. If the second argument is not // NULL, return an abbreviated id if possible. // diff --git a/mod/mod-ci-github-gq.cxx b/mod/mod-ci-github-gq.cxx index 7fbbb4b..954f5a8 100644 --- a/mod/mod-ci-github-gq.cxx +++ b/mod/mod-ci-github-gq.cxx @@ -222,11 +222,11 @@ namespace brep // if unset. Return false and issue diagnostics if the request failed. // static bool - gq_mutate_check_runs (vector<check_run>& crs, + gq_mutate_check_runs (const basic_mark& error, + vector<check_run>& crs, const string& iat, string rq, - build_state st, - const basic_mark& error) noexcept + build_state st) noexcept { vector<gh_check_run> rcrs; @@ -341,13 +341,20 @@ namespace brep // Serialize `createCheckRun` mutations for one or more builds to GraphQL. // + // The conclusion argument (`co`) is required if the build_state is built + // because GitHub does not allow a check run status of completed without a + // conclusion. + // + // The details URL argument (`du`) can be empty for queued but not for the + // other states. + // static string - gq_mutation_create_check_runs ( - const string& ri, // Repository ID - const string& hs, // Head SHA - const vector<reference_wrapper<const build>>& bs, - build_state st, - const build_queued_hints* bh) + gq_mutation_create_check_runs (const string& ri, // Repository ID + const string& hs, // Head SHA + const string& du, // Details URL. + const vector<check_run>& crs, + const string& st, // Check run status. + optional<gq_built_result> br = nullopt) { ostringstream os; @@ -355,28 +362,36 @@ namespace brep // Serialize a `createCheckRun` for each build. // - for (size_t i (0); i != bs.size (); ++i) + for (size_t i (0); i != crs.size (); ++i) { - const build& b (bs[i]); - string al ("cr" + to_string (i)); // Field alias. - // Check run name. - // - string nm (gh_check_run_name (b, bh)); - os << gq_name (al) << ":createCheckRun(input: {" << '\n' - << " name: " << gq_str (nm) << ',' << '\n' - << " repositoryId: " << gq_str (ri) << ',' << '\n' - << " headSha: " << gq_str (hs) << ',' << '\n' - << " status: " << gq_enum (gh_to_status (st)) << '\n' - << "})" << '\n' + << " name: " << gq_str (crs[i].name) << '\n' + << " repositoryId: " << gq_str (ri) << '\n' + << " headSha: " << gq_str (hs) << '\n' + << " status: " << gq_enum (st); + if (!du.empty ()) + { + os << '\n'; + os << " detailsUrl: " << gq_str (du); + } + if (br) + { + os << '\n'; + os << " conclusion: " << gq_enum (br->conclusion) << '\n' + << " output: {" << '\n' + << " title: " << gq_str (br->title) << '\n' + << " summary: " << gq_str (br->summary) << '\n' + << " }"; + } + os << "})" << '\n' // Specify the selection set (fields to be returned). // << "{" << '\n' << " checkRun {" << '\n' - << " id," << '\n' - << " name," << '\n' + << " id" << '\n' + << " name" << '\n' << " status" << '\n' << " }" << '\n' << "}" << '\n'; @@ -389,25 +404,51 @@ namespace brep // Serialize an `updateCheckRun` mutation for one build to GraphQL. // + // The `co` (conclusion) argument is required if the build_state is built + // because GitHub does not allow updating a check run to completed without a + // conclusion. + // static string - gq_mutation_update_check_run (const string& ri, // Repository ID. - const string& ni, // Node ID. - build_state st) + gq_mutation_update_check_run (const string& ri, // Repository ID. + const string& ni, // Node ID. + const string& du, // Details URL. + const string& st, // Check run status. + optional<timestamp> sa, // Started at. + optional<gq_built_result> br) { ostringstream os; os << "mutation {" << '\n' << "cr0:updateCheckRun(input: {" << '\n' - << " checkRunId: " << gq_str (ni) << ',' << '\n' - << " repositoryId: " << gq_str (ri) << ',' << '\n' - << " status: " << gq_enum (gh_to_status (st)) << '\n' - << "})" << '\n' + << " checkRunId: " << gq_str (ni) << '\n' + << " repositoryId: " << gq_str (ri) << '\n' + << " status: " << gq_enum (st); + if (sa) + { + os << '\n'; + os << " startedAt: " << gq_str (gh_to_iso8601 (*sa)); + } + if (!du.empty ()) + { + os << '\n'; + os << " detailsUrl: " << gq_str (du); + } + if (br) + { + os << '\n'; + os << " conclusion: " << gq_enum (br->conclusion) << '\n' + << " output: {" << '\n' + << " title: " << gq_str (br->title) << '\n' + << " summary: " << gq_str (br->summary) << '\n' + << " }"; + } + os << "})" << '\n' // Specify the selection set (fields to be returned). // << "{" << '\n' << " checkRun {" << '\n' - << " id," << '\n' - << " name," << '\n' + << " id" << '\n' + << " name" << '\n' << " status" << '\n' << " }" << '\n' << "}" << '\n' @@ -417,34 +458,56 @@ namespace brep } bool - gq_create_check_runs (vector<check_run>& crs, + gq_create_check_runs (const basic_mark& error, + vector<check_run>& crs, const string& iat, const string& rid, const string& hs, - const vector<reference_wrapper<const build>>& bs, - build_state st, - const build_queued_hints& bh, - const basic_mark& error) + build_state st) { - string rq (gq_serialize_request ( - gq_mutation_create_check_runs (rid, hs, bs, st, &bh))); + // No support for result_status so state cannot be built. + // + assert (st != build_state::built); - return gq_mutate_check_runs (crs, iat, move (rq), st, error); + // Empty details URL because it's not available until building. + // + string rq ( + gq_serialize_request ( + gq_mutation_create_check_runs (rid, hs, "", crs, gh_to_status (st)))); + + return gq_mutate_check_runs (error, crs, iat, move (rq), st); } bool - gq_create_check_run (check_run& cr, + gq_create_check_run (const basic_mark& error, + check_run& cr, const string& iat, const string& rid, const string& hs, - const build& b, + const string& du, build_state st, - const build_queued_hints& bh, - const basic_mark& error) + optional<gq_built_result> br) { + // Must have a result if state is built. + // + assert (st != build_state::built || br); + + // Must have a details URL because `st` should never be queued. + // + assert (!du.empty ()); + vector<check_run> crs {move (cr)}; - bool r (gq_create_check_runs (crs, iat, rid, hs, {b}, st, bh, error)); + string rq ( + gq_serialize_request ( + gq_mutation_create_check_runs (rid, + hs, + du, + crs, + gh_to_status (st), + move (br)))); + + bool r (gq_mutate_check_runs (error, crs, iat, move (rq), st)); cr = move (crs[0]); @@ -452,19 +515,42 @@ namespace brep } bool - gq_update_check_run (check_run& cr, + gq_update_check_run (const basic_mark& error, + check_run& cr, const string& iat, const string& rid, const string& nid, + const string& du, build_state st, - const basic_mark& error) + optional<gq_built_result> br) { + // Must have a result if state is built. + // + assert (st != build_state::built || br); + + // Must have a details URL for building and built. + // + assert (!du.empty ()); + + // Set `startedAt` to current time if updating to building. + // + optional<timestamp> sa; + + if (st == build_state::building) + sa = system_clock::now (); + string rq ( - gq_serialize_request (gq_mutation_update_check_run (rid, nid, st))); + gq_serialize_request ( + gq_mutation_update_check_run (rid, + nid, + du, + gh_to_status (st), + sa, + move (br)))); vector<check_run> crs {move (cr)}; - bool r (gq_mutate_check_runs (crs, iat, move (rq), st, error)); + bool r (gq_mutate_check_runs (error, crs, iat, move (rq), st)); cr = move (crs[0]); diff --git a/mod/mod-ci-github-gq.hxx b/mod/mod-ci-github-gq.hxx index 3d8c6cc..8f7a9ca 100644 --- a/mod/mod-ci-github-gq.hxx +++ b/mod/mod-ci-github-gq.hxx @@ -23,29 +23,41 @@ namespace brep // the new states and node IDs. Return false and issue diagnostics if the // request failed. // + // Note: no details_url yet since there will be no entry in the build result + // search page until the task starts building. + // bool - gq_create_check_runs (vector<check_run>& check_runs, + gq_create_check_runs (const basic_mark& error, + vector<check_run>& check_runs, const string& installation_access_token, const string& repository_id, const string& head_sha, - const vector<reference_wrapper<const build>>&, - build_state, - const build_queued_hints&, - const basic_mark& error); + build_state); // Create a new check run on GitHub for a build. Update `cr` with the new // state and the node ID. Return false and issue diagnostics if the request // failed. // + // The gq_built_result is required if the build_state is built because + // GitHub does not allow a check run status of `completed` without at least + // a conclusion. + // + struct gq_built_result + { + string conclusion; + string title; + string summary; + }; + bool - gq_create_check_run (check_run& cr, + gq_create_check_run (const basic_mark& error, + check_run& cr, const string& installation_access_token, const string& repository_id, const string& head_sha, - const build&, + const string& details_url, build_state, - const build_queued_hints&, - const basic_mark& error); + optional<gq_built_result> = nullopt); // Update a check run on GitHub. // @@ -53,15 +65,19 @@ namespace brep // with the new state. Return false and issue diagnostics if the request // failed. // - // @@ TODO Support conclusion, output, etc. + // The gq_built_result is required if the build_state is built because + // GitHub does not allow a check run status of `completed` without at least + // a conclusion. // bool - gq_update_check_run (check_run& cr, + gq_update_check_run (const basic_mark& error, + check_run& cr, const string& installation_access_token, const string& repository_id, const string& node_id, + const string& details_url, build_state, - const basic_mark& error); + optional<gq_built_result> = nullopt); } #endif // MOD_MOD_CI_GITHUB_GQ_HXX diff --git a/mod/mod-ci-github-service-data.cxx b/mod/mod-ci-github-service-data.cxx index ff2af5d..6aee2d7 100644 --- a/mod/mod-ci-github-service-data.cxx +++ b/mod/mod-ci-github-service-data.cxx @@ -26,6 +26,8 @@ namespace brep to_string (version)); } + warning_success = p.next_expect_member_boolean<bool> ("warning_success"); + // Installation access token. // p.next_expect_name ("installation_access"); @@ -40,6 +42,7 @@ namespace brep while (p.next_expect (event::begin_object, event::end_array)) { string bid (p.next_expect_member_string ("build_id")); + string nm (p.next_expect_member_string ("name")); optional<string> nid; { @@ -51,7 +54,7 @@ namespace brep build_state s (to_build_state (p.next_expect_member_string ("state"))); bool ss (p.next_expect_member_boolean<bool> ("state_synced")); - check_runs.emplace_back (move (bid), move (nid), s, ss); + check_runs.emplace_back (move (bid), move (nm), move (nid), s, ss); p.next_expect (event::end_object); } @@ -60,12 +63,14 @@ namespace brep } service_data:: - service_data (string iat_tok, + service_data (bool ws, + string iat_tok, timestamp iat_ea, uint64_t iid, string rid, string hs) - : installation_access (move (iat_tok), iat_ea), + : warning_success (ws), + installation_access (move (iat_tok), iat_ea), installation_id (iid), repository_node_id (move (rid)), head_sha (move (hs)) @@ -82,6 +87,8 @@ namespace brep s.member ("version", 1); + s.member ("warning_success", warning_success); + // Installation access token. // s.member_begin_object ("installation_access"); @@ -98,6 +105,7 @@ namespace brep { s.begin_object (); s.member ("build_id", cr.build_id); + s.member ("name", cr.name); s.member_name ("node_id"); if (cr.node_id) @@ -133,6 +141,7 @@ namespace brep { os << "node_id: " << cr.node_id.value_or ("null") << ", build_id: " << cr.build_id + << ", name: " << cr.name << ", state: " << cr.state_string (); return os; diff --git a/mod/mod-ci-github-service-data.hxx b/mod/mod-ci-github-service-data.hxx index 0d94b55..c4e20b3 100644 --- a/mod/mod-ci-github-service-data.hxx +++ b/mod/mod-ci-github-service-data.hxx @@ -25,6 +25,7 @@ namespace brep struct check_run { string build_id; // Full build id. + string name; // Potentially shortened build id. optional<string> node_id; // GitHub id. build_state state; @@ -46,6 +47,10 @@ namespace brep // uint64_t version = 1; + // Check suite settings. + // + bool warning_success; // See gh_to_conclusion(). + // Check suite-global data. // gh_installation_access_token installation_access; @@ -71,7 +76,8 @@ namespace brep explicit service_data (const string& json); - service_data (string iat_token, + service_data (bool warning_success, + string iat_token, timestamp iat_expires_at, uint64_t installation_id, string repository_node_id, diff --git a/mod/mod-ci-github.cxx b/mod/mod-ci-github.cxx index defb74f..7c56851 100644 --- a/mod/mod-ci-github.cxx +++ b/mod/mod-ci-github.cxx @@ -5,8 +5,12 @@ #include <libbutl/json/parser.hxx> +#include <web/xhtml/serialization.hxx> +#include <web/server/mime-url-encoding.hxx> // mime_url_encode() + #include <mod/jwt.hxx> #include <mod/hmac.hxx> +#include <mod/build.hxx> // build_log_url() #include <mod/module-options.hxx> #include <mod/mod-ci-github-gq.hxx> @@ -226,6 +230,35 @@ namespace brep fail << "unable to compute request HMAC: " << e; } + // Process the `warning` webhook request query parameter. + // + bool warning_success; + { + const name_values& rps (rq.parameters (1024, true /* url_only */)); + + auto i (find_if (rps.begin (), rps.end (), + [] (auto&& rp) {return rp.name == "warning";})); + + if (i == rps.end ()) + throw invalid_request (400, + "missing 'warning' webhook query parameter"); + + if (!i->value) + throw invalid_request ( + 400, "missing 'warning' webhook query parameter value"); + + const string& v (*i->value); + + if (v == "success") warning_success = true; + else if (v == "failure") warning_success = false; + else + { + throw invalid_request ( + 400, + "invalid 'warning' webhook query parameter value: '" + v + '\''); + } + } + // There is a webhook event (specified in the x-github-event header) and // each event contains a bunch of actions (specified in the JSON request // body). @@ -236,6 +269,11 @@ namespace brep // is that we want be "notified" of new actions at which point we can decide // whether to ignore them or to handle. // + // @@ There is also check_run even (re-requested by user, either + // individual check run or all the failed check runs). + // + // @@ There is also the pull_request event. Probably need to handle. + // if (event == "check_suite") { gh_check_suite_event cs; @@ -257,14 +295,16 @@ namespace brep if (cs.action == "requested") { - return handle_check_suite_request (move (cs)); + return handle_check_suite_request (move (cs), warning_success); } else if (cs.action == "rerequested") { // Someone manually requested to re-run the check runs in this check // suite. Treat as a new request. // - return handle_check_suite_request (move (cs)); + // @@ This is probably broken. + // + return handle_check_suite_request (move (cs), warning_success); } else if (cs.action == "completed") { @@ -272,9 +312,8 @@ namespace brep // completed and a conclusion is available". Looks like this one we // ignore? // - // @@ TODO What if our bookkeeping says otherwise? See conclusion - // field which includes timedout. Need to come back to this once - // have the "happy path" implemented. + // What if our bookkeeping says otherwise? But then we can't even + // access the service data easily here. @@ TODO: maybe/later. // return true; } @@ -305,7 +344,7 @@ namespace brep } bool ci_github:: - handle_check_suite_request (gh_check_suite_event cs) + handle_check_suite_request (gh_check_suite_event cs, bool warning_success) { HANDLER_DIAG; @@ -331,25 +370,28 @@ namespace brep cs.check_suite.head_branch, repository_type::git); - string sd (service_data (move (iat->token), + string sd (service_data (warning_success, + move (iat->token), iat->expires_at, cs.installation.id, move (cs.repository.node_id), move (cs.check_suite.head_sha)) .json ()); + // @@ What happens if we call this functions with an already existing + // node_id (e.g., replay attack). + // optional<start_result> r ( - start (error, - warn, - verb_ ? &trace : nullptr, - tenant_service (move (cs.check_suite.node_id), - "ci-github", - move (sd)), - move (rl), - vector<package> {}, - nullopt, // client_ip - nullopt // user_agent - )); + start (error, + warn, + verb_ ? &trace : nullptr, + tenant_service (move (cs.check_suite.node_id), + "ci-github", + move (sd)), + move (rl), + vector<package> {}, + nullopt, /* client_ip */ + nullopt /* user_agent */)); if (!r) fail << "unable to submit CI request"; @@ -411,8 +453,8 @@ namespace brep // building Skip if there is no check run in service data or it's // not in the queued state, otherwise update. // - // built Update if there is check run in service data and its - // state is not built, otherwise create new. + // built Update if there is check run in service data unless its + // state is built, otherwise create new. // // The rationale for this semantics is as follows: the building // notification is a "nice to have" and can be skipped if things are not @@ -747,10 +789,304 @@ namespace brep } function<optional<string> (const tenant_service&)> ci_github:: - build_built (const tenant_service&, const build&, - const diag_epilogue& /* log_writer */) const noexcept + build_built (const tenant_service& ts, + const build& b, + const diag_epilogue& log_writer) const noexcept + { + NOTIFICATION_DIAG (log_writer); + + service_data sd; + try + { + sd = service_data (*ts.data); + } + catch (const invalid_argument& e) + { + error << "failed to parse service data: " << e; + return nullptr; + } + + check_run cr; // Updated check run. + { + string bid (gh_check_run_name (b)); // Full Build ID. + + if (check_run* scr = sd.find_check_run (bid)) + { + if (scr->state != build_state::building) + { + warn << "check run " << bid << ": out of order built notification; " + << "existing state: " << scr->state_string (); + } + + // Do nothing if already built (e.g., rebuild). + // + if (scr->state == build_state::built) + return nullptr; + + cr = move (*scr); + } + else + { + warn << "check run " << bid << ": out of order built notification; " + << "no check run state in service data"; + + cr.build_id = move (bid); + cr.name = cr.build_id; + } + + cr.state_synced = false; + } + + // Get a new installation access token if the current one has expired. + // + const gh_installation_access_token* iat (nullptr); + optional<gh_installation_access_token> new_iat; + + if (system_clock::now () > sd.installation_access.expires_at) + { + if (optional<string> jwt = generate_jwt (trace, error)) + { + new_iat = obtain_installation_access_token (sd.installation_id, + move (*jwt), + error); + if (new_iat) + iat = &*new_iat; + } + } + else + iat = &sd.installation_access; + + // Note: we treat the failure to obtain the installation access token the + // same as the failure to notify GitHub (state is updated but not marked + // synced). + // + if (iat != nullptr) + { + // Return the colored circle corresponding to a result_status. + // + auto circle = [] (result_status rs) -> string + { + switch (rs) + { + case result_status::success: return "\U0001F7E2"; // Green circle. + case result_status::warning: return "\U0001F7E0"; // Orange circle. + case result_status::error: + case result_status::abort: + case result_status::abnormal: return "\U0001F534"; // Red circle. + + // Valid values we should never encounter. + // + case result_status::skip: + case result_status::interrupt: + throw invalid_argument ("unexpected result_status value: " + + to_string (rs)); + } + + return ""; // Should never reach. + }; + + // Prepare the check run's summary field (the build information in an + // XHTML table). + // + string sm; // Summary. + { + using namespace web::xhtml; + + ostringstream os; + xml::serializer s (os, "check_run_summary"); + + // This hack is required to disable XML element name prefixes (which + // GitHub does not like). Note that this adds an xmlns declaration for + // the XHTML namespace which for now GitHub appears to ignore. If that + // ever becomes a problem, then we should redo this with raw XML + // serializer calls. + // + struct table: element + { + table (): element ("table") {} + + void + start (xml::serializer& s) const override + { + s.start_element (xmlns, name); + s.namespace_decl (xmlns, ""); + } + } TABLE; + + // Serialize a result row (colored circle, result text, log URL) for + // an operation and result_status. + // + auto tr_result = [this, circle, &b] (xml::serializer& s, + const string& op, + result_status rs) + { + // The log URL. + // + string lu (build_log_url (options_->host (), + options_->root (), + b, + op != "result" ? &op : nullptr)); + + s << TR + << TD << EM << op << ~EM << ~TD + << TD + << circle (rs) << ' ' + << CODE << to_string (rs) << ~CODE + << " (" << A << HREF << lu << ~HREF << "log" << ~A << ')' + << ~TD + << ~TR; + }; + + // Serialize the summary to an XHTML table. + // + s << TABLE + << TBODY; + + tr_result (s, "result", *b.status); + + s << TR + << TD << EM << "package" << ~EM << ~TD + << TD << CODE << b.package_name << ~CODE << ~TD + << ~TR + << TR + << TD << EM << "version" << ~EM << ~TD + << TD << CODE << b.package_version << ~CODE << ~TD + << ~TR + << TR + << TD << EM << "toolchain" << ~EM << ~TD + << TD + << CODE + << b.toolchain_name << '-' << b.toolchain_version.string () + << ~CODE + << ~TD + << ~TR + << TR + << TD << EM << "target" << ~EM << ~TD + << TD << CODE << b.target.string () << ~CODE << ~TD + << ~TR + << TR + << TD << EM << "target config" << ~EM << ~TD + << TD << CODE << b.target_config_name << ~CODE << ~TD + << ~TR + << TR + << TD << EM << "package config" << ~EM << ~TD + << TD << CODE << b.package_config_name << ~CODE << ~TD + << ~TR; + + for (const operation_result& r: b.results) + tr_result (s, r.operation, r.status); + + s << ~TBODY + << ~TABLE; + + sm = os.str (); + } + + gq_built_result br (gh_to_conclusion (*b.status, sd.warning_success), + circle (*b.status) + ' ' + + ucase (to_string (*b.status)), + move (sm)); + + if (cr.node_id) + { + // Update existing check run to built. + // + if (gq_update_check_run (error, + cr, + iat->token, + sd.repository_node_id, + *cr.node_id, + details_url (b), + build_state::built, + move (br))) + { + assert (cr.state == build_state::built); + + l3 ([&]{trace << "updated check_run { " << cr << " }";}); + } + } + else + { + // Create new check run. + // + // Note that we don't have build hints so will be creating this check + // run with the full build ID as name. In the unlikely event that an + // out of order build_queued() were to run before we've saved this + // check run to the service data it will create another check run with + // the shortened name which will never get to the built state. + // + if (gq_create_check_run (error, + cr, + iat->token, + sd.repository_node_id, + sd.head_sha, + details_url (b), + build_state::built, + move (br))) + { + assert (cr.state == build_state::built); + + l3 ([&]{trace << "created check_run { " << cr << " }";}); + } + } + } + + return [iat = move (new_iat), + cr = move (cr), + error = move (error), + warn = move (warn)] (const tenant_service& ts) -> optional<string> + { + // NOTE: this lambda may be called repeatedly (e.g., due to transaction + // being aborted) and so should not move out of its captures. + + service_data sd; + try + { + sd = service_data (*ts.data); + } + catch (const invalid_argument& e) + { + error << "failed to parse service data: " << e; + return nullopt; + } + + if (iat) + sd.installation_access = *iat; + + if (check_run* scr = sd.find_check_run (cr.build_id)) + { + // This will most commonly generate a duplicate warning (see above). + // We could save the old state and only warn if it differs but let's + // not complicate things for now. + // +#if 0 + if (scr->state != build_state::building) + { + warn << "check run " << cr.build_id << ": out of order built " + << "notification service data update; existing state: " + << scr->state_string (); + } +#endif + *scr = cr; + } + else + sd.check_runs.push_back (cr); + + return sd.json (); + }; + } + + string ci_github:: + details_url (const build& b) const { - return nullptr; + return options_->host () + + "/@" + b.tenant + + "?builds=" + mime_url_encode (b.package_name.string ()) + + "&pv=" + b.package_version.string () + + "&tg=" + mime_url_encode (b.target.string ()) + + "&tc=" + mime_url_encode (b.target_config_name) + + "&pc=" + mime_url_encode (b.package_config_name) + + "&th=" + mime_url_encode (b.toolchain_version.string ()); } optional<string> ci_github:: diff --git a/mod/mod-ci-github.hxx b/mod/mod-ci-github.hxx index 07feca8..6bb01e3 100644 --- a/mod/mod-ci-github.hxx +++ b/mod/mod-ci-github.hxx @@ -60,8 +60,16 @@ namespace brep // Handle the check_suite event `requested` and `rerequested` actions. // + // If warning_success is true, then map result_status::warning to SUCCESS + // and to FAILURE otherwise. + // bool - handle_check_suite_request (gh_check_suite_event); + handle_check_suite_request (gh_check_suite_event, bool warning_success); + + // Build a check run details_url for a build. + // + string + details_url (const build&) const; optional<string> generate_jwt (const basic_mark& trace, const basic_mark& error) const; |