Skip to content

fix(workflow): resolve nested webhook body fields in templates - #7037

Open
sekura-ai wants to merge 1 commit into
block:mainfrom
sekura-ai:fix/workflow-webhook-nested-fields
Open

fix(workflow): resolve nested webhook body fields in templates#7037
sekura-ai wants to merge 1 commit into
block:mainfrom
sekura-ai:fix/workflow-webhook-nested-fields

Conversation

@sekura-ai

Copy link
Copy Markdown

Problem

A GitHub release notification workflow posted its message with the placeholder intact:

GitHub: published on {{trigger.ref}}

Two separate causes stacked up:

  1. ref is not in a release payload. GitHub's release hook (action: "published") carries the tag at release.tag_name; only push payloads have a top-level ref.
  2. Nested fields were unaddressable anyway. Webhook trigger context was built by walking only the top level of the POSTed JSON, stringifying any nested object. So {{trigger.release.tag_name}} would not have resolved either — there was no correct thing to write.

The literal output is the resolver behaving as designed: an unknown {{...}} is emitted verbatim rather than as an empty string (executor.rs), so a visible placeholder means the field was absent from the body.

Fix

Flatten webhook bodies into dotted keys so provider payloads are addressable in place, with no pre-flattening proxy:

Template Resolves to
{{trigger.action}} published
{{trigger.release.tag_name}} v1.4.0
{{trigger.repository.owner.login}} block
{{trigger.commits.0.message}} first commit message
{{trigger.release}} the subtree as compact JSON

Flattening is capped at 6 levels deep / 512 fields to bound the trigger context persisted with every run — a push with many commits is otherwise unbounded.

For if: / filter: expressions, dotted keys are not valid evalexpr identifiers, so the same fields are exposed with separators collapsed to underscores:

filter: 'trigger_release_tag_name == "v1.4.0"'

Sanitized names are collected through a BTreeMap so two keys that sanitize to the same identifier resolve deterministically across runs, and the existing guard preventing webhook fields from shadowing standard trigger_* variables now runs after sanitization — closing a gap where a key literally named trigger.author would previously have sanitized into trigger_author and spoofed the signed trigger author.

Both relay call sites (bridge.rs webhook route, command_executor.rs event trigger) now delegate to the shared helper instead of each open-coding the top-level walk.

Caller impact

The workflow YAML that triggered this becomes:

text: "GitHub: {{trigger.action}} {{trigger.release.tag_name}} in {{trigger.repository.full_name}}"

No existing template breaks: top-level scalars keep their bare names, and objects/arrays still render under their own key as before (as compact JSON).

Testing

  • cargo test -p buzz-workflow --lib — 162 passed / 0 failed. Six new tests cover nested paths, array indices, non-object bodies, the depth/count caps, template resolution, condition registration, and the anti-spoofing guard. All new logic lives in this crate.
  • cargo clippy -p buzz-relay --lib — clean.
  • cargo fmt --all --check — clean.

Not run locally: buzz-relay test binaries failed to link on a full disk (that crate's test tree pulls iroh / hf-xet / mesh-llm-hf-hub). The two relay changes are three-line delegations to the tested helper, but they are unexercised here and I would want CI to confirm them. Integration suite (just test) also not run — needs Postgres + Redis.

Deploy note

This is relay-side, so it needs a relay release to take effect. There is no template-only workaround for a release hook: before this change, only top-level scalars resolved, and action is the sole useful one in that payload.

🤖 Generated with Claude Code

https://claude.ai/code/session_019pLy6t6bJAbuqs9PfruBi8

Webhook trigger context was built by walking only the top level of the
POSTed JSON body, stringifying any nested object. A provider payload
whose interesting fields are nested — a GitHub `release` hook, where the
tag lives at `release.tag_name` — had nothing usable to address, and
`{{trigger.release.tag_name}}` did not resolve either because nested
paths were never registered. Unknown variables are emitted literally by
design, so workflows posted messages containing a raw `{{trigger.ref}}`.

Flatten webhook bodies into dotted keys so provider payloads are
addressable in place: nested paths (`release.tag_name`), array indices
(`commits.0.message`), and containers still kept as compact JSON under
their own key so `{{trigger.release}}` renders the subtree. Flattening is
capped at 6 levels and 512 fields to bound the trigger context persisted
with every run.

Dotted keys are not valid evalexpr identifiers, so `if:`/`filter:`
expressions see the same fields with separators collapsed to underscores
(`trigger_release_tag_name`). Sanitized names are collected through a
BTreeMap so a collision resolves identically on every run, and the
existing guard preventing webhook fields from shadowing standard
`trigger_*` variables now runs after sanitization.

Signed-off-by: Tikka Nagi <tikka@sekura.ai>
@sekura-ai
sekura-ai requested a review from a team as a code owner August 29, 2026 15:45
@github-actions

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

The current range is 00e61eafa917d296104006576b7a2ddbfd58bb5a...b220073fcb638d98e990853265602ff1810c58a5.
A new review must complete for this exact range. When manual authorization
is required, a Block organization member must comment exactly
@buzz-security-review b220073fcb638d98e990853265602ff1810c58a5 to authorize a new review.
Any previous review applies only to its recorded range.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant