From 80f7738d52a140425faf52dfd858ee8dc379d874 Mon Sep 17 00:00:00 2001 From: Francois Kritzinger Date: Mon, 11 Mar 2024 15:17:45 +0200 Subject: Serialize GraphQL --- mod/mod-ci-github.cxx | 214 ++++++++++++++++++++++++++++++++++++++++++++++---- mod/mod-ci-github.hxx | 18 ++++- 2 files changed, 215 insertions(+), 17 deletions(-) diff --git a/mod/mod-ci-github.cxx b/mod/mod-ci-github.cxx index 4bf91fc..d0d880f 100644 --- a/mod/mod-ci-github.cxx +++ b/mod/mod-ci-github.cxx @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -304,6 +305,162 @@ namespace brep } } + // GraphQL serialization functions. + // + // The GraphQL spec: + // https://spec.graphql.org/ + // + // The GitHub GraphQL API reference: + // https://docs.github.com/en/graphql/reference/ + // + + // Check that a string is a valid GraphQL name. + // + // GraphQL names can contain only alphanumeric characters and underscores + // and cannot begin with a digit. + // + // Return the name or throw invalid_argument if it is invalid. + // + static const string& + gq_name (const string& v) + { + if (v.empty () || digit (v[0])) + throw invalid_argument ("invalid GraphQL name: '" + v + '\''); + + for (char c: v) + { + if (!alnum (c) && c != '_') + { + throw invalid_argument ("invalid character in GraphQL name: '" + c + + '\''); + } + } + + return v; + } + + // Serialize a string to GraphQL. + // + // Return the serialized string or throw invalid_argument if the string is + // invalid. + // + static string + gq_string (const string& v) + { + string b; + json::buffer_serializer s (b); + + try + { + s.value (v); + } + catch (const json::invalid_json_output&) + { + throw invalid_argument ("invalid GraphQL string: '" + v + '\''); + } + + return b; + } + + // Serialize an int to GraphQL. + // + static string + gq_int (uint64_t v) + { + string b; + json::buffer_serializer s (b); + s.value (v); + return b; + } + + // Serialize a boolean to GraphQL. + // + static string + gq_boolean (bool v) + { + string b; + json::buffer_serializer s (b); + s.value (v); + return b; + } + + // Check that a string is a valid GraphQL enum value. + // + // GraphQL enum values can be any GraphQL name except for `true`, `false`, + // or `null`. + // + // Return the enum value or throw invalid_argument if it is invalid. + // + static const string& + gq_enum (const string& v) + { + if (v == "true" || v == "false" || v == "null") + throw invalid_argument ("invalid GraphQL enum value: '" + v + '\''); + + return gq_name (v); + } + + // Serialize `createCheckRun` GraphQL mutations for one or more builds. + // + static string + gq_check_runs (const string& ri, // Repository ID + const string& hs, // Head SHA + const vector& bs) + { + // Check run status values. + // + const string queued ("QUEUED"); + const string in_progress ("IN_PROGRESS"); + const string completed ("COMPLETED"); + + ostringstream os; + + os << "mutation {\n"; + + // Serialize a `createCheckRun` for each build. + // + size_t cn (0); // Check run number. + + for (const build& b: bs) + { + string al ("cr" + to_string (cn++)); // Field alias + string nm (b.package_name.string () + '-' + b.target_config_name); // Name + + os << gq_name (al) << ":createCheckRun(input: {" << '\n' + << " name: " << gq_string (nm) << ',' << '\n' + << " repositoryId: " << gq_string (ri) << ',' << '\n' + << " headSha: " << gq_string (hs) << ',' << '\n' + << " status: " << gq_enum (queued) << '\n' + << "})" << '\n' + // Specify the selection set (fields to be returned). + // + << "{" << '\n' + << " CheckRun {" << '\n' + << " id," << '\n' + << " name," << '\n' + << " status" << '\n' + << " }" << '\n' + << "}" << '\n'; + } + + os << "}\n"; + + return os.str (); + } + + // Serialize an "update check run" for a build to JSON for the REST API. + // + // @@ TMP We're going to have to do the check run updates using the REST API + // because the GraphQL API check run updates take only an ID (which + // we would have to store) whereas the REST version takes a name. + // + // static string + // rest_check_run (uint64_t ri, // Repository ID + // const string& hs, // Head SHA + // const build& b) + // { + // } + bool ci_github:: handle_check_suite_request (check_suite_event cs) { @@ -320,18 +477,42 @@ namespace brep cs.check_suite.head_branch, repository_type::git); - optional r (start (error, - warn, - verb_ ? &trace : nullptr, - tenant_service ("", "ci-github"), - move (rl), - vector {}, - nullopt, // client_ip, - nullopt // user_agent, - )); - - if (!r) - fail << "unable to start CI"; + // optional r (start (error, + // warn, + // verb_ ? &trace : nullptr, + // tenant_service ("", "ci-github"), + // move (rl), + // vector {}, + // nullopt, // client_ip, + // nullopt // user_agent, + // )); + + // if (!r) + // fail << "unable to start CI"; + + vector builds; + builds.emplace_back ("tenant", + build::package_name_type (cs.repository.name), + brep::version ("1.2.3"), + target_triplet ("x86_64-linux-gnu"), + "linux_debian_12", + "default", + "gcc", + brep::version ("4.5.6")); + + builds.emplace_back ("tenant", + build::package_name_type (cs.repository.name), + brep::version ("1.2.3"), + target_triplet ("x86_64-linux-gnu"), + "linux_fedora_37", + "default", + "clang", + brep::version ("4.5.6")); + + cout << gq_check_runs (cs.repository.node_id, + cs.check_suite.head_sha, + builds) + << endl; return true; } @@ -734,7 +915,7 @@ namespace brep { p.next_expect (event::begin_object); - bool nm (false), fn (false), db (false), cu (false); + bool ni (false), nm (false), fn (false), db (false), cu (false); // Skip unknown/uninteresting members. // @@ -745,13 +926,15 @@ namespace brep return p.name () == s ? (v = true) : false; }; - if (c (nm, "name")) name = p.next_expect_string (); + 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"); @@ -761,7 +944,8 @@ namespace brep ostream& gh::operator<< (ostream& os, const repository& rep) { - os << "name: " << rep.name << endl + os << "node_id: " << rep.node_id << endl + << "name: " << rep.name << endl << "full_name: " << rep.full_name << endl << "default_branch: " << rep.default_branch << endl << "clone_url: " << rep.clone_url << endl; diff --git a/mod/mod-ci-github.hxx b/mod/mod-ci-github.hxx index 3b696d7..9a68434 100644 --- a/mod/mod-ci-github.hxx +++ b/mod/mod-ci-github.hxx @@ -25,8 +25,21 @@ namespace brep { // GitHub request/response types. // - // Note that having this types directly in brep causes clashes (e.g., for - // the repository name). + // Note that the GitHub REST and GraphQL APIs use different ID types and + // values. In the REST API they are usually integers (but sometimes + // strings!) whereas in GraphQL they are always strings (note: + // base64-encoded and opaque, not just the REST ID value as a string). + // + // In both APIs the ID field is called `id`, but REST responses and webhook + // events also contain the corresponding GraphQL object's ID in the + // `node_id` field. + // + // In the structures below we always use the RESP API/webhook names for ID + // fields. I.e., `id` always refers to the REST/webhook ID, and `node_id` + // always refers to the GraphQL ID. + // + // Note that having the below types directly in brep causes clashes (e.g., + // for the repository name). // namespace gh { @@ -50,6 +63,7 @@ namespace brep struct repository { + string node_id; string name; string full_name; string default_branch; -- cgit v1.1