Watch
0
0
Fork
You've already forked hyperhive
0

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:
atlas 2026-10-24 14:28:27 +02:00
commit 0b1669a364
20 changed files with 1231 additions and 64 deletions

View file

@ -8,13 +8,16 @@ use swarm_wire::router_todo::Op;
use crate::event::{Delivery, Snapshot, fields, is_assign};
use swarm_wire::notify_flows::Flow;
use crate::flow::{Action, evaluate};
use crate::flow::{Action, ReviewRequest, evaluate, requests};
/// What one event comes to.
#[derive(Debug, PartialEq, Eq)]
pub struct Routed {
/// The ops to queue, one per agent that exists.
pub actions: Vec<Action>,
/// The reviews to ask the forge for. Not limited to agents: a reviewer is
/// a forge account.
pub requests: Vec<ReviewRequest>,
/// Recipients that are not agents of this swarm, so got nothing.
pub skipped: Vec<String>,
pub next: Snapshot,
@ -32,6 +35,10 @@ pub fn route(
let fields = fields(event_type, delivery, snapshot);
let mut actions = Vec::new();
let mut skipped = Vec::new();
let mut review_requests = Vec::new();
for flow in flows {
review_requests.extend(requests(flow, event_type, &fields));
}
for action in flows.iter().flat_map(|f| evaluate(f, event_type, &fields)) {
if is_agent(&action.agent) {
actions.push(action);
@ -62,6 +69,7 @@ pub fn route(
next.holders.retain(|h| !cleared.contains(h));
Routed {
actions,
requests: review_requests,
skipped,
next,
}
@ -186,6 +194,44 @@ mod tests {
);
}
/// A push to a PR makes one request for the classifier's review, whether
/// or not the classifier is an agent, and touches nothing else.
#[test]
fn a_push_to_a_pull_request_requests_the_classifiers_review_once() {
let flows = builtins();
let pull = Delivery {
is_pull: true,
..delivery("synchronized", &[])
};
let before = Snapshot {
assignees: Some(BTreeSet::from(["atlas".to_owned()])),
holders: BTreeSet::from(["atlas".to_owned()]),
};
let r = route("pull_request_sync", &pull, &before, &flows, |_| false);
assert_eq!(
r.requests,
[ReviewRequest {
flow: "head-classifier".to_owned(),
node: "request".to_owned(),
repo: "o/r".to_owned(),
number: 9,
reviewer: "review-classifier".to_owned(),
}]
);
assert!(r.actions.is_empty() && r.skipped.is_empty());
assert_eq!(r.next, before);
// The source gates: another PR event requests nothing.
for other in ["pull_request", "pull_request_assign", "issues"] {
let r = route(other, &pull, &before, &flows, |_| true);
assert!(r.requests.is_empty(), "{other}");
}
// Only a PR can be asked to review.
let issue = delivery("synchronized", &[]);
let r = route("pull_request_sync", &issue, &before, &flows, |_| true);
assert!(r.requests.is_empty());
}
#[test]
fn an_event_no_flow_matches_changes_nothing() {
let before = Snapshot {