agents get a todo when assigned to an issue or PR, rewritten when unassigned and cleared on close
A new swarm service, swarm-notify-router, in its own container on the host
that sets deploy.notify-router.enable (defaulted on by allSwarmServices where
the host has the router's store leaf, like the claude login minter).
- swarm-controller registers instance-wide hooks for `issues` +
`issue_assign` and `pull_request` + `pull_request_assign`, updates the
event list of a hook already registered with fewer (leaving its active flag as it is), and keeps each
verified body unchanged in the SWARM_EVENTS stream on
$SWARM.events.forge.<X-Forgejo-Event-Type>, deduped by X-Forgejo-Delivery.
A delivery the stream did not keep answers 503.
- swarm-notify-router drains its durable consumer on that stream, runs its
flows (source -> match -> todo sink, stored as nodes plus edges and checked
to be a tree) and queues each op on $SWARM.router.todo.<agent> in the
work-queue stream ROUTER_TODOS. Built-in flows: assigned, unassigned (same
key, rewritten) and closed (prefix-clears every holder's router todos). A
per-issue snapshot in its notify-router-state bucket holds the last
assignees and the todo holders; only agents in the agent-status bucket get
ops, and an assignee that was not one is left out of the snapshot so the
next assign event retries it. After the first event that must be retried,
the rest of the batch is nakked so redelivery keeps stream order.
- hive-agent creates its own durable consumer todo-<agent> and applies each
op to its todo store as subsystem `router`, then acks; a refused op stops
the rest of its batch the same way. The store's (subsystem, key) dedup
makes a redelivery harmless. New Todos::clear_prefix.
- swarm-nats-auth: a --notify-router-client arm; the controller may publish
$SWARM.events.forge.*; each agent gets three {agent} templates (extended
consumer create whose filter the server checks, MSG.NEXT, ACK).
- Credentials: a notify-router store leaf minted by glue-bao-tls, a
swarm-notify-router bao policy (read on its own queue client secret), an
authelia machine client, the secret publisher entry, a journal forwarder
route. No manual step.
hive-forge-notify is unchanged, so agents also still get forge's own
notifications.
Refs #5571
This commit is contained in:
parent
156dd541e6
commit
0f7b403803
53 changed files with 3587 additions and 96 deletions
78
swarm-queue-client/src/test_nats.rs
Normal file
78
swarm-queue-client/src/test_nats.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
//! A throwaway `nats-server` with `JetStream`, for the tests whose subject is
|
||||
//! the server's own behaviour: whether it accepts a stream or consumer config,
|
||||
//! and what it delivers.
|
||||
//!
|
||||
//! The same shape as `swarm-controller`'s `test_nats`: the binary comes from
|
||||
//! [`NATS_SERVER_ENV`], which `nix/checks.nix` and `nix/devshell.nix` export,
|
||||
//! and a test that needs it panics when it is unset rather than skipping.
|
||||
|
||||
use std::path::Path;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::time::Duration;
|
||||
|
||||
/// The variable naming the `nats-server` binary.
|
||||
const NATS_SERVER_ENV: &str = "HIVE_NATS_SERVER";
|
||||
|
||||
/// The subdirectory holding nothing but the server's ports file.
|
||||
const PORTS_DIR: &str = "ports";
|
||||
|
||||
/// A running server on a loopback port it bound itself. Dropping it kills
|
||||
/// the server and removes its directory.
|
||||
pub(crate) struct TestNats {
|
||||
child: Child,
|
||||
dir: tempfile::TempDir,
|
||||
}
|
||||
|
||||
impl TestNats {
|
||||
pub(crate) fn start() -> Self {
|
||||
let bin = std::env::var(NATS_SERVER_ENV).unwrap_or_else(|_| {
|
||||
panic!("{NATS_SERVER_ENV} is exported by nix/checks.nix and nix/devshell.nix")
|
||||
});
|
||||
let dir = tempfile::Builder::new()
|
||||
.prefix("test-nats-")
|
||||
.tempdir()
|
||||
.expect("a fresh directory for the server");
|
||||
std::fs::create_dir(dir.path().join(PORTS_DIR)).expect("the ports file directory");
|
||||
let child = Command::new(&bin)
|
||||
.args(["-a", "127.0.0.1", "-p", "-1", "-js", "-sd"])
|
||||
.arg(dir.path().join("store"))
|
||||
.arg("--ports_file_dir")
|
||||
.arg(dir.path().join(PORTS_DIR))
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.unwrap_or_else(|e| panic!("starting {bin}: {e}"));
|
||||
Self { child, dir }
|
||||
}
|
||||
|
||||
/// A client connected to the server, once it accepts connections.
|
||||
pub(crate) async fn client(&self) -> async_nats::Client {
|
||||
for _ in 0..100 {
|
||||
if let Some(url) = announced_url(&self.dir.path().join(PORTS_DIR))
|
||||
&& let Ok(client) = async_nats::connect(&url).await
|
||||
{
|
||||
return client;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
panic!(
|
||||
"nats-server in {} never accepted a connection",
|
||||
self.dir.path().display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The client URL in the ports file the server writes once it listens.
|
||||
fn announced_url(dir: &Path) -> Option<String> {
|
||||
let ports = std::fs::read_dir(dir).ok()?.next()?.ok()?;
|
||||
let ports: serde_json::Value =
|
||||
serde_json::from_slice(&std::fs::read(ports.path()).ok()?).ok()?;
|
||||
ports["nats"][0].as_str().map(str::to_owned)
|
||||
}
|
||||
|
||||
impl Drop for TestNats {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.child.kill();
|
||||
let _ = self.child.wait();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue