Compare commits

...

2 Commits

Author SHA1 Message Date
J
b2e3f3809b docs(autopilots): expand "where event name and action come from" with curl examples
Add concrete curl request/inference pairs under each derivation step
(body envelope, headers, body fallback, default) and a "common gotcha"
explaining why filter `event=trigger, action=true` does not match
`{"trigger": true}`. Mirrors the explanation that resolved the
on-call confusion about Event filter semantics.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-27 16:05:30 +08:00
J
00226afa0f MUL-2748 docs(autopilots): document webhook event filters and link from UI
Follow-up to PR #3231. The webhook event filters feature shipped
without user-facing docs and the UI section gave no hint about how
event/action are derived from inbound requests.

- Add an "Event filters" subsection to autopilots.mdx and .zh.mdx
  under the existing "Trigger from a webhook" section: what the
  filter does (with the `event_filtered` outcome), where the event
  name and action come from (body envelope / headers / body
  fallbacks), examples, a non-string-action gotcha, and a curl
  recipe verifying both the allowed and filtered paths.
- Add a small ExternalLink icon next to the "Event filters" label
  in WebhookEventFilterSection that opens the docs section in a new
  tab. Locale-aware: zh users land on the Chinese page anchor, en
  users on the English one.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-27 15:57:44 +08:00
5 changed files with 247 additions and 2 deletions

View File

@@ -99,6 +99,126 @@ nothing matches, the event is `webhook.received`.
When configuring GitHub or similar sources, set the content type to
`application/json` — form-encoded webhook payloads are not accepted.
### Event filters
A new webhook trigger fires on every inbound POST, which is fine for a
single-purpose URL but noisy for sources that fan out many event types
(GitHub being the obvious one — a single repo webhook can deliver
`push`, `pull_request`, `workflow_run`, `check_suite`, and more). The
**Event filters** section on a webhook trigger lets you restrict which
events actually dispatch a run; everything else is recorded in delivery
history with `status = ignored` and `reason = event_filtered`, and no
run or issue is created.
Each row is one rule: an **event name** plus an optional
comma-separated **actions** list. Multica allows a webhook if **any**
row matches; leave the section empty to accept everything (the
pre-filter behavior).
Examples:
| Event name | Actions | Matches |
| -------------- | ------------------- | ------------------------------------------------------------------------ |
| `workflow_run` | `completed, failed` | `workflow_run` events with `action: completed` or `action: failed` only |
| `workflow_run` | _(empty)_ | every `workflow_run` event, regardless of action |
| `push` | _(empty)_ | every `push` event |
#### Where the event name and action come from
Multica derives the `event` name and `action` from the inbound request
in this order — **the first match wins**.
**1. Body envelope.** If the body is a JSON object with a string
`event` field, that value is the event name directly. An optional
`eventPayload` object then supplies action candidates from its
`action` / `state` / `conclusion` / `status` fields.
```bash
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d '{"event":"trigger","eventPayload":{"action":"true"}}'
# inferred: event = trigger, action candidate = true
```
**2. Headers.** When no body envelope is present, Multica reads the
following well-known provider headers:
- `X-GitHub-Event: <event>` — combined with the top-level body
`action` field (when present) to form `github.<event>.<action>`.
- `X-Gitlab-Event: <event>` — becomes `gitlab.<event>`.
- `X-Event-Type: <event>` — passed through verbatim.
```bash
# GitHub-style: header gives the event name, body gives the action.
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'X-GitHub-Event: workflow_run' \
-H 'Content-Type: application/json' \
-d '{"action":"completed"}'
# inferred: event = github.workflow_run.completed
# → matches a filter row of workflow_run / completed
# Generic event-type header — no body fields needed.
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'X-Event-Type: trigger.true' \
-H 'Content-Type: application/json' \
-d '{}'
# inferred: event = trigger.true → matches trigger / true
```
**3. Body fallback.** If neither a body envelope nor a known header is
present, Multica falls back to top-level body string fields in this
order: `event` → `type` → `action`.
```bash
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d '{"type":"trigger","action":"true"}'
# inferred: event = trigger (from `type`), action candidate = true
```
**4. Default.** If nothing above matches, the event is
`webhook.received` and there are no action candidates.
**Action candidates, in full.** Once the event is determined, Multica
considers every value below as a possible action match:
- The event-name suffix, when the event has the form
`provider.event.<action>` (e.g. `github.workflow_run.completed` →
`completed`).
- The body fields `action`, `state`, `conclusion`, and `status` —
**only when they are JSON strings**. A boolean (`{"action": true}`)
or a number does not qualify, so a filter expecting
`event=trigger, action=true` will never match a body of
`{"trigger": true}` because `true` is a bool, not a string.
**Common gotcha.** A filter row like `Event name: trigger` /
`Actions: true` does **not** mean "fire when the body has
`trigger: true`" — Event filters match the *inferred event and
action*, not arbitrary body fields. Send `trigger.true` via
`X-Event-Type` (or use the body envelope shown above) to hit it.
Surrounding whitespace in saved filter rows (`" workflow_run "`) is
stored verbatim and will never match — trim before saving.
#### Quick test
Once a filter is configured, you can confirm both branches with `curl`:
```bash
# Allowed — header drives event=workflow_run, body drives action=completed
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'X-GitHub-Event: workflow_run' \
-H 'Content-Type: application/json' \
-d '{"action":"completed"}'
# → 200 {"status":"accepted", ...}
# Filtered — same event, action not in allowlist
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'X-GitHub-Event: workflow_run' \
-H 'Content-Type: application/json' \
-d '{"action":"in_progress"}'
# → 200 {"status":"ignored","reason":"event_filtered"}
```
### URL is a bearer secret
The generated URL **is** the credential. Anyone with it can fire the

