request the review classifier's review when a PR's head moves
The router gets a request_reviewer node and a pull_request_sync source; the built-in head-classifier flow asks swarm-controller, over $SWARM.router.sink.request-reviewer, to request review-classifier on every push, unless its review of the head already stands. The controller subscribes to pull_request_sync on its pull-request hook and answers the router; nats-auth grants the router the publish and the controller the subscribe.
This commit is contained in:
parent
463c5cbfcc
commit
0b1669a364
1 changed files with 1231 additions and 64 deletions
|
|
@ -5,8 +5,9 @@
|
|||
//! type and this walk runs both. A `match` node passes when every one of its
|
||||
//! conditions holds, and then each of its children runs, so one event can
|
||||
//! reach several branches, in edge order, so a clear listed before an upsert
|
||||
//! is queued before it. A `todo` node emits one op per recipient. The router
|
||||
//! runs only flows that pass [`Flow::validate`].
|
||||
//! is queued before it. A `todo` node emits one op per recipient and a
|
||||
//! `request_reviewer` node one request for a review. The router runs only
|
||||
//! flows that pass [`Flow::validate`].
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
|
|
@ -32,17 +33,27 @@ pub struct Action {
|
|||
pub op: RouterTodoOp,
|
||||
}
|
||||
|
||||
/// Walk `flow` for one event of type `event_type` and return the ops it
|
||||
/// emits. Assumes [`Flow::validate`] passed.
|
||||
pub fn evaluate(flow: &Flow, event_type: &str, fields: &Fields) -> Vec<Action> {
|
||||
/// One request for `reviewer`'s review of pull request `number` of `repo`.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ReviewRequest {
|
||||
pub flow: String,
|
||||
pub node: String,
|
||||
pub repo: String,
|
||||
pub number: u64,
|
||||
pub reviewer: String,
|
||||
}
|
||||
|
||||
/// Walk `flow` for one event of type `event_type`, calling `leaf` on each
|
||||
/// `todo` and `request_reviewer` node the event reaches. Assumes
|
||||
/// [`Flow::validate`] passed.
|
||||
fn walk<'f>(flow: &'f Flow, event_type: &str, fields: &Fields, mut leaf: impl FnMut(&'f Node)) {
|
||||
let Some(root) = flow
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|n| matches!(&n.kind, NodeKind::Source { events } if events.iter().any(|e| e == event_type)))
|
||||
else {
|
||||
return Vec::new();
|
||||
return;
|
||||
};
|
||||
let mut actions = Vec::new();
|
||||
let mut stack = vec![root.id.as_str()];
|
||||
while let Some(id) = stack.pop() {
|
||||
let Some(node) = flow.node(id) else { continue };
|
||||
|
|
@ -53,12 +64,55 @@ pub fn evaluate(flow: &Flow, event_type: &str, fields: &Fields) -> Vec<Action> {
|
|||
stack.extend(flow.children(id).into_iter().rev());
|
||||
}
|
||||
}
|
||||
NodeKind::Todo(sink) => actions.extend(emit(flow, node, sink, fields)),
|
||||
NodeKind::Todo(_) | NodeKind::RequestReviewer { .. } => leaf(node),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Walk `flow` for one event of type `event_type` and return the todo ops it
|
||||
/// emits. Assumes [`Flow::validate`] passed.
|
||||
pub fn evaluate(flow: &Flow, event_type: &str, fields: &Fields) -> Vec<Action> {
|
||||
let mut actions = Vec::new();
|
||||
walk(flow, event_type, fields, |node| {
|
||||
if let NodeKind::Todo(sink) = &node.kind {
|
||||
actions.extend(emit(flow, node, sink, fields));
|
||||
}
|
||||
});
|
||||
actions
|
||||
}
|
||||
|
||||
/// Walk `flow` for one event of type `event_type` and return the review
|
||||
/// requests it makes. An event that is not about a pull request makes none.
|
||||
/// Assumes [`Flow::validate`] passed.
|
||||
pub fn requests(flow: &Flow, event_type: &str, fields: &Fields) -> Vec<ReviewRequest> {
|
||||
let mut requests = Vec::new();
|
||||
walk(flow, event_type, fields, |node| {
|
||||
if let NodeKind::RequestReviewer { reviewer } = &node.kind
|
||||
&& let Some(request) = request(flow, node, reviewer, fields)
|
||||
{
|
||||
requests.push(request);
|
||||
}
|
||||
});
|
||||
requests
|
||||
}
|
||||
|
||||
fn request(flow: &Flow, node: &Node, reviewer: &str, fields: &Fields) -> Option<ReviewRequest> {
|
||||
let text = |name: &str| match fields.get(name) {
|
||||
Some(Value::Text(v)) => Some(v.as_str()),
|
||||
_ => None,
|
||||
};
|
||||
if text("kind") != Some("PR") {
|
||||
return None;
|
||||
}
|
||||
Some(ReviewRequest {
|
||||
flow: flow.id.clone(),
|
||||
node: node.id.clone(),
|
||||
repo: text("repo")?.to_owned(),
|
||||
number: text("number")?.parse().ok()?,
|
||||
reviewer: reviewer.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
fn emit(flow: &Flow, node: &Node, sink: &TodoSink, fields: &Fields) -> Vec<Action> {
|
||||
let recipients = match &sink.to {
|
||||
Recipients::Agent(agent) => vec![agent.clone()],
|
||||
|
|
@ -333,6 +387,41 @@ mod tests {
|
|||
assert_eq!(got[0].op.source, "router:assigned");
|
||||
}
|
||||
|
||||
/// A request sits under the same match gating as a todo, and names the
|
||||
/// pull request the event concerns.
|
||||
#[test]
|
||||
fn a_reviewer_request_runs_under_its_match_and_only_for_a_pull_request() {
|
||||
let mut f = flow("assigned");
|
||||
f.nodes[0].kind = NodeKind::Source {
|
||||
events: vec!["pull_request_sync".to_owned()],
|
||||
};
|
||||
f.nodes[2].kind = NodeKind::RequestReviewer {
|
||||
reviewer: "argus".to_owned(),
|
||||
};
|
||||
assert_eq!(f.validate(), Ok(()));
|
||||
let pull = |action: &str, kind: &'static str| {
|
||||
let mut fields = event(action, &[], &[], &[]);
|
||||
fields.insert("kind", text(kind));
|
||||
fields
|
||||
};
|
||||
let got = requests(&f, "pull_request_sync", &pull("assigned", "PR"));
|
||||
assert_eq!(
|
||||
got,
|
||||
[ReviewRequest {
|
||||
flow: "assigned".to_owned(),
|
||||
node: "sink".to_owned(),
|
||||
repo: "hyperhive/hyperhive".to_owned(),
|
||||
number: 7,
|
||||
reviewer: "argus".to_owned(),
|
||||
}]
|
||||
);
|
||||
assert!(requests(&f, "pull_request_sync", &pull("opened", "PR")).is_empty());
|
||||
assert!(requests(&f, "pull_request_sync", &pull("assigned", "issue")).is_empty());
|
||||
assert!(requests(&f, "pull_request", &pull("assigned", "PR")).is_empty());
|
||||
// A request is no todo.
|
||||
assert!(evaluate(&f, "pull_request_sync", &pull("assigned", "PR")).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_placeholder_expands_to_its_field_and_an_unknown_one_is_an_error() {
|
||||
let fields = event("assigned", &["a", "b"], &[], &[]);
|
||||
|
|
|
|||
Loading…
Reference in a new issue