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
|
|
@ -10,6 +10,10 @@
|
|||
//! the payload and emits a semantic message — *the knowledge repo changed*,
|
||||
//! *deploy agent X at rev Y* — addressed to the hives that need it. One place
|
||||
//! decides what a forge payload means, so no hive re-derives it.
|
||||
//!
|
||||
//! The `issues` and `pull_request` hooks are the exception: their bodies are
|
||||
//! kept unchanged in the forge events stream (`crate::forge_events`), and the
|
||||
//! notification router decides what they mean.
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use axum::{
|
||||
|
|
@ -225,6 +229,14 @@ pub(super) enum DeliveryKind {
|
|||
/// for the review classifier's account (`crate::review_classifier::review`).
|
||||
#[strum(serialize = "review-request")]
|
||||
ReviewRequest,
|
||||
/// Every `issues` event on every repo in the instance (opened, closed,
|
||||
/// assigned, …), kept for the notification router (`crate::forge_events`).
|
||||
#[strum(serialize = "issues")]
|
||||
Issues,
|
||||
/// Every `pull_request` event on every repo in the instance, kept the
|
||||
/// same way.
|
||||
#[strum(serialize = "pull-request")]
|
||||
PullRequest,
|
||||
}
|
||||
|
||||
/// The route prefix a registered `target_url` must point at.
|
||||
|
|
@ -240,11 +252,13 @@ impl DeliveryKind {
|
|||
/// Every kind, for the registration sweep. An array rather than a
|
||||
/// hand-written list at the call site, so adding a kind cannot leave one
|
||||
/// hook unregistered.
|
||||
pub(super) const ALL: [Self; 4] = [
|
||||
pub(super) const ALL: [Self; 6] = [
|
||||
Self::Knowledge,
|
||||
Self::ConfigPr,
|
||||
Self::VcsActivity,
|
||||
Self::ReviewRequest,
|
||||
Self::Issues,
|
||||
Self::PullRequest,
|
||||
];
|
||||
|
||||
/// The `target_url` to register with Forgejo for this kind, given the
|
||||
|
|
@ -352,18 +366,22 @@ pub(super) fn verify(
|
|||
/// comment for why both this and a periodic poll write the same cache), and a
|
||||
/// merge queues a deploy of the merged commit on the agent's hive. A
|
||||
/// `ReviewRequest` delivery naming the review classifier's account starts a
|
||||
/// review run in the background.
|
||||
/// review run in the background. An `Issues` or `PullRequest` delivery is
|
||||
/// kept in the forge events stream for the notification router
|
||||
/// (`crate::forge_events`).
|
||||
///
|
||||
/// Returns 200 on an accepted delivery so Forgejo does not retry. A refused
|
||||
/// one answers 401 (bad signature) or 503 (this daemon has no secret), and
|
||||
/// an unknown `{kind}` answers 404 rather than a cheerful 200.
|
||||
/// an unknown `{kind}` answers 404 rather than a cheerful 200. An `Issues` or
|
||||
/// `PullRequest` delivery the stream did not keep answers 503, so the forge's
|
||||
/// delivery log shows it failed and a redelivery can keep it.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/webhook/forge/{kind}",
|
||||
params(
|
||||
("kind" = String, Path, description = "which swarm-wide hook this delivery arrived on: \
|
||||
`knowledge`, `config-pr`, `vcs-activity` or \
|
||||
`review-request`")
|
||||
`knowledge`, `config-pr`, `vcs-activity`, \
|
||||
`review-request`, `issues` or `pull-request`")
|
||||
),
|
||||
request_body(
|
||||
content = String,
|
||||
|
|
@ -376,7 +394,9 @@ pub(super) fn verify(
|
|||
(status = 200, description = "delivery accepted", body = String),
|
||||
(status = 401, description = "bad or missing HMAC signature"),
|
||||
(status = 404, description = "unknown hook kind in the path"),
|
||||
(status = 503, description = "HMAC secret unavailable at startup"),
|
||||
(status = 503, description = "HMAC secret unavailable at startup, or an `issues` or \
|
||||
`pull-request` delivery the forge events stream did not \
|
||||
keep"),
|
||||
),
|
||||
tag = "webhook"
|
||||
)]
|
||||
|
|
@ -430,6 +450,12 @@ pub(super) async fn post_webhook_forge(
|
|||
}
|
||||
}
|
||||
DeliveryKind::ReviewRequest => crate::review_classifier::review::on_delivery(&state, &body),
|
||||
DeliveryKind::Issues | DeliveryKind::PullRequest => {
|
||||
if let Err(not_kept) = crate::forge_events::publish(&state, &headers, body).await {
|
||||
tracing::warn!(kind = <&str>::from(kind), reason = ?not_kept, "webhook: forge event not kept");
|
||||
return (StatusCode::SERVICE_UNAVAILABLE, "forge event not kept").into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(StatusCode::OK, "ok").into_response()
|
||||
|
|
|
|||
Loading…
Reference in a new issue