From 0c945f6212823cdad748222dcceda17c5ecd2ac0 Mon Sep 17 00:00:00 2001 From: Francois Kritzinger Date: Wed, 17 Apr 2024 08:37:36 +0200 Subject: Add fetch_check_run(), mutate_check_runs() --- mod/mod-ci-github.cxx | 312 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 312 insertions(+) diff --git a/mod/mod-ci-github.cxx b/mod/mod-ci-github.cxx index 2419d32..5c039e8 100644 --- a/mod/mod-ci-github.cxx +++ b/mod/mod-ci-github.cxx @@ -1022,6 +1022,318 @@ namespace brep } } + // @@ TODO Pass error, trace in same order everywhere. + + // Fetch from GitHub the check run with the specified name (hints-shortened + // build ID). + // + // Return the check run or nullopt if no such check run exists. + // + // In case of error diagnostics will be issued and false returned in second. + // + // Note that the existence of more than one check run with the same name is + // considered an error and reported as such. The API docs imply that there + // can be more than one check run with the same name in a check suite, but + // the observed behavior is that creating a check run destroys the extant + // one, leaving only the new one with different node ID. + // + static pair, bool> + fetch_check_run (const string& iat, + const string& check_suite_id, + const string& cr_name, + const basic_mark& error) noexcept + { + try + { + // Example request: + // + // query { + // node(id: "CS_kwDOLc8CoM8AAAAFQPQYEw") { + // ... on CheckSuite { + // checkRuns(last: 100, filterBy: {checkName: "linux_debian_..."}) { + // totalCount, + // edges { + // node { + // id, name, status + // } + // } + // } + // } + // } + // } + // + // This request does the following: + // + // - Look up the check suite by node ID ("direct node lookup"). This + // returns a Node (GraphQL interface). + // + // - Get to the concrete CheckSuite type by using a GraphQL "inline + // fragment" (`... on CheckSuite`). + // + // - Get the check suite's check runs + // - Filter by the sought name + // - Return only two check runs, just enough to be able to tell + // whether there are more than one check runs with this name (which + // is an error). + // + // - Return the id, name, and status fields from the matching check run + // objects. + // + string rq; + { + ostringstream os; + + os << "query {" << '\n'; + + os << "node(id: " << gq_str (check_suite_id) << ") {" << '\n' + << " ... on CheckSuite {" << '\n' + << " checkRuns(last: 2," << '\n' + << " filterBy: {" << '\n' + << "checkName: " << gq_str (cr_name) << '\n' + << " })" << '\n' + // Specify the selection set (fields to be returned). Note that + // edges and node are mandatory. + // + << " {" << '\n' + << " totalCount," << '\n' + << " edges {" << '\n' + << " node {" << '\n' + << " id, name, status" << '\n' + << " }" << '\n' + << " }" << '\n' + << " }" << '\n' + << " }" << '\n' + << "}" << '\n'; + + os << "}" << '\n'; + + rq = os.str (); + } + + // Example response (the part we need to parse here, at least): + // + // { + // "node": { + // "checkRuns": { + // "totalCount": 1, + // "edges": [ + // { + // "node": { + // "id": "CR_kwDOLc8CoM8AAAAFgeoweg", + // "name": "linux_debian_...", + // "status": "IN_PROGRESS" + // } + // } + // ] + // } + // } + // } + // + struct resp + { + optional cr; + size_t cr_count = 0; + + resp (json::parser& p) + { + using event = json::event; + + parse_graphql_response (p, [this] (json::parser& p) + { + p.next_expect (event::begin_object); + p.next_expect_member_object ("node"); + p.next_expect_member_object ("checkRuns"); + + cr_count = p.next_expect_member_number ("totalCount"); + + p.next_expect_member_array ("edges"); + + for (size_t i (0); i != cr_count; ++i) + { + p.next_expect (event::begin_object); + p.next_expect_name ("node"); + check_run cr (p); + p.next_expect (event::end_object); + + if (i == 0) + this->cr = move (cr); + } + + p.next_expect (event::end_array); // edges + p.next_expect (event::end_object); // checkRuns + p.next_expect (event::end_object); // node + p.next_expect (event::end_object); + }); + } + + resp () = default; + } rs; + + uint16_t sc (github_post (rs, + "graphql", + strings {"Authorization: Bearer " + iat}, + graphql_request (rq))); + + if (sc == 200) + { + if (rs.cr_count <= 1) + return {rs.cr, true}; + else + { + error << "unexpected number of check runs (" << rs.cr_count + << ") in response"; + } + } + else + error << "failed to get check run by name: error HTTP " + << "response status " << sc; + } + catch (const json::invalid_json_input& e) + { + // Note: e.name is the GitHub API endpoint. + // + error << "malformed JSON in response from " << e.name + << ", line: " << e.line << ", column: " << e.column + << ", byte offset: " << e.position << ", error: " << e; + } + catch (const invalid_argument& e) + { + error << "malformed header(s) in response: " << e; + } + catch (const system_error& e) + { + error << "unable to get check run by name (errno=" << e.code () + << "): " << e.what (); + } + catch (const std::exception& e) + { + error << "unable to get check run by name: " << e.what (); + } + + return {nullopt, false}; + } + + // Send a GraphQL mutation request `rq` that operates on one or more check + // runs. Update the check runs in `crs` with the new state and the node ID + // if unset (note: both fields are optionals). Return false and issue + // diagnostics if the request failed. + // + static bool + mutate_check_runs (vector& crs, + const vector>& bs, + const string& iat, + string rq, + build_state st, + const basic_mark& error) noexcept + { + vector rcrs; + + try + { + // Response type which parses a GraphQL response containing multiple + // check_run objects. + // + struct resp + { + vector check_runs; // Received check runs. + + resp (json::parser& p) : check_runs (parse_check_runs_response (p)) {} + + resp () = default; + } rs; + + uint16_t sc (github_post (rs, + "graphql", // API Endpoint. + strings {"Authorization: Bearer " + iat}, + move (rq))); + + if (sc == 200) + { + rcrs = move (rs.check_runs); + + if (rcrs.size () == bs.size ()) + { + for (size_t i (0); i != rcrs.size (); ++i) + { + // Validate the check run in the response against the build. + // + const check_run& rcr (rcrs[i]); // Received check run. + const build& b (bs[i]); + + build_state rst (from_string_gh (rcr.status)); // Received state. + + if (rst != build_state::built && rst != st) + { + error << "unexpected check_run status: received '" << rcr.status + << "' but expected '" << to_string_gh (st) << '\''; + + return false; // Fail because something is clearly very wrong. + } + else + { + service_data::check_run& cr (crs[i]); + + if (!cr.node_id) + cr.node_id = move (rcr.node_id); + + cr.state = from_string_gh (rcr.status); + } + } + + return true; + } + else + error << "unexpected number of check_run objects in response"; + } + else + error << "failed to update check run: error HTTP response status " + << sc; + } + catch (const json::invalid_json_input& e) + { + // Note: e.name is the GitHub API endpoint. + // + error << "malformed JSON in response from " << e.name << ", line: " + << e.line << ", column: " << e.column << ", byte offset: " + << e.position << ", error: " << e; + } + catch (const invalid_argument& e) + { + error << "malformed header(s) in response: " << e; + } + catch (const system_error& e) + { + error << "unable to mutate check runs (errno=" << e.code () << "): " + << e.what (); + } + catch (const runtime_error& e) // From parse_check_runs_response(). + { + // GitHub response contained error(s) (could be ours or theirs at this + // point). + // + error << "unable to mutate check runs: " << e; + } + + return false; + } + + static bool + mutate_check_run (service_data::check_run& cr, + const vector>& bs, + const string& iat, + string rq, + build_state st, + const basic_mark& error) noexcept + { + vector crs {move (cr)}; + + bool r (mutate_check_runs (crs, bs, iat, move (rq), st, error)); + + cr = move (crs[0]); + + return r; + } + function (const tenant_service&)> ci_github:: build_queued (const tenant_service& ts, const vector& builds, -- cgit v1.1