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
105
swarm-wire/src/router_sink.rs
Normal file
105
swarm-wire/src/router_sink.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
//! 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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue