aboutsummaryrefslogtreecommitdiff
path: root/mod/mod-ci-github.cxx
diff options
context:
space:
mode:
authorFrancois Kritzinger <francois@codesynthesis.com>2024-06-04 14:29:27 +0200
committerFrancois Kritzinger <francois@codesynthesis.com>2024-12-10 16:34:15 +0200
commit4afea23ad12506fe817d350f858477341fb97e7b (patch)
tree9b9f404035c262cb524250339261ce2eb6cd7a67 /mod/mod-ci-github.cxx
parentdd0fbb6e941bf77204c7172f2e1498d9d5d7a3b3 (diff)
Cancel and create new PR CI requests on base branch update
Also Update GraphQL API: - Fix GraphQL response parsing - Fetch repository pull requests with the specified base branch
Diffstat (limited to 'mod/mod-ci-github.cxx')
-rw-r--r--mod/mod-ci-github.cxx158
1 files changed, 126 insertions, 32 deletions
diff --git a/mod/mod-ci-github.cxx b/mod/mod-ci-github.cxx
index d5131cf..dbc0c4b 100644
--- a/mod/mod-ci-github.cxx
+++ b/mod/mod-ci-github.cxx
@@ -493,6 +493,76 @@ 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 (and, no, GitHub does not invalidate those CI
+ // results automatically; see below).
+ //
+ // 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. Note also that we
+ // want to run this code as early as possible to minimize the window of
+ // the user seeing misleading CI results.
+ //
+ {
+ // 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, 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 +742,27 @@ 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, pr.pull_request.node_id,
+ cancel_first))
{
fail << "pull request " << pr.pull_request.node_id
<< ": unable to create unloaded CI request";
@@ -707,6 +771,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 +975,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.
}
@@ -1819,6 +1882,37 @@ namespace brep
};
}
+ bool ci_github::
+ create_pull_request_ci (const basic_mark& error,
+ const basic_mark& warn,
+ const basic_mark& trace,
+ const service_data& 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 other reasons.
+ //
+ 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", sd.json ());
+
+ // 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
{