aboutsummaryrefslogtreecommitdiff
path: root/mod/mod-ci-github.cxx
diff options
context:
space:
mode:
Diffstat (limited to 'mod/mod-ci-github.cxx')
-rw-r--r--mod/mod-ci-github.cxx863
1 files changed, 583 insertions, 280 deletions
diff --git a/mod/mod-ci-github.cxx b/mod/mod-ci-github.cxx
index 0a28b63..e19a41b 100644
--- a/mod/mod-ci-github.cxx
+++ b/mod/mod-ci-github.cxx
@@ -3,11 +3,14 @@
#include <mod/mod-ci-github.hxx>
+#include <libbutl/curl.hxx>
#include <libbutl/json/parser.hxx>
+#include <mod/jwt.hxx>
#include <mod/module-options.hxx>
-#include <iostream>
+#include <stdexcept>
+#include <iostream> // @@ TODO Remove once debug output has been removed.
// @@ TODO
//
@@ -19,6 +22,7 @@
//
// Webhooks:
// https://docs.github.com/en/webhooks/using-webhooks/best-practices-for-using-webhooks
+// https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries
//
// REST API:
// https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api?apiVersion=2022-11-28
@@ -30,363 +34,662 @@
// https://en.wikipedia.org/wiki/HMAC#Definition. A suitable implementation
// is provided by OpenSSL.
-// @@ Authenticating to use the API
-//
-// There are three types of authentication:
-//
-// 1) Authenticating as an app. Used to access parts of the API concerning
-// the app itself such as getting the list of installations. (Need to
-// authenticate as an app as part of authenticating as an app
-// installation.)
-//
-// 2) Authenticating as an app installation (on a user or organisation
-// account). Used to access resources belonging to the user/repository
-// or organisation the app is installed in.
-//
-// 3) Authenticating as a user. Used to perform actions as the user.
-//
-// We need to authenticate as an app installation (2).
-//
-// How to authenticate as an app installation
-//
-// Reference:
-// https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation
-//
-// The final authentication token we need is an installation access token
-// (IAT), valid for one hour, which we will pass in the `Authentication`
-// header of our Github API requests:
-//
-// Authorization: Bearer <INSTALLATION_ACCESS_TOKEN>
-//
-// To generate an IAT:
-//
-// - Generate a JSON Web Token (JWT)
-//
-// The inputs are (one of) the application's private key(s) and the
-// application ID, which goes into the "issuer" JWT field. Also the
-// token's issued time and expiration time (10 minutes max).
-//
-// The best library for us seems to be libjwt at
-// https://github.com/benmcollins/libjwt which is also widely packaged
-// (most Linux distros and Homebrew).
-//
-// Doing it ourselves:
-//
-// Github requires the RS256 algorithm, which is RSA signing using
-// SHA256.
-//
-// The message consists of a base64url-encoded JSON header and
-// payload. (base64url differs from base64 by having different 62nd and
-// 63rd alphabet characters (- and _ instead of ~ and . to make it
-// filesystem-safe) and no padding because the padding character '=' is
-// unsafe in URIs.)
-//
-// Header:
-//
-// {
-// "alg": "RS256",
-// "typ": "JWT"
-// }
-//
-// Payload:
-//
-// {
-// "iat": 1234567,
-// "exp": 1234577,
-// "iss": "<APP_ID>"
-// }
-//
-// Where:
-// iat := Issued At (NumericDate)
-// exp := Expires At (NumericDate)
-// iss := Issuer
-//
-// Signature:
-//
-// RSA_SHA256(PKEY, base64url($header) + "." + base64url($payload))
-//
-// JWT:
-//
-// base64url($header) + "." +
-// base64url($payload) + "." +
-// base64url($signature)
-//
-// - Get the installation ID. This will be included in the webhook request
-// in our case
-//
-// - Send a POST to /app/installations/<INSTALLATION_ID>/access_tokens
-// which includes the JWT (`Authorization: Bearer <JWT>`). The response
-// will include the IAT. Can pass the name of the repository included in
-// the webhook request to restrict access, otherwise we get access to all
-// repos covered by the installation if installed on an organisation for
-// example.
-
using namespace std;
using namespace butl;
using namespace web;
using namespace brep::cli;
-brep::ci_github::ci_github (const ci_github& r)
- : handler (r),
- options_ (r.initialized_ ? r.options_ : nullptr)
+namespace brep
{
-}
+ using namespace gh;
-void brep::ci_github::
-init (scanner& s)
-{
- options_ = make_shared<options::ci> (
- s, unknown_mode::fail, unknown_mode::fail);
-}
+ ci_github::
+ ci_github (const ci_github& r)
+ : handler (r),
+ options_ (r.initialized_ ? r.options_ : nullptr)
+ {
+ }
-// The "check_suite" object within a check_quite webhook request.
-//
-struct check_suite
-{
- uint64_t id;
- string head_branch;
- string head_sha;
- string before;
- string after;
+ void ci_github::
+ init (scanner& s)
+ {
+ options_ = make_shared<options::ci_github> (
+ s, unknown_mode::fail, unknown_mode::fail);
+ }
- explicit
- check_suite (json::parser&);
+ bool ci_github::
+ handle (request& rq, response&)
+ {
+ using namespace bpkg;
- check_suite () = default;
-};
+ HANDLER_DIAG;
-struct repository
-{
- string name;
- string full_name;
- string default_branch;
+ // @@ TODO: disable service if HMAC is not specified in config.
+ //
+ if (false)
+ throw invalid_request (404, "CI request submission disabled");
- explicit repository (json::parser&);
+ // Process headers.
+ //
+ // @@ TMP Shouldn't we also error<< in some of these header problem cases?
+ //
+ // @@ TMP From GitHub docs: "You can create webhooks that subscribe to the
+ // events listed on this page."
+ //
+ // So it seems appropriate to generally use the term "event" (which
+ // we already do for the most part), and "webhook event" only when
+ // more context would be useful?
+ //
+ string event; // Webhook event.
+ {
+ bool content_type (false);
- repository () = default;
-};
+ for (const name_value& h: rq.headers ())
+ {
+ // This event's UUID.
+ //
+ if (icasecmp (h.name, "x-github-delivery") == 0)
+ {
+ // @@ TODO Check that delivery UUID has not been received before
+ // (replay attack).
+ }
+ else if (icasecmp (h.name, "content-type") == 0)
+ {
+ if (!h.value)
+ throw invalid_request (400, "missing content-type value");
-struct check_suite_event
-{
- string action;
- ::check_suite check_suite;
- ::repository repository;
+ if (icasecmp (*h.value, "application/json") != 0)
+ {
+ throw invalid_request (400,
+ "invalid content-type value: '" + *h.value +
+ '\'');
+ }
- explicit check_suite_event (json::parser&);
+ content_type = true;
+ }
+ // The webhook event.
+ //
+ else if (icasecmp (h.name, "x-github-event") == 0)
+ {
+ if (!h.value)
+ throw invalid_request (400, "missing x-github-event value");
- check_suite_event () = default;
-};
+ event = *h.value;
+ }
+ }
-static ostream&
-operator<< (ostream&, const check_suite&);
+ if (!content_type)
+ throw invalid_request (400, "missing content-type header");
-static ostream&
-operator<< (ostream&, const repository&);
+ if (event.empty ())
+ throw invalid_request (400, "missing x-github-event header");
+ }
-static ostream&
-operator<< (ostream&, const check_suite_event&);
+ // There is a webhook event (specified in the x-github-event header) and
+ // each event contains a bunch of actions (specified in the JSON request
+ // body).
+ //
+ // Note: "GitHub continues to add new event types and new actions to
+ // existing event types." As a result we ignore known actions that we are
+ // not interested in and log and ignore unknown actions. The thinking here
+ // is that we want be "notified" of new actions at which point we can decide
+ // whether to ignore them or to handle.
+ //
+ if (event == "check_suite")
+ {
+ check_suite_event cs;
+ try
+ {
+ json::parser p (rq.content (64 * 1024), "check_suite event");
-bool brep::ci_github::
-handle (request& rq, response& rs)
-{
- using namespace bpkg;
+ cs = check_suite_event (p);
+ }
+ catch (const json::invalid_json_input& e)
+ {
+ string m ("malformed JSON in " + e.name + " request body");
- HANDLER_DIAG;
+ error << m << ", line: " << e.line << ", column: " << e.column
+ << ", byte offset: " << e.position << ", error: " << e;
- // @@ TODO
- if (false)
- throw invalid_request (404, "CI request submission disabled");
+ throw invalid_request (400, move (m));
+ }
- // Process headers.
+ if (cs.action == "requested")
+ {
+ return handle_check_suite_request (move (cs));
+ }
+ else if (cs.action == "rerequested")
+ {
+ // Someone manually requested to re-run the check runs in this check
+ // suite. Treat as a new request.
+ //
+ return handle_check_suite_request (move (cs));
+ }
+ else if (cs.action == "completed")
+ {
+ // GitHub thinks that "all the check runs in this check suite have
+ // completed and a conclusion is available". Looks like this one we
+ // ignore?
+ //
+ // @@ TODO What if our bookkeeping says otherwise? See conclusion
+ // field which includes timedout. Need to come back to this once
+ // have the "happy path" implemented.
+ //
+ return true;
+ }
+ else
+ {
+ // Ignore unknown actions by sending a 200 response with empty body
+ // but also log as an error since we want to notice new actions.
+ //
+ error << "unknown action '" << cs.action << "' in check_suite event";
+
+ return true;
+ }
+ }
+ else if (event == "pull_request")
+ {
+ // @@ TODO
+
+ throw invalid_request (501, "pull request events not implemented yet");
+ }
+ else
+ {
+ // Log to investigate.
+ //
+ error << "unexpected event '" << event << "'";
+
+ throw invalid_request (400, "unexpected event: '" + event + "'");
+ }
+ }
+
+ bool ci_github::
+ handle_check_suite_request (check_suite_event cs) const
+ {
+ cout << "<check_suite event>" << endl << cs << endl;
+
+ installation_access_token iat (
+ obtain_installation_access_token (cs.installation.id, generate_jwt ()));
+
+ cout << endl << "<installation_access_token>" << endl << iat << endl;
+
+ return true;
+ }
+
+ // Send a POST request to the GitHub API endpoint `ep`, parse GitHub's JSON
+ // response into `rs` (only for 200 codes), and return the HTTP status code.
+ //
+ // The endpoint `ep` should not have a leading slash.
+ //
+ // Pass additional HTTP headers in `hdrs`. For example:
+ //
+ // "HeaderName: header value"
+ //
+ // Throw invalid_argument if unable to parse the response headers,
+ // invalid_json_input (derived from invalid_argument) if unable to parse the
+ // response body, and system_error in other cases.
//
- string event;
+ template<typename T>
+ static uint16_t
+ github_post (T& rs, const string& ep, const strings& hdrs)
{
- bool content_type (false);
+ // Convert the header values to curl header option/value pairs.
+ //
+ strings hdr_opts;
- for (const name_value& h: rq.headers ())
+ for (const string& h: hdrs)
{
- if (icasecmp (h.name, "x-github-delivery") == 0)
- {
- // @@ TODO Check that delivery UUID has not been received before
- // (replay attack).
- }
- else if (icasecmp (h.name, "content-type") == 0)
+ hdr_opts.push_back ("--header");
+ hdr_opts.push_back (h);
+ }
+
+ // Run curl.
+ //
+ try
+ {
+ // Pass --include to print the HTTP status line (followed by the response
+ // headers) so that we can get the response status code.
+ //
+ // Suppress the --fail option which causes curl to exit with status 22
+ // in case of an error HTTP response status code (>= 400) otherwise we
+ // can't get the status code.
+ //
+ // Note that butl::curl also adds --location to make curl follow redirects
+ // (which is recommended by GitHub).
+ //
+ // The API version `2022-11-28` is the only one currently supported. If
+ // the X-GitHub-Api-Version header is not passed this version will be
+ // chosen by default.
+ //
+ fdpipe errp (fdopen_pipe ()); // stderr pipe.
+
+ curl c (nullfd,
+ path ("-"), // Write response to curl::in.
+ process::pipe (errp.in.get (), move (errp.out)),
+ curl::post,
+ curl::flags::no_fail,
+ "https://api.github.com/" + ep,
+ "--no-fail", // Don't fail if response status code >= 400.
+ "--include", // Output response headers for status code.
+ "--header", "Accept: application/vnd.github+json",
+ "--header", "X-GitHub-Api-Version: 2022-11-28",
+ move (hdr_opts));
+
+ ifdstream err (move (errp.in));
+
+ // Parse the HTTP response.
+ //
+ int sc; // Status code.
+ try
{
- if (!h.value)
- throw invalid_request (400, "missing content-type value");
+ // Note: re-open in/out so that they get automatically closed on
+ // exception.
+ //
+ ifdstream in (c.in.release (), fdstream_mode::skip);
- if (icasecmp (*h.value, "application/json") != 0)
+ sc = curl::read_http_status (in).code; // May throw invalid_argument.
+
+ // Parse the response body if the status code is in the 200 range.
+ //
+ if (sc >= 200 && sc < 300)
{
- throw invalid_request (400,
- "invalid content-type value: '" + *h.value +
- '\'');
+ // Use endpoint name as input name (useful to have it propagated
+ // in exceptions).
+ //
+ json::parser p (in, ep /* name */);
+ rs = T (p);
}
- content_type = true;
+ in.close ();
+ }
+ catch (const io_error& e)
+ {
+ // If the process exits with non-zero status, assume the IO error is due
+ // to that and fall through.
+ //
+ if (c.wait ())
+ {
+ throw_generic_error (
+ e.code ().value (),
+ (string ("unable to read curl stdout: ") + e.what ()).c_str ());
+ }
}
- else if (icasecmp (h.name, "x-github-event") == 0)
+ catch (const json::invalid_json_input&)
{
- if (!h.value)
- throw invalid_request (400, "missing x-github-event value");
+ // If the process exits with non-zero status, assume the JSON error is
+ // due to that and fall through.
+ //
+ if (c.wait ())
+ throw;
+ }
- event = *h.value;
+ if (!c.wait ())
+ {
+ string et (err.read_text ());
+ throw_generic_error (EINVAL,
+ ("non-zero curl exit status: " + et).c_str ());
}
+
+ err.close ();
+
+ return sc;
+ }
+ catch (const process_error& e)
+ {
+ throw_generic_error (
+ e.code ().value (),
+ (string ("unable to execute curl:") + e.what ()).c_str ());
+ }
+ catch (const io_error& e)
+ {
+ // Unable to read diagnostics from stderr.
+ //
+ throw_generic_error (
+ e.code ().value (),
+ (string ("unable to read curl stderr : ") + e.what ()).c_str ());
}
+ }
- if (!content_type)
- throw invalid_request (400, "missing content-type header");
+ string ci_github::
+ generate_jwt () const
+ {
+ string jwt;
+ try
+ {
+ // Set token's "issued at" time 60 seconds in the past to combat clock
+ // drift (as recommended by GitHub).
+ //
+ jwt = brep::generate_jwt (
+ *options_,
+ options_->ci_github_app_private_key (),
+ to_string (options_->ci_github_app_id ()),
+ chrono::seconds (options_->ci_github_jwt_validity_period ()),
+ chrono::seconds (60));
+
+ cout << "JWT: " << jwt << endl;
+ }
+ catch (const system_error& e)
+ {
+ HANDLER_DIAG;
- if (event.empty ())
- throw invalid_request (400, "missing x-github-event header");
+ fail << "unable to generate JWT (errno=" << e.code () << "): " << e;
+ }
+
+ return jwt;
}
- // There is an event (specified in the x-github-event header) and each event
- // contains a bunch of actions (specified in the JSON request body).
+ // There are three types of GitHub API authentication:
+ //
+ // 1) Authenticating as an app. Used to access parts of the API concerning
+ // the app itself such as getting the list of installations. (Need to
+ // authenticate as an app as part of authenticating as an app
+ // installation.)
+ //
+ // 2) Authenticating as an app installation (on a user or organisation
+ // account). Used to access resources belonging to the user/repository
+ // or organisation the app is installed in.
+ //
+ // 3) Authenticating as a user. Used to perform actions as the user.
+ //
+ // We need to authenticate as an app installation (2).
+ //
+ // How to authenticate as an app installation
+ //
+ // Reference:
+ // https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation
+ //
+ // The final authentication token we need is an installation access token
+ // (IAT), valid for one hour, which we will pass in the `Authentication`
+ // header of our Github API requests:
//
- // Note: "GitHub continues to add new event types and new actions to
- // existing event types." As a result we ignore known actions that we are
- // not interested in and log and ignore unknown actions. The thinking here
- // is that we want be "notified" of new actions at which point we can decide
- // whether to ignore them or to handle.
+ // Authorization: Bearer <INSTALLATION_ACCESS_TOKEN>
//
- try
+ // To generate an IAT:
+ //
+ // - Generate a JSON Web Token (JWT)
+ //
+ // - Get the installation ID. This will be included in the webhook request
+ // in our case
+ //
+ // - Send a POST to /app/installations/<INSTALLATION_ID>/access_tokens which
+ // includes the JWT (`Authorization: Bearer <JWT>`). The response will
+ // include the IAT. Can pass the name of the repository included in the
+ // webhook request to restrict access, otherwise we get access to all
+ // repos covered by the installation if installed on an organisation for
+ // example.
+ //
+ installation_access_token ci_github::
+ obtain_installation_access_token (uint64_t iid, string jwt) const
{
- if (event == "check_suite")
+ HANDLER_DIAG;
+
+ installation_access_token iat;
+ try
{
- json::parser p (rq.content (64 * 1024), "check_suite webhook");
+ // API endpoint.
+ //
+ string ep ("app/installations/" + to_string (iid) + "/access_tokens");
- check_suite_event cs (p);
+ int sc (github_post (iat, ep, strings {"Authorization: Bearer " + jwt}));
- // @@ TODO: log and ignore unknown.
+ // Possible response status codes from the access_tokens endpoint:
//
- if (cs.action == "requested")
- {
- }
- else if (cs.action == "rerequested")
- {
- // Someone manually requested to re-run the check runs in this check
- // suite.
- }
- else if (cs.action == "completed")
+ // 201 Created
+ // 401 Requires authentication
+ // 403 Forbidden
+ // 404 Resource not found
+ // 422 Validation failed, or the endpoint has been spammed.
+ //
+ // Note that the payloads of non-201 status codes are undocumented.
+ //
+ if (sc != 201)
{
- // GitHub thinks that "all the check runs in this check suite have
- // completed and a conclusion is available". Looks like this one we
- // ignore?
+ fail << "unable to get installation access token: "
+ << "error HTTP response status " << sc;
}
- else
- throw invalid_request (400, "unsupported action: " + cs.action);
-
- cout << "<check_suite webhook>" << endl << cs << endl;
-
- return true;
}
- else if (event == "pull_request")
+ catch (const json::invalid_json_input& e)
{
- throw invalid_request (501, "pull request events not implemented yet");
+ // Note: e.name is the GitHub API endpoint.
+ //
+ fail << "malformed JSON in response from " << e.name << ", line: "
+ << e.line << ", column: " << e.column << ", byte offset: "
+ << e.position << ", error: " << e;
}
- else
- throw invalid_request (400, "unexpected event: '" + event + "'");
+ catch (const invalid_argument& e)
+ {
+ fail << "malformed header(s) in response: " << e;
+ }
+ catch (const system_error& e)
+ {
+ fail << "unable to get installation access token (errno=" << e.code ()
+ << "): " << e.what ();
+ }
+
+ return iat;
}
- catch (const json::invalid_json_input& e)
+
+ // The rest is GitHub request/response type parsing and printing.
+ //
+ using event = json::event;
+
+ // 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)
{
- // @@ TODO: should we write more detailed diagnostics to log? Maybe we
- // should do this for all unsuccessful calls to respond().
- //
- // @@ TMP These exceptions end up in the apache error log.
+ 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 i (false), hb (false), hs (false), bf (false), at (false);
+
+ // Skip unknown/uninteresting members.
//
- throw invalid_request (400, "malformed JSON in request body");
+ 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 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 (!i) missing_member (p, "check_suite", "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");
}
-}
-using event = json::event;
+ ostream&
+ gh::operator<< (ostream& os, const check_suite& cs)
+ {
+ os << "id: " << cs.id << endl
+ << "head_branch: " << cs.head_branch << endl
+ << "head_sha: " << cs.head_sha << endl
+ << "before: " << cs.before << endl
+ << "after: " << cs.after << endl;
-// check_suite
-//
-check_suite::check_suite (json::parser& p)
-{
- p.next_expect (event::begin_object);
+ return os;
+ }
- // Skip unknown/uninteresting members.
+ // repository
//
- while (p.next_expect (event::name, event::end_object))
+ gh::repository::
+ repository (json::parser& p)
{
- const string& n (p.name ());
-
- if (n == "id") id = p.next_expect_number<uint64_t> ();
- else if (n == "head_branch") head_branch = p.next_expect_string ();
- else if (n == "head_sha") head_sha = p.next_expect_string ();
- else if (n == "before") before = p.next_expect_string ();
- else if (n == "after") after = p.next_expect_string ();
- else p.next_expect_value_skip ();
- }
-}
+ p.next_expect (event::begin_object);
-static ostream&
-operator<< (ostream& os, const check_suite& cs)
-{
- os << "id: " << cs.id << endl
- << "head_branch: " << cs.head_branch << endl
- << "head_sha: " << cs.head_sha << endl
- << "before: " << cs.before << endl
- << "after: " << cs.after << endl;
+ bool nm (false), fn (false), db (false);
- return os;
-}
+ // 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;
+ };
-// repository
-//
-repository::repository (json::parser& p)
-{
- p.next_expect (event::begin_object);
+ 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 p.next_expect_value_skip ();
+ }
+
+ if (!nm) missing_member (p, "repository", "name");
+ if (!fn) missing_member (p, "repository", "full_name");
+ if (!db) missing_member (p, "repository", "default_branch");
+ }
+
+ ostream&
+ gh::operator<< (ostream& os, const repository& rep)
+ {
+ os << "name: " << rep.name << endl
+ << "full_name: " << rep.full_name << endl
+ << "default_branch: " << rep.default_branch << endl;
+
+ return os;
+ }
- // Skip unknown/uninteresting members.
+ // installation
//
- while (p.next_expect (event::name, event::end_object))
+ gh::installation::
+ installation (json::parser& p)
{
- const string& n (p.name ());
+ 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 (n == "name") name = p.next_expect_string ();
- else if (n == "full_name") full_name = p.next_expect_string ();
- else if (n == "default_branch") default_branch = p.next_expect_string ();
- else p.next_expect_value_skip ();
+ if (!i) missing_member (p, "installation", "id");
}
-}
-static ostream&
-operator<< (ostream& os, const repository& rep)
-{
- os << "name: " << rep.name << endl
- << "full_name: " << rep.full_name << endl
- << "default_branch: " << rep.default_branch << endl;
+ ostream&
+ gh::operator<< (ostream& os, const installation& i)
+ {
+ os << "id: " << i.id << endl;
- return os;
-}
+ return os;
+ }
-// check_suite_event
-//
-check_suite_event::check_suite_event (json::parser& p)
-{
- p.next_expect (event::begin_object);
+ // 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 << endl;
+ os << "<check_suite>" << endl << cs.check_suite;
+ os << "<repository>" << endl << cs.repository;
+ os << "<installation>" << endl << cs.installation;
+
+ return os;
+ }
- // Skip unknown/uninteresting members.
+ // installation_access_token
+ //
+ // Example JSON:
//
- while (p.next_expect (event::name, event::end_object))
+ // {
+ // "token": "ghs_Py7TPcsmsITeVCAWeVtD8RQs8eSos71O5Nzp",
+ // "expires_at": "2024-02-15T16:16:38Z",
+ // ...
+ // }
+ //
+ gh::installation_access_token::
+ installation_access_token (json::parser& p)
{
- const string& n (p.name ());
+ 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 (n == "action") action = p.next_expect_string ();
- else if (n == "check_suite") check_suite = ::check_suite (p);
- else if (n == "repository") repository = ::repository (p);
- else p.next_expect_value_skip ();
+ if (c (tk, "token")) token = p.next_expect_string ();
+ else if (c (ea, "expires_at"))
+ {
+ const string& s (p.next_expect_string ());
+ expires_at = from_string (s.c_str (), "%Y-%m-%dT%TZ", false /* local */);
+ }
+ 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");
}
-}
-static ostream&
-operator<< (ostream& os, const check_suite_event& cs)
-{
- os << "action: " << cs.action << endl;
- os << "<check_suite>" << endl << cs.check_suite;
- os << "<repository>" << endl << cs.repository << endl;
+ ostream&
+ gh::operator<< (ostream& os, const installation_access_token& t)
+ {
+ os << "token: " << t.token << endl;
+ os << "expires_at: ";
+ butl::operator<< (os, t.expires_at) << endl;
- return os;
+ return os;
+ }
}