diff options
Diffstat (limited to 'mod/mod-ci-github-post.hxx')
-rw-r--r-- | mod/mod-ci-github-post.hxx | 161 |
1 files changed, 161 insertions, 0 deletions
diff --git a/mod/mod-ci-github-post.hxx b/mod/mod-ci-github-post.hxx new file mode 100644 index 0000000..d278ae0 --- /dev/null +++ b/mod/mod-ci-github-post.hxx @@ -0,0 +1,161 @@ +// file : mod/mod-ci-github-post.hxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#ifndef MOD_MOD_CI_GITHUB_POST_HXX +#define MOD_MOD_CI_GITHUB_POST_HXX + +#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 + // 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. + // + template <typename T> + uint16_t + github_post (T& rs, + const string& ep, + const strings& hdrs, + const string& body = "") + { + using namespace butl; + + // Convert the header values to curl header option/value pairs. + // + strings hdr_opts; + + for (const string& h: hdrs) + { + 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 (path ("-"), // Read input from curl::out. + 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. + // + uint16_t sc; // Status code. + try + { + // Note: re-open in/out so that they get automatically closed on + // exception. + // + ifdstream in (c.in.release (), fdstream_mode::skip); + ofdstream out (c.out.release ()); + + // Write request body to out. + // + if (!body.empty ()) + out << body; + out.close (); + + 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) + { + // Use endpoint name as input name (useful to have it propagated + // in exceptions). + // + json::parser p (in, ep /* name */); + rs = T (p); + } + + 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 ()); + } + } + catch (const json::invalid_json_input&) + { + // If the process exits with non-zero status, assume the JSON error is + // due to that and fall through. + // + if (c.wait ()) + throw; + } + + 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 ()); + } + } +} + +#endif // MOD_MOD_CI_GITHUB_POST_HXX |