From 4108a65af34829c5dd7e350ca1058eb4e0e4eee4 Mon Sep 17 00:00:00 2001 From: Francois Kritzinger Date: Thu, 12 Dec 2024 14:22:18 +0200 Subject: ci-github: Cancel CI when history is overwritten Cancel CI for previous, now-overwritten head commit when a forced push is done. --- mod/mod-ci-github-gh.cxx | 45 +++++++++++++++++++++++++++++++++ mod/mod-ci-github-gh.hxx | 33 ++++++++++++++++++++++++ mod/mod-ci-github.cxx | 66 ++++++++++++++++++++++++++++++++++++++++++++++++ mod/mod-ci-github.hxx | 6 +++++ 4 files changed, 150 insertions(+) (limited to 'mod') diff --git a/mod/mod-ci-github-gh.cxx b/mod/mod-ci-github-gh.cxx index 021ff6b..70155ad 100644 --- a/mod/mod-ci-github-gh.cxx +++ b/mod/mod-ci-github-gh.cxx @@ -656,6 +656,51 @@ namespace brep return os; } + // gh_push_event + // + gh_push_event:: + gh_push_event (json::parser& p) + { + p.next_expect (event::begin_object); + + bool rf (false), bf (false), af (false), fd (false), rp (false); + + // Skip unknown/uninteresting members. + // + while (p.next_expect (event::name, event::end_object)) + { + auto c = [&p] (bool& v, const char* s) + { + return p.name () == s ? (v = true) : false; + }; + + if (c (rf, "ref")) ref = p.next_expect_string (); + else if (c (bf, "before")) before = p.next_expect_string (); + else if (c (af, "after")) after = p.next_expect_string (); + else if (c (fd, "forced")) forced = p.next_expect_boolean (); + else if (c (rp, "repository")) repository = gh_repository (p); + else p.next_expect_value_skip (); + } + + if (!rf) missing_member (p, "gh_push_event", "ref"); + if (!bf) missing_member (p, "gh_push_event", "before"); + if (!af) missing_member (p, "gh_push_event", "after"); + if (!fd) missing_member (p, "gh_push_event", "forced"); + if (!rp) missing_member (p, "gh_push_event", "repository"); + } + + ostream& + operator<< (ostream& os, const gh_push_event& p) + { + os << "ref: " << p.ref + << ", before: " << p.before + << ", after: " << p.after + << ", forced: " << p.forced + << ", repository { " << p.repository << " }"; + + return os; + } + // gh_installation_access_token // // Example JSON: diff --git a/mod/mod-ci-github-gh.hxx b/mod/mod-ci-github-gh.hxx index ab6dbaa..24e9cda 100644 --- a/mod/mod-ci-github-gh.hxx +++ b/mod/mod-ci-github-gh.hxx @@ -215,6 +215,36 @@ namespace brep gh_pull_request_event () = default; }; + // The push webhook event. + // + struct gh_push_event + { + // The full git ref that was pushed. Example: refs/heads/main or + // refs/tags/v3.14.1. + // + string ref; + + // The SHA of the most recent commit on ref before the push. + // + string before; + + // The SHA of the most recent commit on ref after the push. + // + string after; + + // True if this was a forced push of the ref. I.e., history was + // overwritten. + // + bool forced; + + gh_repository repository; + + explicit + gh_push_event (json::parser&); + + gh_push_event () = default; + }; + // Installation access token (IAT) returned when we authenticate as a GitHub // app installation. // @@ -297,6 +327,9 @@ namespace brep operator<< (ostream&, const gh_pull_request_event&); ostream& + operator<< (ostream&, const gh_push_event&); + + ostream& operator<< (ostream&, const gh_installation_access_token&); } diff --git a/mod/mod-ci-github.cxx b/mod/mod-ci-github.cxx index 721c047..0f9a926 100644 --- a/mod/mod-ci-github.cxx +++ b/mod/mod-ci-github.cxx @@ -494,6 +494,27 @@ namespace brep return true; } } + else if (event == "push") + { + gh_push_event ps; + try + { + json::parser p (body.data (), body.size (), "push event"); + + ps = gh_push_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)); + } + + return handle_push_request (move (ps)); + } else { // Log to investigate. @@ -1465,6 +1486,51 @@ namespace brep return true; } + bool ci_github:: + handle_push_request (gh_push_event ps) + { + HANDLER_DIAG; + + l3 ([&]{trace << "push event { " << ps << " }";}); + + // Do nothing if this is a fast-forwarding push. + // + if (!ps.forced) + { + l3 ([&]{trace << "ignoring fast-forward push " + << ps.after << " to " << ps.ref;}); + return true; + } + + // Cancel the CI tenant associated with the overwritten previous head + // commit. + + // Service id that will uniquely identify the CI tenant. + // + string sid (ps.repository.node_id + ':' + ps.before); + + if (optional ts = cancel (error, warn, + verb_ ? &trace : nullptr, + *build_db_, retry_, + "ci-github", sid)) + { + l3 ([&]{trace << "forced push to " << ps.ref + << ": canceled CI of previous head commit" + << " with tenant_service id " << sid;}); + } + else + { + // It's possible that there was no CI for the previous commit for + // various reasons (e.g., CI was not enabled). + // + l3 ([&]{trace << "forced push to " << ps.ref + << ": failed to cancel CI of previous head commit" + << " with tenant_service id " << sid;}); + } + + return true; + } + function (const string&, const tenant_service&)> ci_github:: build_unloaded (const string& ti, tenant_service&& ts, diff --git a/mod/mod-ci-github.hxx b/mod/mod-ci-github.hxx index 059801a..8f7c10b 100644 --- a/mod/mod-ci-github.hxx +++ b/mod/mod-ci-github.hxx @@ -114,6 +114,12 @@ namespace brep bool handle_pull_request (gh_pull_request_event, bool warning_success); + // Handle forced push events by canceling the overwritten previous head + // commit's CI request. + // + bool + handle_push_request (gh_push_event); + // Build a check run details_url for a build. // string -- cgit v1.1