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.cxx521
1 files changed, 299 insertions, 222 deletions
diff --git a/mod/mod-ci-github.cxx b/mod/mod-ci-github.cxx
index 96c5889..6cf3b80 100644
--- a/mod/mod-ci-github.cxx
+++ b/mod/mod-ci-github.cxx
@@ -564,6 +564,31 @@ namespace brep
//
return handle_branch_push (move (ps), warning_success);
}
+ // Ignore marketplace_purchase events (sent by the GitHub Marketplace) by
+ // sending a 200 response with empty body. We offer a free plan only and
+ // do not support user accounts so there is nothing to be done.
+ //
+ else if (event == "marketplace_purchase")
+ {
+ return true;
+ }
+ // Ignore GitHub App installation events by sending a 200 response with
+ // empty body. These are triggered when a user installs a GitHub App in a
+ // repository or organization.
+ //
+ else if (event == "installation")
+ {
+ return true;
+ }
+ // Ignore ping events by sending a 200 response with empty body. This
+ // event occurs when you create a new webhook. The ping event is a
+ // confirmation from GitHub that you configured the webhook correctly. One
+ // of its triggers is listing an App on the GitHub Marketplace.
+ //
+ else if (event == "ping")
+ {
+ return true;
+ }
else
{
// Log to investigate.
@@ -577,11 +602,51 @@ namespace brep
// Let's capitalize the synthetic conclusion check run name to make it
// easier to distinguish from the regular ones.
//
- static string conclusion_check_run_name ("CONCLUSION");
+ static const string conclusion_check_run_name ("CONCLUSION");
+
+ // Yellow circle.
+ //
+ static const string conclusion_building_title ("\U0001F7E1 IN PROGRESS");
+ static const string conclusion_building_summary (
+ "Waiting for all the builds to complete.");
+
+ // "Medium white" circle.
+ //
+ static const string check_run_queued_title ("\U000026AA QUEUED");
+ static const string check_run_queued_summary (
+ "Waiting for the build to start.");
+
+ // Yellow circle.
+ //
+ static const string check_run_building_title ("\U0001F7E1 BUILDING");
+ static const string check_run_building_summary (
+ "Waiting for the build to complete.");
- static check_run::description_type conclusion_check_run_building_description {
- "\U000026AA IN PROGRESS", // "Medium white" circle.
- "Waiting for all builds to complete"};
+ // Return the colored circle corresponding to a result_status.
+ //
+ // Note: the rest of the title is produced by to_string(result_status).
+ //
+ static string
+ circle (result_status rs)
+ {
+ 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.
+ }
bool ci_github::
handle_branch_push (gh_push_event ps, bool warning_success)
@@ -1115,30 +1180,6 @@ namespace brep
return true;
}
- // Return the colored circle corresponding to a result_status.
- //
- static string
- circle (result_status rs)
- {
- 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.
- }
-
// Make a check run summary from a CI start_result.
//
static string
@@ -1329,7 +1370,7 @@ namespace brep
if (gq_update_check_run (error, bcr, iat->token,
repo_node_id, cr.check_run.node_id,
- build_state::built, br))
+ br))
{
l3 ([&]{trace << "updated check_run { " << bcr << " }";});
}
@@ -1342,7 +1383,7 @@ namespace brep
if (gq_update_check_run (error, ccr, iat->token,
repo_node_id, *sd.conclusion_node_id,
- build_state::built, move (br)))
+ move (br)))
{
l3 ([&]{trace << "updated conclusion check_run { " << ccr << " }";});
}
@@ -1390,7 +1431,7 @@ namespace brep
//
if (gq_update_check_run (error, ccr, iat->token,
repo_node_id, *sd.conclusion_node_id,
- build_state::built, move (br)))
+ move (br)))
{
l3 ([&]{trace << "updated conclusion check_run { " << ccr << " }";});
}
@@ -1467,11 +1508,13 @@ namespace brep
bcr.state = build_state::queued;
bcr.state_synced = false;
bcr.details_url = cr.check_run.details_url;
+ bcr.description = {check_run_queued_title, check_run_queued_summary};
ccr.state = build_state::building;
ccr.state_synced = false;
ccr.details_url = details_url (tenant_id);
- ccr.description = conclusion_check_run_building_description;
+ ccr.description = {conclusion_building_title,
+ conclusion_building_summary};
if (gq_create_check_runs (error, check_runs, iat->token,
repo_node_id, head_sha))
@@ -1622,7 +1665,7 @@ namespace brep
//
if (gq_update_check_run (error, bcr, iat->token,
repo_node_id, *bcr.node_id,
- build_state::built, br))
+ br))
{
l3 ([&]{trace << "updated check_run { " << bcr << " }";});
}
@@ -1638,7 +1681,7 @@ namespace brep
//
if (gq_update_check_run (error, ccr, iat->token,
repo_node_id, *ccr.node_id,
- build_state::built, move (br)))
+ move (br)))
{
l3 ([&]{trace << "updated conclusion check_run { " << ccr << " }";});
}
@@ -1941,7 +1984,8 @@ namespace brep
&sd,
&error,
this] (string name,
- const check_run::description_type& output)
+ const string& title,
+ const string& summary)
-> optional<check_run>
{
check_run cr;
@@ -1956,7 +2000,7 @@ namespace brep
sd.report_sha,
details_url (tenant_id),
build_state::building,
- output.title, output.summary))
+ title, summary))
{
return cr;
}
@@ -1991,7 +2035,6 @@ namespace brep
iat->token,
sd.repository_node_id,
node_id,
- build_state::built,
move (br)))
{
assert (cr.state == build_state::built);
@@ -2013,9 +2056,9 @@ namespace brep
if (!sd.conclusion_node_id)
{
- if (auto cr =
- create_synthetic_cr (conclusion_check_run_name,
- conclusion_check_run_building_description))
+ if (auto cr = create_synthetic_cr (conclusion_check_run_name,
+ conclusion_building_title,
+ conclusion_building_summary))
{
l3 ([&]{trace << "created check_run { " << *cr << " }";});
@@ -2316,7 +2359,9 @@ namespace brep
build_state::queued,
false /* state_synced */,
nullopt /* status */,
- details_url (b)});
+ details_url (b),
+ check_run::description_type {check_run_queued_title,
+ check_run_queued_summary}});
}
}
@@ -2520,7 +2565,9 @@ namespace brep
iat->token,
sd.repository_node_id,
*cr->node_id,
- build_state::building))
+ build_state::building,
+ check_run_building_title,
+ check_run_building_summary))
{
// Do nothing further if the state was already built on GitHub (note
// that this is based on the above-mentioned special GitHub semantics
@@ -2595,7 +2642,8 @@ namespace brep
return nullptr;
}
- function<optional<string> (const string&, const tenant_service&)> ci_github::
+ function<pair<optional<string>, bool> (const string&,
+ const tenant_service&)> ci_github::
build_built (const string& tenant_id,
const tenant_service& ts,
const build& b,
@@ -2626,89 +2674,17 @@ namespace brep
if (sd.completed)
return nullptr;
- // Here we need to update the state of this check run and, if there are no
- // more unbuilt ones, update the synthetic conclusion check run and mark
- // the check suite as completed.
- //
- // Absent means we still have unbuilt check runs.
- //
- optional<result_status> conclusion (*b.status);
-
- // Conclusion check run summary. Will include the success/warning/failure
- // count breakdown.
- //
- string summary;
+ // Here we only update the state of this check run. If there are no more
+ // unbuilt ones, then the synthetic conclusion check run will be updated
+ // in build_completed(). Note that determining whether we have no more
+ // unbuilt would be racy here so instead we do it in the service data
+ // update function that we return.
check_run cr; // Updated check run.
{
- // The success/warning/failure counts.
- //
- // Note that the warning count will be included in the success or
- // failure count (depending on the value of sd.warning_success).
- //
- size_t succ_count (0), warn_count (0), fail_count (0);
-
- // Count a result_status under the appropriate category.
- //
- auto count = [&succ_count,
- &warn_count,
- &fail_count,
- ws = sd.warning_success] (result_status rs)
- {
- switch (rs)
- {
- case result_status::success: ++succ_count; break;
-
- case result_status::error:
- case result_status::abort:
- case result_status::abnormal: ++fail_count; break;
-
- case result_status::warning:
- {
- ++warn_count;
-
- if (ws)
- ++succ_count;
- else
- ++fail_count;
-
- break;
- }
- }
- };
-
- count (*b.status);
-
string bid (gh_check_run_name (b)); // Full build id.
- optional<check_run> scr;
- for (check_run& cr: sd.check_runs)
- {
- if (cr.build_id == bid)
- {
- assert (!scr);
- scr = move (cr);
- }
- else
- {
- if (cr.state == build_state::built)
- {
- assert (cr.status);
-
- if (conclusion)
- *conclusion |= *cr.status;
-
- count (*cr.status);
- }
- else
- conclusion = nullopt;
- }
-
- if (scr && !conclusion.has_value ())
- break;
- }
-
- if (scr)
+ if (check_run* scr = sd.find_check_run (bid))
{
if (scr->state != build_state::building)
{
@@ -2736,29 +2712,6 @@ namespace brep
}
cr.state_synced = false;
-
- // Construct the conclusion check run summary if all check runs are
- // built.
- //
- if (conclusion)
- {
- ostringstream os;
-
- // Note: the warning count has already been included in the success or
- // failure count.
- //
- os << fail_count << " failed";
- if (!sd.warning_success && warn_count != 0)
- os << " (" << warn_count << " due to warnings)";
-
- os << ", " << succ_count << " succeeded";
- if (sd.warning_success && warn_count != 0)
- os << " (" << warn_count << " with warnings)";
-
- os << ", " << (succ_count + fail_count) << " total";
-
- summary = os.str ();
- }
}
// Get a new installation access token if the current one has expired.
@@ -2780,8 +2733,6 @@ namespace brep
else
iat = &sd.installation_access;
- bool completed (false);
-
// 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).
@@ -2903,7 +2854,6 @@ namespace brep
iat->token,
sd.repository_node_id,
*cr.node_id,
- build_state::built,
move (br)))
{
assert (cr.state == build_state::built);
@@ -2935,69 +2885,27 @@ namespace brep
if (cr.state_synced)
{
- // Check run was created/updated successfully to built (with
- // status we specified).
+ // Check run was created/updated successfully to built (with status we
+ // specified).
//
cr.status = b.status;
-
- // Update the conclusion check run if all check runs are now built.
- //
- if (conclusion)
- {
- assert (sd.conclusion_node_id);
-
- result_status rs (*conclusion);
-
- gq_built_result br (
- make_built_result (rs, sd.warning_success, move (summary)));
-
- check_run cr;
-
- // Set some fields for display purposes.
- //
- cr.node_id = *sd.conclusion_node_id;
- cr.name = conclusion_check_run_name;
-
- // Let unlikely invalid_argument propagate.
- //
- if (gq_update_check_run (error,
- cr,
- iat->token,
- sd.repository_node_id,
- *sd.conclusion_node_id,
- build_state::built,
- move (br)))
- {
- assert (cr.state == build_state::built);
- l3 ([&]{trace << "updated conclusion check_run { " << cr << " }";});
- }
- else
- {
- // Nothing we can do here except log the error.
- //
- error << "tenant_service id " << ts.id
- << ": unable to update conclusion check run "
- << *sd.conclusion_node_id;
- }
-
- completed = true;
- }
}
}
return [tenant_id,
iat = move (new_iat),
cr = move (cr),
- completed = completed,
error = move (error),
warn = move (warn)] (const string& ti,
- const tenant_service& ts) -> optional<string>
+ const tenant_service& ts)
{
// NOTE: this lambda may be called repeatedly (e.g., due to transaction
// being aborted) and so should not move out of its captures.
+ // Do nothing if the tenant has been replaced.
+ //
if (tenant_id != ti)
- return nullopt; // Do nothing if the tenant has been replaced.
+ return make_pair (optional<string> (), false);
service_data sd;
try
@@ -3007,7 +2915,19 @@ namespace brep
catch (const invalid_argument& e)
{
error << "failed to parse service data: " << e;
- return nullopt;
+ return make_pair (optional<string> (), false);
+ }
+
+ // Feel like this could potentially happen in case of an out of order
+ // notification (see above).
+ //
+ if (sd.completed)
+ {
+ // @@ Perhaps this should be a warning but let's try error for now (we
+ // essentially missed a build, which could have failed).
+ //
+ error << "built notification for completed check suite";
+ return make_pair (optional<string> (), false);
}
if (iat)
@@ -3037,46 +2957,203 @@ namespace brep
else
sd.check_runs.push_back (cr);
- if (bool c = completed)
+ // Determine of this check suite is completed.
+ //
+ sd.completed = find_if (sd.check_runs.begin (), sd.check_runs.end (),
+ [] (const check_run& scr)
+ {
+ return scr.state != build_state::built;
+ }) == sd.check_runs.end ();
+ }
+
+ return make_pair (optional<string> (sd.json ()), sd.completed);
+ };
+ }
+ catch (const std::exception& e)
+ {
+ NOTIFICATION_DIAG (log_writer);
+
+ string bid (gh_check_run_name (b)); // Full build id.
+
+ error << "check run " << bid << ": unhandled exception: " << e.what ();
+
+ return nullptr;
+ }
+
+ void ci_github::
+ build_completed (const string& /* tenant_id */,
+ const tenant_service& ts,
+ const diag_epilogue& log_writer) const noexcept
+ try
+ {
+ // NOTE: this function is noexcept and should not throw.
+
+ 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;
+ }
+
+ // This could have been reset by handle_check_run_rerequest().
+ //
+ if (!sd.completed)
+ return;
+
+ assert (!sd.check_runs.empty ());
+
+ // Here we need to update the state of the synthetic conclusion check run.
+ //
+ result_status result (result_status::success);
+
+ // Conclusion check run summary. Will include the success/warning/failure
+ // count breakdown.
+ //
+ string summary;
+ {
+ // The success/warning/failure counts.
+ //
+ // Note that the warning count will be included in the success or
+ // failure count (depending on the value of sd.warning_success).
+ //
+ size_t succ_count (0), warn_count (0), fail_count (0);
+
+ // Count a result_status under the appropriate category.
+ //
+ auto count = [&succ_count,
+ &warn_count,
+ &fail_count,
+ ws = sd.warning_success] (result_status rs)
+ {
+ switch (rs)
{
- // Note that this can be racy: while we calculated the completed
- // value based on the snapshot of the service data, it could have
- // been changed (e.g., by handle_check_run_rerequest()). So we
- // re-calculate it based on the check run states and only update if
- // it matches. Otherwise, we log an error.
- //
- for (const check_run& scr: sd.check_runs)
+ case result_status::success: ++succ_count; break;
+
+ case result_status::error:
+ case result_status::abort:
+ case result_status::abnormal: ++fail_count; break;
+
+ case result_status::warning:
{
- if (scr.state != build_state::built)
- {
- string sid (sd.repository_node_id + ':' + sd.report_sha);
+ ++warn_count;
- error << "tenant_service id " << sid
- << ": out of order built notification service data update; "
- << "check suite is no longer complete";
+ if (ws)
+ ++succ_count;
+ else
+ ++fail_count;
- c = false;
- break;
- }
+ break;
}
- if (c)
- sd.completed = true;
+ case result_status::skip:
+ case result_status::interrupt:
+ {
+ assert (false);
+ }
}
+ };
+
+ for (const check_run& cr: sd.check_runs)
+ {
+ assert (cr.state == build_state::built && cr.status);
+
+ result |= *cr.status;
+ count (*cr.status);
}
- return sd.json ();
- };
+ // Construct the conclusion check run summary.
+ //
+ ostringstream os;
+
+ // Note: the warning count has already been included in the success or
+ // failure count.
+ //
+ os << fail_count << " failed";
+ if (!sd.warning_success && warn_count != 0)
+ os << " (" << warn_count << " due to warnings)";
+
+ os << ", " << succ_count << " succeeded";
+ if (sd.warning_success && warn_count != 0)
+ os << " (" << warn_count << " with warnings)";
+
+ os << ", " << (succ_count + fail_count) << " total";
+
+ summary = os.str ();
+ }
+
+ // Get a new installation access token if the current one has expired
+ // (unlikely since we just returned from build_built()). Note also that we
+ // are not saving the new token in the service data.
+ //
+ 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 (sd.app_id, 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.
+ //
+ if (iat != nullptr)
+ {
+ // Update the conclusion check run if all check runs are now built.
+ //
+ assert (sd.conclusion_node_id);
+
+ gq_built_result br (
+ make_built_result (result, sd.warning_success, move (summary)));
+
+ check_run cr;
+
+ // Set some fields for display purposes.
+ //
+ cr.node_id = *sd.conclusion_node_id;
+ cr.name = conclusion_check_run_name;
+
+ // Let unlikely invalid_argument propagate.
+ //
+ if (gq_update_check_run (error,
+ cr,
+ iat->token,
+ sd.repository_node_id,
+ *sd.conclusion_node_id,
+ move (br)))
+ {
+ assert (cr.state == build_state::built);
+ l3 ([&]{trace << "updated conclusion check_run { " << cr << " }";});
+ }
+ else
+ {
+ // Nothing we can do here except log the error.
+ //
+ error << "tenant_service id " << ts.id
+ << ": unable to update conclusion check run "
+ << *sd.conclusion_node_id;
+ }
+ }
}
catch (const std::exception& e)
{
NOTIFICATION_DIAG (log_writer);
- string bid (gh_check_run_name (b)); // Full build id.
-
- error << "check run " << bid << ": unhandled exception: " << e.what();
-
- return nullptr;
+ error << "unhandled exception: " << e.what ();
}
string ci_github::