diff options
author | Francois Kritzinger <francois@codesynthesis.com> | 2024-03-11 15:17:45 +0200 |
---|---|---|
committer | Francois Kritzinger <francois@codesynthesis.com> | 2024-10-15 09:05:27 +0200 |
commit | f365f6a6c3646cc587bdb0a9335ed8e69c7d0b7d (patch) | |
tree | b98b86418bb1906537658159f51c8580242a763f | |
parent | c3516e06d4409485a1b5ee018438526e41331028 (diff) |
Serialize GraphQL
-rw-r--r-- | mod/mod-ci-github.cxx | 214 | ||||
-rw-r--r-- | 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 <libbutl/curl.hxx> #include <libbutl/json/parser.hxx> +#include <libbutl/json/serializer.hxx> #include <mod/jwt.hxx> #include <mod/hmac.hxx> @@ -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<build>& 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<start_result> r (start (error, - warn, - verb_ ? &trace : nullptr, - tenant_service ("", "ci-github"), - move (rl), - vector<package> {}, - nullopt, // client_ip, - nullopt // user_agent, - )); - - if (!r) - fail << "unable to start CI"; + // optional<start_result> r (start (error, + // warn, + // verb_ ? &trace : nullptr, + // tenant_service ("", "ci-github"), + // move (rl), + // vector<package> {}, + // nullopt, // client_ip, + // nullopt // user_agent, + // )); + + // if (!r) + // fail << "unable to start CI"; + + vector<build> 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; |