mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
feat(filters): JSONPath field filters, operators, AST groups, fragments, and test-match
Add a field filter type reading any payload value by JSONPath with array expansion, 12 comparison operators (eq/ne/contains/startsWith/endsWith/regex/gt/gte/lt/lte/in/exists), a visual AST builder (all/any/not) in the route editor, chip-based multi-value input, a stateless POST /admin/api/test-match dry-run, and named filter fragments stored in D1 (d1_fragments, migration 0010) inlined into route ASTs on insert.
This commit is contained in:
parent
c955db03c3
commit
c090281cb2
30 changed files with 1793 additions and 422 deletions
|
|
@ -73,6 +73,7 @@ See the [Filter Tutorial](./filters) for a hands-on guide with worked examples.
|
|||
| `actor` | Sender login | `username`, `[bot]`, `*[bot]` |
|
||||
| `action` | Event action | `opened`, `closed`, `published` |
|
||||
| `branch` | Branch name | `main`, `feature-?`, `/^release-/` |
|
||||
| `field` | Any payload field (JSONPath) | `path: "pull_request.user.login"` |
|
||||
| `keyword` | Text in payload body | `deploy`, `/fix\s+\d+/` |
|
||||
|
||||
### Filter Behavior
|
||||
|
|
@ -80,7 +81,10 @@ See the [Filter Tutorial](./filters) for a hands-on guide with worked examples.
|
|||
- All filters in a route must match for the route to trigger (AND logic)
|
||||
- Set `"exclude": true` on any filter to invert it (NOT logic)
|
||||
- Every filter type supports the same pattern forms: plain text, `*`/`?` **globs** (`*` = any run, `?` = one character), and `/regular expression/` — all case-insensitive
|
||||
- Field filters (`event`/`repo`/`actor`/`action`/`branch`) glob-match the whole value; `keyword` globs and regexes search anywhere in the payload; plain `keyword` text is a substring search
|
||||
- Field filters (`event`/`repo`/`actor`/`action`/`branch`/`field`) glob-match the whole value; `keyword` globs and regexes search anywhere in the payload; plain `keyword` text is a substring search
|
||||
- Every filter except `keyword` accepts an `op` operator — `eq` (default, classic behaviour), `ne`, `contains`, `startsWith`, `endsWith`, `regex`, `gt`, `gte`, `lt`, `lte`, `in`, `exists` — see the [Filter Tutorial](./filters#operators)
|
||||
- `field` filters use a dot-separated JSONPath `path` into the payload; arrays are expanded so any matching element satisfies the filter
|
||||
- Routes may nest filters in an `ast` node (`all` / `any` / `not`) instead of a flat `filters` list; `ast` takes precedence when present
|
||||
- Patterns longer than 200 characters are not compiled as glob/regex; an invalid `//`-wrapped regex matches nothing
|
||||
- `branch` filter works for push, pull_request, pull_request_review, pull_request_review_comment, create/delete, workflow_run, workflow_job, check_suite, deployment, and code_scanning_alert events
|
||||
|
||||
|
|
@ -91,4 +95,5 @@ Filters accept either a single string or an array of strings:
|
|||
```json
|
||||
{ "type": "event", "match": "push" }
|
||||
{ "type": "event", "match": ["push", "pull_request"] }
|
||||
{ "type": "field", "path": "pull_request.commits", "op": "gt", "match": "1" }
|
||||
```
|
||||
|
|
|
|||
|
|
@ -194,6 +194,70 @@ Just like the other filters, `exclude` inverts the keyword match:
|
|||
|
||||
Skips events whose payload mentions `wip` or `draft`.
|
||||
|
||||
### `field` — Any payload field (JSONPath)
|
||||
|
||||
Matches an arbitrary field of the webhook payload using a dot-separated path, e.g. `pull_request.user.login`, `repository.private`, or `check_run.conclusion`. Array fields are expanded automatically — the filter matches if **any** element matches.
|
||||
|
||||
```json
|
||||
{ "type": "field", "path": "pull_request.user.login", "match": "dependabot[bot]" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "type": "field", "path": "labels.name", "match": "bug" }
|
||||
```
|
||||
|
||||
### Operators
|
||||
|
||||
Field filters (and every filter type except `keyword`) accept an `op` to change how the value is compared. The default `eq` keeps the classic glob/regex/exact behaviour.
|
||||
|
||||
| Operator | Meaning |
|
||||
| -------------- | -------------------------------------------------------------------- |
|
||||
| `eq` (default) | Equal — globs, regexes and plain text, case-insensitive |
|
||||
| `ne` | Not equal (inverse of `eq`) |
|
||||
| `contains` | Value contains the pattern (substring) |
|
||||
| `startsWith` | Value starts with the pattern |
|
||||
| `endsWith` | Value ends with the pattern |
|
||||
| `regex` | Explicit regular expression match |
|
||||
| `gt` / `gte` | Numeric greater-than / greater-or-equal |
|
||||
| `lt` / `lte` | Numeric less-than / less-or-equal |
|
||||
| `in` | Value equals any of the listed patterns |
|
||||
| `exists` | The field is present (non-null); `match` is ignored |
|
||||
|
||||
```json
|
||||
{ "type": "field", "path": "pull_request.commits", "op": "gt", "match": "1" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "type": "field", "path": "label.name", "op": "startsWith", "match": "area/" }
|
||||
```
|
||||
|
||||
### Grouping (all / any / not)
|
||||
|
||||
A route can use a nested `ast` to combine filters with explicit grouping instead of a flat AND list. The `ast` node is one of `{ "all": [...] }`, `{ "any": [...] }`, or `{ "not": {...} }`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "grouped",
|
||||
"name": "Grouped",
|
||||
"ast": {
|
||||
"all": [
|
||||
{ "type": "event", "match": "pull_request" },
|
||||
{ "any": [
|
||||
{ "type": "field", "path": "pull_request.user.login", "match": "alice" },
|
||||
{ "type": "field", "path": "pull_request.user.login", "match": "bob" }
|
||||
]}
|
||||
]
|
||||
},
|
||||
"targets": [{ "channelId": "..." }]
|
||||
}
|
||||
```
|
||||
|
||||
When `ast` is present it takes precedence over `filters`. The admin console's route editor builds `ast` visually (all/any/not groups), shows a live explanation of the tree, and can test it against a pasted JSON payload via the **Test match** panel.
|
||||
|
||||
### Named filter fragments
|
||||
|
||||
The route editor can save the current filter tree as a **named fragment** and insert it into other routes. Fragments are editor-side templates stored in D1 (`d1_fragments`); inserting a fragment inlines its node into the route's `ast`, so the matching engine itself never resolves fragment references.
|
||||
|
||||
## Worked Example 1: PR alerts that skip bots and drafts
|
||||
|
||||
Forward pull request activity, but ignore bot authors and draft PRs, to a `#prs` channel:
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ Each entry of `targets` is a push destination, so one route can forward to sever
|
|||
| `fallback` | boolean | No | When `true`, fires only if no non-fallback route matched the event; its own filters are ignored |
|
||||
| `stop` | boolean | No | When `true` and this route matches, no further routes are evaluated for this event |
|
||||
| `discordRoleIds` | string[] | No | Discord role ids to ping when this route fires; applied to Discord targets only |
|
||||
| `ast` | object | No | Boolean filter tree (`{all:[...]}` / `{any:[...]}` / `{not:{...}}`); takes precedence over `filters` when present |
|
||||
|
||||
## Discord Role Mentions
|
||||
|
||||
|
|
@ -58,7 +59,7 @@ You can add role ids in the admin console under _Discord role mentions_.
|
|||
|
||||
## Filters
|
||||
|
||||
Every route carries a `filters` array (all must match — AND logic). See the [Filter Types](./configuration#filter-types) reference and the [Filter Tutorial](./filters).
|
||||
Every route carries a `filters` array (all must match — AND logic). When the route has an `ast` field (a nested `all`/`any`/`not` tree), it is evaluated instead of `filters`, so it can express arbitrary boolean combinations. See the [Filter Types](./configuration#filter-types) reference and the [Filter Tutorial](./filters).
|
||||
|
||||
## Custom Route Example
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue