swarm: the notification router tells a PR's author when CI on it fails or is cancelled
This commit is contained in:
parent
d4e58f8741
commit
c8eaa93268
1 changed files with 490 additions and 60 deletions
|
|
@ -26,6 +26,11 @@ pub struct Delivery {
|
|||
pub title: String,
|
||||
pub url: String,
|
||||
pub sender: String,
|
||||
/// The account that opened the issue or PR.
|
||||
pub author: String,
|
||||
/// The commit at the PR's head when the delivery was made; empty for an
|
||||
/// issue. A CI run's delivery carries the PR as it was when the run began.
|
||||
pub head: String,
|
||||
pub is_pull: bool,
|
||||
pub assignees: BTreeSet<String>,
|
||||
/// The PR is a draft: the forge's `pull_request.draft`, true for a title
|
||||
|
|
@ -82,6 +87,15 @@ impl Delivery {
|
|||
.get("sender")
|
||||
.map(|s| text(s, "login"))
|
||||
.unwrap_or_default(),
|
||||
author: item
|
||||
.get("user")
|
||||
.map(|u| text(u, "login"))
|
||||
.unwrap_or_default(),
|
||||
head: item
|
||||
.pointer("/head/sha")
|
||||
.and_then(Json::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_owned(),
|
||||
is_pull,
|
||||
assignees,
|
||||
draft,
|
||||
|
|
@ -89,6 +103,30 @@ impl Delivery {
|
|||
})
|
||||
}
|
||||
|
||||
/// Read the delivery of Forgejo event type `event_type`. A `workflow_run_*`
|
||||
/// delivery carries the webhook that started the run as a JSON string in
|
||||
/// `run.event_payload`; for a run on a PR that is the PR's own delivery, so
|
||||
/// the PR is read from it, `action` is the run's outcome (`failure`,
|
||||
/// `cancelled`) and `sender` is the account that triggered the run. A run
|
||||
/// on anything else (a push to a branch with no PR) names no PR and gives
|
||||
/// `None`.
|
||||
pub fn from_event(event_type: &str, body: &[u8]) -> Option<Self> {
|
||||
let Some(outcome) = event_type.strip_prefix("workflow_run_") else {
|
||||
return Self::parse(body);
|
||||
};
|
||||
let json: Json = serde_json::from_slice(body).ok()?;
|
||||
let run = json.get("run")?;
|
||||
let mut delivery = Self::parse(run.get("event_payload")?.as_str()?.as_bytes())?;
|
||||
if !delivery.is_pull {
|
||||
return None;
|
||||
}
|
||||
outcome.clone_into(&mut delivery.action);
|
||||
if let Some(trigger) = run.pointer("/trigger_user/login").and_then(Json::as_str) {
|
||||
trigger.clone_into(&mut delivery.sender);
|
||||
}
|
||||
Some(delivery)
|
||||
}
|
||||
|
||||
/// The state bucket key for this delivery's issue: `<repo id>.<number>`,
|
||||
/// numeric so it is always a valid key whatever the repo is named.
|
||||
pub fn state_key(&self) -> String {
|
||||
|
|
@ -122,6 +160,11 @@ pub struct Snapshot {
|
|||
/// The agents holding a router todo for this issue.
|
||||
#[serde(default)]
|
||||
pub holders: BTreeSet<String>,
|
||||
/// The commit at the head of the PR as of the last delivery about the PR
|
||||
/// that was no CI run. A run that began at another commit is superseded
|
||||
/// and tells nobody anything.
|
||||
#[serde(default)]
|
||||
pub head: Option<String>,
|
||||
}
|
||||
|
||||
/// Whether `event_type` is an assign delivery, whose assignee list is the
|
||||
|
|
@ -153,6 +196,7 @@ pub fn fields(event_type: &str, delivery: &Delivery, snapshot: &Snapshot) -> Fie
|
|||
("title", text(&delivery.title)),
|
||||
("url", text(&delivery.url)),
|
||||
("sender", text(&delivery.sender)),
|
||||
("author", text(&delivery.author)),
|
||||
("draft", text(bool_text(delivery.draft))),
|
||||
("undrafted", text(bool_text(delivery.undrafted))),
|
||||
(
|
||||
|
|
@ -266,6 +310,89 @@ mod tests {
|
|||
serde_json::to_vec(&body).expect("encodes")
|
||||
}
|
||||
|
||||
/// A `workflow_run_*` delivery: the run, holding the webhook that started
|
||||
/// it as a JSON string.
|
||||
fn run_body(started_by: &Json) -> Vec<u8> {
|
||||
serde_json::to_vec(&serde_json::json!({
|
||||
"action": "failure",
|
||||
"run": {
|
||||
"id": 5,
|
||||
"trigger_user": { "login": "atlas" },
|
||||
"event_payload": serde_json::to_string(started_by).expect("encodes"),
|
||||
},
|
||||
}))
|
||||
.expect("encodes")
|
||||
}
|
||||
|
||||
fn pull_payload(author: &str) -> Json {
|
||||
serde_json::json!({
|
||||
"action": "synchronized",
|
||||
"number": 7,
|
||||
"pull_request": {
|
||||
"number": 7, "title": "t", "html_url": "u", "user": { "login": author },
|
||||
"head": { "sha": "abc123" },
|
||||
},
|
||||
"repository": { "id": 1, "full_name": "o/r" },
|
||||
"sender": { "login": "pusher" },
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_run_on_a_pull_request_reads_as_that_pull_request_with_the_runs_outcome() {
|
||||
let body = run_body(&pull_payload("iris"));
|
||||
for (event, outcome) in [
|
||||
("workflow_run_failure", "failure"),
|
||||
("workflow_run_cancelled", "cancelled"),
|
||||
] {
|
||||
let d = Delivery::from_event(event, &body).expect("a PR run");
|
||||
assert!(d.is_pull);
|
||||
assert_eq!((d.repo.as_str(), d.number), ("o/r", 7));
|
||||
assert_eq!(d.action, outcome);
|
||||
assert_eq!(d.author, "iris");
|
||||
assert_eq!(d.head, "abc123", "the commit the run began at");
|
||||
assert_eq!(d.sender, "atlas", "the account that triggered the run");
|
||||
assert_eq!(d.state_key(), "1.7");
|
||||
let f = fields(event, &d, &Snapshot::default());
|
||||
assert_eq!(f.get("author"), Some(&Value::Text("iris".to_owned())));
|
||||
assert_eq!(f.get("kind"), Some(&Value::Text("PR".to_owned())));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_run_that_names_no_pull_request_is_ignored() {
|
||||
let push = serde_json::json!({
|
||||
"ref": "refs/heads/main",
|
||||
"repository": { "id": 1, "full_name": "o/r" },
|
||||
});
|
||||
assert_eq!(
|
||||
Delivery::from_event("workflow_run_failure", &run_body(&push)),
|
||||
None
|
||||
);
|
||||
let issue: Json =
|
||||
serde_json::from_slice(&issue_assign("assigned", &["atlas"])).expect("json");
|
||||
assert_eq!(
|
||||
Delivery::from_event("workflow_run_failure", &run_body(&issue)),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
Delivery::from_event("workflow_run_failure", b"not json"),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
Delivery::from_event("workflow_run_failure", b"{\"run\":{}}"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_events_read_the_body_directly() {
|
||||
let body = pull_body("opened", "t", false, None);
|
||||
assert_eq!(
|
||||
Delivery::from_event("pull_request", &body),
|
||||
Delivery::parse(&body)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_forges_draft_flag_marks_a_draft() {
|
||||
let d = Delivery::parse(&pull_body("synchronized", "WIP: t", true, None)).expect("parses");
|
||||
|
|
@ -307,6 +434,7 @@ mod tests {
|
|||
let before = Snapshot {
|
||||
assignees: Some(BTreeSet::from(["atlas".to_owned(), "iris".to_owned()])),
|
||||
holders: BTreeSet::new(),
|
||||
..Snapshot::default()
|
||||
};
|
||||
let f = fields("issue_assign", &d, &before);
|
||||
assert_eq!(list(&f, "assignees_removed"), ["atlas"]);
|
||||
|
|
@ -316,6 +444,7 @@ mod tests {
|
|||
let before = Snapshot {
|
||||
assignees: Some(BTreeSet::from(["iris".to_owned()])),
|
||||
holders: BTreeSet::new(),
|
||||
..Snapshot::default()
|
||||
};
|
||||
assert_eq!(
|
||||
list(&fields("issue_assign", &d, &before), "assignees_added"),
|
||||
|
|
|
|||
Loading…
Reference in a new issue