View File

@@ -98,6 +98,116 @@ curl -X POST "$MULTICA_WEBHOOK_URL" \
配置 GitHub 之类的来源时,请把 content type 设为 `application/json`——
表单编码的 webhook payload 在 v1 里不接受。
### 事件过滤
新建的 webhook 触发器对每一条入站 POST 都会触发,这在单一用途的 URL
上没问题,但对那种会扇出很多事件类型的来源(典型就是 GitHub——
一个仓库 webhook 就会同时下发 `push`、`pull_request`、`workflow_run`、
`check_suite` 等等就会很吵。webhook 触发器上的**事件过滤**区块用来
限制哪些事件真正派发一次 run其它的只记录到投递历史里
`status = ignored`、`reason = event_filtered`,不会建任何 issue 或 run。
每一行是一条规则:一个**事件名**加可选的、逗号分隔的 **action** 列表。
**任意一行命中**即放行;区块留空则接受所有事件(即过滤前的行为)。
例子:
| 事件名 | Actions | 命中范围 |
| ---------------- | ------------------- | ------------------------------------------------------------------------ |
| `workflow_run` | `completed, failed` | 只有 `action` 为 `completed` 或 `failed` 的 `workflow_run` 事件 |
| `workflow_run` | _(留空)_ | 所有 `workflow_run` 事件,不限 action |
| `push` | _(留空)_ | 所有 `push` 事件 |
#### 事件名和 action 从哪来
Multica 按下面的顺序从入站请求里推断 `event` 和 `action`**先命中先用**。
**1. Body envelope。** 如果 body 是一个 JSON 对象、且带字符串字段
`event`,就直接用它作为事件名。可选的 `eventPayload` 对象再
从自己的 `action` / `state` / `conclusion` / `status` 字段里提供
action 候选。
```bash
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d '{"event":"trigger","eventPayload":{"action":"true"}}'
# 推断结果event = triggeraction 候选 = true
```
**2. 请求头。** 没有 body envelope 时按以下头部识别:
- `X-GitHub-Event: <event>` —— 结合 body 顶层的 `action` 字段
(如果有),拼成 `github.<event>.<action>`。
- `X-Gitlab-Event: <event>` —— 拼成 `gitlab.<event>`。
- `X-Event-Type: <event>` —— 原样使用。
```bash
# GitHub 风格:事件名来自 headeraction 来自 body。
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'X-GitHub-Event: workflow_run' \
-H 'Content-Type: application/json' \
-d '{"action":"completed"}'
# 推断结果event = github.workflow_run.completed
# → 命中 workflow_run / completed 的过滤规则
# 通用 event-type 头部 —— 不需要任何 body 字段。
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'X-Event-Type: trigger.true' \
-H 'Content-Type: application/json' \
-d '{}'
# 推断结果event = trigger.true → 命中 trigger / true
```
**3. Body 兜底。** Body envelope 和上面的 header 都没有时,
从 body 顶层字符串字段里依次找:`event` → `type` → `action`。
```bash
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d '{"type":"trigger","action":"true"}'
# 推断结果event = trigger取自 `type`action 候选 = true
```
**4. 默认值。** 以上都没命中时,事件名取 `webhook.received`
没有 action 候选。
**action 候选的完整清单。** 事件名确定后,下面这些值都会被列为
可能的 action
- 事件名后缀,当事件形如 `provider.event.<action>` 时
(例如 `github.workflow_run.completed` → `completed`)。
- body 里 `action`、`state`、`conclusion`、`status` 这四个字段——
**必须是 JSON 字符串**。布尔(`{"action": true}`)或数字都不算
候选,所以 `event=trigger, action=true` 的过滤规则**永远命中不了**
`{"trigger": true}` 这种 body因为 `true` 是 bool 不是字符串。
**常见误区。** 一条 `Event name: trigger` / `Actions: true` 的规则
**不是**「body 里出现 `trigger: true` 就放行」的意思——事件过滤匹配的是
**推断出来的事件和 action**,不是任意 body 字段。要命中它,请用
`X-Event-Type` 头发送 `trigger.true`,或者用上面的 body envelope。
保存时带空格的值(例如 `" workflow_run "`)会被原样保存,但永远命中
不了——保存前请先 trim。
#### 快速验证
配好过滤后,可以用 `curl` 同时验证「命中」和「被过滤」两条路径:
```bash
# 命中 —— 请求头给出 event=workflow_runbody 给出 action=completed
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'X-GitHub-Event: workflow_run' \
-H 'Content-Type: application/json' \
-d '{"action":"completed"}'
# → 200 {"status":"accepted", ...}
# 被过滤 —— 同样的事件,但 action 不在白名单
curl -X POST "$MULTICA_WEBHOOK_URL" \
-H 'X-GitHub-Event: workflow_run' \
-H 'Content-Type: application/json' \
-d '{"action":"in_progress"}'
# → 200 {"status":"ignored","reason":"event_filtered"}
```
### URL 即 bearer secret
生成的 URL **就是凭证**,谁拿到都能触发这个 Autopilot。请按 token 对待:

View File

@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { X, Plus, Filter } from "lucide-react";
import { X, Plus, Filter, ExternalLink } from "lucide-react";
import { cn } from "@multica/ui/lib/utils";
import type { WebhookEventFilter } from "@multica/core/types";
import { useT } from "../../i18n";
@@ -15,9 +15,12 @@ export function WebhookEventFilterSection({
filters,
onChange,
}: WebhookEventFilterSectionProps) {
const { t } = useT("autopilots");
const { t, i18n } = useT("autopilots");
const [newEvent, setNewEvent] = useState("");
const [newActions, setNewActions] = useState("");
const docsHref = i18n.language?.startsWith("zh")
? `https://multica.ai/docs/zh/autopilots#${encodeURIComponent("事件过滤")}`
: "https://multica.ai/docs/autopilots#event-filters";
const addFilter = () => {
const event = newEvent.trim();
@@ -42,6 +45,16 @@ export function WebhookEventFilterSection({
<div className="flex items-center gap-1.5 text-[11px] font-semibold tracking-[0.08em] text-muted-foreground uppercase">
<Filter className="size-3" />
{t(($) => $.dialog.event_filter_label)}
<a
href={docsHref}
target="_blank"
rel="noopener noreferrer"
aria-label={t(($) => $.dialog.event_filter_docs_link_label)}
title={t(($) => $.dialog.event_filter_docs_link_label)}
className="ml-0.5 inline-flex items-center text-muted-foreground/80 hover:text-foreground transition-colors"
>
<ExternalLink className="size-3" />
</a>
</div>
{filters.length > 0 && (

View File

@@ -259,6 +259,7 @@
"webhook_created_warning": "Treat this URL like a password. Anyone who has it can trigger the autopilot.",
"webhook_created_done": "Done",
"event_filter_label": "Event filters",
"event_filter_docs_link_label": "Learn how event filters work",
"event_filter_event_placeholder": "e.g. workflow_run",
"event_filter_actions_placeholder": "completed, failed",
"event_filter_hint": "Only process webhooks matching these events. Leave empty to accept all.",

View File

@@ -259,6 +259,7 @@
"webhook_created_warning": "请把该 URL 当作密码对待,任何拿到它的人都能触发该 Autopilot。",
"webhook_created_done": "完成",
"event_filter_label": "事件过滤",
"event_filter_docs_link_label": "了解事件过滤的工作方式",
"event_filter_event_placeholder": "例如 workflow_run",
"event_filter_actions_placeholder": "completed, failed",
"event_filter_hint": "只处理匹配这些事件的 webhook。留空则接受所有事件。",