aboutsummaryrefslogtreecommitdiff
path: root/mod/mod-ci-github-post.hxx
diff options
context:
space:
mode:
Diffstat (limited to 'mod/mod-ci-github-post.hxx')
-rw-r--r--mod/mod-ci-github-post.hxx161
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