aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--mod/mod-ci-github-gh.cxx349
-rw-r--r--mod/mod-ci-github-gh.hxx115
-rw-r--r--mod/mod-ci-github-gq.cxx (renamed from mod/mod-ci-github-qg.cxx)625
-rw-r--r--mod/mod-ci-github-gq.hxx233
-rw-r--r--mod/mod-ci-github-post.hxx4
-rw-r--r--mod/mod-ci-github-service-data.cxx150
-rw-r--r--mod/mod-ci-github-service-data.hxx29
-rw-r--r--mod/mod-ci-github.cxx561
-rw-r--r--mod/mod-ci-github.hxx6
9 files changed, 1105 insertions, 967 deletions
diff --git a/mod/mod-ci-github-gh.cxx b/mod/mod-ci-github-gh.cxx
new file mode 100644
index 0000000..dc1447f
--- /dev/null
+++ b/mod/mod-ci-github-gh.cxx
@@ -0,0 +1,349 @@
+// file : mod/mod-ci-github-gh.cxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#include <mod/mod-ci-github-gh.hxx>
+
+#include <libbutl/json/parser.hxx>
+
+namespace brep
+{
+ static const string gh_status[] {"QUEUED", "IN_PROGRESS", "COMPLETED"};
+
+ // Return the GitHub check run status corresponding to a build_state.
+ //
+ string
+ gh_to_status (build_state st)
+ {
+ // @@ Just return by value (small string optimization).
+ //
+ // @@ TMP Keep this comment, right?
+ //
+ return gh_status[static_cast<size_t> (st)];
+ }
+
+ // Return the build_state corresponding to a GitHub check run status
+ // string. Throw invalid_argument if the passed status was invalid.
+ //
+ build_state
+ gh_from_status (const string& s)
+ {
+ if (s == "QUEUED") return build_state::queued;
+ 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 +
+ '\'');
+ }
+
+ string
+ gh_check_run_name (const build& b,
+ const tenant_service_base::build_hints* bh)
+ {
+ string r;
+
+ if (bh == nullptr || !bh->single_package_version)
+ {
+ r += b.package_name.string ();
+ r += '/';
+ r += b.package_version.string ();
+ r += '/';
+ }
+
+ r += b.target_config_name;
+ r += '/';
+ r += b.target.string ();
+ r += '/';
+
+ if (bh == nullptr || !bh->single_package_config)
+ {
+ r += b.package_config_name;
+ r += '/';
+ }
+
+ r += b.toolchain_name;
+ r += '-';
+ r += b.toolchain_version.string ();
+
+ return r;
+ }
+
+ // Throw invalid_json_input when a required member `m` is missing from a
+ // JSON object `o`.
+ //
+ [[noreturn]] static void
+ missing_member (const json::parser& p, const char* o, const char* m)
+ {
+ throw json::invalid_json_input (
+ p.input_name,
+ p.line (), p.column (), p.position (),
+ o + string (" object is missing member '") + m + '\'');
+ }
+
+ using event = json::event;
+
+ // gh_check_suite
+ //
+ gh_check_suite::
+ gh_check_suite (json::parser& p)
+ {
+ p.next_expect (event::begin_object);
+
+ bool ni (false), hb (false), hs (false), bf (false), at (false);
+
+ // Skip unknown/uninteresting members.
+ //
+ while (p.next_expect (event::name, event::end_object))
+ {
+ auto c = [&p] (bool& v, const char* s)
+ {
+ return p.name () == s ? (v = true) : false;
+ };
+
+ if (c (ni, "node_id")) node_id = p.next_expect_string ();
+ else if (c (hb, "head_branch")) head_branch = p.next_expect_string ();
+ else if (c (hs, "head_sha")) head_sha = p.next_expect_string ();
+ else if (c (bf, "before")) before = p.next_expect_string ();
+ else if (c (at, "after")) after = p.next_expect_string ();
+ else p.next_expect_value_skip ();
+ }
+
+ if (!ni) missing_member (p, "gh_check_suite", "node_id");
+ if (!hb) missing_member (p, "gh_check_suite", "head_branch");
+ if (!hs) missing_member (p, "gh_check_suite", "head_sha");
+ if (!bf) missing_member (p, "gh_check_suite", "before");
+ if (!at) missing_member (p, "gh_check_suite", "after");
+ }
+
+ ostream&
+ operator<< (ostream& os, const gh_check_suite& cs)
+ {
+ os << "node_id: " << cs.node_id
+ << ", head_branch: " << cs.head_branch
+ << ", head_sha: " << cs.head_sha
+ << ", before: " << cs.before
+ << ", after: " << cs.after;
+
+ return os;
+ }
+
+ // gh_check_run
+ //
+ gh_check_run::
+ gh_check_run (json::parser& p)
+ {
+ p.next_expect (event::begin_object);
+
+ bool ni (false), nm (false), st (false);
+
+ while (p.next_expect (event::name, event::end_object))
+ {
+ auto c = [&p] (bool& v, const char* s)
+ {
+ return p.name () == s ? (v = true) : false;
+ };
+
+ if (c (ni, "id")) node_id = p.next_expect_string ();
+ else if (c (nm, "name")) name = p.next_expect_string ();
+ else if (c (st, "status")) status = p.next_expect_string ();
+ }
+
+ if (!ni) missing_member (p, "gh_check_run", "id");
+ if (!nm) missing_member (p, "gh_check_run", "name");
+ if (!st) missing_member (p, "gh_check_run", "status");
+ }
+
+ ostream&
+ operator<< (ostream& os, const gh_check_run& cr)
+ {
+ os << "node_id: " << cr.node_id
+ << ", name: " << cr.name
+ << ", status: " << cr.status;
+
+ return os;
+ }
+
+ // gh_repository
+ //
+ gh_repository::
+ gh_repository (json::parser& p)
+ {
+ p.next_expect (event::begin_object);
+
+ bool ni (false), nm (false), fn (false), db (false), cu (false);
+
+ // Skip unknown/uninteresting members.
+ //
+ while (p.next_expect (event::name, event::end_object))
+ {
+ auto c = [&p] (bool& v, const char* s)
+ {
+ return p.name () == s ? (v = true) : false;
+ };
+
+ if (c (ni, "node_id")) node_id = p.next_expect_string ();
+ else if (c (nm, "name")) name = p.next_expect_string ();
+ else if (c (fn, "full_name")) full_name = p.next_expect_string ();
+ else if (c (db, "default_branch")) default_branch = p.next_expect_string ();
+ else if (c (cu, "clone_url")) clone_url = p.next_expect_string ();
+ else p.next_expect_value_skip ();
+ }
+
+ if (!ni) missing_member (p, "gh_repository", "node_id");
+ if (!nm) missing_member (p, "gh_repository", "name");
+ if (!fn) missing_member (p, "gh_repository", "full_name");
+ if (!db) missing_member (p, "gh_repository", "default_branch");
+ if (!cu) missing_member (p, "gh_repository", "clone_url");
+ }
+
+ ostream&
+ operator<< (ostream& os, const gh_repository& rep)
+ {
+ os << "node_id: " << rep.node_id
+ << ", name: " << rep.name
+ << ", full_name: " << rep.full_name
+ << ", default_branch: " << rep.default_branch
+ << ", clone_url: " << rep.clone_url;
+
+ return os;
+ }
+
+ // gh_installation
+ //
+ gh_installation::
+ gh_installation (json::parser& p)
+ {
+ p.next_expect (event::begin_object);
+
+ bool i (false);
+
+ // Skip unknown/uninteresting members.
+ //
+ while (p.next_expect (event::name, event::end_object))
+ {
+ auto c = [&p] (bool& v, const char* s)
+ {
+ return p.name () == s ? (v = true) : false;
+ };
+
+ if (c (i, "id")) id = p.next_expect_number<uint64_t> ();
+ else p.next_expect_value_skip ();
+ }
+
+ if (!i) missing_member (p, "gh_installation", "id");
+ }
+
+ ostream&
+ operator<< (ostream& os, const gh_installation& i)
+ {
+ os << "id: " << i.id;
+
+ return os;
+ }
+
+ // gh_check_suite_event
+ //
+ gh_check_suite_event::
+ gh_check_suite_event (json::parser& p)
+ {
+ p.next_expect (event::begin_object);
+
+ bool ac (false), cs (false), rp (false), in (false);
+
+ // Skip unknown/uninteresting members.
+ //
+ while (p.next_expect (event::name, event::end_object))
+ {
+ auto c = [&p] (bool& v, const char* s)
+ {
+ return p.name () == s ? (v = true) : false;
+ };
+
+ if (c (ac, "action")) action = p.next_expect_string ();
+ else if (c (cs, "check_suite")) check_suite = gh_check_suite (p);
+ else if (c (rp, "repository")) repository = gh_repository (p);
+ else if (c (in, "installation")) installation = gh_installation (p);
+ else p.next_expect_value_skip ();
+ }
+
+ if (!ac) missing_member (p, "gh_check_suite_event", "action");
+ if (!cs) missing_member (p, "gh_check_suite_event", "check_suite");
+ if (!rp) missing_member (p, "gh_check_suite_event", "repository");
+ if (!in) missing_member (p, "gh_check_suite_event", "installation");
+ }
+
+ ostream&
+ operator<< (ostream& os, const gh_check_suite_event& cs)
+ {
+ os << "action: " << cs.action;
+ os << ", check_suite { " << cs.check_suite << " }";
+ os << ", repository { " << cs.repository << " }";
+ os << ", installation { " << cs.installation << " }";
+
+ return os;
+ }
+
+ // gh_installation_access_token
+ //
+ // Example JSON:
+ //
+ // {
+ // "token": "ghs_Py7TPcsmsITeVCAWeVtD8RQs8eSos71O5Nzp",
+ // "expires_at": "2024-02-15T16:16:38Z",
+ // ...
+ // }
+ //
+ gh_installation_access_token::
+ gh_installation_access_token (json::parser& p)
+ {
+ p.next_expect (event::begin_object);
+
+ bool tk (false), ea (false);
+
+ // Skip unknown/uninteresting members.
+ //
+ while (p.next_expect (event::name, event::end_object))
+ {
+ auto c = [&p] (bool& v, const char* s)
+ {
+ return p.name () == s ? (v = true) : false;
+ };
+
+ if (c (tk, "token")) token = p.next_expect_string ();
+ else if (c (ea, "expires_at")) expires_at = from_iso8601 (p.next_expect_string ());
+ else p.next_expect_value_skip ();
+ }
+
+ if (!tk) missing_member (p, "gh_installation_access_token", "token");
+ if (!ea) missing_member (p, "gh_installation_access_token", "expires_at");
+ }
+
+ gh_installation_access_token::
+ gh_installation_access_token (string tk, timestamp ea)
+ : token (move (tk)), expires_at (ea)
+ {
+ }
+
+ ostream&
+ operator<< (ostream& os, const gh_installation_access_token& t)
+ {
+ os << "token: " << t.token << ", expires_at: ";
+ butl::operator<< (os, t.expires_at);
+
+ return os;
+ }
+
+ string
+ to_iso8601 (timestamp t)
+ {
+ return butl::to_string (t,
+ "%Y-%m-%dT%TZ",
+ false /* special */,
+ false /* local */);
+ }
+
+ timestamp
+ from_iso8601 (const string& s)
+ {
+ return butl::from_string (s.c_str (), "%Y-%m-%dT%TZ", false /* local */);
+ }
+}
diff --git a/mod/mod-ci-github-gh.hxx b/mod/mod-ci-github-gh.hxx
index 23ad247..c7ac00c 100644
--- a/mod/mod-ci-github-gh.hxx
+++ b/mod/mod-ci-github-gh.hxx
@@ -7,6 +7,10 @@
#include <libbrep/types.hxx>
#include <libbrep/utility.hxx>
+#include <libbrep/build.hxx>
+
+#include <mod/tenant-service.hxx> // build_hints
+
namespace butl
{
namespace json
@@ -45,85 +49,42 @@ namespace brep
string after;
explicit
- check_suite (json::parser&);
+ gh_check_suite (json::parser&);
- check_suite () = default;
+ gh_check_suite () = default;
};
- struct check_run
+ struct gh_check_run
{
string node_id;
string name;
string status;
explicit
- check_run (json::parser&);
+ gh_check_run (json::parser&);
- check_run () = default;
+ gh_check_run () = default;
};
// Return the GitHub check run status corresponding to a build_state.
//
string
- gh_to_status (build_state st)
- {
- // @@ Just return by value (small string optimization).
- //
- static const string sts[] {"QUEUED", "IN_PROGRESS", "COMPLETED"};
-
- return sts[static_cast<size_t> (st)];
- }
+ gh_to_status (build_state st);
// Return the build_state corresponding to a GitHub check run status
// string. Throw invalid_argument if the passed status was invalid.
//
build_state
- gh_from_status (const string& s)
- {
- if (s == "QUEUED") return build_state::queued;
- 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 +
- '\'');
- }
+ gh_from_status (const string&);
// Create a check_run name from a build. If the second argument is not
// NULL, return an abbreviated id if possible.
//
string
- gh_check_run_name (const build& b,
- const tenant_service_base::build_hints* bh = nullptr)
- {
- string r;
-
- if (bh == nullptr || !bh->single_package_version)
- {
- r += b.package_name.string ();
- r += '/';
- r += b.package_version.string ();
- r += '/';
- }
-
- r += b.target_config_name;
- r += '/';
- r += b.target.string ();
- r += '/';
-
- if (bh == nullptr || !bh->single_package_config)
- {
- r += b.package_config_name;
- r += '/';
- }
-
- r += b.toolchain_name;
- r += '-';
- r += b.toolchain_version.string ();
-
- return r;
- }
+ gh_check_run_name (const build&,
+ const tenant_service_base::build_hints* = nullptr);
- struct repository
+ struct gh_repository
{
string node_id;
string name;
@@ -132,66 +93,72 @@ namespace brep
string clone_url;
explicit
- repository (json::parser&);
+ gh_repository (json::parser&);
- repository () = default;
+ gh_repository () = default;
};
- struct installation
+ struct gh_installation
{
uint64_t id; // Note: used for installation access token (REST API).
explicit
- installation (json::parser&);
+ gh_installation (json::parser&);
- installation () = default;
+ gh_installation () = default;
};
// The check_suite webhook event request.
//
- struct check_suite_event
+ struct gh_check_suite_event
{
string action;
- gh::check_suite check_suite;
- gh::repository repository;
- gh::installation installation;
+ gh_check_suite check_suite;
+ gh_repository repository;
+ gh_installation installation;
explicit
- check_suite_event (json::parser&);
+ gh_check_suite_event (json::parser&);
- check_suite_event () = default;
+ gh_check_suite_event () = default;
};
- struct installation_access_token
+ struct gh_installation_access_token
{
string token;
timestamp expires_at;
explicit
- installation_access_token (json::parser&);
+ gh_installation_access_token (json::parser&);
- installation_access_token (string token, timestamp expires_at);
+ gh_installation_access_token (string token, timestamp expires_at);
- installation_access_token () = default;
+ gh_installation_access_token () = default;
};
ostream&
- operator<< (ostream&, const check_suite&);
+ operator<< (ostream&, const gh_check_suite&);
ostream&
- operator<< (ostream&, const check_run&);
+ operator<< (ostream&, const gh_check_run&);
ostream&
- operator<< (ostream&, const repository&);
+ operator<< (ostream&, const gh_repository&);
ostream&
- operator<< (ostream&, const installation&);
+ operator<< (ostream&, const gh_installation&);
ostream&
- operator<< (ostream&, const check_suite_event&);
+ operator<< (ostream&, const gh_check_suite_event&);
ostream&
- operator<< (ostream&, const installation_access_token&);
+ operator<< (ostream&, const gh_installation_access_token&);
+
+ string
+ to_iso8601 (timestamp);
+
+ timestamp
+ from_iso8601 (const string&);
}
#endif // MOD_MOD_CI_GITHUB_GH_HXX
diff --git a/mod/mod-ci-github-qg.cxx b/mod/mod-ci-github-gq.cxx
index b0e40a6..4fe9190 100644
--- a/mod/mod-ci-github-qg.cxx
+++ b/mod/mod-ci-github-gq.cxx
@@ -3,6 +3,14 @@
#include <mod/mod-ci-github-gq.hxx>
+#include <libbutl/json/parser.hxx>
+#include <libbutl/json/serializer.hxx>
+
+#include <mod/mod-ci-github-post.hxx>
+
+using namespace std;
+using namespace butl;
+
namespace brep
{
// GraphQL serialization functions (see definitions and documentation at the
@@ -13,20 +21,191 @@ namespace brep
static string gq_bool (bool);
static const string& gq_enum (const string&);
+ [[noreturn]] static void
+ throw_json (json::parser& p, const string& m)
+ {
+ throw json::invalid_json_input (
+ p.input_name,
+ p.line (), p.column (), p.position (),
+ m);
+ }
+
+ // Parse a JSON-serialized GraphQL response.
+ //
+ // Throw runtime_error if the response indicated errors and
+ // invalid_json_input if the GitHub response contained invalid JSON.
+ //
+ // The response format is defined in the GraphQL spec:
+ // https://spec.graphql.org/October2021/#sec-Response.
+ //
+ // Example response:
+ //
+ // {
+ // "data": {...},
+ // "errors": {...}
+ // }
+ //
+ // The contents of `data`, including its opening and closing braces, are
+ // parsed by the `parse_data` function.
+ //
+ // @@ TODO: specify what parse_data may throw (probably only
+ // invalid_json_input).
+ //
+ // @@ TODO errors comes before data in GitHub's responses.
+ //
+ static void
+ gq_parse_response (json::parser& p,
+ function<void (json::parser&)> parse_data)
+ {
+ using event = json::event;
+
+ // True if the data/errors fields are present.
+ //
+ // Although the spec merely recommends that the `errors` field, if
+ // present, comes before the `data` field, assume it always does because
+ // letting the client parse data in the presence of field errors
+ // (unexpected nulls) would not make sense.
+ //
+ bool dat (false), err (false);
+
+ p.next_expect (event::begin_object);
+
+ while (p.next_expect (event::name, event::end_object))
+ {
+ if (p.name () == "data")
+ {
+ dat = true;
+
+ // Currently we're not handling fields that are null due to field
+ // errors (see below for details) so don't parse any further.
+ //
+ if (err)
+ break;
+
+ parse_data (p);
+ }
+ else if (p.name () == "errors")
+ {
+ // Don't stop parsing because the error semantics depends on whether
+ // or not `data` is present.
+ //
+ err = true; // Handled below.
+ }
+ else
+ {
+ // The spec says the response will never contain any top-level fields
+ // other than data, errors, and extensions.
+ //
+ if (p.name () != "extensions")
+ {
+ throw_json (p,
+ "unexpected top-level GraphQL response field: '" +
+ p.name () + '\'');
+ }
+
+ p.next_expect_value_skip ();
+ }
+ }
+
+ // If the `errors` field was present in the response, error(s) occurred
+ // before or during execution of the operation.
+ //
+ // If the `data` field was not present the errors are request errors which
+ // occur before execution and are typically the client's fault.
+ //
+ // If the `data` field was also present in the response the errors are
+ // field errors which occur during execution and are typically the GraphQL
+ // endpoint's fault, and some fields in `data` that should not be are
+ // likely to be null.
+ //
+ if (err)
+ {
+ if (dat)
+ {
+ // @@ TODO: Consider parsing partial data?
+ //
+ throw runtime_error ("field error(s) received from GraphQL endpoint; "
+ "incomplete data received");
+ }
+ else
+ throw runtime_error ("request error(s) received from GraphQL endpoint");
+ }
+ }
+
+ // Parse a response to a check_run GraphQL mutation such as `createCheckRun`
+ // or `updateCheckRun`.
+ //
+ // Example response (only the part we need to parse here):
+ //
+ // {
+ // "cr0": {
+ // "checkRun": {
+ // "id": "CR_kwDOLc8CoM8AAAAFQ5GqPg",
+ // "name": "libb2/0.98.1+2/x86_64-linux-gnu/linux_debian_12-gcc_13.1-O3/default/dev/0.17.0-a.1",
+ // "status": "QUEUED"
+ // }
+ // },
+ // "cr1": {
+ // "checkRun": {
+ // "id": "CR_kwDOLc8CoM8AAAAFQ5GqhQ",
+ // "name": "libb2/0.98.1+2/x86_64-linux-gnu/linux_debian_12-gcc_13.1/default/dev/0.17.0-a.1",
+ // "status": "QUEUED"
+ // }
+ // }
+ // }
+ //
+ // @@ TODO Handle response errors properly.
+ //
+ static vector<gh_check_run>
+ gq_parse_response_check_runs (json::parser& p)
+ {
+ using event = json::event;
+
+ vector<gh_check_run> r;
+
+ gq_parse_response (p, [&r] (json::parser& p)
+ {
+ p.next_expect (event::begin_object);
+
+ // Parse the "cr0".."crN" members (field aliases).
+ //
+ while (p.next_expect (event::name, event::end_object))
+ {
+ // Parse `"crN": { "checkRun":`.
+ //
+ if (p.name () != "cr" + to_string (r.size ()))
+ throw_json (p, "unexpected field alias: '" + p.name () + '\'');
+ p.next_expect (event::begin_object);
+ p.next_expect_name ("checkRun");
+
+ r.emplace_back (p); // Parse the check_run object.
+
+ p.next_expect (event::end_object); // Parse end of crN object.
+ }
+ });
+
+ // Our requests always operate on at least one check run so if there were
+ // none in the data field something went wrong.
+ //
+ if (r.empty ())
+ throw_json (p, "data object is empty");
+
+ return r;
+ }
+
// 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.
+ // if unset. Return false and issue diagnostics if the request failed.
//
static bool
- gq_mutate_check_runs (vector<service_data::check_run>& crs,
- const vector<reference_wrapper<const build>>& bs,
+ gq_mutate_check_runs (vector<check_run>& crs,
const string& iat,
+ const vector<reference_wrapper<const build>>& bs,
string rq,
build_state st,
const basic_mark& error) noexcept
{
- vector<check_run> rcrs;
+ vector<gh_check_run> rcrs;
try
{
@@ -35,9 +214,9 @@ namespace brep
//
struct resp
{
- vector<check_run> check_runs; // Received check runs.
+ vector<gh_check_run> check_runs; // Received check runs.
- resp (json::parser& p) : check_runs (parse_check_runs_response (p)) {}
+ resp (json::parser& p) : check_runs (gq_parse_response_check_runs (p)) {}
resp () = default;
} rs;
@@ -57,26 +236,26 @@ namespace brep
{
// Validate the check run in the response against the build.
//
- const check_run& rcr (rcrs[i]); // Received check run.
+ const gh_check_run& rcr (rcrs[i]); // Received check run.
const build& b (bs[i]);
- build_state rst (from_string_gh (rcr.status)); // Received state.
+ build_state rst (gh_from_status (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) << '\'';
+ << "' but expected '" << gh_to_status (st) << '\'';
return false; // Fail because something is clearly very wrong.
}
else
{
- service_data::check_run& cr (crs[i]);
+ check_run& cr (crs[i]);
if (!cr.node_id)
cr.node_id = move (rcr.node_id);
- cr.state = from_string_gh (rcr.status);
+ cr.state = gh_from_status (rcr.status);
}
}
@@ -106,7 +285,7 @@ namespace brep
error << "unable to mutate check runs (errno=" << e.code () << "): "
<< e.what ();
}
- catch (const runtime_error& e) // From parse_check_runs_response().
+ catch (const runtime_error& e) // From gq_parse_response_check_runs().
{
// GitHub response contained error(s) (could be ours or theirs at this
// point).
@@ -117,31 +296,35 @@ namespace brep
return false;
}
- static bool
- gq_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
+ // Serialize a GraphQL operation (query/mutation) into a GraphQL request.
+ //
+ // This is essentially a JSON object with a "query" string member containing
+ // the GraphQL operation. For example:
+ //
+ // { "query": "mutation { cr0:createCheckRun(... }" }
+ //
+ static string
+ gq_serialize_request (const string& o)
{
- vector<service_data::check_run> crs {move (cr)};
-
- bool r (mutate_check_runs (crs, bs, iat, move (rq), st, error));
+ string b;
+ json::buffer_serializer s (b);
- cr = move (crs[0]);
+ s.begin_object ();
+ s.member ("query", o);
+ s.end_object ();
- return r;
+ return b;
}
// Serialize `createCheckRun` mutations for one or more builds to GraphQL.
//
static string
- gq_create_check_runs (const string& ri, // Repository ID
- const string& hs, // Head SHA
- const vector<reference_wrapper<const build>>& bs,
- build_state st,
- const tenant_service_base::build_hints* bh = nullptr)
+ 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 tenant_service_base::build_hints* bh)
{
ostringstream os;
@@ -157,13 +340,13 @@ namespace brep
// Check run name.
//
- string nm (check_run_name (b, bh));
+ 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 (to_string_gh (st)) << '\n'
+ << " status: " << gq_enum (gh_to_status (st)) << '\n'
<< "})" << '\n'
// Specify the selection set (fields to be returned).
//
@@ -186,17 +369,17 @@ namespace brep
// @@ TODO Support conclusion, output, etc.
//
static string
- gq_update_check_run (const string& ri, // Repository ID
- const string& ci, // Check run node_id
- build_state st)
+ gq_mutation_update_check_run (const string& ri, // Repository ID.
+ const string& ni, // Node ID.
+ build_state st)
{
ostringstream os;
os << "mutation {" << '\n'
<< "cr0:updateCheckRun(input: {" << '\n'
- << " checkRunId: " << gq_str (ci) << ',' << '\n'
+ << " checkRunId: " << gq_str (ni) << ',' << '\n'
<< " repositoryId: " << gq_str (ri) << ',' << '\n'
- << " status: " << gq_enum (to_string_gh (st)) << '\n'
+ << " status: " << gq_enum (gh_to_status (st)) << '\n'
<< "})" << '\n'
// Specify the selection set (fields to be returned).
//
@@ -212,198 +395,236 @@ namespace brep
return os.str ();
}
- // Serialize a GraphQL operation (query/mutation) into a GraphQL request.
- //
- // This is essentially a JSON object with a "query" string member containing
- // the GraphQL operation. For example:
- //
- // { "query": "mutation { cr0:createCheckRun(... }" }
- //
- static string
- gq_serialize_request (const string& o)
+ bool
+ gq_create_check_runs (vector<check_run>& crs,
+ const string& iat,
+ const string& rid,
+ const string& hs,
+ const vector<reference_wrapper<const build>>& bs,
+ build_state st,
+ const tenant_service_base::build_hints& bh,
+ const basic_mark& error)
{
- string b;
- json::buffer_serializer s (b);
+ string rq (gq_serialize_request (
+ gq_mutation_create_check_runs (rid, hs, bs, st, &bh)));
- s.begin_object ();
- s.member ("query", o);
- s.end_object ();
-
- return b;
+ return gq_mutate_check_runs (crs, iat, bs, move (rq), st, error);
}
- [[noreturn]] void
- throw_json (json::parser& p, const string& m)
+ bool
+ gq_create_check_run (check_run& cr,
+ const string& iat,
+ const string& rid,
+ const string& hs,
+ const build& b,
+ build_state st,
+ const tenant_service_base::build_hints& bh,
+ const basic_mark& error)
{
- throw json::invalid_json_input (
- p.input_name,
- p.line (), p.column (), p.position (),
- m);
+ vector<check_run> crs {move (cr)};
+
+ bool r (gq_create_check_runs (crs, iat, rid, hs, {b}, st, bh, error));
+
+ cr = move (crs[0]);
+
+ return r;
}
- // Parse a JSON-serialized GraphQL response.
- //
- // Throw runtime_error if the response indicated errors and
- // invalid_json_input if the GitHub response contained invalid JSON.
- //
- // The response format is defined in the GraphQL spec:
- // https://spec.graphql.org/October2021/#sec-Response.
- //
- // Example response:
- //
- // {
- // "data": {...},
- // "errors": {...}
- // }
- //
- // The contents of `data`, including its opening and closing braces, are
- // parsed by the `parse_data` function.
- //
- // @@ TODO: specify what parse_data may throw (probably only
- // invalid_json_input).
- //
- // @@ TODO errors comes before data in GitHub's responses.
- //
- static void
- gq_parse_response (json::parser& p,
- function<void (json::parser&)> parse_data)
+ bool
+ gq_update_check_run (check_run& cr,
+ const string& iat,
+ const string& rid,
+ const string& nid,
+ const build& b,
+ build_state st,
+ const basic_mark& error)
{
- using event = json::event;
+ string rq (
+ gq_serialize_request (gq_mutation_update_check_run (rid, nid, st)));
- // True if the data/errors fields are present.
- //
- // Although the spec merely recommends that the `errors` field, if
- // present, comes before the `data` field, assume it always does because
- // letting the client parse data in the presence of field errors
- // (unexpected nulls) would not make sense.
- //
- bool dat (false), err (false);
+ vector<check_run> crs {move (cr)};
- p.next_expect (event::begin_object);
+ bool r (gq_mutate_check_runs (crs, iat, {b}, move (rq), st, error));
- while (p.next_expect (event::name, event::end_object))
- {
- if (p.name () == "data")
- {
- dat = true;
+ cr = move (crs[0]);
- // Currently we're not handling fields that are null due to field
- // errors (see below for details) so don't parse any further.
- //
- if (err)
- break;
+ return r;
+ }
- parse_data (p);
- }
- else if (p.name () == "errors")
+ pair<optional<gh_check_run>, bool>
+ gq_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;
{
- // Don't stop parsing because the error semantics depends on whether
- // or not `data` is present.
- //
- err = true; // Handled below.
+ 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 ();
}
- else
+
+ // 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
{
- // The spec says the response will never contain any top-level fields
- // other than data, errors, and extensions.
- //
- if (p.name () != "extensions")
+ optional<gh_check_run> cr;
+ size_t cr_count = 0;
+
+ resp (json::parser& p)
{
- throw_json (p,
- "unexpected top-level GraphQL response field: '" +
- p.name () + '\'');
- }
+ using event = json::event;
- p.next_expect_value_skip ();
- }
- }
+ gq_parse_response (p, [this] (json::parser& p)
+ {
+ p.next_expect (event::begin_object);
+ p.next_expect_member_object ("node");
+ p.next_expect_member_object ("checkRuns");
- // If the `errors` field was present in the response, error(s) occurred
- // before or during execution of the operation.
- //
- // If the `data` field was not present the errors are request errors which
- // occur before execution and are typically the client's fault.
- //
- // If the `data` field was also present in the response the errors are
- // field errors which occur during execution and are typically the GraphQL
- // endpoint's fault, and some fields in `data` that should not be are
- // likely to be null.
- //
- if (err)
- {
- if (dat)
- {
- // @@ TODO: Consider parsing partial data?
- //
- throw runtime_error ("field error(s) received from GraphQL endpoint; "
- "incomplete data received");
- }
- else
- throw runtime_error ("request error(s) received from GraphQL endpoint");
- }
- }
+ cr_count = p.next_expect_member_number<size_t> ("totalCount");
- // Parse a response to a check_run GraphQL mutation such as `createCheckRun`
- // or `updateCheckRun`.
- //
- // Example response (only the part we need to parse here):
- //
- // {
- // "cr0": {
- // "checkRun": {
- // "id": "CR_kwDOLc8CoM8AAAAFQ5GqPg",
- // "name": "libb2/0.98.1+2/x86_64-linux-gnu/linux_debian_12-gcc_13.1-O3/default/dev/0.17.0-a.1",
- // "status": "QUEUED"
- // }
- // },
- // "cr1": {
- // "checkRun": {
- // "id": "CR_kwDOLc8CoM8AAAAFQ5GqhQ",
- // "name": "libb2/0.98.1+2/x86_64-linux-gnu/linux_debian_12-gcc_13.1/default/dev/0.17.0-a.1",
- // "status": "QUEUED"
- // }
- // }
- // }
- //
- // @@ TODO Handle response errors properly.
- //
- static vector<check_run>
- gq_parse_response_check_runs (json::parser& p)
- {
- using event = json::event;
+ p.next_expect_member_array ("edges");
- vector<check_run> r;
+ for (size_t i (0); i != cr_count; ++i)
+ {
+ p.next_expect (event::begin_object);
+ p.next_expect_name ("node");
+ gh_check_run cr (p);
+ p.next_expect (event::end_object);
- parse_graphql_response (
- p,
- [&r] (json::parser& p)
- {
- p.next_expect (event::begin_object);
+ if (i == 0)
+ this->cr = move (cr);
+ }
- // Parse the "cr0".."crN" members (field aliases).
- //
- while (p.next_expect (event::name, event::end_object))
- {
- // Parse `"crN": { "checkRun":`.
- //
- if (p.name () != "cr" + to_string (r.size ()))
- throw_json (p, "unexpected field alias: '" + p.name () + '\'');
- p.next_expect (event::begin_object);
- p.next_expect_name ("checkRun");
+ 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);
+ });
+ }
- r.emplace_back (p); // Parse the check_run object.
+ resp () = default;
+ } rs;
- p.next_expect (event::end_object); // Parse end of crN object.
- }
- });
+ uint16_t sc (github_post (rs,
+ "graphql",
+ strings {"Authorization: Bearer " + iat},
+ gq_serialize_request (rq)));
- // Our requests always operate on at least one check run so if there were
- // none in the data field something went wrong.
- //
- if (r.empty ())
- throw_json (p, "data object is empty");
+ 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 r;
+ return {nullopt, false};
}
// GraphQL serialization functions.
diff --git a/mod/mod-ci-github-gq.hxx b/mod/mod-ci-github-gq.hxx
index de7021a..994f8d1 100644
--- a/mod/mod-ci-github-gq.hxx
+++ b/mod/mod-ci-github-gq.hxx
@@ -7,21 +7,60 @@
#include <libbrep/types.hxx>
#include <libbrep/utility.hxx>
+#include <libbrep/build.hxx>
+
+#include <mod/tenant-service.hxx> // build_hints
+
#include <mod/mod-ci-github-gh.hxx>
#include <mod/mod-ci-github-service-data.hxx>
-
namespace brep
{
// GraphQL functions (all start with gq_).
//
- // @@ TODO:
-
- gq_create_check_run ();
- gq_update_check_run ();
-
- // @@ TODO Pass error, trace in same order everywhere.
+ // Create a new check run on GitHub for each build. Update `check_runs` with
+ // the new states and node IDs. Return false and issue diagnostics if the
+ // request failed.
+ //
+ bool
+ gq_create_check_runs (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 tenant_service_base::build_hints&,
+ const basic_mark& error);
+
+ // 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.
+ //
+ bool
+ gq_create_check_run (check_run& cr,
+ const string& installation_access_token,
+ const string& repository_id,
+ const string& head_sha,
+ const build&,
+ build_state,
+ const tenant_service_base::build_hints&,
+ const basic_mark& error);
+
+ // Update a check run on GitHub.
+ //
+ // Send a GraphQL request that updates an existing check run. Update `cr`
+ // with the new state. Return false and issue diagnostics if the request
+ // failed.
+ //
+ bool
+ gq_update_check_run (check_run& cr,
+ const string& installation_access_token,
+ const string& repository_id,
+ const string& node_id,
+ const build&,
+ build_state,
+ const basic_mark& error);
// Fetch from GitHub the check run with the specified name (hints-shortened
// build ID).
@@ -33,184 +72,14 @@ namespace brep
// 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.
+ // the observed behavior is that creating a check run destroys the existent
+ // one, leaving only the new one with a different node ID.
//
- pair<optional<gh::check_run>, bool>
- gq_fetch_check_run (const string& iat,
+ pair<optional<gh_check_run>, bool>
+ gq_fetch_check_run (const string& installation_access_token,
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};
- }
+ const basic_mark& error) noexcept;
}
#endif // MOD_MOD_CI_GITHUB_GQ_HXX
diff --git a/mod/mod-ci-github-post.hxx b/mod/mod-ci-github-post.hxx
index f1ed914..d278ae0 100644
--- a/mod/mod-ci-github-post.hxx
+++ b/mod/mod-ci-github-post.hxx
@@ -7,6 +7,8 @@
#include <libbrep/types.hxx>
#include <libbrep/utility.hxx>
+#include <libbutl/curl.hxx>
+
namespace brep
{
// Send a POST request to the GitHub API endpoint `ep`, parse GitHub's JSON
@@ -29,6 +31,8 @@ namespace brep
const strings& hdrs,
const string& body = "")
{
+ using namespace butl;
+
// Convert the header values to curl header option/value pairs.
//
strings hdr_opts;
diff --git a/mod/mod-ci-github-service-data.cxx b/mod/mod-ci-github-service-data.cxx
new file mode 100644
index 0000000..9f6d86d
--- /dev/null
+++ b/mod/mod-ci-github-service-data.cxx
@@ -0,0 +1,150 @@
+// file : mod/mod-ci-github-service-data.cxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#include <mod/mod-ci-github-service-data.hxx>
+
+#include <libbutl/json/parser.hxx>
+#include <libbutl/json/serializer.hxx>
+
+namespace brep
+{
+ using event = json::event;
+
+ service_data::
+ service_data (const string& json)
+ {
+ json::parser p (json.data (), json.size (), "service_data");
+
+ p.next_expect (event::begin_object);
+
+ // Throw if the schema version is not supported.
+ //
+ version = p.next_expect_member_number<uint64_t> ("version");
+ if (version != 1)
+ {
+ throw invalid_argument ("unsupported service_data schema version: " +
+ to_string (version));
+ }
+
+ // Installation access token.
+ //
+ p.next_expect_member_object ("installation_access");
+ installation_access.token = p.next_expect_member_string ("token");
+ installation_access.expires_at =
+ from_iso8601 (p.next_expect_member_string ("expires_at"));
+ p.next_expect (event::end_object);
+
+ installation_id =
+ p.next_expect_member_number<uint64_t> ("installation_id");
+ repository_id = p.next_expect_member_string ("repository_id");
+ head_sha = p.next_expect_member_string ("head_sha");
+
+ p.next_expect_member_array ("check_runs");
+ while (p.next_expect (event::begin_object, event::end_array))
+ {
+ string bid (p.next_expect_member_string ("build_id"));
+
+ optional<string> nid;
+ {
+ string* v (p.next_expect_member_string_null ("node_id"));
+ if (v != nullptr)
+ nid = *v;
+ }
+
+ optional<build_state> s;
+ {
+ string* v (p.next_expect_member_string_null ("state"));
+ if (v != nullptr)
+ s = to_build_state (*v);
+ }
+
+ check_runs.emplace_back (move (bid), move (nid), s);
+
+ p.next_expect (event::end_object);
+ }
+
+ p.next_expect (event::end_object);
+ }
+
+ service_data::
+ service_data (string iat_tok,
+ timestamp iat_ea,
+ uint64_t iid,
+ string rid,
+ string hs)
+ : installation_access (move (iat_tok), iat_ea),
+ installation_id (iid),
+ repository_id (move (rid)),
+ head_sha (move (hs))
+ {
+ }
+
+ string service_data::
+ json () const
+ {
+ string b;
+ json::buffer_serializer s (b);
+
+ s.begin_object ();
+
+ s.member ("version", 1);
+
+ // Installation access token.
+ //
+ s.member_begin_object ("installation_access");
+ s.member ("token", installation_access.token);
+ s.member ("expires_at", to_iso8601 (installation_access.expires_at));
+ s.end_object ();
+
+ s.member ("installation_id", installation_id);
+ s.member ("repository_id", repository_id);
+ s.member ("head_sha", head_sha);
+
+ s.member_begin_array ("check_runs");
+ for (const check_run& cr: check_runs)
+ {
+ s.begin_object ();
+ s.member ("build_id", cr.build_id);
+
+ s.member_name ("node_id");
+ if (cr.node_id)
+ s.value (*cr.node_id);
+ else
+ s.value (nullptr);
+
+ s.member_name ("state");
+ if (cr.state)
+ s.value (to_string (*cr.state));
+ else
+ s.value (nullptr);
+
+ s.end_object ();
+ }
+ s.end_array ();
+
+ s.end_object ();
+
+ return b;
+ }
+
+ check_run* service_data::
+ find_check_run (const string& bid)
+ {
+ for (check_run& cr: check_runs)
+ {
+ if (cr.build_id == bid)
+ return &cr;
+ }
+ return nullptr;
+ }
+
+ ostream&
+ operator<< (ostream& os, const check_run& cr)
+ {
+ os << "node_id: " << cr.node_id.value_or ("null")
+ << ", build_id: " << cr.build_id
+ << ", state: " << (cr.state ? to_string (*cr.state) : "null");
+
+ return os;
+ }
+}
diff --git a/mod/mod-ci-github-service-data.hxx b/mod/mod-ci-github-service-data.hxx
index 4d0af96..7ea01ff 100644
--- a/mod/mod-ci-github-service-data.hxx
+++ b/mod/mod-ci-github-service-data.hxx
@@ -1,4 +1,4 @@
-// file : mod/mod-ci-github-serice-data.hxx -*- C++ -*-
+// file : mod/mod-ci-github-service-data.hxx -*- C++ -*-
// license : MIT; see accompanying LICENSE file
#ifndef MOD_MOD_CI_GITHUB_SERVICE_DATA_HXX
@@ -26,16 +26,27 @@ namespace brep
{
string build_id; // Full build id.
optional<string> node_id; // GitHub id.
- build_state state;
- bool state_synced;
+
+ // @@ TODO
+ //
+ // build_state state;
+ // bool state_synced;
+
+ // string
+ // state_string () const
+ // {
+ // string r (to_string (*state));
+ // if (!state_synced)
+ // r += "(unsynchronized)";
+ // return r;
+ // }
+
+ optional<build_state> state;
string
state_string () const
{
- string r (to_string (*state));
- if (!state_synced)
- r += "(unsynchronized)";
- return r;
+ return state ? to_string (*state) : "null";
}
};
@@ -47,7 +58,7 @@ namespace brep
// Check suite-global data.
//
- installation_access_token installation_access;
+ gh_installation_access_token installation_access;
uint64_t installation_id;
// @@ TODO Rename to repository_node_id.
@@ -86,7 +97,7 @@ namespace brep
};
ostream&
- operator<< (ostream&, const service_data::check_run&);
+ operator<< (ostream&, const check_run&);
}
#endif // MOD_MOD_CI_GITHUB_SERVICE_DATA_HXX
diff --git a/mod/mod-ci-github.cxx b/mod/mod-ci-github.cxx
index 179eeba..b9b24f4 100644
--- a/mod/mod-ci-github.cxx
+++ b/mod/mod-ci-github.cxx
@@ -3,14 +3,16 @@
#include <mod/mod-ci-github.hxx>
-#include <libbutl/curl.hxx>
#include <libbutl/json/parser.hxx>
-#include <libbutl/json/serializer.hxx>
#include <mod/jwt.hxx>
#include <mod/hmac.hxx>
#include <mod/module-options.hxx>
+#include <mod/mod-ci-github-gq.hxx>
+#include <mod/mod-ci-github-post.hxx>
+#include <mod/mod-ci-github-service-data.hxx>
+
#include <stdexcept>
// @@ TODO
@@ -71,8 +73,6 @@ using namespace brep::cli;
namespace brep
{
- using namespace gh;
-
ci_github::
ci_github (tenant_service_map& tsm)
: tenant_service_map_ (tsm)
@@ -263,12 +263,12 @@ namespace brep
//
if (event == "check_suite")
{
- check_suite_event cs;
+ gh_check_suite_event cs;
try
{
json::parser p (body.data (), body.size (), "check_suite event");
- cs = check_suite_event (p);
+ cs = gh_check_suite_event (p);
}
catch (const json::invalid_json_input& e)
{
@@ -330,7 +330,7 @@ namespace brep
}
bool ci_github::
- handle_check_suite_request (check_suite_event cs)
+ handle_check_suite_request (gh_check_suite_event cs)
{
HANDLER_DIAG;
@@ -340,7 +340,7 @@ namespace brep
if (!jwt)
throw server_error ();
- optional<installation_access_token> iat (
+ optional<gh_installation_access_token> iat (
obtain_installation_access_token (cs.installation.id,
move (*jwt),
error));
@@ -406,7 +406,7 @@ namespace brep
// thus would cause a spurious backwards state transition.
//
vector<reference_wrapper<const build>> bs;
- vector<service_data::check_run> crs; // Parallel to bs.
+ vector<check_run> crs; // Parallel to bs.
// Exclude builds for which this is an out of order notification.
//
@@ -423,9 +423,9 @@ namespace brep
//
// Note: never go back on the built state.
//
- string bid (check_run_name (b)); // Full Build ID.
+ string bid (gh_check_run_name (b)); // Full Build ID.
- const service_data::check_run* scr (sd.find_check_run (bid));
+ const check_run* scr (sd.find_check_run (bid));
if (scr == nullptr)
{
@@ -459,8 +459,8 @@ namespace brep
// Get a new installation access token if the current one has expired.
//
- const installation_access_token* iat (nullptr);
- optional<installation_access_token> new_iat;
+ const gh_installation_access_token* iat (nullptr);
+ optional<gh_installation_access_token> new_iat;
if (system_clock::now () > sd.installation_access.expires_at)
{
@@ -483,20 +483,15 @@ namespace brep
//
// Queue a check_run for each build.
//
- string rq (graphql_request (create_check_runs (sd.repository_id,
- sd.head_sha,
- bs,
- build_state::queued,
- &hs)));
-
- if (mutate_check_runs (crs,
- bs,
- iat->token,
- move (rq),
- build_state::queued,
- error))
- {
- for (service_data::check_run& cr: crs)
+ if (gq_create_check_runs (crs,
+ iat->token,
+ sd.repository_id, sd.head_sha,
+ bs,
+ build_state::queued,
+ hs,
+ error))
+ {
+ for (check_run& cr: crs)
l3 ([&] { trace << "created check_run { " << cr << " }"; });
}
}
@@ -529,14 +524,14 @@ namespace brep
//
for (size_t i (0); i != bs.size (); ++i)
{
- const service_data::check_run& cr (crs[i]);
+ const check_run& cr (crs[i]);
// Note that this service data may not be the same as what we observed
// in the build_queued() function above. For example, some check runs
// that we have queued may have already transitioned to building. So
// we skip any check runs that are already present.
//
- if (service_data::check_run* scr = sd.find_check_run (cr.build_id))
+ if (check_run* scr = sd.find_check_run (cr.build_id))
{
warn << cr << " state " << scr->state_string ()
<< " was stored before notified state " << cr.state_string ()
@@ -579,43 +574,12 @@ namespace brep
return nullptr;
}
- service_data::check_run cr; // Updated check run.
-
- // Create a new check run on GitHub.
- //
- auto create = [&cr, &b, &hs, &sd, &error] (const string& iat)
- {
- string rq (graphql_request (create_check_runs (sd.repository_id,
- sd.head_sha,
- {b},
- build_state::building,
- &hs)));
-
- return mutate_check_run (cr,
- {b},
- iat,
- move (rq),
- build_state::building,
- error);
- };
-
- // Update a check run that already exists on GitHub.
- //
- auto update =
- [&cr, &b, &sd, &error] (const string& iat,
- const string& nid,
- build_state st = build_state::building)
- {
- string rq (graphql_request (
- update_check_run (sd.repository_id, nid, build_state::building)));
-
- return mutate_check_run (cr, {b}, iat, move (rq), st, error);
- };
+ check_run cr; // Updated check run.
// Get a new installation access token if the current one has expired.
//
- const installation_access_token* iat (nullptr);
- optional<installation_access_token> new_iat;
+ const gh_installation_access_token* iat (nullptr);
+ optional<gh_installation_access_token> new_iat;
if (system_clock::now () > sd.installation_access.expires_at)
{
@@ -633,11 +597,11 @@ namespace brep
if (iat != nullptr)
{
- string bid (check_run_name (b)); // Full Build ID.
+ string bid (gh_check_run_name (b)); // Full Build ID.
// Stored check run.
//
- const service_data::check_run* scr (sd.find_check_run (bid));
+ const check_run* scr (sd.find_check_run (bid));
if (scr != nullptr && scr->node_id)
{
@@ -683,7 +647,13 @@ namespace brep
build_state st (scr->state ? build_state::building
: build_state::built);
- if (update (iat->token, *cr.node_id, st))
+ if (gq_update_check_run (cr,
+ iat->token,
+ sd.repository_id,
+ *cr.node_id,
+ b,
+ st,
+ error))
{
// @@ TODO If !scr->state and GH had built then we probably don't
// want to run the lambda either but currently it will run
@@ -743,11 +713,11 @@ namespace brep
// Fetch the check run by name from GitHub.
//
- pair<optional<check_run>, bool> pr (
- fetch_check_run (iat->token,
- ts.id,
- check_run_name (b, &hs),
- error));
+ pair<optional<gh_check_run>, bool> pr (
+ gq_fetch_check_run (iat->token,
+ ts.id,
+ gh_check_run_name (b, &hs),
+ error));
if (pr.second) // No errors.
{
@@ -762,12 +732,20 @@ namespace brep
// @@ TODO Create with whatever the failed state was if we decide
// to store it.
//
- if (create (iat->token))
+ if (gq_create_check_run (cr,
+ iat->token,
+ sd.repository_id, sd.head_sha,
+ b,
+ build_state::queued,
+ hs,
+ error))
+ {
l3 ([&]{trace << "created check_run { " << cr << " }";});
+ }
}
else // Check run exists on GitHub.
{
- if (pr.first->status == to_string_gh (build_state::queued))
+ if (pr.first->status == gh_to_status (build_state::queued))
{
if (scr != nullptr)
{
@@ -775,8 +753,16 @@ namespace brep
cr.state = nullopt;
}
- if (update (iat->token, pr.first->node_id))
+ if (gq_update_check_run (cr,
+ iat->token,
+ sd.repository_id,
+ pr.first->node_id,
+ b,
+ build_state::building,
+ error))
+ {
l3 ([&]{trace << "updated check_run { " << cr << " }";});
+ }
}
else
{
@@ -823,7 +809,7 @@ namespace brep
if (iat)
sd.installation_access = *iat;
- if (service_data::check_run* scr = sd.find_check_run (cr.build_id))
+ if (check_run* scr = sd.find_check_run (cr.build_id))
{
// Update existing check run.
//
@@ -929,12 +915,12 @@ namespace brep
// repos covered by the installation if installed on an organisation for
// example.
//
- optional<installation_access_token> ci_github::
+ optional<gh_installation_access_token> ci_github::
obtain_installation_access_token (uint64_t iid,
string jwt,
const basic_mark& error) const
{
- installation_access_token iat;
+ gh_installation_access_token iat;
try
{
// API endpoint.
@@ -988,425 +974,4 @@ namespace brep
return iat;
}
-
- static string
- to_iso8601 (timestamp t)
- {
- return butl::to_string (t,
- "%Y-%m-%dT%TZ",
- false /* special */,
- false /* local */);
- }
-
- static timestamp
- from_iso8601 (const string& s)
- {
- return butl::from_string (s.c_str (), "%Y-%m-%dT%TZ", false /* local */);
- }
-
- using event = json::event;
-
- service_data::
- service_data (const string& json)
- {
- json::parser p (json.data (), json.size (), "service_data");
-
- p.next_expect (event::begin_object);
-
- // Throw if the schema version is not supported.
- //
- version = p.next_expect_member_number<uint64_t> ("version");
- if (version != 1)
- {
- throw invalid_argument ("unsupported service_data schema version: " +
- to_string (version));
- }
-
- // Installation access token.
- //
- p.next_expect_member_object ("installation_access");
- installation_access.token = p.next_expect_member_string ("token");
- installation_access.expires_at =
- from_iso8601 (p.next_expect_member_string ("expires_at"));
- p.next_expect (event::end_object);
-
- installation_id =
- p.next_expect_member_number<uint64_t> ("installation_id");
- repository_id = p.next_expect_member_string ("repository_id");
- head_sha = p.next_expect_member_string ("head_sha");
-
- p.next_expect_member_array ("check_runs");
- while (p.next_expect (event::begin_object, event::end_array))
- {
- string bid (p.next_expect_member_string ("build_id"));
-
- optional<string> nid;
- {
- string* v (p.next_expect_member_string_null ("node_id"));
- if (v != nullptr)
- nid = *v;
- }
-
- optional<build_state> s;
- {
- string* v (p.next_expect_member_string_null ("state"));
- if (v != nullptr)
- s = to_build_state (*v);
- }
-
- check_runs.emplace_back (move (bid), move (nid), s);
-
- p.next_expect (event::end_object);
- }
-
- p.next_expect (event::end_object);
- }
-
- service_data::
- service_data (string iat_tok,
- timestamp iat_ea,
- uint64_t iid,
- string rid,
- string hs)
- : installation_access (move (iat_tok), iat_ea),
- installation_id (iid),
- repository_id (move (rid)),
- head_sha (move (hs))
- {
- }
-
- string service_data::
- json () const
- {
- string b;
- json::buffer_serializer s (b);
-
- s.begin_object ();
-
- s.member ("version", 1);
-
- // Installation access token.
- //
- s.member_begin_object ("installation_access");
- s.member ("token", installation_access.token);
- s.member ("expires_at", to_iso8601 (installation_access.expires_at));
- s.end_object ();
-
- s.member ("installation_id", installation_id);
- s.member ("repository_id", repository_id);
- s.member ("head_sha", head_sha);
-
- s.member_begin_array ("check_runs");
- for (const check_run& cr: check_runs)
- {
- s.begin_object ();
- s.member ("build_id", cr.build_id);
-
- s.member_name ("node_id");
- if (cr.node_id)
- s.value (*cr.node_id);
- else
- s.value (nullptr);
-
- s.member_name ("state");
- if (cr.state)
- s.value (to_string (*cr.state));
- else
- s.value (nullptr);
-
- s.end_object ();
- }
- s.end_array ();
-
- s.end_object ();
-
- return b;
- }
-
- service_data::check_run* service_data::
- find_check_run (const string& bid)
- {
- for (check_run& cr: check_runs)
- {
- if (cr.build_id == bid)
- return &cr;
- }
- return nullptr;
- }
-
- ostream&
- operator<< (ostream& os, const service_data::check_run& cr)
- {
- os << "node_id: " << cr.node_id.value_or ("null")
- << ", build_id: " << cr.build_id
- << ", state: " << (cr.state ? to_string (*cr.state) : "null");
-
- return os;
- }
-
- // The rest is GitHub request/response type parsing and printing.
- //
-
- // Throw invalid_json_input when a required member `m` is missing from a
- // JSON object `o`.
- //
- [[noreturn]] static void
- missing_member (const json::parser& p, const char* o, const char* m)
- {
- throw json::invalid_json_input (
- p.input_name,
- p.line (), p.column (), p.position (),
- o + string (" object is missing member '") + m + '\'');
- }
-
- // check_suite
- //
- gh::check_suite::
- check_suite (json::parser& p)
- {
- p.next_expect (event::begin_object);
-
- bool ni (false), hb (false), hs (false), bf (false), at (false);
-
- // Skip unknown/uninteresting members.
- //
- while (p.next_expect (event::name, event::end_object))
- {
- auto c = [&p] (bool& v, const char* s)
- {
- return p.name () == s ? (v = true) : false;
- };
-
- if (c (ni, "node_id")) node_id = p.next_expect_string ();
- else if (c (hb, "head_branch")) head_branch = p.next_expect_string ();
- else if (c (hs, "head_sha")) head_sha = p.next_expect_string ();
- else if (c (bf, "before")) before = p.next_expect_string ();
- else if (c (at, "after")) after = p.next_expect_string ();
- else p.next_expect_value_skip ();
- }
-
- if (!ni) missing_member (p, "check_suite", "node_id");
- if (!hb) missing_member (p, "check_suite", "head_branch");
- if (!hs) missing_member (p, "check_suite", "head_sha");
- if (!bf) missing_member (p, "check_suite", "before");
- if (!at) missing_member (p, "check_suite", "after");
- }
-
- ostream& gh::
- operator<< (ostream& os, const check_suite& cs)
- {
- os << "node_id: " << cs.node_id
- << ", head_branch: " << cs.head_branch
- << ", head_sha: " << cs.head_sha
- << ", before: " << cs.before
- << ", after: " << cs.after;
-
- return os;
- }
-
- // check_run
- //
- gh::check_run::
- check_run (json::parser& p)
- {
- p.next_expect (event::begin_object);
-
- bool ni (false), nm (false), st (false);
-
- while (p.next_expect (event::name, event::end_object))
- {
- auto c = [&p] (bool& v, const char* s)
- {
- return p.name () == s ? (v = true) : false;
- };
-
- if (c (ni, "id")) node_id = p.next_expect_string ();
- else if (c (nm, "name")) name = p.next_expect_string ();
- else if (c (st, "status")) status = p.next_expect_string ();
- }
-
- if (!ni) missing_member (p, "check_run", "id");
- if (!nm) missing_member (p, "check_run", "name");
- if (!st) missing_member (p, "check_run", "status");
- }
-
- ostream& gh::
- operator<< (ostream& os, const check_run& cr)
- {
- os << "node_id: " << cr.node_id
- << ", name: " << cr.name
- << ", status: " << cr.status;
-
- return os;
- }
-
- // repository
- //
- gh::repository::
- repository (json::parser& p)
- {
- p.next_expect (event::begin_object);
-
- bool ni (false), nm (false), fn (false), db (false), cu (false);
-
- // Skip unknown/uninteresting members.
- //
- while (p.next_expect (event::name, event::end_object))
- {
- auto c = [&p] (bool& v, const char* s)
- {
- return p.name () == s ? (v = true) : false;
- };
-
- if (c (ni, "node_id")) node_id = p.next_expect_string ();
- else if (c (nm, "name")) name = p.next_expect_string ();
- else if (c (fn, "full_name")) full_name = p.next_expect_string ();
- else if (c (db, "default_branch")) default_branch = p.next_expect_string ();
- else if (c (cu, "clone_url")) clone_url = p.next_expect_string ();
- else p.next_expect_value_skip ();
- }
-
- if (!ni) missing_member (p, "repository", "node_id");
- if (!nm) missing_member (p, "repository", "name");
- if (!fn) missing_member (p, "repository", "full_name");
- if (!db) missing_member (p, "repository", "default_branch");
- if (!cu) missing_member (p, "repository", "clone_url");
- }
-
- ostream& gh::
- operator<< (ostream& os, const repository& rep)
- {
- os << "node_id: " << rep.node_id
- << ", name: " << rep.name
- << ", full_name: " << rep.full_name
- << ", default_branch: " << rep.default_branch
- << ", clone_url: " << rep.clone_url;
-
- return os;
- }
-
- // installation
- //
- gh::installation::
- installation (json::parser& p)
- {
- p.next_expect (event::begin_object);
-
- bool i (false);
-
- // Skip unknown/uninteresting members.
- //
- while (p.next_expect (event::name, event::end_object))
- {
- auto c = [&p] (bool& v, const char* s)
- {
- return p.name () == s ? (v = true) : false;
- };
-
- if (c (i, "id")) id = p.next_expect_number<uint64_t> ();
- else p.next_expect_value_skip ();
- }
-
- if (!i) missing_member (p, "installation", "id");
- }
-
- ostream& gh::
- operator<< (ostream& os, const installation& i)
- {
- os << "id: " << i.id;
-
- return os;
- }
-
- // check_suite_event
- //
- gh::check_suite_event::
- check_suite_event (json::parser& p)
- {
- p.next_expect (event::begin_object);
-
- bool ac (false), cs (false), rp (false), in (false);
-
- // Skip unknown/uninteresting members.
- //
- while (p.next_expect (event::name, event::end_object))
- {
- auto c = [&p] (bool& v, const char* s)
- {
- return p.name () == s ? (v = true) : false;
- };
-
- if (c (ac, "action")) action = p.next_expect_string ();
- else if (c (cs, "check_suite")) check_suite = gh::check_suite (p);
- else if (c (rp, "repository")) repository = gh::repository (p);
- else if (c (in, "installation")) installation = gh::installation (p);
- else p.next_expect_value_skip ();
- }
-
- if (!ac) missing_member (p, "check_suite_event", "action");
- if (!cs) missing_member (p, "check_suite_event", "check_suite");
- if (!rp) missing_member (p, "check_suite_event", "repository");
- if (!in) missing_member (p, "check_suite_event", "installation");
- }
-
- ostream& gh::
- operator<< (ostream& os, const check_suite_event& cs)
- {
- os << "action: " << cs.action;
- os << ", check_suite { " << cs.check_suite << " }";
- os << ", repository { " << cs.repository << " }";
- os << ", installation { " << cs.installation << " }";
-
- return os;
- }
-
- // installation_access_token
- //
- // Example JSON:
- //
- // {
- // "token": "ghs_Py7TPcsmsITeVCAWeVtD8RQs8eSos71O5Nzp",
- // "expires_at": "2024-02-15T16:16:38Z",
- // ...
- // }
- //
- gh::installation_access_token::
- installation_access_token (json::parser& p)
- {
- p.next_expect (event::begin_object);
-
- bool tk (false), ea (false);
-
- // Skip unknown/uninteresting members.
- //
- while (p.next_expect (event::name, event::end_object))
- {
- auto c = [&p] (bool& v, const char* s)
- {
- return p.name () == s ? (v = true) : false;
- };
-
- if (c (tk, "token")) token = p.next_expect_string ();
- else if (c (ea, "expires_at")) expires_at = from_iso8601 (p.next_expect_string ());
- else p.next_expect_value_skip ();
- }
-
- if (!tk) missing_member (p, "installation_access_token", "token");
- if (!ea) missing_member (p, "installation_access_token", "expires_at");
- }
-
- gh::installation_access_token::
- installation_access_token (string tk, timestamp ea)
- : token (move (tk)), expires_at (ea)
- {
- }
-
- ostream& gh::
- operator<< (ostream& os, const installation_access_token& t)
- {
- os << "token: " << t.token << ", expires_at: ";
- butl::operator<< (os, t.expires_at);
-
- return os;
- }
}
diff --git a/mod/mod-ci-github.hxx b/mod/mod-ci-github.hxx
index a6cd180..4b23d85 100644
--- a/mod/mod-ci-github.hxx
+++ b/mod/mod-ci-github.hxx
@@ -13,6 +13,8 @@
#include <mod/ci-common.hxx>
#include <mod/tenant-service.hxx>
+#include <mod/mod-ci-github-gh.hxx>
+
namespace brep
{
class ci_github: public handler,
@@ -61,14 +63,14 @@ namespace brep
// Handle the check_suite event `requested` and `rerequested` actions.
//
bool
- handle_check_suite_request (gh::check_suite_event);
+ handle_check_suite_request (gh_check_suite_event);
optional<string>
generate_jwt (const basic_mark& trace, const basic_mark& error) const;
// Authenticate to GitHub as an app installation.
//
- optional<gh::installation_access_token>
+ optional<gh_installation_access_token>
obtain_installation_access_token (uint64_t install_id,
string jwt,
const basic_mark& error) const;