aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrancois Kritzinger <francois@codesynthesis.com>2025-02-20 10:50:16 +0200
committerFrancois Kritzinger <francois@codesynthesis.com>2025-02-20 15:56:51 +0200
commit8921de8fa65a2144e2eacf89b3009922ed849973 (patch)
treec775c6aba367c7195c9b7fa5013e4177ce27c137
parent2abb3ab35426189a9c478564a6426680c7cd3af0 (diff)
ci-github: Re-request check suite on internal CI cancellation
-rw-r--r--mod/mod-ci-github-gq.cxx125
-rw-r--r--mod/mod-ci-github-gq.hxx15
-rw-r--r--mod/mod-ci-github.cxx80
-rw-r--r--mod/mod-ci-github.hxx7
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&);