aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrancois Kritzinger <francois@codesynthesis.com>2024-06-06 10:50:58 +0200
committerFrancois Kritzinger <francois@codesynthesis.com>2024-06-06 11:11:39 +0200
commit65f90058ec7b1534fd93c525c9d171353737734a (patch)
treee2cb6054639d0846796c66e8a02e7b070e40241b
parent1398404033c24cb6c482c9547d5ef287143ae44f (diff)
Cancel and create new PR CI requests on base branch update
-rw-r--r--mod/mod-ci-github.cxx157
-rw-r--r--mod/mod-ci-github.hxx19
2 files changed, 144 insertions, 32 deletions
diff --git a/mod/mod-ci-github.cxx b/mod/mod-ci-github.cxx
index 96cd536..8ea730f 100644
--- a/mod/mod-ci-github.cxx
+++ b/mod/mod-ci-github.cxx
@@ -493,6 +493,73 @@ namespace brep
}
}
+ // The merge commits of any open pull requests with this branch as base
+ // branch will now be out of date, and thus so will be their CI builds and
+ // associated check runs.
+ //
+ // Unfortunately GitHub does not provide a webhook for PR base branch
+ // updates (as it does for PR head branch updates) so we have to handle it
+ // here. We do so by fetching the open pull requests with this branch as
+ // base branch and then recreating the CI requests (cancel existing,
+ // create new) for each pull request.
+ //
+ // If we fail to recreate any of the PR CI requests, they and their check
+ // runs will be left reflecting outdated merge commits. If the new merge
+ // commit failed to be generated (merge conflicts) the PR will not be
+ // mergeable which is not entirely catastrophic. But on the other hand, if
+ // all of the existing CI request's check runs have already succeeded and
+ // the new merge commit succeeds (no conflicts) with logic errors then a
+ // user would be able to merge a broken PR.
+ //
+ // Regardless of the nature of the error, we have to let the check suite
+ // handling code proceed so we only issue diagnostics.
+ //
+ {
+ // Fetch open pull requests with the check suite's head branch as base
+ // branch.
+ //
+ optional<vector<gh_pull_request>> prs (
+ gq_fetch_open_pull_requests (error,
+ iat->token,
+ sd.repository_node_id,
+ cs.check_suite.head_branch));
+
+ if (prs)
+ {
+ // Recreate each PR's CI request.
+ //
+ for (gh_pull_request& pr: *prs)
+ {
+ service_data prsd (sd.warning_success,
+ sd.installation_access.token,
+ sd.installation_access.expires_at,
+ sd.installation_id,
+ sd.repository_node_id,
+ pr.head_sha,
+ cs.repository.clone_url,
+ pr.number);
+
+ // Cancel the existing CI request and create a new unloaded CI
+ // request. After this call we will start getting the
+ // build_unloaded() notifications until (1) we load the request, (2)
+ // we cancel it, or (3) it gets archived after some timeout.
+ //
+ if (!create_pull_request_ci (error, warn, trace,
+ prsd.json (), pr.node_id,
+ true /* cancel_first */))
+ {
+ error << "pull request " << pr.node_id
+ << ": unable to create unloaded CI request";
+ }
+ }
+ }
+ else
+ {
+ error << "unable to fetch open pull requests with base branch "
+ << cs.check_suite.head_branch;
+ }
+ }
+
// Start CI for the check suite.
//
repository_location rl (cs.repository.clone_url + '#' +
@@ -672,33 +739,28 @@ namespace brep
l3 ([&]{trace << "installation_access_token { " << *iat << " }";});
- string sd (service_data (warning_success,
- move (iat->token),
- iat->expires_at,
- pr.installation.id,
- move (pr.repository.node_id),
- pr.pull_request.head_sha,
- pr.repository.clone_url,
- pr.pull_request.number)
- .json ());
-
- // Create unloaded CI request. After this call we will start getting the
- // build_unloaded() notifications until (1) we load the request, (2) we
- // cancel it, or (3) it gets archived after some timeout.
+ service_data sd (warning_success,
+ move (iat->token),
+ iat->expires_at,
+ pr.installation.id,
+ move (pr.repository.node_id),
+ pr.pull_request.head_sha,
+ pr.repository.clone_url,
+ pr.pull_request.number);
+
+ // Create unloaded CI request. Cancel the existing CI request first if the
+ // head branch has been updated (action is `synchronize`).
//
- // Note: use no delay since we need to (re)create the synthetic merge
- // check run as soon as possible.
+ // After this call we will start getting the build_unloaded()
+ // notifications until (1) we load the request, (2) we cancel it, or (3)
+ // it gets archived after some timeout.
//
- optional<string> tid (
- create (error, warn, &trace,
- *build_db_,
- tenant_service (move (pr.pull_request.node_id),
- "ci-github",
- move (sd)),
- chrono::seconds (30) /* interval */,
- chrono::seconds (0) /* delay */));
-
- if (!tid)
+ bool cancel_first (pr.action == "synchronize");
+
+ if (!create_pull_request_ci (error, warn, trace,
+ sd.json (),
+ pr.pull_request.node_id,
+ cancel_first))
{
fail << "pull request " << pr.pull_request.node_id
<< ": unable to create unloaded CI request";
@@ -707,6 +769,8 @@ namespace brep
return true;
}
+ // Note: only handles pull requests (not check suites).
+ //
function<optional<string> (const tenant_service&)> ci_github::
build_unloaded (tenant_service&& ts,
const diag_epilogue& log_writer) const noexcept
@@ -909,14 +973,11 @@ namespace brep
// Cancel the CI request.
//
+ // Ignore failure because this CI request may have been cancelled
+ // elsewhere due to an update to the PR base or head branches.
+ //
if (!cancel (error, warn, &trace, *build_db_, ts.type, ts.id))
- {
- // Nothing we can do but also highly unlikely.
- //
- error << "unable to cancel CI request: "
- << "no tenant for service type/ID "
- << ts.type << '/' << ts.id;
- }
+ l3 ([&]{trace << "CI for PR " << ts.id << " already cancelled";});
return nullptr; // No need to update service data in this case.
}
@@ -1798,6 +1859,38 @@ namespace brep
};
}
+ bool ci_github::
+ create_pull_request_ci (const basic_mark& error,
+ const basic_mark& warn,
+ const basic_mark& trace,
+ string sd,
+ const string& nid,
+ bool cf) const
+ {
+ // Cancel the existing CI request if asked to do so. Ignore failure
+ // because the request may already have been cancelled for a legitimate
+ // reason.
+ //
+ if (cf)
+ {
+ if (!cancel (error, warn, &trace, *build_db_, "ci-github", nid))
+ l3 ([&] {trace << "unable to cancel CI for pull request " << nid;});
+ }
+
+ // Create a new unloaded CI request.
+ //
+ tenant_service ts (nid, "ci-github", move (sd));
+
+ // Note: use no delay since we need to (re)create the synthetic merge
+ // check run as soon as possible.
+ //
+ return create (error, warn, &trace,
+ *build_db_, move (ts),
+ chrono::seconds (30) /* interval */,
+ chrono::seconds (0) /* delay */)
+ .has_value ();
+ }
+
string ci_github::
details_url (const build& b) const
{
diff --git a/mod/mod-ci-github.hxx b/mod/mod-ci-github.hxx
index 8cd085d..07295f2 100644
--- a/mod/mod-ci-github.hxx
+++ b/mod/mod-ci-github.hxx
@@ -79,6 +79,25 @@ namespace brep
bool
handle_pull_request (gh_pull_request_event, bool warning_success);
+ // Create an unloaded CI request for a pull request. If `cancel_first` is
+ // true, cancel its existing CI request first.
+ //
+ // Return true if an unloaded CI request was created. Ignore failure to
+ // cancel because the CI request may already have been cancelled for a
+ // legitimate reason.
+ //
+ // After this call we will start getting the build_unloaded()
+ // notifications until (1) we load the request, (2) we cancel it, or (3)
+ // it gets archived after some timeout.
+ //
+ bool
+ create_pull_request_ci (const basic_mark& error,
+ const basic_mark& warn,
+ const basic_mark& trace,
+ string service_data,
+ const string& pull_request_node_id,
+ bool cancel_first) const;
+
// Build a check run details_url for a build.
//
string