Webhook icon
Commands icon
Return icon
Switch icon
Log icon
SlackIncomingWebhook icon
Fail icon

Buf Protobuf Breaking-Change Deployment Gate

Block Protobuf schema changes that break wire compatibility while only warning on reflection/JSON-only changes.

Categories
Data

A schema gate that fails on every finding buf reports trains teams to bypass it, because plenty of findings (a changed JSON field name, a changed default value) affect reflection or JSON serialization but not the binary wire format existing compiled clients and stored messages depend on. This blueprint runs buf's own breaking-change detector between a schema's previous and current version, splits findings into "breaks wire compatibility or removes something still referenced" and "affects reflection or JSON only," and only fails the execution outright on the first group, while treating a schema that fails to even compile as its own distinct failure mode rather than silently reporting zero breaking changes.

How it works

  1. check_on_pr_webhook (io.kestra.plugin.core.trigger.Webhook) lets a CI pipeline POST a pull request's new .proto file to this flow directly. fetchType: STORE streams that POST body into Kestra's internal storage and exposes it as trigger.uri, a safe file reference, rather than as raw trigger.body text, which the trigger's own documentation flags as untrusted input unsafe to interpolate into a script task's commands. Running this flow manually or as a Subflow instead uses the current_proto_file input, the same kind of safe file reference; previous_proto_file is always supplied as an input either way.
  2. check_breaking_changes (io.kestra.plugin.scripts.shell.Commands) runs bufbuild/buf's own container. buf CLI v2 requires a minimal buf.yaml to treat a bare directory as a module, so one is generated for both the workspace and a baseline/ directory holding the previous schema, then buf breaking --against baseline --error-format=json runs. Its JSON output is newline-delimited (one object per line), not a single array, so awk converts it to a proper array before Kestra's jq filter (which expects one JSON document) ever reads it. stdout (report.json, the violations) and stderr (stderr.txt, a schema compile error) are captured separately, because buf breaking returns the same exit code for both a validation failure and a tool/compile error, so the exit code alone cannot tell them apart.
  3. classify_schema_compatibility (io.kestra.plugin.core.debug.Return) checks whether both output files exist; if not, UNREACHABLE. Otherwise a non-empty stderr.txt (the schema did not even compile) or any finding whose type is a *_NO_DELETE rule, FIELD_SAME_TYPE, RPC_SAME_REQUEST_TYPE, or RPC_SAME_RESPONSE_TYPE (wire-level breaking changes) classifies BLOCK; any remaining finding classifies WARN; otherwise PASS.
  4. route_by_schema_compatibility (io.kestra.plugin.core.flow.Switch) branches on that string: PASS logs a quiet confirmation, WARN pages Slack with the non-breaking finding types but lets the execution succeed, BLOCK pages Slack with either the compile error or the specific breaking rule types and then runs fail_gate (io.kestra.plugin.core.execution.Fail) to deliberately fail the execution, and UNREACHABLE pages Slack separately.
  5. log_audit_status records a status line whenever the execution reaches it, i.e. every outcome except BLOCK, which fails before reaching this task by design.
  6. The errors block fires both for a genuine flow break and, intentionally, every time fail_gate fires, a chained parent pipeline sees either case as the same FAILED execution and stops.

What you get

  • A schema gate that distinguishes "breaks the wire format or removes something a client still calls" from "affects reflection or JSON serialization only," instead of a blanket finding-count threshold that trains people to ignore it.
  • A separate, correctly identified failure mode for a schema that does not even compile, instead of that case being silently misreported as zero breaking changes because buf breaking's own exit code cannot distinguish the two.
  • A hard stop: fail_gate makes the execution itself FAILED on a breaking change, so a parent flow using this as a Subflow task before a schema registry push or a service deploy is actually blocked, not just notified into a channel someone might not read before merging.
  • A status output (PASS / WARN / BLOCK / UNREACHABLE) you can also read directly if you prefer branching in the caller instead of relying on failure propagation.

Who it's for

  • Platform and API teams enforcing Protobuf/gRPC schema compatibility who want CI to fail loudly on an actual wire-breaking change, not on every cosmetic finding buf reports.
  • Teams who have tried a naive "any finding, fail" gate and abandoned it because reflection/JSON-only findings made it fail on changes the team deliberately decided to ship anyway.

Why orchestrate this with Kestra

buf produces the findings; nothing in the CLI itself enforces a policy or blocks a deploy pipeline on your terms, and its exit code conflates a real validation failure with a tool error. Kestra turns that output into an actual pass/fail decision that correctly separates those two failure modes, with a real execution-history trail of exactly which rule types triggered a block versus a warning, and Fail gives a parent pipeline a real signal to stop on instead of a Slack message it has to notice in time.

Prerequisites

  • Docker available to the Kestra worker, to run buf's own bufbuild/buf image, no schema registry, server, or account is required; buf compares the two schema files directly.
  • The previous merged .proto file and the new pending .proto file.
  • A Slack incoming webhook URL.

Secrets

  • SCHEMA_CHECK_WEBHOOK_KEY: the key segment of the webhook URL (.../executions/webhook/{namespace}/{flowId}/{key}). Only needed if you enable check_on_pr_webhook; a manual or Subflow run using current_proto_file does not need it.
  • SLACK_WEBHOOK_URL: Slack incoming webhook used for the WARN alert, the BLOCK alert, the unreachable alert, and the flow-failure alert.
  • OSS note: if your Kestra instance reads secrets from environment variables instead of a secrets backend, set SECRET_SCHEMA_CHECK_WEBHOOK_KEY and SECRET_SLACK_WEBHOOK_URL as base64, for example echo -n "<your-value>" | base64.

Quick start

  1. Add the SLACK_WEBHOOK_URL secret, and the SCHEMA_CHECK_WEBHOOK_KEY secret if you plan to enable the webhook.
  2. Create two .proto files: a baseline, and a modified copy that both removes an RPC method (a wire-breaking change) and renames a field's JSON name (a non-breaking change), to exercise both BLOCK and WARN in one pair (or split them into two runs).
  3. Upload them as previous_proto_file and current_proto_file, run the flow manually, and confirm status matches what you expect.
  4. Either enable check_on_pr_webhook and have your CI pipeline curl --data-binary @new_schema.proto the webhook URL on each pull request, or wire a Subflow task pointing at this flow into your pipeline immediately before a schema registry push or service deploy.

How to extend

  • Point --against at a .git#branch=main reference instead of a second file input, if the Kestra worker has access to the schema's git history, to avoid needing a separately supplied baseline file.
  • Add a buf lint step alongside buf breaking, so style and naming convention violations surface in the same gate without being treated as breaking changes.
  • Store each BLOCK's finding types in a KV store keyed by schema file name, to build a team-wide record of what got stopped and why.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.