aboutsummaryrefslogtreecommitdiff
path: root/mod
diff options
context:
space:
mode:
authorFrancois Kritzinger <francois@codesynthesis.com>2024-12-10 16:19:52 +0200
committerFrancois Kritzinger <francois@codesynthesis.com>2024-12-10 16:32:40 +0200
commitd716c64e4501a741ac40b0c2fd0303534a7d7f05 (patch)
tree076dfa192c65a84b2272579a40fb8a564f74e583 /mod
parentc7cb981910fd97128ca5e3f7aa77355a18a56738 (diff)
Handle built notification
Diffstat (limited to 'mod')
-rw-r--r--mod/mod-ci-github-gh.cxx34
-rw-r--r--mod/mod-ci-github-gh.hxx6
-rw-r--r--mod/mod-ci-github-gq.cxx184
-rw-r--r--mod/mod-ci-github-gq.hxx40
-rw-r--r--mod/mod-ci-github-service-data.cxx15
-rw-r--r--mod/mod-ci-github-service-data.hxx8
-rw-r--r--mod/mod-ci-github.cxx382
-rw-r--r--mod/mod-ci-github.hxx10
8 files changed, 586 insertions, 93 deletions
diff --git a/mod/mod-ci-github-gh.cxx b/mod/mod-ci-github-gh.cxx
index 0bc6595..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,11 +35,38 @@ 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, bool warning_success)
+ {
+ switch (rs)
+ {
+ case result_status::success:
+ return "SUCCESS";
+
+ case result_status::warning:
+ return warning_success ? "SUCCESS" : "FAILURE";
+
+ case result_status::error:
+ case result_status::abort:
+ case result_status::abnormal:
+ return "FAILURE";
+
+ // 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.
+ }
+
+ string
gh_check_run_name (const build& b, const build_queued_hints* bh)
{
string r;
diff --git a/mod/mod-ci-github-gh.hxx b/mod/mod-ci-github-gh.hxx
index 6fa8590..b3da197 100644
--- a/mod/mod-ci-github-gh.hxx
+++ b/mod/mod-ci-github-gh.hxx
@@ -77,6 +77,12 @@ 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, 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 7fbbb4b..954f5a8 100644
--- a/mod/mod-ci-github-gq.cxx
+++ b/mod/mod-ci-github-gq.cxx
@@ -222,11 +222,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;
@@ -341,13 +341,20 @@ namespace brep
// Serialize `createCheckRun` mutations for one or more builds to GraphQL.
//
+ // 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,
- 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;
@@ -355,28 +362,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)) << '\n'
- << "})" << '\n'
+ << " 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';
+ 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';
@@ -389,25 +404,51 @@ namespace brep
// Serialize an `updateCheckRun` mutation for one build to GraphQL.
//
+ // 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)
+ 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)) << '\n'
- << "})" << '\n'
+ << " 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';
+ 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'
@@ -417,34 +458,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)
{
- string rq (gq_serialize_request (
- gq_mutation_create_check_runs (rid, hs, bs, st, &bh)));
+ // No support for result_status so state cannot be built.
+ //
+ assert (st != build_state::built);
- return gq_mutate_check_runs (crs, iat, move (rq), st, error);
+ // 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 (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,
- 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 || br);
+
+ // Must have a details URL because `st` should never be queued.
+ //
+ assert (!du.empty ());
+
vector<check_run> crs {move (cr)};
- bool r (gq_create_check_runs (crs, iat, rid, hs, {b}, st, bh, 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]);
@@ -452,19 +515,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,
- const basic_mark& error)
+ optional<gq_built_result> br)
{
+ // Must have a result if state is built.
+ //
+ 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)));
+ 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 3d8c6cc..8f7a9ca 100644
--- a/mod/mod-ci-github-gq.hxx
+++ b/mod/mod-ci-github-gq.hxx
@@ -23,29 +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 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,
- const build_queued_hints&,
- const basic_mark& error);
+ optional<gq_built_result> = nullopt);
// Update a check run on GitHub.
//
@@ -53,15 +65,19 @@ namespace brep
// with the new state. Return false and issue diagnostics if the request
// failed.
//
- // @@ TODO Support conclusion, output, etc.
+ // 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,
- 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 ff2af5d..6aee2d7 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_name ("installation_access");
@@ -40,6 +42,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;
{
@@ -51,7 +54,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);
}
@@ -60,12 +63,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_node_id (move (rid)),
head_sha (move (hs))
@@ -82,6 +87,8 @@ namespace brep
s.member ("version", 1);
+ s.member ("warning_success", warning_success);
+
// Installation access token.
//
s.member_begin_object ("installation_access");
@@ -98,6 +105,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)
@@ -133,6 +141,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 0d94b55..c4e20b3 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,6 +47,10 @@ 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;
@@ -71,7 +76,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_node_id,
diff --git a/mod/mod-ci-github.cxx b/mod/mod-ci-github.cxx
index defb74f..7c56851 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>
@@ -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).
+ //
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
@@ -747,10 +789,304 @@ namespace brep
}
function<optional<string> (const tenant_service&)> ci_github::
- build_built (const tenant_service&, const build&,
- const diag_epilogue& /* log_writer */) const noexcept
+ build_built (const tenant_service& ts,
+ const build& b,
+ const diag_epilogue& log_writer) const noexcept
+ {
+ NOTIFICATION_DIAG (log_writer);
+
+ service_data sd;
+ try
+ {
+ sd = service_data (*ts.data);
+ }
+ catch (const invalid_argument& e)
+ {
+ error << "failed to parse service data: " << e;
+ return nullptr;
+ }
+
+ 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)
+ {
+ warn << "check run " << bid << ": out of order built notification; "
+ << "existing state: " << scr->state_string ();
+ }
+
+ // 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.build_id = move (bid);
+ cr.name = cr.build_id;
+ }
+
+ cr.state_synced = false;
+ }
+
+ // Get a new installation access token if the current one has expired.
+ //
+ const gh_installation_access_token* iat (nullptr);
+ optional<gh_installation_access_token> new_iat;
+
+ if (system_clock::now () > sd.installation_access.expires_at)
+ {
+ if (optional<string> jwt = generate_jwt (trace, error))
+ {
+ new_iat = obtain_installation_access_token (sd.installation_id,
+ move (*jwt),
+ error);
+ if (new_iat)
+ iat = &*new_iat;
+ }
+ }
+ else
+ iat = &sd.installation_access;
+
+ // Note: we treat the failure to obtain the installation access token the
+ // same as the failure to notify GitHub (state is updated but not marked
+ // synced).
+ //
+ 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 (error,
+ cr,
+ iat->token,
+ sd.repository_node_id,
+ *cr.node_id,
+ details_url (b),
+ build_state::built,
+ move (br)))
+ {
+ assert (cr.state == build_state::built);
+
+ l3 ([&]{trace << "updated check_run { " << cr << " }";});
+ }
+ }
+ else
+ {
+ // Create new check run.
+ //
+ // Note that we don't have build hints so will be creating this check
+ // run with the full build ID as name. In the unlikely event that an
+ // out of order build_queued() were to run before we've saved this
+ // check run to the service data it will create another check run with
+ // the shortened name which will never get to the built state.
+ //
+ if (gq_create_check_run (error,
+ cr,
+ iat->token,
+ sd.repository_node_id,
+ sd.head_sha,
+ details_url (b),
+ build_state::built,
+ move (br)))
+ {
+ assert (cr.state == build_state::built);
+
+ l3 ([&]{trace << "created check_run { " << cr << " }";});
+ }
+ }
+ }
+
+ return [iat = move (new_iat),
+ 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.
+
+ 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 (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)
+ {
+ warn << "check run " << cr.build_id << ": out of order built "
+ << "notification service data update; existing state: "
+ << scr->state_string ();
+ }
+#endif
+ *scr = cr;
+ }
+ else
+ sd.check_runs.push_back (cr);
+
+ return sd.json ();
+ };
+ }
+
+ string ci_github::
+ details_url (const build& b) const
{
- return nullptr;
+ 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;