From a5d65b4a85adf82a7b8f1ea41e9ff35ff4a038d4 Mon Sep 17 00:00:00 2001 From: Reuven Harrison Date: Sun, 17 May 2026 22:08:47 +0300 Subject: [PATCH 1/4] feat: validate action wrapping oasdiff validate Per-finding PR annotations via --format githubactions, plus a notice with a free review-page link when findings are reported. Mirrors the shape of the breaking action: text run for findings count + step output, githubactions run for the annotations, fail-on-finding toggle defaulting to true. allow-external-refs defaults to true (matches oasdiff's binary default); set to false when validating untrusted specs to prevent SSRF. Outputs `findings` (numeric) so downstream steps can branch on it. Co-Authored-By: Claude Opus 4.7 (1M context) --- validate/Dockerfile | 5 +++ validate/action.yml | 24 +++++++++++++++ validate/entrypoint.sh | 70 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 validate/Dockerfile create mode 100644 validate/action.yml create mode 100755 validate/entrypoint.sh diff --git a/validate/Dockerfile b/validate/Dockerfile new file mode 100644 index 0000000..3b3683d --- /dev/null +++ b/validate/Dockerfile @@ -0,0 +1,5 @@ +FROM tufin/oasdiff:v1.15.3 +RUN apk add --no-cache jq +ENV PLATFORM github-action +COPY entrypoint.sh /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] diff --git a/validate/action.yml b/validate/action.yml new file mode 100644 index 0000000..0c18d2b --- /dev/null +++ b/validate/action.yml @@ -0,0 +1,24 @@ +name: 'Validate an OpenAPI spec' +description: 'Validate an OpenAPI spec against the spec, with per-finding PR annotations' +inputs: + spec: + description: 'Path of the OpenAPI spec in YAML or JSON format' + required: true + fail-on-finding: + description: 'Fail with exit code 1 if any finding is reported (default: true)' + required: false + default: 'true' + allow-external-refs: + description: 'Allow external $refs in the spec; disable to prevent SSRF when validating untrusted specs' + required: false + default: 'true' +outputs: + findings: + description: 'Number of findings reported by validate (0 if the spec is well-formed)' +runs: + using: 'docker' + image: 'Dockerfile' + args: + - ${{ inputs.spec }} + - ${{ inputs.fail-on-finding }} + - ${{ inputs.allow-external-refs }} diff --git a/validate/entrypoint.sh b/validate/entrypoint.sh new file mode 100755 index 0000000..e747366 --- /dev/null +++ b/validate/entrypoint.sh @@ -0,0 +1,70 @@ +#!/bin/sh +set -e + +if [ -n "$GITHUB_WORKSPACE" ]; then + git config --global --get-all safe.directory | grep -q "$GITHUB_WORKSPACE" || \ + git config --global --add safe.directory "$GITHUB_WORKSPACE" +fi + +readonly spec="$1" +readonly fail_on_finding="$2" +readonly allow_external_refs="$3" + +echo "running oasdiff validate... spec: $spec, fail_on_finding: $fail_on_finding, allow_external_refs: $allow_external_refs" + +# Build flags. --allow-external-refs defaults to true in oasdiff so we +# only pass --allow-external-refs=false when the input explicitly opts +# out; otherwise rely on the binary's default. +flags="" +if [ "$allow_external_refs" = "false" ]; then + flags="$flags --allow-external-refs=false" +fi +echo "flags: $flags" + +# Run 1: capture the text-format findings count for GITHUB_OUTPUT and +# the user-facing step log. Tolerate non-zero exit — oasdiff returns 1 +# when any finding is reported, but we render annotations and the +# fail-on-finding decision below regardless. +validate_exit=0 +findings_text=$(oasdiff validate $flags "$spec") || validate_exit=$? + +# Run 2: render annotations to stdout via --format githubactions so +# GitHub parses them onto the PR's "Files changed" tab. Tolerate +# non-zero exit (same reason as Run 1). +oasdiff validate $flags --format githubactions "$spec" || true + +# *** GitHub Action step output *** + +# Extract the finding count from the first line of the text output: +# "N findings: N error, N warning, N info" +findings_count=0 +if [ -n "$findings_text" ]; then + header=$(printf '%s' "$findings_text" | head -n 1) + findings_count=$(printf '%s' "$header" | awk '{print $1}') + if ! printf '%s' "$findings_count" | grep -qE '^[0-9]+$'; then + findings_count=0 + fi +fi +echo "findings=$findings_count" >>"$GITHUB_OUTPUT" + +# Emit upgrade notice with a clickable summary link pointing at the +# free review surface. Same pattern as the breaking action: notice +# annotation + GITHUB_STEP_SUMMARY markdown link. +if [ "$findings_count" -gt 0 ]; then + notice_url="https://www.oasdiff.com/review?owner=$(printf '%s' "${GITHUB_REPOSITORY%%/*}" | jq -sRr @uri)&repo=$(printf '%s' "${GITHUB_REPOSITORY#*/}" | jq -sRr @uri)" + echo "::notice::🔎 ${findings_count} OpenAPI validation finding(s) — see annotations above. oasdiff.com → ${notice_url}" + { + echo "### 🔎 oasdiff validate found ${findings_count} OpenAPI spec issue(s)" + echo "" + echo "See annotations on the Files Changed tab for the precise line and column of each finding." + echo "" + echo "[Learn more about oasdiff →](${notice_url})" + } >> "$GITHUB_STEP_SUMMARY" +fi + +# Honour fail-on-finding (default true). When false, we report findings +# but the step still passes — useful for non-blocking visibility runs. +if [ "$fail_on_finding" = "false" ]; then + exit 0 +fi +exit "$validate_exit" From 32bf55b71035a69308491f76f9a84652374a378d Mon Sep 17 00:00:00 2001 From: Reuven Harrison Date: Sun, 24 May 2026 20:16:57 +0300 Subject: [PATCH 2/4] validate action: align with shipped oasdiff validate, add tests and docs The wrapper was written against an assumed boolean `fail-on-finding`; the shipped `oasdiff validate` (oasdiff #894) uses a severity threshold instead. - action.yml: replace `fail-on-finding` with `fail-on` (ERR/WARN/INFO, like the breaking action); expose findings plus error_count/warning_count/info_count. - entrypoint.sh: pass `--fail-on`, run `-f githubactions` for inline annotations (authoritative exit), parse the total for the `findings` output, and emit zero counts for a valid spec. - Dockerfile: FROM tufin/oasdiff:v1.16.0 (the release that ships validate). - test.yaml: jobs for a valid spec (0 findings), an invalid spec (fails), and the severity threshold (a warning passes by default, fails with --fail-on WARN). - specs: valid.yaml, invalid.yaml, validate-warning.yaml fixtures. - release.sh: include validate/Dockerfile in the oasdiff-pin bump list. - README: document the validate action. CI stays red until tufin/oasdiff:v1.16.0 is published; entrypoint logic verified locally against a build of oasdiff main. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/test.yaml | 76 +++++++++++++++++++++++++++++++++++++ README.md | 28 ++++++++++++++ release.sh | 2 +- specs/invalid.yaml | 4 ++ specs/valid.yaml | 10 +++++ specs/validate-warning.yaml | 8 ++++ validate/Dockerfile | 2 +- validate/action.yml | 16 +++++--- validate/entrypoint.sh | 70 +++++++++++++++++++--------------- 9 files changed, 179 insertions(+), 37 deletions(-) create mode 100644 specs/invalid.yaml create mode 100644 specs/valid.yaml create mode 100644 specs/validate-warning.yaml diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c46ec96..d01e36d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -581,3 +581,79 @@ jobs: exit 1 fi echo "PASS" + oasdiff_validate_valid: + runs-on: ubuntu-latest + name: Test validate on a valid spec + steps: + - name: checkout + uses: actions/checkout@v6 + - name: Running validate action on a valid spec + id: test_validate_valid + uses: ./validate + with: + spec: 'specs/valid.yaml' + - name: Test validate reports zero findings + run: | + findings="${{ steps.test_validate_valid.outputs.findings }}" + if [ "$findings" != "0" ]; then + echo "Expected 0 findings, got '$findings'" >&2 + exit 1 + fi + oasdiff_validate_findings: + runs-on: ubuntu-latest + name: Test validate fails on an invalid spec + steps: + - name: checkout + uses: actions/checkout@v6 + - name: Running validate action on an invalid spec + id: test_validate_findings + continue-on-error: true + uses: ./validate + with: + spec: 'specs/invalid.yaml' + - name: Test validate failed and reported findings + run: | + if [ "${{ steps.test_validate_findings.outcome }}" != "failure" ]; then + echo "Expected the validate step to fail on error-level findings" >&2 + exit 1 + fi + findings="${{ steps.test_validate_findings.outputs.findings }}" + if [ "$findings" = "0" ] || [ -z "$findings" ]; then + echo "Expected findings > 0, got '$findings'" >&2 + exit 1 + fi + oasdiff_validate_fail_on: + runs-on: ubuntu-latest + name: Test validate severity threshold (--fail-on) + steps: + - name: checkout + uses: actions/checkout@v6 + - name: Validate a warning-only spec with the default threshold + id: test_validate_warn_default + uses: ./validate + with: + spec: 'specs/validate-warning.yaml' + - name: Default threshold reports the warning but passes + run: | + if [ "${{ steps.test_validate_warn_default.outcome }}" != "success" ]; then + echo "Expected the step to pass (warnings don't fail by default)" >&2 + exit 1 + fi + warnings="${{ steps.test_validate_warn_default.outputs.warning_count }}" + if [ "$warnings" != "1" ]; then + echo "Expected warning_count 1, got '$warnings'" >&2 + exit 1 + fi + - name: Validate the same spec with fail-on WARN + id: test_validate_warn_failon + continue-on-error: true + uses: ./validate + with: + spec: 'specs/validate-warning.yaml' + fail-on: 'WARN' + - name: fail-on WARN escalates the warning to a failure + run: | + if [ "${{ steps.test_validate_warn_failon.outcome }}" != "failure" ]; then + echo "Expected the step to fail with fail-on WARN" >&2 + exit 1 + fi diff --git a/README.md b/README.md index a843fc1..f7056c8 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ GitHub Actions for comparing OpenAPI specs and detecting breaking changes, based - [Check for breaking changes](#check-for-breaking-changes) - [Generate a changelog](#generate-a-changelog) - [Generate a diff report](#generate-a-diff-report) + - [Validate a single spec](#validate-a-single-spec) - [Configuring with `.oasdiff.yaml`](#configuring-with-oasdiffyaml) - [Spec paths](#spec-paths) - [Pro: Rich PR comment](#pro-rich-pr-comment) @@ -156,6 +157,33 @@ jobs: | `flatten-allof` | `false` | Merge allOf subschemas into a single schema before diff | `true`, `false` | | `output-to-file` | `''` | Write output to this file path instead of stdout | file path | +### Validate a single spec + +Validates one OpenAPI spec against the OpenAPI and JSON Schema rules and writes an inline GitHub annotation for each finding. Unlike the other actions it takes a single spec, not a base/revision pair. Findings are classified by severity (error, warning, info); by default the workflow fails only on errors. + +```yaml +name: oasdiff +on: + pull_request: + branches: [ "main" ] +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: oasdiff/oasdiff-action/validate@v0.0.47 + with: + spec: 'openapi.yaml' +``` + +| Input | Default | Description | Accepted values | +|---|---|---|---| +| `spec` | — (required) | Path to the OpenAPI spec to validate | file path, URL, git ref | +| `fail-on` | `''` | Fail with exit code 1 when a finding is at or above this severity (empty uses the oasdiff default, `ERR`) | `ERR`, `WARN`, `INFO` | +| `allow-external-refs` | `true` | Resolve external `$ref`s; set `false` to prevent SSRF when validating untrusted specs | `true`, `false` | + +For a non-blocking, report-only run, leave `fail-on` and set `continue-on-error: true` on the step. Outputs: `findings` (total), `error_count`, `warning_count`, `info_count`. + --- ## Configuring with `.oasdiff.yaml` diff --git a/release.sh b/release.sh index ccfc9b2..a98b7de 100755 --- a/release.sh +++ b/release.sh @@ -11,7 +11,7 @@ set -e REPO_DIR="$(cd "$(dirname "$0")" && pwd)" -DOCKERFILES="breaking/Dockerfile changelog/Dockerfile diff/Dockerfile pr-comment/Dockerfile" +DOCKERFILES="breaking/Dockerfile changelog/Dockerfile diff/Dockerfile validate/Dockerfile pr-comment/Dockerfile" # ── Resolve action version ─────────────────────────────────────────────────── diff --git a/specs/invalid.yaml b/specs/invalid.yaml new file mode 100644 index 0000000..f0e851e --- /dev/null +++ b/specs/invalid.yaml @@ -0,0 +1,4 @@ +openapi: 3.0.0 +info: + title: invalid +paths: {} diff --git a/specs/valid.yaml b/specs/valid.yaml new file mode 100644 index 0000000..b034c88 --- /dev/null +++ b/specs/valid.yaml @@ -0,0 +1,10 @@ +openapi: 3.0.0 +info: + title: valid + version: "1.0.0" +paths: + /things: + get: + responses: + '200': + description: ok diff --git a/specs/validate-warning.yaml b/specs/validate-warning.yaml new file mode 100644 index 0000000..a816f09 --- /dev/null +++ b/specs/validate-warning.yaml @@ -0,0 +1,8 @@ +openapi: 3.0.0 +info: + title: warning + version: "1.0.0" + license: + name: MIT + identifier: MIT +paths: {} diff --git a/validate/Dockerfile b/validate/Dockerfile index 3b3683d..8770bf5 100644 --- a/validate/Dockerfile +++ b/validate/Dockerfile @@ -1,4 +1,4 @@ -FROM tufin/oasdiff:v1.15.3 +FROM tufin/oasdiff:v1.16.0 RUN apk add --no-cache jq ENV PLATFORM github-action COPY entrypoint.sh /entrypoint.sh diff --git a/validate/action.yml b/validate/action.yml index 0c18d2b..b358264 100644 --- a/validate/action.yml +++ b/validate/action.yml @@ -4,21 +4,27 @@ inputs: spec: description: 'Path of the OpenAPI spec in YAML or JSON format' required: true - fail-on-finding: - description: 'Fail with exit code 1 if any finding is reported (default: true)' + fail-on: + description: 'Fail with exit code 1 when a finding has this severity or higher: ERR, WARN, or INFO. Defaults to ERR (errors fail the build; warnings and info are reported but do not). For a non-blocking, report-only run, set continue-on-error on the step.' required: false - default: 'true' + default: '' allow-external-refs: description: 'Allow external $refs in the spec; disable to prevent SSRF when validating untrusted specs' required: false default: 'true' outputs: findings: - description: 'Number of findings reported by validate (0 if the spec is well-formed)' + description: 'Total number of findings reported by validate (0 if the spec is valid)' + error_count: + description: 'Number of error-level findings' + warning_count: + description: 'Number of warning-level findings' + info_count: + description: 'Number of info-level findings' runs: using: 'docker' image: 'Dockerfile' args: - ${{ inputs.spec }} - - ${{ inputs.fail-on-finding }} + - ${{ inputs.fail-on }} - ${{ inputs.allow-external-refs }} diff --git a/validate/entrypoint.sh b/validate/entrypoint.sh index e747366..4b954c6 100755 --- a/validate/entrypoint.sh +++ b/validate/entrypoint.sh @@ -7,52 +7,67 @@ if [ -n "$GITHUB_WORKSPACE" ]; then fi readonly spec="$1" -readonly fail_on_finding="$2" +readonly fail_on="$2" readonly allow_external_refs="$3" -echo "running oasdiff validate... spec: $spec, fail_on_finding: $fail_on_finding, allow_external_refs: $allow_external_refs" +echo "running oasdiff validate... spec: $spec, fail_on: $fail_on, allow_external_refs: $allow_external_refs" -# Build flags. --allow-external-refs defaults to true in oasdiff so we -# only pass --allow-external-refs=false when the input explicitly opts -# out; otherwise rely on the binary's default. +# Build flags. --allow-external-refs defaults to true in oasdiff, so only +# pass it when the input opts out. --fail-on defaults to ERR in oasdiff +# (errors fail the build; warnings and info are reported but don't), so only +# pass it when the input overrides the threshold. flags="" if [ "$allow_external_refs" = "false" ]; then flags="$flags --allow-external-refs=false" fi +if [ -n "$fail_on" ]; then + flags="$flags --fail-on $fail_on" +fi echo "flags: $flags" -# Run 1: capture the text-format findings count for GITHUB_OUTPUT and -# the user-facing step log. Tolerate non-zero exit — oasdiff returns 1 -# when any finding is reported, but we render annotations and the -# fail-on-finding decision below regardless. -validate_exit=0 -findings_text=$(oasdiff validate $flags "$spec") || validate_exit=$? +# Run 1: render annotations to stdout via --format githubactions so GitHub +# parses them onto the PR's "Files changed" tab. This is the authoritative +# run: its exit code honours --fail-on (1 when a finding is at or above the +# threshold, 0 otherwise). Tolerate non-zero so we can still set the output +# and emit the notice below; the exit code is reapplied at the end. +exit_code=0 +oasdiff validate $flags --format githubactions "$spec" || exit_code=$? -# Run 2: render annotations to stdout via --format githubactions so -# GitHub parses them onto the PR's "Files changed" tab. Tolerate -# non-zero exit (same reason as Run 1). -oasdiff validate $flags --format githubactions "$spec" || true +# Run 2: text format, captured for the finding count. Tolerate non-zero +# exit (the authoritative decision is already captured above). +findings_text=$(oasdiff validate $flags "$spec") || true # *** GitHub Action step output *** -# Extract the finding count from the first line of the text output: -# "N findings: N error, N warning, N info" +# Total finding count from the header "N findings: N error, N warning, N info". +# A valid spec prints nothing, so the count stays 0. findings_count=0 if [ -n "$findings_text" ]; then header=$(printf '%s' "$findings_text" | head -n 1) - findings_count=$(printf '%s' "$header" | awk '{print $1}') - if ! printf '%s' "$findings_count" | grep -qE '^[0-9]+$'; then - findings_count=0 + n=$(printf '%s' "$header" | awk '{print $1}') + if printf '%s' "$n" | grep -qE '^[0-9]+$'; then + findings_count="$n" fi fi echo "findings=$findings_count" >>"$GITHUB_OUTPUT" -# Emit upgrade notice with a clickable summary link pointing at the -# free review surface. Same pattern as the breaking action: notice -# annotation + GITHUB_STEP_SUMMARY markdown link. +# The --format githubactions run above writes error_count/warning_count/ +# info_count to GITHUB_OUTPUT, but only when there are findings. Emit zeros +# for a valid spec so those outputs are always present for callers. +if [ "$findings_count" -eq 0 ]; then + { + echo "error_count=0" + echo "warning_count=0" + echo "info_count=0" + } >>"$GITHUB_OUTPUT" +fi + +# When there are findings, point the user at oasdiff.com, the same way the +# breaking action does (notice annotation + step-summary link). The precise +# location of each finding is already on the annotations above. if [ "$findings_count" -gt 0 ]; then notice_url="https://www.oasdiff.com/review?owner=$(printf '%s' "${GITHUB_REPOSITORY%%/*}" | jq -sRr @uri)&repo=$(printf '%s' "${GITHUB_REPOSITORY#*/}" | jq -sRr @uri)" - echo "::notice::🔎 ${findings_count} OpenAPI validation finding(s) — see annotations above. oasdiff.com → ${notice_url}" + echo "::notice::🔎 ${findings_count} OpenAPI validation finding(s). See annotations above. oasdiff.com → ${notice_url}" { echo "### 🔎 oasdiff validate found ${findings_count} OpenAPI spec issue(s)" echo "" @@ -62,9 +77,4 @@ if [ "$findings_count" -gt 0 ]; then } >> "$GITHUB_STEP_SUMMARY" fi -# Honour fail-on-finding (default true). When false, we report findings -# but the step still passes — useful for non-blocking visibility runs. -if [ "$fail_on_finding" = "false" ]; then - exit 0 -fi -exit "$validate_exit" +exit "$exit_code" From 8ba456dbccb70ff58a141508281beb17f2101dbb Mon Sep 17 00:00:00 2001 From: Reuven Harrison Date: Tue, 26 May 2026 00:06:11 +0300 Subject: [PATCH 3/4] validate: drop the /review notice; it has no diff to review The validate action checks a single spec, so the free /review link (which needs a base/revision diff) would just hit the review wall. Remove the notice + step-summary block and the now-unused jq install. The per-finding annotations on the Files Changed tab (--format githubactions) and the findings / error_count / warning_count / info_count outputs are unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- validate/Dockerfile | 1 - validate/entrypoint.sh | 19 ++----------------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/validate/Dockerfile b/validate/Dockerfile index 8770bf5..d77b893 100644 --- a/validate/Dockerfile +++ b/validate/Dockerfile @@ -1,5 +1,4 @@ FROM tufin/oasdiff:v1.16.0 -RUN apk add --no-cache jq ENV PLATFORM github-action COPY entrypoint.sh /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] diff --git a/validate/entrypoint.sh b/validate/entrypoint.sh index 4b954c6..4899134 100755 --- a/validate/entrypoint.sh +++ b/validate/entrypoint.sh @@ -28,8 +28,8 @@ echo "flags: $flags" # Run 1: render annotations to stdout via --format githubactions so GitHub # parses them onto the PR's "Files changed" tab. This is the authoritative # run: its exit code honours --fail-on (1 when a finding is at or above the -# threshold, 0 otherwise). Tolerate non-zero so we can still set the output -# and emit the notice below; the exit code is reapplied at the end. +# threshold, 0 otherwise). Tolerate non-zero so we can still set the outputs +# below; the exit code is reapplied at the end. exit_code=0 oasdiff validate $flags --format githubactions "$spec" || exit_code=$? @@ -62,19 +62,4 @@ if [ "$findings_count" -eq 0 ]; then } >>"$GITHUB_OUTPUT" fi -# When there are findings, point the user at oasdiff.com, the same way the -# breaking action does (notice annotation + step-summary link). The precise -# location of each finding is already on the annotations above. -if [ "$findings_count" -gt 0 ]; then - notice_url="https://www.oasdiff.com/review?owner=$(printf '%s' "${GITHUB_REPOSITORY%%/*}" | jq -sRr @uri)&repo=$(printf '%s' "${GITHUB_REPOSITORY#*/}" | jq -sRr @uri)" - echo "::notice::🔎 ${findings_count} OpenAPI validation finding(s). See annotations above. oasdiff.com → ${notice_url}" - { - echo "### 🔎 oasdiff validate found ${findings_count} OpenAPI spec issue(s)" - echo "" - echo "See annotations on the Files Changed tab for the precise line and column of each finding." - echo "" - echo "[Learn more about oasdiff →](${notice_url})" - } >> "$GITHUB_STEP_SUMMARY" -fi - exit "$exit_code" From d70e40e7638cb8577a9219ce36f5245671c0efbb Mon Sep 17 00:00:00 2001 From: Reuven Harrison Date: Tue, 26 May 2026 00:10:45 +0300 Subject: [PATCH 4/4] validate: reference @v0.0.48 in the README example for consistency Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e8ddc11..4fe905f 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: oasdiff/oasdiff-action/validate@v0.0.47 + - uses: oasdiff/oasdiff-action/validate@v0.0.48 with: spec: 'openapi.yaml' ```