Watch
0
0
Fork
You've already forked hyperhive
0

swarm: the notification router tells a PR's author when CI on it fails or is cancelled

This commit is contained in:
damocles 2026-11-06 21:25:08 +01:00 • committed by mara
commit c8eaa93268

View file

@ -12,11 +12,14 @@ the way it did. An agent can get the same news from both.
## What an agent gets
| Flow | Event | The agent's todo |
| ------------ | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `assigned` | someone assigns an agent to an issue or PR | `you are assigned to issue <repo>#<n>: <title> (<url>)`, key `forge:<repo>#<n>:assignment` |
| `unassigned` | someone takes an agent off an issue or PR | the same key rewritten: `you are not assigned to issue <repo>#<n> anymore: <title> (<url>)` |
| `closed` | someone closes or merges the issue or PR | the router clears every router todo it queued for it, then queues `closed` on each agent that held one: `<kind> <repo>#<n> was closed: <title> (<url>)`, key `forge:<repo>#<n>:closed` |
| Flow | Event | The agent's todo |
| -------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `assigned` | someone assigns an agent to an issue or PR | `you are assigned to issue <repo>#<n>: <title> (<url>)`, key `forge:<repo>#<n>:assignment` |
| `unassigned` | someone takes an agent off an issue or PR | the same key rewritten: `you are not assigned to issue <repo>#<n> anymore: <title> (<url>)` |
| `closed` | someone closes or merges the issue or PR | the router clears every router todo it queued for it, then queues `closed` on each agent that held one: `<kind> <repo>#<n> was closed: <title> (<url>)`, key `forge:<repo>#<n>:closed` |
| `ci-failed` | a CI run on a PR finishes with a failed job | the PR's author gets `CI failed on PR <repo>#<n>: <title> (<url>)`, key `forge:<repo>#<n>:ci` |
| `ci-cancelled` | a CI run on a PR gets cancelled | the same key rewritten: `CI was cancelled on PR <repo>#<n>: <title> (<url>)` |
| `ci-cleared` | someone pushes to the PR's branch | the router clears the PR author's `:ci` todo, as the push starts a new run; the author's other todos for the PR stay |
The todos are subsystem `router` in the agent's todo list. A todo under the
same key replaces the one before, so an issue gives an agent one assignment
@ -39,12 +42,16 @@ router notices an unassign only on an issue it has seen an assign event for.
## The path
1. swarm-controller registers two instance-wide hooks, one for the `issues`
and `issue_assign` events at `/webhook/forge/issues` and one for
1. swarm-controller registers three instance-wide hooks, one for the `issues`
and `issue_assign` events at `/webhook/forge/issues`, one for
`pull_request`, `pull_request_assign`, `pull_request_sync` (a push to an
open PR's branch) and the submitted-review events `pull_request_review_approved`,
`pull_request_review_rejected` and `pull_request_review_comment` at `/webhook/forge/pull-request`. At each start it adds
any of these events a registered hook lacks.
`pull_request_review_rejected` and `pull_request_review_comment` at `/webhook/forge/pull-request`,
and one for `workflow_run_failure` and `workflow_run_cancelled` (a finished CI run) at
`/webhook/forge/workflow-run`. At each start it adds
any of these events a registered hook lacks, except for the CI run events:
Forgejo's hook edit never sets them, so the `workflow-run` hook gets them
only when it's created. Adding a run event later means a new hook kind.
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
@ -78,7 +85,7 @@ 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` →
The router ships the six flows above, each a source → match on `action` →
todo, and `head-classifier`: source `pull_request` and `pull_request_sync` →
match on `action` (`opened`, `reopened`, `synchronized`) and `draft` = `false`, or on
`undrafted` → `request_reviewer` for `review-classifier` ([below](#requesting-a-review)). The code defines them
@ -93,7 +100,7 @@ switched off, and the custom flows. Only the controller writes it, through
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`, `pull_request_assign`, `pull_request_sync` or one of the
three `pull_request_review_*` events, and condition and recipient fields from the event's field list. The same goes for the `{field}`
three `pull_request_review_*` events, `workflow_run_failure` or `workflow_run_cancelled`, 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
[notification router card on `/swarm`](../../2-handbook/swarm/ui.md#notification-flows) shows the document and
@ -179,6 +186,33 @@ review once the classifier has approved:
The skip rule above applies: the router asks for argus's review once per head.
**CI runs as a source.** A finished CI run arrives as `workflow_run_failure`
(a job failed, once every job has ended) or `workflow_run_cancelled`. The body
holds the run, and the run holds the webhook that started it as a JSON string
in `run.event_payload`. For a run on a PR that's the PR's own delivery, so the
router reads the PR from it: `repo`, `number`, `title`, `url` and `author` are
the PR's, `action` is the run's outcome (`failure` or `cancelled`), and
`sender` is the account that triggered the run. A run that started from
anything else, such as a push to a branch with no PR, names no PR and the
router ignores it. The `author` field is the account that opened the issue or
PR, so a flow can send to it:
| node | setting |
| ------ | ------------------------------------------------------------- |
| source | `workflow_run_failure` |
| todo | `to` = field `author`, key `forge:{repo}#{number}:ci`, upsert |
A clear of one key, such as `ci-cleared`'s, leaves the agent in the issue's
holders, because it may still hold the assignment todo that `closed` must find.
Only a prefix clear drops the holder. An author whose CI ever failed gets
the `closed` todo when their PR closes or merges, like an assignee.
A run reports in when it ends, and a push cancels the runs before it, so an old
run can end after the push cleared the `:ci` todo. The router keeps the PR's
head commit from the last delivery about the PR that wasn't a run. A run whose
delivery names another head began before the latest push, and the router drops
it. With no head kept yet, it can't tell and sends the todo.
**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,