diff options
author | Francois Kritzinger <francois@codesynthesis.com> | 2025-02-20 10:50:16 +0200 |
---|---|---|
committer | Francois Kritzinger <francois@codesynthesis.com> | 2025-02-20 15:56:51 +0200 |
commit | 8921de8fa65a2144e2eacf89b3009922ed849973 (patch) | |
tree | c775c6aba367c7195c9b7fa5013e4177ce27c137 | |
parent | 2abb3ab35426189a9c478564a6426680c7cd3af0 (diff) |
ci-github: Re-request check suite on internal CI cancellation
-rw-r--r-- | mod/mod-ci-github-gq.cxx | 125 | ||||
-rw-r--r-- | mod/mod-ci-github-gq.hxx | 15 | ||||
-rw-r--r-- | mod/mod-ci-github.cxx | 80 | ||||
-rw-r--r-- | mod/mod-ci-github.hxx | 7 |
4 files changed, 225 insertions, 2 deletions
diff --git a/mod/mod-ci-github-gq.cxx b/mod/mod-ci-github-gq.cxx index 24f8b47..9d20c9e 100644 --- a/mod/mod-ci-github-gq.cxx +++ b/mod/mod-ci-github-gq.cxx @@ -1038,6 +1038,131 @@ namespace brep return r; } + + // Serialize a GraphQL query that rerequests a check suite. + // + // Throw invalid_argument if any of the node ids are not a valid GraphQL + // string. + // + static string + gq_mutation_rerequest_check_suite (const string& rid, const string& nid) + { + ostringstream os; + + os << "mutation {" << '\n' + << " rerequestCheckSuite(input: {repositoryId: " << gq_str (rid) << '\n' + << " checkSuiteId: " << gq_str (nid) << '\n' + << " }) {" << '\n' + << " checkSuite { id }" << '\n' + << " }" << '\n' + << "}"; + + return os.str (); + } + + bool + gq_rerequest_check_suite (const basic_mark& error, + const string& iat, + const string& rid, + const string& nid) + { + // Let invalid_argument from gq_mutation_rerequest_check_suite() + // propagate. + // + string rq ( + gq_serialize_request (gq_mutation_rerequest_check_suite (rid, nid))); + + try + { + // Response parser. + // + struct resp + { + // True if the check suite was found (i.e., the node id was valid). + // + bool found = false; + + // Example response (note: outer `data` object already stripped at + // this point): + // + // {"rerequestCheckSuite":{"checkSuite":{"id":"CS_kwDOLc8CoM8AAAAIDgO-Qw"}}} + // + resp (json::parser& p) + { + using event = json::event; + + gq_parse_response (p, [this] (json::parser& p) + { + p.next_expect (event::begin_object); // Outer { + + // This object will be null if the repository or check suite node + // ids were invalid. + // + if (p.next_expect_member_object_null ("rerequestCheckSuite")) + { + found = true; + + p.next_expect_member_object ("checkSuite"); + p.next_expect_member_string ("id"); + p.next_expect (event::end_object); // checkSuite + p.next_expect (event::end_object); // rerequestCheckSuite + } + + p.next_expect (event::end_object); // Outer } + }); + } + + resp () = default; + } rs; + + uint16_t sc (github_post (rs, + "graphql", // API Endpoint. + strings {"Authorization: Bearer " + iat}, + move (rq))); + + if (sc == 200) + { + if (!rs.found) + { + error << "check suite '" << nid + << "' not found in repository '" << rid << '\''; + return false; + } + + return true; + } + else + error << "failed to re-request check suite: error HTTP response status " + << sc; + } + catch (const json::invalid_json_input& e) // struct resp (via github_post()) + { + // Note: e.name is the GitHub API endpoint. + // + error << "malformed JSON in response from " << e.name << ", line: " + << e.line << ", column: " << e.column << ", byte offset: " + << e.position << ", error: " << e; + } + catch (const invalid_argument& e) // github_post() + { + error << "malformed header(s) in response: " << e; + } + catch (const system_error& e) // github_post() + { + error << "unable to re-request check suite (errno=" << e.code () << "): " + << e.what (); + } + catch (const runtime_error& e) // struct resp + { + // GitHub response contained error(s) (could be ours or theirs at this + // point). + // + error << "unable to re-request check suite: " << e; + } + + return false; + } + // Serialize a GraphQL query that fetches a pull request from GitHub. // // Throw invalid_argument if the node id is not a valid GraphQL string. diff --git a/mod/mod-ci-github-gq.hxx b/mod/mod-ci-github-gq.hxx index 77b78e4..4e13606 100644 --- a/mod/mod-ci-github-gq.hxx +++ b/mod/mod-ci-github-gq.hxx @@ -124,6 +124,21 @@ namespace brep const string& node_id, gq_built_result); + // Re-request a check suite. This will result in the delivery of a + // check_suite webhook with the "rerequested" action, just as if the user + // had clicked "re-run all checks" in the GitHub UI. + // + // Return false and issue diagnostics if the request failed. + // + // Throw invalid_argument if the passed data is invalid, missing, or + // inconsistent. + // + bool + gq_rerequest_check_suite (const basic_mark& error, + const string& installation_access_token, + const string& repository_id, + const string& node_id); + // Fetch pre-check information for a pull request from GitHub. This // information is used to decide whether or not to CI the PR and is // comprised of the PR's head commit SHA, whether its head branch is behind diff --git a/mod/mod-ci-github.cxx b/mod/mod-ci-github.cxx index 8f0af2e..c59b1af 100644 --- a/mod/mod-ci-github.cxx +++ b/mod/mod-ci-github.cxx @@ -1034,8 +1034,9 @@ namespace brep // Replace the existing CI tenant if it exists. // // Note that GitHub UI does not allow re-running the entire check suite - // until all the check runs are completed. - // + // until all the check runs are completed. However if we got here as a + // result of re-requesting the check suite from build_canceled() the check + // runs could be in any state. // Create an unloaded CI tenant. // @@ -3213,6 +3214,81 @@ namespace brep error << "unhandled exception: " << e.what (); } + void ci_github:: + build_canceled (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); + + // We end up here when the service data could not be saved so re-request + // the check suite in order to restart all of the builds. + + // Load the unsaved service data. + // + service_data sd; + try + { + sd = service_data (*ts.data); + } + catch (const invalid_argument& e) + { + error << "failed to parse service data: " << e; + return; + } + + // 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 (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; + + if (iat != nullptr) + { + // Re-request the check suite. + + // Note that the conclusion check run is created before the tenant is + // loaded so the unsaved service data should always contain the check + // suite node id. + // + assert (sd.check_suite_node_id.has_value ()); + + // Let unlikely invalid_argument propagate. + // + if (gq_rerequest_check_suite (error, + iat->token, + sd.repository_node_id, + *sd.check_suite_node_id)) + { + l3 ([&]{trace << "re-requested check suite " << *sd.check_suite_node_id;}); + } + else + error << "failed to re-request check suite " << *sd.check_suite_node_id; + } + } + catch (const std::exception& e) + { + NOTIFICATION_DIAG (log_writer); + + error << "unhandled exception: " << e.what (); + } + string ci_github:: details_url (const build& b) const { diff --git a/mod/mod-ci-github.hxx b/mod/mod-ci-github.hxx index 0c90bb1..9a271aa 100644 --- a/mod/mod-ci-github.hxx +++ b/mod/mod-ci-github.hxx @@ -84,6 +84,13 @@ namespace brep const tenant_service& ts, const diag_epilogue& log_writer) const noexcept override; + // @@ TODO + //virtual void + void + build_canceled (const string& tenant_id, + const tenant_service&, + const diag_epilogue& log_writer) const noexcept; //override; + private: virtual void init (cli::scanner&); |