aboutsummaryrefslogtreecommitdiff
path: root/mod
diff options
context:
space:
mode:
authorFrancois Kritzinger <francois@codesynthesis.com>2024-12-12 14:22:18 +0200
committerFrancois Kritzinger <francois@codesynthesis.com>2024-12-13 11:09:58 +0200
commit4108a65af34829c5dd7e350ca1058eb4e0e4eee4 (patch)
treecc8e84e64951172a168f3633361e601bb256dc8d /mod
parentd50e2ae861180d6f965b0c4dee4d401d01f571b5 (diff)
ci-github: Cancel CI when history is overwritten
Cancel CI for previous, now-overwritten head commit when a forced push is done.
Diffstat (limited to 'mod')
-rw-r--r--mod/mod-ci-github-gh.cxx45
-rw-r--r--mod/mod-ci-github-gh.hxx33
-rw-r--r--mod/mod-ci-github.cxx66
-rw-r--r--mod/mod-ci-github.hxx6
4 files changed, 150 insertions, 0 deletions
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<bool> ();
+ 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<tenant_service> 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<optional<string> (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