aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xlibbrep/odb.sh7
-rw-r--r--libbrep/version.hxx.in4
-rw-r--r--mod/mod-ci-github-gh.cxx31
-rw-r--r--mod/mod-ci-github-gh.hxx9
-rw-r--r--mod/mod-ci-github-gq.cxx272
-rw-r--r--mod/mod-ci-github-gq.hxx48
-rw-r--r--mod/mod-ci-github-service-data.cxx15
-rw-r--r--mod/mod-ci-github-service-data.hxx10
-rw-r--r--mod/mod-ci-github.cxx412
-rw-r--r--mod/mod-ci-github.hxx10
-rw-r--r--mod/module.cli8
-rw-r--r--repositories.manifest6
12 files changed, 561 insertions, 271 deletions
diff --git a/libbrep/odb.sh b/libbrep/odb.sh
index 608ca41..89dc5be 100755
--- a/libbrep/odb.sh
+++ b/libbrep/odb.sh
@@ -16,6 +16,8 @@ if test -d ../.bdep; then
sed -r -ne 's#^(@[^ ]+ )?([^ ]+)/ .*default.*$#\2#p')"
fi
+ # Note: here we use libodb*, not libbutl-odb.
+ #
inc+=("-I$(echo "$cfg"/libodb-[1-9]*/)")
inc+=("-I$(echo "$cfg"/libodb-pgsql-[1-9]*/)")
@@ -33,6 +35,11 @@ sed -r -ne 's#^(@[^ ]+ )?([^ ]+)/ .*default.*$#\2#p')"
else
+ # Feels like this case should not be necessary (unlike in bpkg/bdep).
+ #
+ echo "not bdep-initialized" 1>&2
+ exit 1
+
inc+=("-I$HOME/work/odb/builds/default/libodb-pgsql-default")
inc+=("-I$HOME/work/odb/libodb-pgsql")
diff --git a/libbrep/version.hxx.in b/libbrep/version.hxx.in
index 3ac3752..9adb5ab 100644
--- a/libbrep/version.hxx.in
+++ b/libbrep/version.hxx.in
@@ -49,11 +49,11 @@ $libbbot.check(LIBBBOT_VERSION, LIBBBOT_SNAPSHOT)$
#include <odb/version.hxx>
-$libodb.check(LIBODB_VERSION, LIBODB_SNAPSHOT)$
+$libodb.check(LIBODB_VERSION_FULL, LIBODB_SNAPSHOT)$
#include <odb/pgsql/version.hxx>
-$libodb_pgsql.check(LIBODB_PGSQL_VERSION, LIBODB_PGSQL_SNAPSHOT)$
+$libodb_pgsql.check(LIBODB_PGSQL_VERSION_FULL, LIBODB_PGSQL_SNAPSHOT)$
// For now these are the same.
//
diff --git a/mod/mod-ci-github-gh.cxx b/mod/mod-ci-github-gh.cxx
index 5c4809c..7007db8 100644
--- a/mod/mod-ci-github-gh.cxx
+++ b/mod/mod-ci-github-gh.cxx
@@ -20,10 +20,9 @@ namespace brep
case build_state::queued: return "QUEUED";
case build_state::building: return "IN_PROGRESS";
case build_state::built: return "COMPLETED";
- default:
- throw invalid_argument ("invalid build_state value: " +
- to_string (static_cast<int> (st)));
}
+
+ return ""; // Should never reach.
}
// Return the build_state corresponding to a GitHub check run status
@@ -36,19 +35,21 @@ namespace brep
else if (s == "IN_PROGRESS") return build_state::building;
else if (s == "COMPLETED") return build_state::built;
else
- throw invalid_argument ("invalid GitHub check run status: '" + s +
+ throw invalid_argument ("unexpected GitHub check run status: '" + s +
'\'');
}
string
- gh_to_conclusion (result_status rs)
+ gh_to_conclusion (result_status rs, bool warning_success)
{
switch (rs)
{
case result_status::success:
- case result_status::warning:
return "SUCCESS";
+ case result_status::warning:
+ return warning_success ? "SUCCESS" : "FAILURE";
+
case result_status::error:
case result_status::abort:
case result_status::abnormal:
@@ -60,13 +61,9 @@ namespace brep
case result_status::interrupt:
throw invalid_argument ("unexpected result_status value: " +
to_string (rs));
-
- // Invalid value.
- //
- default:
- throw invalid_argument ("invalid result_status value: " +
- to_string (static_cast<int> (rs)));
}
+
+ return ""; // Should never reach.
}
string
@@ -121,7 +118,7 @@ namespace brep
{
p.next_expect (event::begin_object);
- bool ni (false), hb (false), hs (false), bf (false), at (false);
+ bool ni (false), hb (false), hs (false);
// Skip unknown/uninteresting members.
//
@@ -135,16 +132,12 @@ namespace brep
if (c (ni, "node_id")) node_id = p.next_expect_string ();
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 (!ni) missing_member (p, "gh_check_suite", "node_id");
if (!hb) missing_member (p, "gh_check_suite", "head_branch");
if (!hs) missing_member (p, "gh_check_suite", "head_sha");
- if (!bf) missing_member (p, "gh_check_suite", "before");
- if (!at) missing_member (p, "gh_check_suite", "after");
}
ostream&
@@ -152,9 +145,7 @@ namespace brep
{
os << "node_id: " << cs.node_id
<< ", head_branch: " << cs.head_branch
- << ", head_sha: " << cs.head_sha
- << ", before: " << cs.before
- << ", after: " << cs.after;
+ << ", head_sha: " << cs.head_sha;
return os;
}
diff --git a/mod/mod-ci-github-gh.hxx b/mod/mod-ci-github-gh.hxx
index 45b5359..b3da197 100644
--- a/mod/mod-ci-github-gh.hxx
+++ b/mod/mod-ci-github-gh.hxx
@@ -42,15 +42,11 @@ namespace brep
// The "check_suite" object within a check_suite webhook event request.
//
- // @@ TODO Remove unused fields.
- //
struct gh_check_suite
{
string node_id;
string head_branch;
string head_sha;
- string before;
- string after;
explicit
gh_check_suite (json::parser&);
@@ -81,8 +77,11 @@ namespace brep
build_state
gh_from_status (const string&);
+ // If warning_success is true, then map result_status::warning to SUCCESS
+ // and to FAILURE otherwise.
+ //
string
- gh_to_conclusion (result_status);
+ gh_to_conclusion (result_status, bool warning_success);
// Create a check_run name from a build. If the second argument is not
// NULL, return an abbreviated id if possible.
diff --git a/mod/mod-ci-github-gq.cxx b/mod/mod-ci-github-gq.cxx
index 4e13199..bea9a7f 100644
--- a/mod/mod-ci-github-gq.cxx
+++ b/mod/mod-ci-github-gq.cxx
@@ -35,6 +35,8 @@ namespace brep
// Throw runtime_error if the response indicated errors and
// invalid_json_input if the GitHub response contained invalid JSON.
//
+ // The parse_data function should not throw anything but invalid_json_input.
+ //
// The response format is defined in the GraphQL spec:
// https://spec.graphql.org/October2021/#sec-Response.
//
@@ -48,10 +50,21 @@ namespace brep
// The contents of `data`, including its opening and closing braces, are
// parsed by the `parse_data` function.
//
- // @@ TODO: specify what parse_data may throw (probably only
- // invalid_json_input).
+ // If the `errors` field is present in the response, error(s) occurred
+ // before or during execution of the operation.
+ //
+ // If the `data` field is not present the errors are request errors which
+ // occur before execution and are typically the client's fault.
+ //
+ // If the `data` field is also present in the response the errors are field
+ // errors which occur during execution and are typically the GraphQL
+ // endpoint's fault, and some fields in `data` that should not be are likely
+ // to be null.
//
- // @@ TODO data comes before errors in GitHub's responses.
+ // Although the spec recommends that the errors field (if present) should
+ // come before the data field, GitHub places data before errors. Therefore
+ // we need to check that the errors field is not present before parsing the
+ // data field as it might contain nulls if errors is present.
//
static void
gq_parse_response (json::parser& p,
@@ -61,13 +74,16 @@ namespace brep
// True if the data/errors fields are present.
//
- // Although the spec merely recommends that the `errors` field, if
- // present, comes before the `data` field, assume it always does because
- // letting the client parse data in the presence of field errors
- // (unexpected nulls) would not make sense.
- //
bool dat (false), err (false);
+ // Because the errors field is likely to come before the data field,
+ // serialize data to a stringstream and only parse it later once we're
+ // sure there are no errors.
+ //
+ ostringstream data; // The value of the data field.
+
+ // @@@ Use iostringstream for both output and input.
+
p.next_expect (event::begin_object);
while (p.next_expect (event::name, event::end_object))
@@ -76,13 +92,27 @@ namespace brep
{
dat = true;
- // Currently we're not handling fields that are null due to field
- // errors (see below for details) so don't parse any further.
+ // Serialize the data field to a string.
+ //
+ // Note that the JSON payload sent by GitHub is not pretty-printed so
+ // there is no need to worry about that.
//
- if (err)
- break;
+ json::stream_serializer s (data, 0 /* indentation */);
- parse_data (p);
+ try
+ {
+ for (event e: p)
+ {
+ if (!s.next (e, p.data ()))
+ break; // Stop if data object is complete.
+ }
+ }
+ catch (const json::invalid_json_output& e)
+ {
+ throw_json (p,
+ string ("serializer rejected response 'data' field: ") +
+ e.what ());
+ }
}
else if (p.name () == "errors")
{
@@ -107,23 +137,22 @@ namespace brep
}
}
- // If the `errors` field was present in the response, error(s) occurred
- // before or during execution of the operation.
- //
- // If the `data` field was not present the errors are request errors which
- // occur before execution and are typically the client's fault.
- //
- // If the `data` field was also present in the response the errors are
- // field errors which occur during execution and are typically the GraphQL
- // endpoint's fault, and some fields in `data` that should not be are
- // likely to be null.
- //
- if (err)
+ if (!err)
+ {
+ if (!dat)
+ throw runtime_error ("no data received from GraphQL endpoint");
+
+ // Parse the data field now that we know there are no errors.
+ //
+ string d (data.str ());
+ json::parser dp (d, p.input_name);
+
+ parse_data (dp);
+ }
+ else
{
if (dat)
{
- // @@ TODO: Consider parsing partial data?
- //
throw runtime_error ("field error(s) received from GraphQL endpoint; "
"incomplete data received");
}
@@ -154,7 +183,7 @@ namespace brep
// }
// }
//
- // @@ TODO Handle response errors properly.
+ // @@@ TODO Handle response errors properly.
//
static vector<gh_check_run>
gq_parse_response_check_runs (json::parser& p)
@@ -198,11 +227,11 @@ namespace brep
// if unset. Return false and issue diagnostics if the request failed.
//
static bool
- gq_mutate_check_runs (vector<check_run>& crs,
+ gq_mutate_check_runs (const basic_mark& error,
+ vector<check_run>& crs,
const string& iat,
string rq,
- build_state st,
- const basic_mark& error) noexcept
+ build_state st) noexcept
{
vector<gh_check_run> rcrs;
@@ -317,16 +346,20 @@ namespace brep
// Serialize `createCheckRun` mutations for one or more builds to GraphQL.
//
- // The result_status is required if the build_state is built because GitHub
- // does not allow a check run status of completed without a conclusion.
+ // The conclusion argument (`co`) is required if the build_state is built
+ // because GitHub does not allow a check run status of completed without a
+ // conclusion.
+ //
+ // The details URL argument (`du`) can be empty for queued but not for the
+ // other states.
//
static string
- gq_mutation_create_check_runs (
- const string& ri, // Repository ID
- const string& hs, // Head SHA
- const vector<reference_wrapper<const build>>& bs,
- build_state st, optional<result_status> rs,
- const build_queued_hints* bh)
+ gq_mutation_create_check_runs (const string& ri, // Repository ID
+ const string& hs, // Head SHA
+ const string& du, // Details URL.
+ const vector<check_run>& crs,
+ const string& st, // Check run status.
+ optional<gq_built_result> br = nullopt)
{
ostringstream os;
@@ -334,33 +367,36 @@ namespace brep
// Serialize a `createCheckRun` for each build.
//
- for (size_t i (0); i != bs.size (); ++i)
+ for (size_t i (0); i != crs.size (); ++i)
{
- const build& b (bs[i]);
-
string al ("cr" + to_string (i)); // Field alias.
- // Check run name.
- //
- string nm (gh_check_run_name (b, bh));
-
os << gq_name (al) << ":createCheckRun(input: {" << '\n'
- << " name: " << gq_str (nm) << ',' << '\n'
- << " repositoryId: " << gq_str (ri) << ',' << '\n'
- << " headSha: " << gq_str (hs) << ',' << '\n'
- << " status: " << gq_enum (gh_to_status (st));
- if (rs)
+ << " name: " << gq_str (crs[i].name) << '\n'
+ << " repositoryId: " << gq_str (ri) << '\n'
+ << " headSha: " << gq_str (hs) << '\n'
+ << " status: " << gq_enum (st);
+ if (!du.empty ())
+ {
+ os << '\n';
+ os << " detailsUrl: " << gq_str (du);
+ }
+ if (br)
{
- os << ',' << '\n'
- << " conclusion: " << gq_enum (gh_to_conclusion (*rs));
+ os << '\n';
+ os << " conclusion: " << gq_enum (br->conclusion) << '\n'
+ << " output: {" << '\n'
+ << " title: " << gq_str (br->title) << '\n'
+ << " summary: " << gq_str (br->summary) << '\n'
+ << " }";
}
os << "})" << '\n'
// Specify the selection set (fields to be returned).
//
<< "{" << '\n'
<< " checkRun {" << '\n'
- << " id," << '\n'
- << " name," << '\n'
+ << " id" << '\n'
+ << " name" << '\n'
<< " status" << '\n'
<< " }" << '\n'
<< "}" << '\n';
@@ -373,34 +409,51 @@ namespace brep
// Serialize an `updateCheckRun` mutation for one build to GraphQL.
//
- // The result_status is required if the build_state is built because GitHub
- // does not allow updating a check run to completed without a conclusion.
+ // The `co` (conclusion) argument is required if the build_state is built
+ // because GitHub does not allow updating a check run to completed without a
+ // conclusion.
//
static string
- gq_mutation_update_check_run (const string& ri, // Repository ID.
- const string& ni, // Node ID.
- build_state st,
- optional<result_status> rs)
+ gq_mutation_update_check_run (const string& ri, // Repository ID.
+ const string& ni, // Node ID.
+ const string& du, // Details URL.
+ const string& st, // Check run status.
+ optional<timestamp> sa, // Started at.
+ optional<gq_built_result> br)
{
ostringstream os;
os << "mutation {" << '\n'
<< "cr0:updateCheckRun(input: {" << '\n'
- << " checkRunId: " << gq_str (ni) << ',' << '\n'
- << " repositoryId: " << gq_str (ri) << ',' << '\n'
- << " status: " << gq_enum (gh_to_status (st));
- if (rs)
+ << " checkRunId: " << gq_str (ni) << '\n'
+ << " repositoryId: " << gq_str (ri) << '\n'
+ << " status: " << gq_enum (st);
+ if (sa)
+ {
+ os << '\n';
+ os << " startedAt: " << gq_str (gh_to_iso8601 (*sa));
+ }
+ if (!du.empty ())
{
- os << ',' << '\n'
- << " conclusion: " << gq_enum (gh_to_conclusion (*rs));
+ os << '\n';
+ os << " detailsUrl: " << gq_str (du);
+ }
+ if (br)
+ {
+ os << '\n';
+ os << " conclusion: " << gq_enum (br->conclusion) << '\n'
+ << " output: {" << '\n'
+ << " title: " << gq_str (br->title) << '\n'
+ << " summary: " << gq_str (br->summary) << '\n'
+ << " }";
}
os << "})" << '\n'
// Specify the selection set (fields to be returned).
//
<< "{" << '\n'
<< " checkRun {" << '\n'
- << " id," << '\n'
- << " name," << '\n'
+ << " id" << '\n'
+ << " name" << '\n'
<< " status" << '\n'
<< " }" << '\n'
<< "}" << '\n'
@@ -410,51 +463,56 @@ namespace brep
}
bool
- gq_create_check_runs (vector<check_run>& crs,
+ gq_create_check_runs (const basic_mark& error,
+ vector<check_run>& crs,
const string& iat,
const string& rid,
const string& hs,
- const vector<reference_wrapper<const build>>& bs,
- build_state st,
- const build_queued_hints& bh,
- const basic_mark& error)
+ build_state st)
{
// No support for result_status so state cannot be built.
//
assert (st != build_state::built);
- string rq (gq_serialize_request (
- gq_mutation_create_check_runs (rid,
- hs,
- bs,
- st,
- nullopt /* result_status */,
- &bh)));
+ // Empty details URL because it's not available until building.
+ //
+ string rq (
+ gq_serialize_request (
+ gq_mutation_create_check_runs (rid, hs, "", crs, gh_to_status (st))));
- return gq_mutate_check_runs (crs, iat, move (rq), st, error);
+ return gq_mutate_check_runs (error, crs, iat, move (rq), st);
}
bool
- gq_create_check_run (check_run& cr,
+ gq_create_check_run (const basic_mark& error,
+ check_run& cr,
const string& iat,
const string& rid,
const string& hs,
- const build& b,
+ const string& du,
build_state st,
- optional<result_status> rs,
- const build_queued_hints& bh,
- const basic_mark& error)
+ optional<gq_built_result> br)
{
// Must have a result if state is built.
//
- assert (st != build_state::built || rs);
+ assert (st != build_state::built || br);
- string rq (gq_serialize_request (
- gq_mutation_create_check_runs (rid, hs, {b}, st, rs, &bh)));
+ // Must have a details URL because `st` should never be queued.
+ //
+ assert (!du.empty ());
vector<check_run> crs {move (cr)};
- bool r (gq_mutate_check_runs (crs, iat, move (rq), st, error));
+ string rq (
+ gq_serialize_request (
+ gq_mutation_create_check_runs (rid,
+ hs,
+ du,
+ crs,
+ gh_to_status (st),
+ move (br))));
+
+ bool r (gq_mutate_check_runs (error, crs, iat, move (rq), st));
cr = move (crs[0]);
@@ -462,24 +520,42 @@ namespace brep
}
bool
- gq_update_check_run (check_run& cr,
+ gq_update_check_run (const basic_mark& error,
+ check_run& cr,
const string& iat,
const string& rid,
const string& nid,
+ const string& du,
build_state st,
- optional<result_status> rs,
- const basic_mark& error)
+ optional<gq_built_result> br)
{
// Must have a result if state is built.
//
- assert (st != build_state::built || rs);
+ assert (st != build_state::built || br);
+
+ // Must have a details URL for building and built.
+ //
+ assert (!du.empty ());
+
+ // Set `startedAt` to current time if updating to building.
+ //
+ optional<timestamp> sa;
+
+ if (st == build_state::building)
+ sa = system_clock::now ();
- string rq (gq_serialize_request (
- gq_mutation_update_check_run (rid, nid, st, rs)));
+ string rq (
+ gq_serialize_request (
+ gq_mutation_update_check_run (rid,
+ nid,
+ du,
+ gh_to_status (st),
+ sa,
+ move (br))));
vector<check_run> crs {move (cr)};
- bool r (gq_mutate_check_runs (crs, iat, move (rq), st, error));
+ bool r (gq_mutate_check_runs (error, crs, iat, move (rq), st));
cr = move (crs[0]);
diff --git a/mod/mod-ci-github-gq.hxx b/mod/mod-ci-github-gq.hxx
index 9331b2b..8f7a9ca 100644
--- a/mod/mod-ci-github-gq.hxx
+++ b/mod/mod-ci-github-gq.hxx
@@ -23,35 +23,41 @@ namespace brep
// the new states and node IDs. Return false and issue diagnostics if the
// request failed.
//
+ // Note: no details_url yet since there will be no entry in the build result
+ // search page until the task starts building.
+ //
bool
- gq_create_check_runs (vector<check_run>& check_runs,
+ gq_create_check_runs (const basic_mark& error,
+ vector<check_run>& check_runs,
const string& installation_access_token,
const string& repository_id,
const string& head_sha,
- const vector<reference_wrapper<const build>>&,
- build_state,
- const build_queued_hints&,
- const basic_mark& error);
+ build_state);
// Create a new check run on GitHub for a build. Update `cr` with the new
// state and the node ID. Return false and issue diagnostics if the request
// failed.
//
- // The result_status is required if the build_state is built because GitHub
- // does not allow a check run status of `completed` without a conclusion.
- //
- // @@ TODO Support output (title, summary, text).
+ // The gq_built_result is required if the build_state is built because
+ // GitHub does not allow a check run status of `completed` without at least
+ // a conclusion.
//
+ struct gq_built_result
+ {
+ string conclusion;
+ string title;
+ string summary;
+ };
+
bool
- gq_create_check_run (check_run& cr,
+ gq_create_check_run (const basic_mark& error,
+ check_run& cr,
const string& installation_access_token,
const string& repository_id,
const string& head_sha,
- const build&,
+ const string& details_url,
build_state,
- optional<result_status>,
- const build_queued_hints&,
- const basic_mark& error);
+ optional<gq_built_result> = nullopt);
// Update a check run on GitHub.
//
@@ -59,19 +65,19 @@ namespace brep
// with the new state. Return false and issue diagnostics if the request
// failed.
//
- // The result_status is required if the build_state is built because GitHub
- // does not allow updating a check run to `completed` without a conclusion.
- //
- // @@ TODO Support output (title, summary, text).
+ // The gq_built_result is required if the build_state is built because
+ // GitHub does not allow a check run status of `completed` without at least
+ // a conclusion.
//
bool
- gq_update_check_run (check_run& cr,
+ gq_update_check_run (const basic_mark& error,
+ check_run& cr,
const string& installation_access_token,
const string& repository_id,
const string& node_id,
+ const string& details_url,
build_state,
- optional<result_status>,
- const basic_mark& error);
+ optional<gq_built_result> = nullopt);
}
#endif // MOD_MOD_CI_GITHUB_GQ_HXX
diff --git a/mod/mod-ci-github-service-data.cxx b/mod/mod-ci-github-service-data.cxx
index f79550c..83e952b 100644
--- a/mod/mod-ci-github-service-data.cxx
+++ b/mod/mod-ci-github-service-data.cxx
@@ -26,6 +26,8 @@ namespace brep
to_string (version));
}
+ warning_success = p.next_expect_member_boolean<bool> ("warning_success");
+
// Installation access token.
//
p.next_expect_member_object ("installation_access");
@@ -43,6 +45,7 @@ namespace brep
while (p.next_expect (event::begin_object, event::end_array))
{
string bid (p.next_expect_member_string ("build_id"));
+ string nm (p.next_expect_member_string ("name"));
optional<string> nid;
{
@@ -54,7 +57,7 @@ namespace brep
build_state s (to_build_state (p.next_expect_member_string ("state")));
bool ss (p.next_expect_member_boolean<bool> ("state_synced"));
- check_runs.emplace_back (move (bid), move (nid), s, ss);
+ check_runs.emplace_back (move (bid), move (nm), move (nid), s, ss);
p.next_expect (event::end_object);
}
@@ -63,12 +66,14 @@ namespace brep
}
service_data::
- service_data (string iat_tok,
+ service_data (bool ws,
+ string iat_tok,
timestamp iat_ea,
uint64_t iid,
string rid,
string hs)
- : installation_access (move (iat_tok), iat_ea),
+ : warning_success (ws),
+ installation_access (move (iat_tok), iat_ea),
installation_id (iid),
repository_id (move (rid)),
head_sha (move (hs))
@@ -85,6 +90,8 @@ namespace brep
s.member ("version", 1);
+ s.member ("warning_success", warning_success);
+
// Installation access token.
//
s.member_begin_object ("installation_access");
@@ -101,6 +108,7 @@ namespace brep
{
s.begin_object ();
s.member ("build_id", cr.build_id);
+ s.member ("name", cr.name);
s.member_name ("node_id");
if (cr.node_id)
@@ -136,6 +144,7 @@ namespace brep
{
os << "node_id: " << cr.node_id.value_or ("null")
<< ", build_id: " << cr.build_id
+ << ", name: " << cr.name
<< ", state: " << cr.state_string ();
return os;
diff --git a/mod/mod-ci-github-service-data.hxx b/mod/mod-ci-github-service-data.hxx
index d4a47d5..ff1c1a3 100644
--- a/mod/mod-ci-github-service-data.hxx
+++ b/mod/mod-ci-github-service-data.hxx
@@ -25,6 +25,7 @@ namespace brep
struct check_run
{
string build_id; // Full build id.
+ string name; // Potentially shortened build id.
optional<string> node_id; // GitHub id.
build_state state;
@@ -46,12 +47,16 @@ namespace brep
//
uint64_t version = 1;
+ // Check suite settings.
+ //
+ bool warning_success; // See gh_to_conclusion().
+
// Check suite-global data.
//
gh_installation_access_token installation_access;
uint64_t installation_id;
- // @@ TODO Rename to repository_node_id.
+ // @@@ TODO Rename to repository_node_id.
//
string repository_id; // GitHub-internal opaque repository id.
@@ -72,7 +77,8 @@ namespace brep
explicit
service_data (const string& json);
- service_data (string iat_token,
+ service_data (bool warning_success,
+ string iat_token,
timestamp iat_expires_at,
uint64_t installation_id,
string repository_id,
diff --git a/mod/mod-ci-github.cxx b/mod/mod-ci-github.cxx
index 9449f97..5aa4e6d 100644
--- a/mod/mod-ci-github.cxx
+++ b/mod/mod-ci-github.cxx
@@ -5,8 +5,12 @@
#include <libbutl/json/parser.hxx>
+#include <web/xhtml/serialization.hxx>
+#include <web/server/mime-url-encoding.hxx> // mime_url_encode()
+
#include <mod/jwt.hxx>
#include <mod/hmac.hxx>
+#include <mod/build.hxx> // build_log_url()
#include <mod/module-options.hxx>
#include <mod/mod-ci-github-gq.hxx>
@@ -15,13 +19,27 @@
#include <stdexcept>
-// @@ TODO
+// @@ Remaining TODOs
+//
+// - Rerequested checks
+//
+// - check_suite (action: rerequested): received when user re-runs all
+// checks.
+//
+// - check_run (action: rerequested): received when user re-runs a
+// specific check or all failed checks.
//
-// Building CI checks with a GitHub App
-// https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/building-ci-checks-with-a-github-app
+// Will need to extract a few more fields from check_runs, but the layout
+// is very similar to that of check_suite.
+//
+// - Pull requests. Handle
+//
+// - Choose strong webhook secret
+//
+// - Check that delivery UUID has not been received before (replay attack).
//
-// @@ TODO Best practices
+// Resources:
//
// Webhooks:
// https://docs.github.com/en/webhooks/using-webhooks/best-practices-for-using-webhooks
@@ -29,18 +47,11 @@
//
// REST API:
// https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api?apiVersion=2022-11-28
+// @@@ Add link to GraphQL?
//
// Creating an App:
// https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/best-practices-for-creating-a-github-app
-//
-// Use a webhook secret to ensure request is coming from Github. HMAC:
-// https://en.wikipedia.org/wiki/HMAC#Definition. A suitable implementation
-// is provided by OpenSSL.
-// @@ TODO Centralize exception/error handling around calls to
-// github_post(). Currently it's mostly duplicated and there is quite
-// a lot of it.
-//
using namespace std;
using namespace butl;
using namespace web;
@@ -98,14 +109,7 @@ namespace brep
// 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?
+ // @@@ TMP Shouldn't we also error<< in some of these header problem cases?
//
string event; // Webhook event.
string hmac; // Received HMAC.
@@ -178,8 +182,8 @@ namespace brep
// Read the entire request body into a buffer because we need to compute
// an HMAC over it and then parse it as JSON. The alternative of reading
- // from the stream twice works out to be more complicated (see also @@
- // TODO item in web/server/module.hxx).
+ // from the stream twice works out to be more complicated (see also a TODO
+ // item in web/server/module.hxx).
//
string body;
{
@@ -226,6 +230,35 @@ namespace brep
fail << "unable to compute request HMAC: " << e;
}
+ // Process the `warning` webhook request query parameter.
+ //
+ bool warning_success;
+ {
+ const name_values& rps (rq.parameters (1024, true /* url_only */));
+
+ auto i (find_if (rps.begin (), rps.end (),
+ [] (auto&& rp) {return rp.name == "warning";}));
+
+ if (i == rps.end ())
+ throw invalid_request (400,
+ "missing 'warning' webhook query parameter");
+
+ if (!i->value)
+ throw invalid_request (
+ 400, "missing 'warning' webhook query parameter value");
+
+ const string& v (*i->value);
+
+ if (v == "success") warning_success = true;
+ else if (v == "failure") warning_success = false;
+ else
+ {
+ throw invalid_request (
+ 400,
+ "invalid 'warning' webhook query parameter value: '" + v + '\'');
+ }
+ }
+
// 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).
@@ -236,6 +269,11 @@ namespace brep
// is that we want be "notified" of new actions at which point we can decide
// whether to ignore them or to handle.
//
+ // @@ There is also check_run even (re-requested by user, either
+ // individual check run or all the failed check runs).
+ //
+ // @@ There is also the pull_request event. Probably need to handle.
+ //
if (event == "check_suite")
{
gh_check_suite_event cs;
@@ -257,14 +295,16 @@ namespace brep
if (cs.action == "requested")
{
- return handle_check_suite_request (move (cs));
+ return handle_check_suite_request (move (cs), warning_success);
}
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));
+ // @@ This is probably broken.
+ //
+ return handle_check_suite_request (move (cs), warning_success);
}
else if (cs.action == "completed")
{
@@ -272,9 +312,8 @@ namespace brep
// 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.
+ // What if our bookkeeping says otherwise? But then we can't even
+ // access the service data easily here. @@ TODO: maybe/later.
//
return true;
}
@@ -305,7 +344,7 @@ namespace brep
}
bool ci_github::
- handle_check_suite_request (gh_check_suite_event cs)
+ handle_check_suite_request (gh_check_suite_event cs, bool warning_success)
{
HANDLER_DIAG;
@@ -331,25 +370,28 @@ namespace brep
cs.check_suite.head_branch,
repository_type::git);
- string sd (service_data (move (iat->token),
+ string sd (service_data (warning_success,
+ move (iat->token),
iat->expires_at,
cs.installation.id,
move (cs.repository.node_id),
move (cs.check_suite.head_sha))
.json ());
+ // @@ What happens if we call this functions with an already existing
+ // node_id (e.g., replay attack). See the UUID header above.
+ //
optional<start_result> r (
- start (error,
- warn,
- verb_ ? &trace : nullptr,
- tenant_service (move (cs.check_suite.node_id),
- "ci-github",
- move (sd)),
- move (rl),
- vector<package> {},
- nullopt, // client_ip
- nullopt // user_agent
- ));
+ start (error,
+ warn,
+ verb_ ? &trace : nullptr,
+ tenant_service (move (cs.check_suite.node_id),
+ "ci-github",
+ move (sd)),
+ move (rl),
+ vector<package> {},
+ nullopt, /* client_ip */
+ nullopt /* user_agent */));
if (!r)
fail << "unable to submit CI request";
@@ -411,8 +453,8 @@ namespace brep
// building Skip if there is no check run in service data or it's
// not in the queued state, otherwise update.
//
- // built Update if there is check run in service data and its
- // state is not built, otherwise create new.
+ // built Update if there is check run in service data unless its
+ // state is built, otherwise create new.
//
// The rationale for this semantics is as follows: the building
// notification is a "nice to have" and can be skipped if things are not
@@ -501,6 +543,7 @@ namespace brep
bs.push_back (b);
crs.emplace_back (move (bid),
+ gh_check_run_name (b, &hs),
nullopt, /* node_id */
build_state::queued,
false /* state_synced */);
@@ -537,13 +580,11 @@ namespace brep
{
// Create a check_run for each build.
//
- if (gq_create_check_runs (crs,
+ if (gq_create_check_runs (error,
+ crs,
iat->token,
sd.repository_id, sd.head_sha,
- bs,
- build_state::queued,
- hs,
- error))
+ build_state::queued))
{
for (const check_run& cr: crs)
{
@@ -603,7 +644,8 @@ namespace brep
}
function<optional<string> (const tenant_service&)> ci_github::
- build_building (const tenant_service& ts, const build& b,
+ build_building (const tenant_service& ts,
+ const build& b,
const diag_epilogue& log_writer) const noexcept
{
NOTIFICATION_DIAG (log_writer);
@@ -674,13 +716,13 @@ namespace brep
//
if (iat != nullptr)
{
- if (gq_update_check_run (*cr,
+ if (gq_update_check_run (error,
+ *cr,
iat->token,
sd.repository_id,
*cr->node_id,
- build_state::building,
- nullopt, /* result_status */
- error))
+ details_url (b),
+ build_state::building))
{
// Do nothing further if the state was already built on GitHub (note
// that this is based on the above-mentioned special GitHub semantics
@@ -743,7 +785,8 @@ namespace brep
}
function<optional<string> (const tenant_service&)> ci_github::
- build_built (const tenant_service& ts, const build& b,
+ build_built (const tenant_service& ts,
+ const build& b,
const diag_epilogue& log_writer) const noexcept
{
NOTIFICATION_DIAG (log_writer);
@@ -760,32 +803,35 @@ namespace brep
}
check_run cr; // Updated check run.
- string bid (gh_check_run_name (b)); // Full Build ID.
-
- if (check_run* scr = sd.find_check_run (bid))
{
- if (scr->state != build_state::building)
+ string bid (gh_check_run_name (b)); // Full Build ID.
+
+ if (check_run* scr = sd.find_check_run (bid))
{
- warn << "check run " << bid << ": out of order built "
- << "notification; existing state: " << scr->state_string ();
- }
+ if (scr->state != build_state::building)
+ {
+ warn << "check run " << bid << ": out of order built notification; "
+ << "existing state: " << scr->state_string ();
+ }
- // Do nothing if already built.
- //
- if (scr->state == build_state::built)
- return nullptr;
+ // Do nothing if already built (e.g., rebuild).
+ //
+ if (scr->state == build_state::built)
+ return nullptr;
- cr = move (*scr);
- }
- else
- {
- warn << "check run " << bid << ": out of order built "
- << "notification; no check run state in service data";
+ cr = move (*scr);
+ }
+ else
+ {
+ warn << "check run " << bid << ": out of order built notification; "
+ << "no check run state in service data";
- cr.build_id = move (bid);
- }
+ cr.build_id = move (bid);
+ cr.name = cr.build_id;
+ }
- cr.state_synced = false;
+ cr.state_synced = false;
+ }
// Get a new installation access token if the current one has expired.
//
@@ -812,16 +858,143 @@ namespace brep
//
if (iat != nullptr)
{
+ // Return the colored circle corresponding to a result_status.
+ //
+ auto circle = [] (result_status rs) -> string
+ {
+ switch (rs)
+ {
+ case result_status::success: return "\U0001F7E2"; // Green circle.
+ case result_status::warning: return "\U0001F7E0"; // Orange circle.
+ case result_status::error:
+ case result_status::abort:
+ case result_status::abnormal: return "\U0001F534"; // Red circle.
+
+ // Valid values we should never encounter.
+ //
+ case result_status::skip:
+ case result_status::interrupt:
+ throw invalid_argument ("unexpected result_status value: " +
+ to_string (rs));
+ }
+
+ return ""; // Should never reach.
+ };
+
+ // Prepare the check run's summary field (the build information in an
+ // XHTML table).
+ //
+ string sm; // Summary.
+ {
+ using namespace web::xhtml;
+
+ ostringstream os;
+ xml::serializer s (os, "check_run_summary");
+
+ // This hack is required to disable XML element name prefixes (which
+ // GitHub does not like). Note that this adds an xmlns declaration for
+ // the XHTML namespace which for now GitHub appears to ignore. If that
+ // ever becomes a problem, then we should redo this with raw XML
+ // serializer calls.
+ //
+ struct table: element
+ {
+ table (): element ("table") {}
+
+ void
+ start (xml::serializer& s) const override
+ {
+ s.start_element (xmlns, name);
+ s.namespace_decl (xmlns, "");
+ }
+ } TABLE;
+
+ // Serialize a result row (colored circle, result text, log URL) for
+ // an operation and result_status.
+ //
+ auto tr_result = [this, circle, &b] (xml::serializer& s,
+ const string& op,
+ result_status rs)
+ {
+ // The log URL.
+ //
+ string lu (build_log_url (options_->host (),
+ options_->root (),
+ b,
+ op != "result" ? &op : nullptr));
+
+ s << TR
+ << TD << EM << op << ~EM << ~TD
+ << TD
+ << circle (rs) << ' '
+ << CODE << to_string (rs) << ~CODE
+ << " (" << A << HREF << lu << ~HREF << "log" << ~A << ')'
+ << ~TD
+ << ~TR;
+ };
+
+ // Serialize the summary to an XHTML table.
+ //
+ s << TABLE
+ << TBODY;
+
+ tr_result (s, "result", *b.status);
+
+ s << TR
+ << TD << EM << "package" << ~EM << ~TD
+ << TD << CODE << b.package_name << ~CODE << ~TD
+ << ~TR
+ << TR
+ << TD << EM << "version" << ~EM << ~TD
+ << TD << CODE << b.package_version << ~CODE << ~TD
+ << ~TR
+ << TR
+ << TD << EM << "toolchain" << ~EM << ~TD
+ << TD
+ << CODE
+ << b.toolchain_name << '-' << b.toolchain_version.string ()
+ << ~CODE
+ << ~TD
+ << ~TR
+ << TR
+ << TD << EM << "target" << ~EM << ~TD
+ << TD << CODE << b.target.string () << ~CODE << ~TD
+ << ~TR
+ << TR
+ << TD << EM << "target config" << ~EM << ~TD
+ << TD << CODE << b.target_config_name << ~CODE << ~TD
+ << ~TR
+ << TR
+ << TD << EM << "package config" << ~EM << ~TD
+ << TD << CODE << b.package_config_name << ~CODE << ~TD
+ << ~TR;
+
+ for (const operation_result& r: b.results)
+ tr_result (s, r.operation, r.status);
+
+ s << ~TBODY
+ << ~TABLE;
+
+ sm = os.str ();
+ }
+
+ gq_built_result br (gh_to_conclusion (*b.status, sd.warning_success),
+ circle (*b.status) + ' ' +
+ ucase (to_string (*b.status)),
+ move (sm));
+
if (cr.node_id)
{
// Update existing check run to built.
//
- if (gq_update_check_run (cr,
+ if (gq_update_check_run (error,
+ cr,
iat->token,
sd.repository_id,
*cr.node_id,
- build_state::built, b.status,
- error))
+ details_url (b),
+ build_state::built,
+ move (br)))
{
assert (cr.state == build_state::built);
@@ -838,17 +1011,14 @@ namespace brep
// check run to the service data it will create another check run with
// the shortened name which will never get to the built state.
//
- // @@ TMP If build_queued() runs but fails to create we could store
- // the build hints and use them now.
- //
- if (gq_create_check_run (cr,
+ if (gq_create_check_run (error,
+ cr,
iat->token,
sd.repository_id,
sd.head_sha,
- b,
- build_state::built, b.status,
- build_queued_hints (false, false),
- error))
+ details_url (b),
+ build_state::built,
+ move (br)))
{
assert (cr.state == build_state::built);
@@ -861,40 +1031,58 @@ namespace brep
cr = move (cr),
error = move (error),
warn = move (warn)] (const tenant_service& ts) -> optional<string>
- {
- // NOTE: this lambda may be called repeatedly (e.g., due to transaction
- // being aborted) and so should not move out of its captures.
+ {
+ // NOTE: this lambda may be called repeatedly (e.g., due to transaction
+ // being aborted) and so should not move out of its captures.
- service_data sd;
- try
- {
- sd = service_data (*ts.data);
- }
- catch (const invalid_argument& e)
- {
- error << "failed to parse service data: " << e;
- return nullopt;
- }
+ service_data sd;
+ try
+ {
+ sd = service_data (*ts.data);
+ }
+ catch (const invalid_argument& e)
+ {
+ error << "failed to parse service data: " << e;
+ return nullopt;
+ }
- if (iat)
- sd.installation_access = *iat;
+ if (iat)
+ sd.installation_access = *iat;
- if (check_run* scr = sd.find_check_run (cr.build_id))
+ if (check_run* scr = sd.find_check_run (cr.build_id))
+ {
+ // This will most commonly generate a duplicate warning (see above).
+ // We could save the old state and only warn if it differs but let's
+ // not complicate things for now.
+ //
+#if 0
+ if (scr->state != build_state::building)
{
- if (scr->state != build_state::building)
- {
- warn << "check run " << cr.build_id << ": out of order built "
- << "notification service data update; existing state: "
- << scr->state_string ();
- }
-
- *scr = cr;
+ warn << "check run " << cr.build_id << ": out of order built "
+ << "notification service data update; existing state: "
+ << scr->state_string ();
}
- else
- sd.check_runs.push_back (cr);
+#endif
+ *scr = cr;
+ }
+ else
+ sd.check_runs.push_back (cr);
- return sd.json ();
- };
+ return sd.json ();
+ };
+ }
+
+ string ci_github::
+ details_url (const build& b) const
+ {
+ return options_->host () +
+ "/@" + b.tenant +
+ "?builds=" + mime_url_encode (b.package_name.string ()) +
+ "&pv=" + b.package_version.string () +
+ "&tg=" + mime_url_encode (b.target.string ()) +
+ "&tc=" + mime_url_encode (b.target_config_name) +
+ "&pc=" + mime_url_encode (b.package_config_name) +
+ "&th=" + mime_url_encode (b.toolchain_version.string ());
}
optional<string> ci_github::
diff --git a/mod/mod-ci-github.hxx b/mod/mod-ci-github.hxx
index 07feca8..6bb01e3 100644
--- a/mod/mod-ci-github.hxx
+++ b/mod/mod-ci-github.hxx
@@ -60,8 +60,16 @@ namespace brep
// Handle the check_suite event `requested` and `rerequested` actions.
//
+ // If warning_success is true, then map result_status::warning to SUCCESS
+ // and to FAILURE otherwise.
+ //
bool
- handle_check_suite_request (gh_check_suite_event);
+ handle_check_suite_request (gh_check_suite_event, bool warning_success);
+
+ // Build a check run details_url for a build.
+ //
+ string
+ details_url (const build&) const;
optional<string>
generate_jwt (const basic_mark& trace, const basic_mark& error) const;
diff --git a/mod/module.cli b/mod/module.cli
index 166adbd..a23b62a 100644
--- a/mod/module.cli
+++ b/mod/module.cli
@@ -815,10 +815,14 @@ namespace brep
}
};
- // @@ TODO Is etc/brep-module.conf updated manually? Yes, will need to
+ // @@@ TODO Is etc/brep-module.conf updated manually? Yes, will need to
// replicate there eventually.
//
- class ci_github: ci_start, ci_cancel, build_db, handler, openssl_options
+ class ci_github: repository_url,
+ ci_start, ci_cancel,
+ build_db,
+ handler,
+ openssl_options
{
// GitHub CI-specific options.
//
diff --git a/repositories.manifest b/repositories.manifest
index da9ee2b..faed09f 100644
--- a/repositories.manifest
+++ b/repositories.manifest
@@ -35,11 +35,7 @@ location: https://git.build2.org/packaging/cmark-gfm/cmark-gfm.git##HEAD
:
role: prerequisite
-location: https://git.codesynthesis.com/odb/libodb.git##HEAD
-
-:
-role: prerequisite
-location: https://git.codesynthesis.com/odb/libodb-pgsql.git##HEAD
+location: https://git.codesynthesis.com/odb/odb.git##HEAD
:
role: prerequisite