aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--mod/mod-ci-github.cxx312
1 files changed, 312 insertions, 0 deletions
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<optional<gh::check_run>, 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<check_run> 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<size_t> ("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<service_data::check_run>& crs,
+ const vector<reference_wrapper<const build>>& bs,
+ const string& iat,
+ string rq,
+ build_state st,
+ const basic_mark& error) noexcept
+ {
+ vector<check_run> rcrs;
+
+ try
+ {
+ // Response type which parses a GraphQL response containing multiple
+ // check_run objects.
+ //
+ struct resp
+ {
+ vector<check_run> 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<reference_wrapper<const build>>& bs,
+ const string& iat,
+ string rq,
+ build_state st,
+ const basic_mark& error) noexcept
+ {
+ vector<service_data::check_run> crs {move (cr)};
+
+ bool r (mutate_check_runs (crs, bs, iat, move (rq), st, error));
+
+ cr = move (crs[0]);
+
+ return r;
+ }
+
function<optional<string> (const tenant_service&)> ci_github::
build_queued (const tenant_service& ts,
const vector<build>& builds,