Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/swarm-wire/src/router_sink.rs
atlas 0b1669a364 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.
2026-10-24 17:23:36 +02:00

105 lines
3.6 KiB
Rust

//! The notification router's sinks that act on the forge, which it asks
//! swarm-controller to run.
//!
//! The router has no forge token. For a `request_reviewer` node
//! ([`crate::notify_flows::NodeKind::RequestReviewer`]) it sends a request on
//! [`REQUEST_REVIEWER_SUBJECT`] and swarm-controller answers on the request's
//! reply subject with an [`Outcome`], having acted under its own token.
//!
//! Three crates name these: the router sends, the controller answers, and the
//! auth-callout responder grants each its side. The controller is the
//! subject's one subscriber and the router its one publisher.
use std::time::Duration;
use serde::{Deserialize, Serialize};
/// The subject a request for a review goes to.
pub const REQUEST_REVIEWER_SUBJECT: &str = "$SWARM.router.sink.request-reviewer";
/// How long the router waits for the answer. The controller's reply grant
/// lasts this long, so its own work stops sooner ([`WORK_DEADLINE`]).
pub const REPLY_DEADLINE: Duration = Duration::from_secs(30);
/// How long the controller works on one request before it answers
/// [`Outcome::Error`].
pub const WORK_DEADLINE: Duration = Duration::from_secs(20);
/// Ask for `reviewer`'s review of pull request `number` of `repo`.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RequestReviewer {
/// `owner/name`.
pub repo: String,
pub number: u64,
/// The reviewer's forge login.
pub reviewer: String,
}
/// What the controller did with a [`RequestReviewer`], on the wire as one
/// line: `requested`, `skipped:<why>` or `error:<message>`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Outcome {
/// The forge accepted the request.
Requested,
/// Nothing was sent, and why.
Skipped(String),
/// The request failed, and why.
Error(String),
}
impl Outcome {
/// The wire line.
#[must_use]
pub fn encode(&self) -> String {
match self {
Self::Requested => "requested".to_owned(),
Self::Skipped(why) => format!("skipped:{why}"),
Self::Error(why) => format!("error:{why}"),
}
}
/// The outcome a reply line says; a line that is none of the three is an
/// [`Outcome::Error`] saying so.
#[must_use]
pub fn decode(line: &str) -> Self {
if line == "requested" {
Self::Requested
} else if let Some(why) = line.strip_prefix("skipped:") {
Self::Skipped(why.to_owned())
} else if let Some(why) = line.strip_prefix("error:") {
Self::Error(why.to_owned())
} else {
Self::Error(format!("unreadable reply {line:?}"))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_outcome_round_trips_its_line() {
for outcome in [
Outcome::Requested,
Outcome::Skipped("a review stands".to_owned()),
Outcome::Error("the forge said: no: why".to_owned()),
] {
assert_eq!(Outcome::decode(&outcome.encode()), outcome);
}
assert_eq!(Outcome::Requested.encode(), "requested");
assert_eq!(Outcome::Skipped("x".to_owned()).encode(), "skipped:x");
assert_eq!(Outcome::Error("x".to_owned()).encode(), "error:x");
}
#[test]
fn a_line_that_is_no_outcome_reads_as_an_error() {
assert!(matches!(Outcome::decode("ok"), Outcome::Error(e) if e.contains("ok")));
}
/// The reply grant covers the work and the router's wait covers the grant.
#[test]
fn the_controller_stops_working_before_the_router_stops_waiting() {
assert!(WORK_DEADLINE < REPLY_DEADLINE);
}
}