diff options
Diffstat (limited to 'mod/mod-ci-github.cxx')
-rw-r--r-- | mod/mod-ci-github.cxx | 1011 |
1 files changed, 991 insertions, 20 deletions
diff --git a/mod/mod-ci-github.cxx b/mod/mod-ci-github.cxx index b72bf93..9d2606b 100644 --- a/mod/mod-ci-github.cxx +++ b/mod/mod-ci-github.cxx @@ -334,6 +334,51 @@ namespace brep return true; } } + else if (event == "check_run") + { + gh_check_run_event cr; + try + { + json::parser p (body.data (), body.size (), "check_run event"); + + cr = gh_check_run_event (p); + } + catch (const json::invalid_json_input& e) + { + string m ("malformed JSON in " + e.name + " request body"); + + error << m << ", line: " << e.line << ", column: " << e.column + << ", byte offset: " << e.position << ", error: " << e; + + throw invalid_request (400, move (m)); + } + + if (cr.action == "rerequested") + { + // Someone manually requested to re-run a specific check run. + // + return handle_check_run_rerequest (move (cr), warning_success); + } +#if 0 + // It looks like we shouldn't be receiving these since we are not + // subscribed to them. + // + else if (cr.action == "created" || + cr.action == "completed" || + cr.action == "requested_action") + { + } +#endif + else + { + // Ignore unknown actions by sending a 200 response with empty body + // but also log as an error since we want to notice new actions. + // + error << "unknown action '" << cr.action << "' in check_run event"; + + return true; + } + } else if (event == "pull_request") { gh_pull_request_event pr; @@ -470,7 +515,7 @@ namespace brep // Service id that uniquely identifies the CI tenant. // - string sid (cs.repository.node_id + ":" + cs.check_suite.head_sha); + string sid (cs.repository.node_id + ':' + cs.check_suite.head_sha); // If the user requests a rebuild of the (entire) PR, then this manifests // as the check_suite rather than pull_request event. Specifically: @@ -496,11 +541,14 @@ namespace brep { kind = service_data::remote; - if (optional<tenant_service> ts = find (*build_db_, "ci-github", sid)) + if (optional<pair<tenant_service, bool>> p = + find (*build_db_, "ci-github", sid)) { + tenant_service& ts (p->first); + try { - service_data sd (*ts->data); + service_data sd (*ts.data); check_sha = move (sd.check_sha); // Test merge commit. } catch (const invalid_argument& e) @@ -585,6 +633,787 @@ namespace brep return true; } + // Create a gq_built_result. + // + static gq_built_result + make_built_result (result_status rs, bool warning_success, string message) + { + return {gh_to_conclusion (rs, warning_success), + circle (rs) + ' ' + ucase (to_string (rs)), + move (message)}; + } + + // Parse a check run details URL into a build_id. + // + // Return nullopt if the URL is invalid. + // + static optional<build_id> + parse_details_url (const string& details_url); + + // Note that GitHub always posts a message to their GUI saying "You have + // successfully requested <check_run_name> be rerun", regardless of what + // HTTP status code we respond with. However we do return error status codes + // when there is no better option (like failing the conclusion) in case they + // start handling them someday. + // + bool ci_github:: + handle_check_run_rerequest (const gh_check_run_event& cr, + bool warning_success) + { + HANDLER_DIAG; + + l3 ([&]{trace << "check_run event { " << cr << " }";}); + + // The overall plan is as follows: + // + // 1. Load service data. + // + // 2. If the tenant is archived, then fail (re-create) both the check run + // and the conclusion with appropriate diagnostics. + // + // 3. If the check run is in the queued state, then do nothing. + // + // 4. Re-create the check run in the queued state and the conclusion in + // the building state. Note: do in a single request to make sure we + // either "win" or "loose" the potential race for both (important + // for #7). + // + // 5. Call the rebuild() function to attempt to schedule a rebuild. Pass + // the update function that does the following (if called): + // + // a. Save new node ids. + // + // b. Update the check run state (may also not exist). + // + // c. Clear the completed flag if true. + // + // 6. If the result of rebuild() indicates the tenant is archived, then + // fail (update) both the check run and conclusion with appropriate + // diagnostics. + // + // 7. If original state is queued (no rebuild was scheduled), then fail + // (update) both the check run and the conclusion. + // + // Note that while conceptually we are updating existing check runs, in + // practice we have to re-create as new check runs in order to replace the + // existing ones because GitHub does not allow transitioning out of the + // built state. + + // Get a new installation access token. + // + auto get_iat = [this, &trace, &error, &cr] () + -> optional<gh_installation_access_token> + { + optional<string> jwt (generate_jwt (trace, error)); + if (!jwt) + return nullopt; + + optional<gh_installation_access_token> iat ( + obtain_installation_access_token (cr.installation.id, + move (*jwt), + error)); + + if (iat) + l3 ([&]{trace << "installation_access_token { " << *iat << " }";}); + + return iat; + }; + + const string& repo_node_id (cr.repository.node_id); + const string& head_sha (cr.check_run.check_suite.head_sha); + + // Prepare the build and conclusion check runs. They are sent to GitHub in + // a single request (unless something goes wrong) so store them together + // from the outset. + // + vector<check_run> check_runs (2); + check_run& bcr (check_runs[0]); // Build check run + check_run& ccr (check_runs[1]); // Conclusion check run + + ccr.name = conclusion_check_run_name; + + // Load the service data, failing the check runs if the tenant has been + // archived. + // + service_data sd; + { + // Service id that uniquely identifies the CI tenant. + // + string sid (repo_node_id + ':' + head_sha); + + if (optional<pair<tenant_service, bool>> p = + find (*build_db_, "ci-github", sid)) + { + if (p->second) // Tenant is archived + { + // Fail (re-create) the check runs. + // + optional<gh_installation_access_token> iat (get_iat ()); + if (!iat) + throw server_error (); + + gq_built_result br ( + make_built_result ( + result_status::error, warning_success, + "Unable to rebuild individual configuration: build has " + "been archived")); + + // Try to update the conclusion check run even if the first update + // fails. + // + bool f (false); // Failed. + + if (gq_create_check_run (error, bcr, iat->token, + repo_node_id, head_sha, + cr.check_run.details_url, + build_state::built, br)) + { + l3 ([&]{trace << "created check_run { " << bcr << " }";}); + } + else + { + error << "check_run " << cr.check_run.node_id + << ": unable to re-create check run"; + f = true; + } + + if (gq_create_check_run (error, ccr, iat->token, + repo_node_id, head_sha, + nullopt /* details_url */, + build_state::built, move (br))) + { + l3 ([&]{trace << "created conclusion check_run { " << ccr << " }";}); + } + else + { + error << "check_run " << cr.check_run.node_id + << ": unable to re-create conclusion check run"; + f = true; + } + + // Fail the handler if either of the check runs could not be + // updated. + // + if (f) + throw server_error (); + + return true; + } + + tenant_service& ts (p->first); + + try + { + sd = service_data (*ts.data); + } + catch (const invalid_argument& e) + { + fail << "failed to parse service data: " << e; + } + } + else + { + // No such tenant. + // + fail << "check run " << cr.check_run.node_id + << " re-requested but tenant_service with id " << sid + << " does not exist"; + } + } + + // Get a new IAT if the one from the service data 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 (new_iat = get_iat ()) + iat = &*new_iat; + else + throw server_error (); + } + else + iat = &sd.installation_access; + + // Fail if it's the conclusion check run that is being re-requested. + // + if (cr.check_run.name == conclusion_check_run_name) + { + l3 ([&]{trace << "re-requested conclusion check_run";}); + + if (!sd.conclusion_node_id) + fail << "no conclusion node id for check run " << cr.check_run.node_id; + + gq_built_result br ( + make_built_result (result_status::error, warning_success, + "Conclusion check run cannot be rebuilt")); + + // Fail (update) the conclusion check run. + // + if (gq_update_check_run (error, ccr, iat->token, + repo_node_id, *sd.conclusion_node_id, + nullopt /* details_url */, + build_state::built, move (br))) + { + l3 ([&]{trace << "updated conclusion check_run { " << ccr << " }";}); + } + else + { + fail << "check run " << cr.check_run.node_id + << ": unable to update conclusion check run " + << *sd.conclusion_node_id; + } + + return true; + } + + // Parse the check_run's details_url to extract build id. + // + // While this is a bit hackish, there doesn't seem to be a better way + // (like associating custom data with a check run). Note that the GitHub + // UI only allows rebuilding completed check runs, so the details URL + // should be there. + // + optional<build_id> bid (parse_details_url (cr.check_run.details_url)); + if (!bid) + { + fail << "check run " << cr.check_run.node_id + << ": failed to extract build id from details_url"; + } + + // Initialize the check run (`bcr`) with state from the service data. + // + { + // Search for the check run in the service data. + // + // Note that we look by name in case node id got replaced by a racing + // re-request (in which case we ignore this request). + // + auto i (find_if (sd.check_runs.begin (), sd.check_runs.end (), + [&cr] (const check_run& scr) + { + return scr.name == cr.check_run.name; + })); + + if (i == sd.check_runs.end ()) + fail << "check_run " << cr.check_run.node_id + << " (" << cr.check_run.name << "): " + << "re-requested but does not exist in service data"; + + // Do nothing if node ids don't match. + // + if (i->node_id && *i->node_id != cr.check_run.node_id) + { + l3 ([&]{trace << "check_run " << cr.check_run.node_id + << " (" << cr.check_run.name << "): " + << "node id has changed in service data";}); + return true; + } + + // Do nothing if the build is already queued. + // + if (i->state == build_state::queued) + { + l3 ([&]{trace << "ignoring already-queued check run";}); + return true; + } + + bcr.name = i->name; + bcr.build_id = i->build_id; + bcr.state = i->state; + } + + // Transition the build and conclusion check runs out of the built state + // (or any other state) by re-creating them. + // + bcr.state = build_state::queued; + bcr.state_synced = false; + bcr.details_url = cr.check_run.details_url; + + ccr.state = build_state::building; + ccr.state_synced = false; + + if (gq_create_check_runs (error, check_runs, iat->token, + repo_node_id, head_sha)) + { + assert (bcr.state == build_state::queued); + assert (ccr.state == build_state::building); + + l3 ([&]{trace << "created check_run { " << bcr << " }";}); + l3 ([&]{trace << "created conclusion check_run { " << ccr << " }";}); + } + else + { + fail << "check run " << cr.check_run.node_id + << ": unable to re-create build and conclusion check runs"; + } + + // Request the rebuild and update service data. + // + bool race (false); + + // Callback function called by rebuild() to update the service data (but + // only if the build is actually restarted). + // + auto update_sd = [&error, &new_iat, &race, + &cr, &bcr, &ccr] (const tenant_service& ts, + build_state) -> 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. + + race = false; // Reset. + + service_data sd; + try + { + sd = service_data (*ts.data); + } + catch (const invalid_argument& e) + { + error << "failed to parse service data: " << e; + return nullopt; + } + + // Note that we again look by name in case node id got replaced by a + // racing re-request. In this case, however, it's impossible to decide + // who won that race, so let's fail the check suite to be on the safe + // side (in a sense, similar to the rebuild() returning queued below). + // + auto i (find_if ( + sd.check_runs.begin (), sd.check_runs.end (), + [&cr] (const check_run& scr) + { + return scr.name == cr.check_run.name; + })); + + if (i == sd.check_runs.end ()) + { + error << "check_run " << cr.check_run.node_id + << " (" << cr.check_run.name << "): " + << "re-requested but does not exist in service data"; + return nullopt; + } + + if (i->node_id && *i->node_id != cr.check_run.node_id) + { + // Keep the old conclusion node id to make sure any further state + // transitions are ignored. A bit of a hack. + // + race = true; + return nullopt; + } + + *i = bcr; // Update with new node_id, state, state_synced. + + sd.conclusion_node_id = ccr.node_id; + sd.completed = false; + + // Save the IAT if we created a new one. + // + if (new_iat) + sd.installation_access = *new_iat; + + return sd.json (); + }; + + optional<build_state> bs (rebuild (*build_db_, retry_, *bid, update_sd)); + + // If the build has been archived or re-enqueued since we loaded the + // service data, fail (by updating) both the build check run and the + // conclusion check run. Otherwise the build has been successfully + // re-enqueued so do nothing further. + // + if (!race && bs && *bs != build_state::queued) + return true; + + gq_built_result br; // Built result for both check runs. + + if (race || bs) // Race or re-enqueued. + { + // The re-enqueued case: this build has been re-enqueued since we first + // loaded the service data. This could happen if the user clicked + // "re-run" multiple times and another handler won the rebuild() race. + // + // However the winner of the check runs race cannot be determined. + // + // Best case the other handler won the check runs race as well and + // thus everything will proceed normally. Our check runs will be + // invisible and disregarded. + // + // Worst case we won the check runs race and the other handler's check + // runs -- the ones that will be updated by the build_*() notifications + // -- are no longer visible, leaving things quite broken. + // + // Either way, we fail our check runs. In the best case scenario it + // will have no effect; in the worst case scenario it lets the user + // know something has gone wrong. + // + br = make_built_result (result_status::error, warning_success, + "Unable to rebuild, try again"); + } + else // Archived. + { + // The build has expired since we loaded the service data. Most likely + // the tenant has been archived. + // + br = make_built_result ( + result_status::error, warning_success, + "Unable to rebuild individual configuration: build has been archived"); + } + + // Try to update the conclusion check run even if the first update fails. + // + bool f (false); // Failed. + + // Fail the build check run. + // + if (gq_update_check_run (error, bcr, iat->token, + repo_node_id, *bcr.node_id, + nullopt /* details_url */, + build_state::built, br)) + { + l3 ([&]{trace << "updated check_run { " << bcr << " }";}); + } + else + { + error << "check run " << cr.check_run.node_id + << ": unable to update (replacement) check run " + << *bcr.node_id; + f = true; + } + + // Fail the conclusion check run. + // + if (gq_update_check_run (error, ccr, iat->token, + repo_node_id, *ccr.node_id, + nullopt /* details_url */, + build_state::built, move (br))) + { + l3 ([&]{trace << "updated conclusion check_run { " << ccr << " }";}); + } + else + { + error << "check run " << cr.check_run.node_id + << ": unable to update conclusion check run " << *ccr.node_id; + f = true; + } + + // Fail the handler if either of the check runs could not be updated. + // + if (f) + throw server_error (); + + return true; + } + + // @@ TMP + // +#if 0 + bool ci_github:: + handle_check_run_rerequest (const gh_check_run_event& cr, + bool warning_success) + { + HANDLER_DIAG; + + l3 ([&]{trace << "check_run event { " << cr << " }";}); + + // Fail if this is the conclusion check run. + // + if (cr.check_run.name == conclusion_check_run_name) + { + // @@ Fail conclusion check run with appropriate message and reurn + // true. + + l3 ([&]{trace << "ignoring conclusion check_run";}); + + // 422 Unprocessable Content: The request was well-formed (i.e., + // syntactically correct) but could not be processed. + // + throw invalid_request (422, "Conclusion check run cannot be rebuilt"); + } + + // Get a new installation access token. + // + auto get_iat = [this, &trace, &error, &cr] () + -> optional<gh_installation_access_token> + { + optional<string> jwt (generate_jwt (trace, error)); + if (!jwt) + return nullopt; + + optional<gh_installation_access_token> iat ( + obtain_installation_access_token (cr.installation.id, + move (*jwt), + error)); + + if (iat) + l3 ([&]{trace << "installation_access_token { " << *iat << " }";}); + + return iat; + }; + + // Create a new conclusion check run, replacing the existing one. + // + // Return the check run on success or nullopt on failure. + // + auto create_conclusion_cr = + [&cr, &error, warning_success] (const gh_installation_access_token& iat, + build_state bs, + optional<result_status> rs = nullopt, + optional<string> msg = nullopt) + -> optional<check_run> + { + optional<gq_built_result> br; + if (rs) + { + assert (msg); + + br = make_built_result (*rs, warning_success, move (*msg)); + } + + check_run r; + r.name = conclusion_check_run_name; + + if (gq_create_check_run (error, r, iat.token, + rni, hs, + nullopt /* details_url */, + bs, move (br))) + { + return r; + } + else + return nullopt; + }; + + // The overall plan is as follows: + // + // 1. Call the rebuild() function to attempt to schedule a rebuild. Pass + // the update function that does the following (if called): + // + // a. Update the check run being rebuilt (may also not exist). + // + // b. Clear the completed flag if true. + // + // c. "Return" the service data to be used after the call. + // + // 2. If the result of rebuild() indicates the tenant is archived, fail + // the conclusion check run with appropriate diagnostics. + // + // 3. If original state is queued, then no rebuild was scheduled and we do + // nothing. + // + // 4. Otherwise (the original state is building or built): + // + // a. Change the check run state to queued. + // + // b. Change the conclusion check run to building (do unconditionally + // to mitigate races). + // + // Note that while conceptually we are updating existing check runs, in + // practice we have to create new check runs to replace the existing ones + // because GitHub does not allow transitioning out of the built state. + // + // This results in a new node id for each check run but we can't save them + // to the service data after the rebuild() call. As a workaround, when + // updating the service data we 1) clear the re-requested check run's node + // id and set the state_synced flag to true to signal to build_building() + // and build_built() that it needs to create a new check run; and 2) clear + // the conclusion check run's node id to cause build_built() to create a + // new conclusion check run. And these two check runs' node ids will be + // saved to the service data. + + // Parse the check_run's details_url to extract build id. + // + // While this is a bit hackish, there doesn't seem to be a better way + // (like associating custom data with a check run). Note that the GitHub + // UI only allows rebuilding completed check runs, so the details URL + // should be there. + // + optional<build_id> bid (parse_details_url (cr.check_run.details_url)); + if (!bid) + { + fail << "check run " << cr.check_run.node_id + << ": failed to extract build id from details_url"; + } + + // The IAT retrieved from the service data. + // + optional<gh_installation_access_token> iat; + + // True if the check run exists in the service data. + // + bool cr_found (false); + + // Update the state of the check run in the service data. Return (via + // captured references) the IAT and whether the check run was found. + // + // Called by rebuild(), but only if the build is actually restarted. + // + auto update_sd = [&iat, + &cr_found, + &error, + &cr] (const tenant_service& ts, build_state) + -> 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 nullptr; + } + + if (!iat) + iat = sd.installation_access; + + // If the re-requested check run is found, update it in the service + // data. + // + const string& nid (cr.check_run.node_id); + + for (check_run& cr: sd.check_runs) + { + if (cr.node_id && *cr.node_id == nid) + { + cr_found = true; + cr.state = build_state::queued; + sd.completed = false; + + // Clear the check run node ids and set state_synced to true to + // cause build_building() and/or build_built() to create new check + // runs (see the plan above for details). + // + cr.node_id = nullopt; + cr.state_synced = true; + sd.conclusion_node_id = nullopt; + + return sd.json (); + } + } + + return nullopt; + }; + + optional<build_state> bs (rebuild (*build_db_, retry_, *bid, update_sd)); + + if (!bs) + { + // Build has expired (most probably the tenant has been archived). + // + // Update the conclusion check run to notify the user (but have to + // replace it with a new one because we don't know the existing one's + // node id). + // + optional<gh_installation_access_token> iat (get_iat ()); + if (!iat) + throw server_error (); + + if (optional<check_run> ccr = create_conclusion_cr ( + *iat, + build_state::built, + result_status::error, + "Unable to rebuild: tenant has been archived or no such build")) + { + l3 ([&]{trace << "created conclusion check_run { " << *ccr << " }";}); + } + else + { + // Log the error and return failure to GitHub which will presumably + // indicate this in its GUI. + // + fail << "check run " << cr.check_run.node_id + << ": unable to create conclusion check run"; + } + } + else if (*bs == build_state::queued) + { + // The build was already queued so nothing to be done. This might happen + // if the user clicked "re-run" multiple times before we managed to + // update the check run. + } + else + { + // The build has been requeued. + // + assert (*bs == build_state::building || *bs == build_state::built); + + if (!cr_found) + { + // Respond with an error otherwise GitHub will post a message in its + // GUI saying "you have successfully requested a rebuild of ..." + // + fail << "check_run " << cr.check_run.node_id + << ": build restarted but check run does not exist " + << "in service data"; + } + + // Get a new IAT if the one from the service data has expired. + // + assert (iat.has_value ()); + + if (system_clock::now () > iat->expires_at) + { + iat = get_iat (); + if (!iat) + throw server_error (); + } + + // Update (by replacing) the re-requested and conclusion check runs to + // queued and building, respectively. + // + // If either fails we can only log the error but build_building() and/or + // build_built() should correct the situation (see above for details). + // + + // Update re-requested check run. + // + { + check_run ncr; // New check run. + ncr.name = cr.check_run.name; + + if (gq_create_check_run (error, + ncr, + iat->token, + cr.repository.node_id, + cr.check_run.check_suite.head_sha, + cr.check_run.details_url, + build_state::queued)) + { + l3 ([&]{trace << "created check_run { " << ncr << " }";}); + } + else + { + error << "check_run " << cr.check_run.node_id + << ": unable to create (to update) check run in queued state"; + } + } + + // Update conclusion check run. + // + if (optional<check_run> ccr = + create_conclusion_cr (*iat, build_state::building)) + { + l3 ([&]{trace << "created conclusion check_run { " << *ccr << " }";}); + } + else + { + error << "check_run " << cr.check_run.node_id + << ": unable to create (to update) conclusion check run"; + } + } + + return true; + } +#endif + // Miscellaneous pull request facts // // - Although some of the GitHub documentation makes it sound like they @@ -821,7 +1650,7 @@ namespace brep // Service id that will uniquely identify the CI tenant. // - string sid (sd.repository_node_id + ":" + sd.report_sha); + string sid (sd.repository_node_id + ':' + sd.report_sha); // Create an unloaded CI tenant, doing nothing if one already exists // (which could've been created by a head branch push or another PR @@ -967,10 +1796,8 @@ namespace brep { assert (!node_id.empty ()); - optional<gq_built_result> br ( - gq_built_result (gh_to_conclusion (rs, sd.warning_success), - circle (rs) + ' ' + ucase (to_string (rs)), - move (summary))); + gq_built_result br ( + make_built_result (rs, sd.warning_success, move (summary))); check_run cr; cr.name = name; // For display purposes only. @@ -1196,6 +2023,13 @@ namespace brep return nullptr; } + // Ignore attempts to add new builds to a completed check suite. This can + // happen, for example, if a new build configuration is added before + // the tenant is archived. + // + if (sd.completed) + return nullptr; + // The builds for which we will be creating check runs. // vector<reference_wrapper<const build>> bs; @@ -1278,8 +2112,7 @@ namespace brep if (gq_create_check_runs (error, crs, iat->token, - sd.repository_node_id, sd.report_sha, - build_state::queued)) + sd.repository_node_id, sd.report_sha)) { for (const check_run& cr: crs) { @@ -1358,6 +2191,12 @@ namespace brep return nullptr; } + // Similar to build_queued(), ignore attempts to add new builds to a + // completed check suite. + // + if (sd.completed) + return nullptr; + optional<check_run> cr; // Updated check run. string bid (gh_check_run_name (b)); // Full build id. @@ -1502,8 +2341,15 @@ namespace brep return nullptr; } + // Similar to build_queued(), ignore attempts to add new builds to a + // completed check suite. + // + 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. + // more unbuilt ones, update the synthetic conclusion check run and mark + // the check suite as completed. // // Absent means we still have unbuilt check runs. // @@ -1587,6 +2433,8 @@ 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). @@ -1691,9 +2539,7 @@ namespace brep } gq_built_result br ( - gh_to_conclusion (*b.status, sd.warning_success), - circle (*b.status) + ' ' + ucase (to_string (*b.status)), - move (sm)); + make_built_result (*b.status, sd.warning_success, move (sm))); if (cr.node_id) { @@ -1751,10 +2597,9 @@ namespace brep result_status rs (*conclusion); - optional<gq_built_result> br ( - gq_built_result (gh_to_conclusion (rs, sd.warning_success), - circle (rs) + ' ' + ucase (to_string (rs)), - "All configurations are built")); + gq_built_result br ( + make_built_result (rs, sd.warning_success, + "All configurations are built")); check_run cr; @@ -1783,12 +2628,15 @@ namespace brep << ": unable to update conclusion check run " << *sd.conclusion_node_id; } + + completed = true; } } } return [iat = move (new_iat), cr = move (cr), + completed = completed, error = move (error), warn = move (warn)] (const tenant_service& ts) -> optional<string> { @@ -1828,10 +2676,37 @@ namespace brep << scr->state_string (); } #endif - *scr = cr; + *scr = cr; // Note: also updates node id if created. } else sd.check_runs.push_back (cr); + + if (bool c = completed) + { + // 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) + { + if (scr.state != build_state::built) + { + string sid (sd.repository_node_id + ':' + sd.report_sha); + + error << "tenant_service id " << sid + << ": out of order built notification service data update; " + << "check suite is no longer complete"; + + c = false; + break; + } + } + + if (c) + sd.completed = true; + } } return sd.json (); @@ -1841,6 +2716,8 @@ namespace brep string ci_github:: details_url (const build& b) const { + // This code is based on build_force_url() in mod/build.cxx. + // return options_->host () + "/@" + b.tenant + "?builds=" + mime_url_encode (b.package_name.string ()) + @@ -1848,7 +2725,101 @@ namespace brep "&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 ()); + "&th=" + mime_url_encode (b.toolchain_name) + '-' + + b.toolchain_version.string (); + } + + static optional<build_id> + parse_details_url (const string& details_url) + try + { + // See details_url() above for an idea of what the URL looks like. + + url u (details_url); + + build_id r; + + // Extract the tenant from the URL path. + // + // Example path: @d2586f57-21dc-40b7-beb2-6517ad7917dd + // + if (!u.path || u.path->size () != 37 || (*u.path)[0] != '@') + return nullopt; + + r.package.tenant = u.path->substr (1); + + // Extract the rest of the build_id members from the URL query. + // + if (!u.query) + return nullopt; + + bool pn (false), pv (false), tg (false), tc (false), pc (false), + th (false); + + // This URL query parsing code is based on + // web::apache::request::parse_url_parameters(). + // + for (const char* qp (u.query->c_str ()); qp != nullptr; ) + { + const char* vp (strchr (qp, '=')); + const char* ep (strchr (qp, '&')); + + if (vp == nullptr || (ep != nullptr && ep < vp)) + return nullopt; // Missing value. + + string n (mime_url_decode (qp, vp)); // Name. + + ++vp; // Skip '=' + + const char* ve (ep != nullptr ? ep : vp + strlen (vp)); // Value end. + + // Get the value as-is or URL-decode it. + // + auto rawval = [vp, ve] () { return string (vp, ve); }; + auto decval = [vp, ve] () { return mime_url_decode (vp, ve); }; + + auto make_version = [] (string&& v) + { + return canonical_version (brep::version (move (v))); + }; + + auto c = [&n] (bool& b, const char* s) + { + return n == s ? (b = true) : false; + }; + + if (c (pn, "builds")) r.package.name = package_name (decval ()); + else if (c (pv, "pv")) r.package.version = make_version (rawval ()); + else if (c (tg, "tg")) r.target = target_triplet (decval ()); + else if (c (tc, "tc")) r.target_config_name = decval (); + else if (c (pc, "pc")) r.package_config_name = decval (); + else if (c (th, "th")) + { + // Toolchain name and version. E.g. "public-0.17.0" + + string v (rawval ()); + + // Note: parsing code based on mod/mod-builds.cxx. + // + size_t p (v.find_first_of ('-')); + if (p >= v.size () - 1) + return nullopt; // Invalid format. + + r.toolchain_name = v.substr (0, p); + r.toolchain_version = make_version (v.substr (p + 1)); + } + + qp = ep != nullptr ? ep + 1 : nullptr; + } + + if (!pn || !pv || !tg || !tc || !pc || !th) + return nullopt; // Fail if any query parameters are absent. + + return r; + } + catch (const invalid_argument&) // Invalid url, brep::version, etc. + { + return nullopt; } optional<string> ci_github:: |