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

@ -7,6 +7,12 @@
//! Each agent's harness applies them to its own todo list. The flows are the
//! built-in ones, changed by the `notify-flows` document ([`stored`]).
//!
//! A `request_reviewer` node is a request to swarm-controller, which holds the
//! forge token (`swarm_wire::router_sink`). It is sent once, after the event's
//! ops are queued and its snapshot is written, and the answer is only logged:
//! a controller that is down or slow must not hold up every agent's todos, and
//! the next push asks again.
//!
//! Per issue it keeps a snapshot in its state bucket ([`event::Snapshot`]):
//! the assignees it last saw and the agents it queued a todo for. An event is
//! acked once its ops are queued and the snapshot is written, so a crash
@ -26,6 +32,7 @@ use async_nats::jetstream::{self, consumer::pull, kv, message::PublishMessage};
use futures_util::{StreamExt as _, TryStreamExt as _};
use swarm_queue_client::{ClientSecret, QueueConfig};
use swarm_secret_client::stored_secret::StoredSecret;
use swarm_wire::router_sink::{self, Outcome, RequestReviewer};
use swarm_wire::{forge_events, notify_router, router_todo};
mod event;
@ -103,6 +110,7 @@ async fn main() -> Result<()> {
struct Router {
js: jetstream::Context,
nats: async_nats::Client,
events: jetstream::consumer::Consumer<pull::Config>,
state: kv::Store,
flows: stored::Flows,
@ -144,6 +152,7 @@ impl Router {
};
Ok(Self {
js,
nats: queue.clone(),
events,
state,
flows: stored::Flows::builtins(),
@ -200,9 +209,52 @@ impl Router {
let bytes = serde_json::to_vec(&routed.next).expect("a snapshot encodes");
self.state.put(&key, bytes.into()).await.map_err(retry)?;
}
for request in &routed.requests {
self.request_review(request).await;
}
Ok(())
}
/// Ask swarm-controller for one review and log what it answered. One
/// attempt: nothing here fails the event.
async fn request_review(&self, request: &flow::ReviewRequest) {
let wire = RequestReviewer {
repo: request.repo.clone(),
number: request.number,
reviewer: request.reviewer.clone(),
};
let body = serde_json::to_vec(&wire).expect("a review request encodes");
let sent = async_nats::Request::new()
.payload(body.into())
.timeout(Some(router_sink::REPLY_DEADLINE));
let (flow, repo, number, reviewer) = (
&request.flow,
&request.repo,
request.number,
&request.reviewer,
);
match self
.nats
.send_request(router_sink::REQUEST_REVIEWER_SUBJECT, sent)
.await
{
Ok(reply) => match Outcome::decode(&String::from_utf8_lossy(&reply.payload)) {
Outcome::Requested => {
tracing::info!(%flow, %repo, number, %reviewer, "notify router: review requested");
}
Outcome::Skipped(why) => {
tracing::info!(%flow, %repo, number, %reviewer, %why, "notify router: review not requested");
}
Outcome::Error(why) => {
tracing::warn!(%flow, %repo, number, %reviewer, %why, "notify router: the controller could not request the review");
}
},
Err(e) => {
tracing::warn!(%flow, %repo, number, %reviewer, error = %e, "notify router: no answer to a review request; not retried");
}
}
}
/// Queue one op on its agent's subject and wait for the stream's ack.
async fn queue(&self, seq: u64, action: &flow::Action) -> Result<()> {
let subject = router_todo::subject(&action.agent);