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
20 changed files with 1231 additions and 64 deletions
|
|
@ -32,7 +32,8 @@ grafana, the controller, authelia, …) →
|
|||
its repo's own yes/no rules and posts the result as a review. →
|
||||
[`review-classifier.md`](review-classifier.md)
|
||||
- **notification router** — forge events turned into todos on the agents
|
||||
they concern: assigned, unassigned, cleared on close. →
|
||||
they concern: assigned, unassigned, cleared on close; and the classifier's
|
||||
review requested on a push. →
|
||||
[`notify-router.md`](notify-router.md)
|
||||
- **swarm UI** — the operator's day-to-day surface, on the swarm apex,
|
||||
`admins` only. → [`ui.md`](ui.md)
|
||||
|
|
@ -415,7 +416,7 @@ itself, all under `https://<swarm.domain>/webhook/forge/`:
|
|||
- an instance-wide `pull_request_review_request` system hook, which starts
|
||||
the [review classifier](review-classifier.md#trigger-and-lifecycle)
|
||||
- instance-wide `issues` + `issue_assign` and `pull_request` +
|
||||
`pull_request_assign` system hooks, whose deliveries it
|
||||
`pull_request_assign` + `pull_request_sync` system hooks, whose deliveries it
|
||||
keeps in the `SWARM_EVENTS` stream for the
|
||||
[notification router](notify-router.md#the-path)
|
||||
|
||||
|
|
|
|||
|
|
@ -41,8 +41,9 @@ router notices an unassign only on an issue it has seen an assign event for.
|
|||
|
||||
1. swarm-controller registers two instance-wide hooks, one for the `issues`
|
||||
and `issue_assign` events at `/webhook/forge/issues` and one for
|
||||
`pull_request` and `pull_request_assign` at `/webhook/forge/pull-request`.
|
||||
At each start it adds any of these events a registered hook lacks.
|
||||
`pull_request`, `pull_request_assign` and `pull_request_sync` (a push to an
|
||||
open PR's branch) at `/webhook/forge/pull-request`. At each start it adds
|
||||
any of these events a registered hook lacks.
|
||||
2. On each delivery the controller checks the HMAC and publishes the body,
|
||||
unchanged, to the `SWARM_EVENTS` stream on
|
||||
`$SWARM.events.forge.<X-Forgejo-Event-Type>`, such as
|
||||
|
|
@ -68,16 +69,19 @@ The router holds no forge token and no webhook secret, and registers no hook.
|
|||
|
||||
A flow is a graph of nodes joined by edges: one `source` node naming the forge
|
||||
event types it starts from, `match` nodes that pass when every condition
|
||||
holds, and `todo` nodes that emit one todo op per recipient. A match node can
|
||||
have more than one child, and each one runs, so one event can reach several
|
||||
todos. A flow must be a tree: one source, one parent for every other node,
|
||||
every node reachable, no children under a `todo`. The most is 64 nodes, 16
|
||||
holds, `todo` nodes that emit one todo op per recipient, and `request_reviewer`
|
||||
nodes that ask for a review. A match node can have more than one child, and
|
||||
each one runs, so one event can reach several leaves. A flow must be a tree:
|
||||
one source, one parent for every other node, every node reachable, no children
|
||||
under a `todo` or a `request_reviewer`. The most is 64 nodes, 16
|
||||
levels deep, and 100 custom flows. A `todo` node sends to the agents an event
|
||||
field holds (`assignees_added`, `todo_holders`, …) or to one named agent.
|
||||
|
||||
The router ships the three flows above, each a source → match on `action` →
|
||||
todo. The code defines them (`swarm_wire::notify_flows::builtins`), so a
|
||||
build that changes one reaches every swarm. A custom flow has the same type and
|
||||
todo, and `head-classifier`: source `pull_request_sync` → `request_reviewer`
|
||||
for `review-classifier` ([below](#requesting-a-review)). The code defines them
|
||||
(`swarm_wire::notify_flows::builtins`), so a build that changes one reaches
|
||||
every swarm. Each switches off on `/notifications`. A custom flow has the same type and
|
||||
the same evaluator, so it can do anything a built-in does.
|
||||
|
||||
**The stored document.** swarm-controller keeps the rest in one document, the
|
||||
|
|
@ -86,8 +90,8 @@ switched off, and the custom flows. Only the controller writes it, through
|
|||
`GET` and `PUT /api/notify/flows`. A `PUT` replaces the whole document in one
|
||||
compare-and-set. The controller answers 400 unless every flow passes the tree
|
||||
check above and reads only what exists: a source event from `issues`,
|
||||
`issue_assign`, `pull_request` or `pull_request_assign`, and condition and
|
||||
recipient fields from the event's field list. The same goes for the `{field}`
|
||||
`issue_assign`, `pull_request`, `pull_request_assign` or `pull_request_sync`,
|
||||
and condition and recipient fields from the event's field list. The same goes for the `{field}`
|
||||
placeholders in a todo's key and text. A `PUT` must carry the `revision` it read, `0` for no
|
||||
document, and gets 409 when the stored revision is another. Swarm UI's
|
||||
[`/notifications`](ui.md#notification-flows) page shows the document and
|
||||
|
|
@ -100,8 +104,8 @@ accepts:
|
|||
- `source_events`: `[{name, description}]`, the source events.
|
||||
- `fields`: `[{name, description}]`, the condition and recipient fields, and
|
||||
the `{field}` placeholders.
|
||||
- `node_kinds`: `[{kind, description, fields}]` for `source`, `match` and
|
||||
`todo`; each field is `{name, description, required, values}`, and `values`
|
||||
- `node_kinds`: `[{kind, description, fields}]` for `source`, `match`, `todo`
|
||||
and `request_reviewer`; each field is `{name, description, required, values}`, and `values`
|
||||
lists the accepted `{name, description}` pairs (the `upsert` and `clear` of a
|
||||
todo's `op`), empty for other fields.
|
||||
- `limits`: `{max_nodes, max_depth, max_flows}`.
|
||||
|
|
@ -109,6 +113,39 @@ accepts:
|
|||
A controller that predates `schema` leaves it out of the answer; a client reads
|
||||
it as optional.
|
||||
|
||||
**Requesting a review.** A `request_reviewer` node has one field, `reviewer`,
|
||||
a forge login. For the PR the event concerns (`repo` and `number`; an event
|
||||
about an issue sends nothing), the router sends a NATS request on
|
||||
`$SWARM.router.sink.request-reviewer` with `{repo, number, reviewer}` and
|
||||
swarm-controller answers with one line:
|
||||
|
||||
| Reply | Meaning |
|
||||
| --------------- | ---------------------------------------------------------------------------------- |
|
||||
| `requested` | the forge accepted the request |
|
||||
| `skipped:<why>` | the reviewer's review of the PR's head already stands; the controller sent nothing |
|
||||
| `error:<msg>` | the forge refused or didn't answer, or the request didn't decode |
|
||||
|
||||
The controller asks the forge under its own token, which the router never
|
||||
holds. It skips a reviewer whose latest review is neither stale (the PR's head
|
||||
moved past the commit it reviewed) nor dismissed, because requesting a review
|
||||
from someone who already submitted one makes the forge dismiss it, approval
|
||||
included. The controller asks again once the review is older than the head.
|
||||
`hive-forge pr assign-reviewer` applies the same rule (`hive_types::review`).
|
||||
|
||||
The router sends the request once, after it queued the event's todo ops and
|
||||
wrote the snapshot, and logs the answer. A controller that's down or answers
|
||||
`error:` doesn't hold up the event or any agent's todos, and the request is
|
||||
not retried: the next push asks again. A request repeated for the same head is
|
||||
harmless: a reviewer with a pending request is a no-op for the forge, and one
|
||||
who has reviewed gets `skipped`.
|
||||
|
||||
`head-classifier` requests the review classifier's review on a push to any open
|
||||
PR. The classifier then runs as it does for any requested review
|
||||
([`review-classifier.md`](review-classifier.md#trigger-and-lifecycle)), through
|
||||
its own request queue, which replaces a waiting request for the same PR. A
|
||||
repo the classifier's account can't read, or a PR that account opened, answers
|
||||
`error:`.
|
||||
|
||||
**What the router runs.** It reads the key at most every 10 seconds, when an
|
||||
event arrives, and runs the built-ins you left on, then the custom flows that
|
||||
are on and pass the tree check. It logs a custom flow that fails the check,
|
||||
|
|
@ -127,20 +164,25 @@ document never leaves it with no flows. The controller refuses to overwrite a
|
|||
document of a version it doesn't know.
|
||||
|
||||
**Deploying in any order.** The controller, the router and the UI can be on
|
||||
different hosts. A controller that predates this leaves the key absent, and the
|
||||
different hosts. A router that predates `request_reviewer` reads a document
|
||||
holding such a node (a custom flow with one) as one it can't read and keeps the
|
||||
flows it had; it also has no `head-classifier`. A router that has it, with a
|
||||
controller that predates it, gets no answer to the request and logs that. A controller that predates this leaves the key absent, and the
|
||||
router runs the built-ins as before. A router that predates this ignores the
|
||||
document, so the page and the API show a state the router isn't yet running.
|
||||
|
||||
## Credentials and grants
|
||||
|
||||
| Principal | Grant |
|
||||
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `swarm-notify-router`, in its container | store: `read` on `swarm/services/<router client id>/oidc/client`. Queue: its own consumer on `SWARM_EVENTS` (create, pull, ack), publish on `$SWARM.router.todo.*`, create `ROUTER_TODOS`, its own `notify-router-state` bucket, read on the `notify-flows` bucket, and `STREAM.INFO` on the agent-status bucket, which lists the agents |
|
||||
| swarm-controller | publish on `$SWARM.events.forge.*`; creates and writes the `notify-flows` bucket (every bucket, by its `$KV.*.>` grant) |
|
||||
| an agent, under its own credential | create its own consumer `todo-<agent>` in the form whose filter is `$SWARM.router.todo.<agent>`, pull from it, and ack what it delivered |
|
||||
| Principal | Grant |
|
||||
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `swarm-notify-router`, in its container | store: `read` on `swarm/services/<router client id>/oidc/client`. Queue: its own consumer on `SWARM_EVENTS` (create, pull, ack), publish on `$SWARM.router.todo.*` and on `$SWARM.router.sink.request-reviewer`, create `ROUTER_TODOS`, its own `notify-router-state` bucket, read on the `notify-flows` bucket, and `STREAM.INFO` on the agent-status bucket, which lists the agents |
|
||||
| swarm-controller | publish on `$SWARM.events.forge.*`; subscribe to `$SWARM.router.sink.request-reviewer` and answer each request once, for 30 seconds; creates and writes the `notify-flows` bucket (every bucket, by its `$KV.*.>` grant) |
|
||||
| an agent, under its own credential | create its own consumer `todo-<agent>` in the form whose filter is `$SWARM.router.todo.<agent>`, pull from it, and ack what it delivered |
|
||||
|
||||
No other client may publish on `$SWARM.router.todo.*` or
|
||||
`$SWARM.events.forge.*`, so each has one writer. The agent's create subject
|
||||
No other client may publish on `$SWARM.router.todo.*`, `$SWARM.router.sink.request-reviewer` or
|
||||
`$SWARM.events.forge.*`, so each has one writer, and the controller alone
|
||||
hears the review requests. The review request needs no secret of its own: the
|
||||
controller asks the forge under the token it already holds. The agent's create subject
|
||||
carries the consumer's filter, and the queue server checks it against the
|
||||
request, so an agent can't create a consumer that delivers another agent's
|
||||
todos.
|
||||
|
|
|
|||
|
|
@ -86,6 +86,10 @@ as a reviewer on a PR starts a run:
|
|||
4. The controller posts the result on the PR as the service account's
|
||||
review, on the commit it read.
|
||||
|
||||
A push starts no run by itself. The notification router's `head-classifier`
|
||||
flow requests the account's review on every push to an open PR, unless its
|
||||
review of the new head already stands ([`notify-router.md`](notify-router.md#requesting-a-review)).
|
||||
|
||||
When a post can't reach the secret store or the forge, because the
|
||||
connection fails or a try gets no answer within two minutes, the controller
|
||||
tries it up to six times over about five minutes, enough to outlast a
|
||||
|
|
|
|||
|
|
@ -491,7 +491,8 @@ swarm-controller keeps every `issues` and `pull_request` delivery from its
|
|||
instance-wide hooks in the `SWARM_EVENTS` stream. The router reads that
|
||||
stream and queues todos for the agents an event concerns: an agent assigned to
|
||||
an issue or pull request gets a todo, one taken off it gets that todo rewritten
|
||||
to say so, and closing it clears them. Each agent's harness takes its todos
|
||||
to say so, and closing it clears them. A push to an open pull request makes
|
||||
it ask swarm-controller to request the review classifier's review. Each agent's harness takes its todos
|
||||
from the `ROUTER_TODOS` stream, so an agent that's down gets them when it's
|
||||
back.
|
||||
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ in two groups. Each **built-in** flow has an on/off switch; switching one off
|
|||
stops the router running it from its next event, and switching it on again
|
||||
restores it. Each **custom** flow shows its name and whether it's on. Every
|
||||
flow draws as an indented tree: the forge events it starts from, the
|
||||
conditions under it, and the todo each branch ends in.
|
||||
conditions under it, and the todo or review request each branch ends in.
|
||||
|
||||
A built-in flow can be **duplicated** into a custom one, and **new flow** starts a
|
||||
custom flow with just a source; either opens the flow in the editor. A custom
|
||||
|
|
|
|||
Loading…
Reference in a new issue