Watch
0
0
Fork
You've already forked hyperhive
0

swarm: the notification router tells a PR's author when CI on it fails or is cancelled

This commit is contained in:
damocles 2026-11-06 21:25:08 +01:00 • committed by mara
commit c8eaa93268

View file

@ -74,6 +74,14 @@ pub const SOURCE_EVENTS: &[(&str, &str)] = &[
"pull_request_review_comment",
"A review submitted on a pull request that only comments",
),
(
"workflow_run_failure",
"A CI run on a pull request that finished with a failed job",
),
(
"workflow_run_cancelled",
"A CI run on a pull request that was cancelled",
),
];
/// The fields of an event a condition or a recipient can name, with what each
@ -87,6 +95,10 @@ pub const FIELDS: &[(&str, &str)] = &[
("title", "The issue or pull request title"),
("url", "The web link to the issue or pull request"),
("sender", "The account that caused the event"),
(
"author",
"The account that opened the issue or pull request",
),
(
"draft",
"true when the pull request is a draft (its title starts WIP: or [WIP]), else false",
@ -781,10 +793,15 @@ impl TodoSink {
/// `closed` key.
/// - `head-classifier`: a PR opened, reopened or pushed to requests the review
/// classifier's review.
/// - `ci-failed` and `ci-cancelled`: a CI run on a PR that failed or was
/// cancelled gives the PR's author a todo. Both write the same key, so the
/// later run's outcome replaces the earlier one.
/// - `ci-cleared`: a push to the PR's branch clears that todo, as the new run
/// replaces the old one.
#[must_use]
pub fn builtins() -> Vec<Flow> {
let assignment_key = "forge:{repo}#{number}:assignment";
vec![
let mut flows = vec![
chain(
"assigned",
&["issue_assign", "pull_request_assign"],
@ -812,6 +829,47 @@ pub fn builtins() -> Vec<Flow> {
),
closed(),
head_classifier(),
];
flows.extend(ci_flows());
flows
}
/// The flows that tell a PR's author about its CI: the todo for a failed run,
/// the same key rewritten for a cancelled one, and the clear a push makes.
fn ci_flows() -> Vec<Flow> {
let ci_key = "forge:{repo}#{number}:ci";
let to_author = |op, text: &str| TodoSink {
to: Recipients::Field("author".to_owned()),
op,
key: ci_key.to_owned(),
prefix: false,
text: text.to_owned(),
};
vec![
chain(
"ci-failed",
&["workflow_run_failure"],
"failure",
to_author(
Op::Upsert,
"CI failed on {kind} {repo}#{number}: {title} ({url})",
),
),
chain(
"ci-cancelled",
&["workflow_run_cancelled"],
"cancelled",
to_author(
Op::Upsert,
"CI was cancelled on {kind} {repo}#{number}: {title} ({url})",
),
),
chain(
"ci-cleared",
&["pull_request_sync"],
"synchronized",
to_author(Op::Clear, ""),
),
]
}
@ -1385,7 +1443,15 @@ mod tests {
let ids = |d: &Document| d.active().0.into_iter().map(|f| f.id).collect::<Vec<_>>();
assert_eq!(
ids(&Document::default()),
["assigned", "unassigned", "closed", "head-classifier"],
[
"assigned",
"unassigned",
"closed",
"head-classifier",
"ci-failed",
"ci-cancelled",
"ci-cleared"
],
"an empty document is the built-ins"
);
let mut off = custom("off");
@ -1399,7 +1465,15 @@ mod tests {
};
assert_eq!(
ids(&doc),
["assigned", "closed", "head-classifier", "triage"]
[
"assigned",
"closed",
"head-classifier",
"ci-failed",
"ci-cancelled",
"ci-cleared",
"triage"
]
);
let (_, skipped) = doc.active();
assert_eq!(skipped.len(), 2, "{skipped:?}");