Repository navigation
Conversation
…ction An api_trigger fired one run per webhook delivery. With a GitHub hook subscribed to check_run — one delivery per CI job per state — a single push produced dozens of runs: 3,291 runs in 2.5 days on one automation, ~85% of them check_run. The graph's github.pr_state condition cannot prevent that; it runs after the trigger, and by then a trigger_ai_agent node may already have requested a sandbox. Handler now reads the X-GitHub-Event header and the body (bounded), and the service drops a delivery that matches none of the trigger's optional github_events entries, answering 202 as before so the sender has nothing to retry. Absent/empty filter keeps today's behaviour exactly. Fixes Paca-AI#605
There was a problem hiding this comment.
ℹ️ The backend change is correct and well covered; one builder round-trip hazard worth a decision before merge.
Reviewed changes
- Delivery filter wired end to end —
HandleWebhookTriggertakes aWebhookDeliveryand drops non-matching deliveries with anilreturn, so the handler still answers202; empty/absentgithub_eventsfires everything as before. matchesGithubEventsmatcher — empty patterns ⇒ match; entries areeventorevent:action; only the top-levelactionis decoded, so an unparseable body just has no action to match.- Config validation — malformed entries (
"",:opened,pull_request:,a:b:c) are rejected viaErrNodeConfigInvalid, andgithub_eventsis rejected on any non-api_triggerbuiltin trigger. - Handler body read — bounded
io.ReadAll(io.LimitReader(r.Body, 1<<20))before dispatch; previously the body was ignored. - Tests —
TestMatchesGithubEvents,TestValidateTriggerConfig_GithubEvents, and the real-HTTP e2eTestE2EAutomation_WebhookGithubEventsFilter(zero runs for filtered deliveries, fires on a wanted one). I ran the two unit tests locally and they pass.
ℹ️ The builder's api_trigger form will silently wipe github_events
The new filter can only be set through the API/MCP — the web config panel has no field for it. The api_trigger branch of TriggerConfigForm saves only { target_task_id } (automation-node-config-panel.tsx:1192), the PATCH sends just that object (automation-api.ts:516), and UpdateNode replaces the node config wholesale (automation_service.go:313-317). So a github_events configured on the deployment is removed the next time anyone opens that node in the builder and clicks Save — silently restoring the exact runaway-run behavior this PR fixes. Worth either surfacing the field in the panel or preserving unrecognized keys on save.
Technical details
# Builder save replaces trigger config, dropping API-set fields
## Affected sites
- `apps/web/src/components/projects/automation/automation-node-config-panel.tsx:1188-1195` — api_trigger SaveButton calls `onSave({ target_task_id: … })` only.
- `apps/web/src/routes/_authenticated/projects/$projectId/automation/$automationId.tsx:732-734` — forwards that object as `config`.
- `apps/web/src/lib/automation-api.ts:516-532` — PATCH body is only `{ config }`.
- `services/api/internal/service/automation/automation_service.go:313-317` — `UpdateNode` validates and stores `*in.Config` as the whole config (`normalizeConfig` only maps empty → `{}`).
- `apps/web/src/lib/automation-api.ts:194-213` — `TriggerConfig` TS interface has no `github_events`, so a field can't round-trip through the panel.
## Required outcome
- A `github_events` value set via the API survives a builder save of the trigger node (either the panel renders/edits it, or the save path preserves unknown keys).
## Suggested approach (optional)
- Add a `github_events` editor to the api_trigger form, or have the panel/E? merge the existing `node.config` with the fields it edits instead of sending a fresh object.
## Open questions for the human
- Is UI configuration of `github_events` intended for a later PR, and is API/MCP-only configuration acceptable for now?
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
The api_trigger form saved only {target_task_id} and the node PATCH replaces
the config wholesale, so a github_events set through the API was silently
dropped the next time anyone opened the trigger in the builder and pressed
Save — restoring the runaway-run behaviour the filter exists to prevent.
Spread the stored config into the saved object (target_task_id still comes
from the form, so clearing it still clears) and document the field on the
TriggerConfig interface.
|
Good catch — that was a real hole, fixed in What changed: the On the open question: UI editing of Not done, on purpose: I did not make Also added Verification: |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
The delta since the prior Pullfrog review (d69d7c6) is the follow-up commit ef63d8f, which closes the builder round-trip hazard flagged then.
github_eventssurvives a builder save — theapi_triggerform now spreads the node's storedconfigbefore overridingtarget_task_id, so the whole-config PATCH carries an API/MCP-set filter through instead of dropping it.github_eventsdeclared on the webTriggerConfig— typedstring[]with a doc comment stating it is API-only and preserved rather than overwritten.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
|
Hi @kayahk , Thank you so much for bringing this up and for your contribution! The PR looks great! However, following the Paca design concept, we believe this feature would fit better inside the Github plugin repository: https://github.com/Paca-AI/paca-plugin-github That repository already handles triggers for PR creation, state changes, and branch creation. Feel free to open a PR over there to enhance it! Thanks again for your support and hard work. |
|
Following your guidance, I've moved the webhook-protection work to the GitHub plugin: Paca-AI/paca-plugin-github#27. What it fixes there: On this PR: you're right that the filtering concept belongs with the plugin. What's left here is generic — |
|
Closing this in favour of the plugin, as suggested.
For the record, nothing here was wrong — the change tested clean and the review's catch (a builder save wiping a config field the panel does not render) was real and fixed in |

Why
An
api_triggerfired one graph-walk run per webhook delivery, with no way to say which deliveries it wanted. Pointed at a GitHub hook subscribed tocheck_run— one delivery per CI job per state — a single push fans out into dozens of runs. On our deployment that was 3,291 runs in ~2.5 days, ~85% of themcheck_run, all against one task, arriving in ~20/minute bursts. The graph'sgithub.pr_statecondition can't help: it runs after the trigger, and by then atrigger_ai_agentnode has already asked for a sandbox.Closes #605.
What
An optional allow-list on the trigger config:
{ "target_task_id": "…", "github_events": ["pull_request:opened", "pull_request:synchronize"] }eventorevent:action, matched againstX-GitHub-Eventand the body'saction.202, no run, no stream entry, so the sender never retries and the response can't be used to probe the filter."",":opened","pull_request:","a:b:c") are rejected at save time via the existingErrNodeConfigInvalidpath, andgithub_eventsis rejected on any trigger type other thanapi_trigger— nothing else consumes a delivery.io.LimitReader(…, 1<<20); onlyactionis ever decoded, and a malformed body simply matches noevent:actionentry.Kept GitHub-shaped deliberately (
action+ header, not a generic matcher): that pair is the discriminator every GitHub integration has, and an expression language would be far more surface for the same result.Tests
TestMatchesGithubEvents— 9 cases: no filter,eventalone,event:actionmatch/mismatch on both event and action, any-entry-wins, unparseable body, missing header.TestValidateTriggerConfig_GithubEvents— valid list accepted; the four malformed shapes rejected; rejected on a non-api_triggertype.TestE2EAutomation_WebhookGithubEventsFilter— real HTTP: acheck_run/completedand apull_request/labeleddelivery both return202and leave zero runs and the task untouched; apull_request/openeddelivery then fires and assigns the task. Confirmed to fail without the fix (expected no runs for filtered deliveries, got 2).go build,go vet,golangci-lint(0 issues),go test -raceclean; e2e run green including the pre-existing webhook and call_api suites.Compatibility
Config is JSON and the field is omitempty, so existing automations keep their current behaviour byte-for-byte. The only signature change is
HandleWebhookTrigger, which gains the delivery as a value — one caller.