id: openapi-breaking-change-guardian
namespace: company.platform
description: |
Guard a GitHub pull request against breaking OpenAPI changes. The flow checks out the exact
base and head commits, runs oasdiff with a configurable compatibility policy, preserves JSON
and Markdown evidence, updates one PR comment and commit status, and fails the release gate.
inputs:
- id: mode
type: SELECT
displayName: Run mode
description: Use either demo to try the gate without credentials, or GITHUB_PR
for a real pull request.
values:
- DEMO_BREAKING
- DEMO_COMPATIBLE
- GITHUB_PR
defaults: DEMO_BREAKING
- id: repository
type: STRING
displayName: GitHub repository
description: Base repository in owner/name form. The webhook payload must name
this same repository.
defaults: your-org/your-api
validator: '^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?/[A-Za-z0-9_.-]{1,100}$'
- id: pr_number
type: INT
displayName: Pull request number
description: Used for manual GITHUB_PR runs. Webhook runs map the number from
the GitHub event.
defaults: 1
min: 1
- id: spec_path
type: STRING
displayName: Proposed specification path
description: OpenAPI YAML or JSON file at the PR head, relative to the repository root.
defaults: openapi.yaml
validator: '^(?!/)(?!.*(?:^|/)\.\.?(?:/|$))[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*\.(?:yaml|yml|json)$'
- id: base_spec_path
type: STRING
displayName: Baseline specification path
description: Optional old path when the PR renames the spec. Empty means the
same value as spec_path.
defaults: ""
validator: '^(?:|(?!/)(?!.*(?:^|/)\.\.?(?:/|$))[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*\.(?:yaml|yml|json))$'
- id: baseline_strategy
type: SELECT
displayName: Baseline selection
description: Compare with the PR base SHA, the latest target branch, or an
explicit branch, tag, or SHA.
values:
- PR_BASE
- TARGET_BRANCH_HEAD
- CUSTOM_REF
defaults: PR_BASE
- id: baseline_ref
type: STRING
displayName: Custom baseline ref
description: Used only with CUSTOM_REF. May be a branch, tag, or full commit SHA
in the base repository.
defaults: ""
validator: '^[A-Za-z0-9._/-]{0,200}$'
- id: fail_on
type: SELECT
displayName: Gate threshold
description: ERR blocks definite breaks only. WARN also blocks potential
breaking changes.
values:
- ERR
- WARN
defaults: ERR
- id: stability_level
type: SELECT
displayName: Minimum API stability
description: ALL checks every operation. Other values use oasdiff stability
extensions to limit the gate.
values:
- ALL
- draft
- alpha
- beta
- stable
defaults: ALL
- id: stable_deprecation_days
type: INT
displayName: Stable API deprecation window
description: Minimum days a stable operation must be deprecated before removal.
Zero disables the grace rule.
defaults: 0
min: 0
- id: beta_deprecation_days
type: INT
displayName: Beta API deprecation window
description: Minimum days a beta operation must be deprecated before removal.
Zero disables the grace rule.
defaults: 0
min: 0
- id: flatten_allof
type: BOOL
displayName: Flatten allOf schemas
description: Merge allOf branches before comparison to make inherited-schema
findings exact.
defaults: true
- id: use_repository_policy
type: BOOL
displayName: Use repository .oasdiff.yaml
description: Apply .oasdiff.yaml from the PR head when present, including custom
severities and ignore files.
defaults: true
- id: publish_to_github
type: BOOL
displayName: Publish GitHub feedback
description: In GITHUB_PR mode, update one PR comment and set the head commit status.
defaults: true
- id: service_id
type: STRING
displayName: Service identifier
description: Stable key used to record the latest compatibility result in Kestra
KV storage.
defaults: petstore
validator: '^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$'
concurrency:
limit: 2
behavior: QUEUE
tasks:
- id: relevant_event
type: io.kestra.plugin.core.flow.If
description: Run manual executions and GitHub PR events that can change
mergeability; ignore close, label and review noise.
condition: "{{ ['manual', 'opened', 'reopened', 'synchronize',
'ready_for_review'] contains (trigger.body.action ?? 'manual') }}"
then:
- id: guardian
type: io.kestra.plugin.core.flow.WorkingDirectory
description: A shared, ephemeral workspace holds the exact checkouts and both
downloadable reports.
tasks:
- id: write_breaking_demo
type: io.kestra.plugin.scripts.python.Script
runIf: "{{ inputs.mode == 'DEMO_BREAKING' }}"
description: Create a baseline and a revision that removes GET /pets/{petId} and
a required response property.
containerImage: python:3.13-alpine
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
script: |
import json
from pathlib import Path
base = '''openapi: 3.0.3
info:
title: Guardian demo
version: 1.0.0
paths:
/pets:
get:
operationId: listPets
responses:
"200":
description: Pets
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Pet"
/pets/{petId}:
get:
operationId: getPet
parameters:
- in: path
name: petId
required: true
schema:
type: string
responses:
"200":
description: Pet
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
components:
schemas:
Pet:
type: object
required: [id, name]
properties:
id:
type: string
name:
type: string
'''
revision = '''openapi: 3.0.3
info:
title: Guardian demo
version: 1.1.0
paths:
/pets:
get:
operationId: listPets
responses:
"200":
description: Pets
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Pet"
components:
schemas:
Pet:
type: object
required: [id]
properties:
id:
type: string
name:
type: string
'''
for side, text in (("base", base), ("candidate", revision)):
Path(side).mkdir()
Path(side, "openapi.yaml").write_text(text, encoding="utf-8")
context = {
"mode": "DEMO_BREAKING", "repository": "demo/petstore", "pr_number": 0,
"title": "Remove pet lookup contract", "url": "", "base_sha": "demo-base",
"head_sha": "demo-breaking", "base_clone_url": "", "head_clone_url": "",
"base_ref": "v1.0.0", "head_ref": "feature/break-api",
"base_spec_path": "openapi.yaml", "spec_path": "openapi.yaml"
}
Path("context.json").write_text(json.dumps(context), encoding="utf-8")
- id: write_compatible_demo
type: io.kestra.plugin.scripts.python.Script
runIf: "{{ inputs.mode == 'DEMO_COMPATIBLE' }}"
description: Create a baseline and a compatible revision that only adds an
optional response property.
containerImage: python:3.13-alpine
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
script: |
import json
from pathlib import Path
base = '''openapi: 3.0.3
info:
title: Guardian demo
version: 1.0.0
paths:
/pets:
get:
operationId: listPets
responses:
"200":
description: Pets
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Pet"
components:
schemas:
Pet:
type: object
required: [id, name]
properties:
id:
type: string
name:
type: string
'''
revision = '''openapi: 3.0.3
info:
title: Guardian demo
version: 1.1.0
paths:
/pets:
get:
operationId: listPets
responses:
"200":
description: Pets
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Pet"
components:
schemas:
Pet:
type: object
required: [id, name]
properties:
id:
type: string
name:
type: string
avatarUrl:
type: string
format: uri
'''
for side, text in (("base", base), ("candidate", revision)):
Path(side).mkdir()
Path(side, "openapi.yaml").write_text(text, encoding="utf-8")
context = {
"mode": "DEMO_COMPATIBLE", "repository": "demo/petstore", "pr_number": 0,
"title": "Add an optional pet avatar", "url": "", "base_sha": "demo-base",
"head_sha": "demo-compatible", "base_clone_url": "", "head_clone_url": "",
"base_ref": "v1.0.0", "head_ref": "feature/add-avatar",
"base_spec_path": "openapi.yaml", "spec_path": "openapi.yaml"
}
Path("context.json").write_text(json.dumps(context), encoding="utf-8")
- id: resolve_pull_request
type: io.kestra.plugin.scripts.python.Script
runIf: "{{ inputs.mode == 'GITHUB_PR' }}"
description: Fetch canonical PR metadata from GitHub, reject repository
spoofing, and resolve the selected baseline to an immutable SHA.
containerImage: python:3.13-alpine
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
env:
GITHUB_TOKEN: "{{ secret('GITHUB_TOKEN') }}"
REPOSITORY: "{{ inputs.repository }}"
PR_NUMBER: "{{ inputs.pr_number }}"
EVENT_REPOSITORY: "{{ trigger.body.repository.full_name ?? '' }}"
BASELINE_STRATEGY: "{{ inputs.baseline_strategy }}"
BASELINE_REF: "{{ inputs.baseline_ref }}"
SPEC_PATH: "{{ inputs.spec_path }}"
BASE_SPEC_PATH: "{{ inputs.base_spec_path }}"
script: |
import json
import os
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
repo = os.environ["REPOSITORY"]
event_repo = os.environ.get("EVENT_REPOSITORY", "")
if event_repo and event_repo.lower() != repo.lower():
raise SystemExit(f"Webhook repository {event_repo!r} does not match configured repository {repo!r}")
token = os.environ["GITHUB_TOKEN"]
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "kestra-openapi-breaking-change-guardian",
}
def get(path):
request = urllib.request.Request("https://api.github.com" + path, headers=headers)
try:
with urllib.request.urlopen(request, timeout=30) as response:
return json.load(response)
except urllib.error.HTTPError as error:
detail = error.read(1000).decode("utf-8", "replace")
raise SystemExit(f"GitHub API {error.code} for {path}: {detail}") from error
pr_number = int(os.environ["PR_NUMBER"])
pr = get(f"/repos/{repo}/pulls/{pr_number}")
strategy = os.environ["BASELINE_STRATEGY"]
if strategy == "PR_BASE":
base_sha = pr["base"]["sha"]
else:
ref = pr["base"]["ref"] if strategy == "TARGET_BRANCH_HEAD" else os.environ.get("BASELINE_REF", "")
if not ref:
raise SystemExit("baseline_ref is required when baseline_strategy is CUSTOM_REF")
encoded_ref = urllib.parse.quote(ref, safe="")
base_sha = get(f"/repos/{repo}/commits/{encoded_ref}")["sha"]
context = {
"mode": "GITHUB_PR",
"repository": repo,
"pr_number": pr_number,
"title": pr["title"],
"url": pr["html_url"],
"base_sha": base_sha,
"head_sha": pr["head"]["sha"],
"base_clone_url": pr["base"]["repo"]["clone_url"],
"head_clone_url": pr["head"]["repo"]["clone_url"],
"base_ref": pr["base"]["ref"] if strategy != "CUSTOM_REF" else os.environ["BASELINE_REF"],
"head_ref": pr["head"]["label"],
"base_spec_path": os.environ.get("BASE_SPEC_PATH") or os.environ["SPEC_PATH"],
"spec_path": os.environ["SPEC_PATH"],
}
Path("context.json").write_text(json.dumps(context), encoding="utf-8")
print("::" + json.dumps({"outputs": context}) + "::")
- id: clone_baseline
type: io.kestra.plugin.git.Clone
runIf: "{{ inputs.mode == 'GITHUB_PR' }}"
description: Check out the immutable baseline SHA from the PR's base repository,
without submodules.
timeout: PT10M
url: "{{ outputs.resolve_pull_request.vars.base_clone_url }}"
commit: "{{ outputs.resolve_pull_request.vars.base_sha }}"
username: "{{ secret('GITHUB_TOKEN') }}"
password: "{{ secret('GITHUB_TOKEN') }}"
cloneSubmodules: false
directory: base
- id: clone_candidate
type: io.kestra.plugin.git.Clone
runIf: "{{ inputs.mode == 'GITHUB_PR' }}"
description: Check out the immutable head SHA from the actual head repository,
including forked pull requests.
timeout: PT10M
url: "{{ outputs.resolve_pull_request.vars.head_clone_url }}"
commit: "{{ outputs.resolve_pull_request.vars.head_sha }}"
username: "{{ secret('GITHUB_TOKEN') }}"
password: "{{ secret('GITHUB_TOKEN') }}"
cloneSubmodules: false
directory: candidate
- id: verify_material
type: io.kestra.plugin.scripts.python.Script
description: Verify exact Git SHAs and safe spec paths, remove symlinks and Git
metadata, and expose no credentials to the comparison container.
containerImage: python:3.13-alpine
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
networkMode: none
script: |
import json
import pathlib
import shutil
context = json.loads(pathlib.Path("context.json").read_text(encoding="utf-8"))
for side, path_key, sha_key in (("base", "base_spec_path", "base_sha"), ("candidate", "spec_path", "head_sha")):
root = pathlib.Path(side).resolve(strict=True)
if context["mode"] == "GITHUB_PR":
actual = pathlib.Path(side, ".git", "HEAD").read_text(encoding="utf-8").strip()
if actual != context[sha_key]:
raise SystemExit(f"{side} checkout is {actual}, expected {context[sha_key]}")
for link in pathlib.Path(side).rglob("*"):
if link.is_symlink():
link.unlink()
target = pathlib.Path(side, context[path_key]).resolve(strict=True)
if root not in target.parents or not target.is_file():
raise SystemExit(f"{path_key} does not resolve to a regular file inside {side}")
shutil.rmtree(pathlib.Path(side, ".git"), ignore_errors=True)
print("Verified immutable inputs and removed Git metadata and symlinks")
- id: compare_contracts
type: io.kestra.plugin.scripts.shell.Commands
description: Run pinned oasdiff offline, capture its semantic findings as JSON,
and preserve exit 1 as a gate decision instead of a task crash.
timeout: PT5M
containerImage: tufin/oasdiff:v1.32.1
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
networkMode: none
cpu:
cpus: 1
memory:
memory: 512MB
env:
FAIL_ON: "{{ inputs.fail_on }}"
STABILITY: "{{ inputs.stability_level }}"
STABLE_DAYS: "{{ inputs.stable_deprecation_days }}"
BETA_DAYS: "{{ inputs.beta_deprecation_days }}"
FLATTEN_ALLOF: "{{ inputs.flatten_allof }}"
USE_REPO_POLICY: "{{ inputs.use_repository_policy }}"
commands:
- |
set -u
BASE_PATH=$(sed -n 's/.*"base_spec_path": *"\([^"]*\)".*/\1/p' context.json)
HEAD_PATH=$(sed -n 's/.*"spec_path": *"\([^"]*\)".*/\1/p' context.json)
set -- breaking --fail-on "$FAIL_ON" --format json --allow-external-refs=false
if [ "$FLATTEN_ALLOF" = "true" ]; then set -- "$@" --flatten-allof; fi
if [ "$STABILITY" != "ALL" ]; then set -- "$@" --stability-level "$STABILITY"; fi
if [ "$STABLE_DAYS" -gt 0 ]; then set -- "$@" --deprecation-days-stable "$STABLE_DAYS"; fi
if [ "$BETA_DAYS" -gt 0 ]; then set -- "$@" --deprecation-days-beta "$BETA_DAYS"; fi
if [ "$USE_REPO_POLICY" = "true" ] && [ -f candidate/.oasdiff.yaml ]; then set -- "$@" --config candidate/.oasdiff.yaml; fi
set -- "$@" -- "base/$BASE_PATH" "candidate/$HEAD_PATH"
set +e
oasdiff "$@" > oasdiff-findings.json 2> oasdiff-error.log
code=$?
set -e
if [ "$code" -gt 1 ]; then
cat oasdiff-error.log >&2
exit "$code"
fi
echo "oasdiff completed with policy exit code $code"
echo "::{\"outputs\":{\"policy_exit\":$code}}::"
outputFiles:
- oasdiff-findings.json
- id: build_evidence
type: io.kestra.plugin.scripts.python.Script
description: Convert structured findings into a versioned JSON artifact and a
concise Markdown review with exact methods, paths, rules and
source lines.
containerImage: python:3.13-alpine
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
networkMode: none
inputFiles:
oasdiff-findings.json: "{{
outputs.compare_contracts.outputFiles['oasdiff-findings.json']
}}"
env:
POLICY_EXIT: "{{ outputs.compare_contracts.vars.policy_exit }}"
FAIL_ON: "{{ inputs.fail_on }}"
STABILITY: "{{ inputs.stability_level }}"
FLATTEN_ALLOF: "{{ inputs.flatten_allof }}"
EXECUTION_ID: "{{ execution.id }}"
outputFiles:
- compatibility-report.json
- compatibility-report.md
script: |
import json
import os
from pathlib import Path
context = json.loads(Path("context.json").read_text(encoding="utf-8"))
raw = Path("oasdiff-findings.json").read_text(encoding="utf-8").strip()
findings = json.loads(raw or "[]")
if not isinstance(findings, list):
raise SystemExit("Unexpected oasdiff JSON: expected a list of findings")
names = {3: "error", 2: "warning", 1: "info"}
counts = {"errors": 0, "warnings": 0, "info": 0}
for item in findings:
level = int(item.get("level", 0))
key = {3: "errors", 2: "warnings", 1: "info"}.get(level, "info")
counts[key] += 1
policy_exit = int(os.environ["POLICY_EXIT"])
verdict = "BLOCK" if policy_exit == 1 else ("WARN" if findings else "PASS")
report = {
"schema": "kestra-openapi-breaking-change-guardian/1.0",
"verdict": verdict,
"policy": {
"failOn": os.environ["FAIL_ON"],
"stabilityLevel": os.environ["STABILITY"],
"flattenAllOf": os.environ["FLATTEN_ALLOF"].lower() == "true",
"externalRefsAllowed": False,
},
"repository": context["repository"],
"pullRequest": context["pr_number"],
"base": {"ref": context["base_ref"], "sha": context["base_sha"], "path": context["base_spec_path"]},
"candidate": {"ref": context["head_ref"], "sha": context["head_sha"], "path": context["spec_path"]},
"counts": counts,
"findings": findings,
"executionId": os.environ["EXECUTION_ID"],
}
Path("compatibility-report.json").write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
badge = {"BLOCK": "\u274c BLOCK", "WARN": "\u26a0\ufe0f PASS WITH WARNINGS", "PASS": "\u2705 PASS"}[verdict]
lines = [
f"## OpenAPI compatibility: {badge}", "",
"| | |", "|---|---|",
f"| Repository | `{context['repository']}` |",
f"| Compared | `{context['base_sha'][:12]}` `{context['base_spec_path']}` \u2192 `{context['head_sha'][:12]}` `{context['spec_path']}` |",
f"| Policy | block on `{os.environ['FAIL_ON']}`; stability `{os.environ['STABILITY']}`; flatten `allOf`: `{os.environ['FLATTEN_ALLOF'].lower()}` |",
f"| Findings | {counts['errors']} error(s), {counts['warnings']} warning(s) |", "",
]
if not findings:
lines += ["No breaking changes were detected.", ""]
else:
lines += ["| Severity | Operation | Location | Evidence | Rule |", "|---|---|---|---|---|"]
for item in findings[:50]:
severity = names.get(int(item.get("level", 0)), "info").upper()
operation = str(item.get("operation") or "\u2014")
location = str(item.get("path") or item.get("section") or "\u2014").replace("|", "\\|")
text = str(item.get("text") or "").replace("|", "\\|").replace("\n", " ")
rule = str(item.get("id") or "\u2014")
source = item.get("baseSource") or item.get("revisionSource") or {}
line = f" (line {source['line']})" if source.get("line") else ""
lines.append(f"| {severity} | `{operation}` | `{location}` | {text}{line} | `{rule}` |")
if len(findings) > 50:
lines += ["", f"Showing 50 of {len(findings)} findings. Download the JSON report for the complete list."]
lines.append("")
if verdict == "BLOCK":
lines += ["The compatibility policy rejected this revision. Restore the contract, introduce a versioned endpoint, or document an intentional exception in `.oasdiff.yaml`.", ""]
lines.append(f"<sub>Generated by Kestra execution `{os.environ['EXECUTION_ID']}` with oasdiff v1.32.1. Full JSON and Markdown reports are attached to the execution.</sub>")
markdown = "\n".join(lines) + "\n"
Path("compatibility-report.md").write_text(markdown, encoding="utf-8")
record = {
"verdict": verdict, "repository": context["repository"], "pr": context["pr_number"],
"baseSha": context["base_sha"], "headSha": context["head_sha"], "counts": counts,
"executionId": os.environ["EXECUTION_ID"]
}
outputs = {
"verdict": verdict, "errors": counts["errors"], "warnings": counts["warnings"],
"findings": len(findings), "repository": context["repository"],
"pr_number": context["pr_number"], "head_sha": context["head_sha"],
"record_json": json.dumps(record, separators=(",", ":")),
}
for item in findings:
severity = names.get(int(item.get("level", 0)), "info").upper()
operation = str(item.get("operation") or "API")
location = str(item.get("path") or item.get("section") or "contract")
rule = str(item.get("id") or "unknown-rule")
text = str(item.get("text") or "").replace("\n", " ")
print(f"{severity} [{rule}] {operation} {location}: {text}")
print(f"{verdict}: {counts['errors']} error(s), {counts['warnings']} warning(s)")
print("::" + json.dumps({"outputs": outputs}) + "::")
- id: record_latest_result
type: io.kestra.plugin.core.kv.Set
description: Record the latest verdict per service so dashboards and later
release flows can query it without scraping logs.
key: "openapi_guardian_{{ inputs.service_id }}"
kvType: JSON
overwrite: true
value: "{{ outputs.build_evidence.vars.record_json }}"
- id: publish_github_feedback
type: io.kestra.plugin.core.flow.If
description: GitHub side effects are isolated from both demo modes and can be
disabled while evaluating the flow.
condition: "{{ inputs.mode == 'GITHUB_PR' and inputs.publish_to_github }}"
then:
- id: set_commit_status
type: io.kestra.plugin.core.http.Request
description: Set a mergeable commit status on the exact PR head SHA after all
evidence has been persisted.
method: POST
uri: "https://api.github.com/repos/{{ outputs.build_evidence.vars.repository
}}/statuses/{{ outputs.build_evidence.vars.head_sha }}"
contentType: application/json
headers:
Authorization: "Bearer {{ secret('GITHUB_TOKEN') }}"
Accept: application/vnd.github+json
X-GitHub-Api-Version: "2022-11-28"
body: |
{
"state": "{{ outputs.build_evidence.vars.verdict == 'BLOCK' ? 'failure' : 'success' }}",
"context": "kestra/openapi-compatibility",
"description": "{{ outputs.build_evidence.vars.verdict }}: {{ outputs.build_evidence.vars.errors }} error(s), {{ outputs.build_evidence.vars.warnings }} warning(s)"
}
- id: comment_on_pull_request
type: io.kestra.plugin.github.issues.Comment
description: Keep one evidence comment per spec path and update it on every PR
synchronize event instead of creating noise.
oauthToken: "{{ secret('GITHUB_TOKEN') }}"
repository: "{{ outputs.build_evidence.vars.repository }}"
issueNumber: "{{ outputs.build_evidence.vars.pr_number }}"
updateTag: "openapi-guardian:{{ inputs.service_id }}"
body: "{{ read(outputs.build_evidence.outputFiles['compatibility-report.md'])
}}"
- id: enforce_gate
type: io.kestra.plugin.core.flow.If
description: Fail only after the evidence, KV record, commit status and PR
comment have been published.
condition: "{{ outputs.build_evidence.vars.verdict == 'BLOCK' }}"
then:
- id: block_release
type: io.kestra.plugin.core.execution.Fail
errorMessage: "OpenAPI compatibility blocked: {{
outputs.build_evidence.vars.errors }} error(s) and {{
outputs.build_evidence.vars.warnings }} warning(s). Download
compatibility-report.json for exact evidence."
else:
- id: pass_release
type: io.kestra.plugin.core.log.Log
message: "OpenAPI compatibility {{ outputs.build_evidence.vars.verdict }}: {{
outputs.build_evidence.vars.errors }} error(s), {{
outputs.build_evidence.vars.warnings }} warning(s)."
else:
- id: ignored_event
type: io.kestra.plugin.core.log.Log
message: "Ignored GitHub pull_request action {{ trigger.body.action }}."
outputs:
- id: verdict
type: STRING
description: PASS, WARN or BLOCK. Ignored webhook events return SKIPPED.
value: "{{ outputs.build_evidence.vars.verdict ?? 'SKIPPED' }}"
- id: json_report
type: FILE
description: Versioned machine-readable report with policy, commits, counts and
every oasdiff finding.
value: "{{ outputs.build_evidence.outputFiles['compatibility-report.json'] ?? ''
}}"
- id: markdown_report
type: FILE
description: Reviewer-friendly report also used as the GitHub pull request comment.
value: "{{ outputs.build_evidence.outputFiles['compatibility-report.md'] ?? '' }}"
triggers:
- id: github_pull_request
type: io.kestra.plugin.core.trigger.Webhook
description: Configure a GitHub repository webhook for Pull requests with JSON
content. The repository remains pinned in the flow; only mode and PR
number come from the event.
key: "{{ secret('OPENAPI_GUARDIAN_WEBHOOK_KEY') }}"
inputs:
mode: GITHUB_PR
pr_number: "{{ trigger.body.number }}"