ci(quality): add OpenAPI breaking-change gate (oasdiff, advisory) (#3951)

New advisory gate (Fase 8 B.4) that diffs the public API contract
docs/reference/openapi.yaml against the base branch via oasdiff, surfacing
removed endpoints, newly-required params, removed response fields, etc.

- scripts/check/check-openapi-breaking.mjs: resolves base spec via
  git show <BASE_REF>:docs/reference/openapi.yaml to a temp file, runs
  oasdiff breaking --format json, parses+counts breaking changes, emits
  openapiBreaking=N (KEY=VALUE for collect-metrics). ADVISORY: exit 0 always;
  graceful SKIP when oasdiff is absent or the base spec can't be resolved.
- package.json: check:openapi-breaking script.
- .github/workflows/ci.yml (quality-extended, advisory): install oasdiff via
  gh release download, add the breaking-change step (BASE_REF via env, never
  shell-interpolated), fetch-depth: 0 so git show can read the base spec.
- tests/unit/build/check-openapi-breaking.test.ts: parser counting/grouping +
  binary-absent SKIP integration.
- docs/architecture/QUALITY_GATES.md: document the quality-extended job table.
- docs/reference/openapi.yaml: define the BadRequest/NotFound/InternalError
  response components that were referenced (14 call sites) but never defined —
  a pre-existing dangling-$ref defect that blocked oasdiff from loading the spec.
  No path behavior changes; purely additive to components.responses.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-15 23:47:43 -03:00
committed by GitHub
parent bc32c6710e
commit dbf9293c7d
6 changed files with 599 additions and 0 deletions

View File

@@ -147,7 +147,12 @@ jobs:
runs-on: ubuntu-latest
continue-on-error: true
steps:
# fetch-depth: 0 — the OpenAPI breaking-change gate (oasdiff) reads the base
# spec via `git show <base_ref>:docs/reference/openapi.yaml`; a shallow clone
# would lack the base ref and the gate would self-skip (base-unresolved).
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
@@ -195,6 +200,10 @@ jobs:
bash <(curl -fsSL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) latest "$HOME/.local/bin"
# zizmor — PyPI (pipx preferred, pip --user fallback); lands in ~/.local/bin
pipx install zizmor || pip install --user zizmor
# oasdiff — download latest linux amd64 tarball via gh (authed), extract binary
rm -rf /tmp/oasd && mkdir -p /tmp/oasd
gh release download --repo oasdiff/oasdiff --pattern '*linux_amd64.tar.gz' --dir /tmp/oasd
tar -xzf /tmp/oasd/*linux_amd64.tar.gz -C "$HOME/.local/bin" oasdiff
# ALWAYS export the bin dir (even if any step above failed)
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
# diagnostics — prove what installed on the next CI run
@@ -202,6 +211,7 @@ jobs:
"$HOME/.local/bin/gitleaks" version || true
"$HOME/.local/bin/actionlint" -version || true
"$HOME/.local/bin/osv-scanner" --version || true
"$HOME/.local/bin/oasdiff" --version || true
zizmor --version || true
- name: Secret scan (gitleaks; skips if absent)
run: npm run check:secrets
@@ -209,6 +219,15 @@ jobs:
run: npm run check:vuln-ratchet
- name: Workflow lint (actionlint+zizmor; skips if absent)
run: npm run check:workflows
# OpenAPI breaking-change detection (oasdiff). Diffs the PR's public API
# contract (docs/reference/openapi.yaml) against the base branch's spec.
# ADVISORY: reports `openapiBreaking=N` and self-skips when oasdiff is absent
# or the base spec can't be resolved. BASE_REF is read by the script from the
# env (never interpolated into a shell body) — workflow-injection-safe.
- name: OpenAPI breaking-change (oasdiff; advisory)
env:
BASE_REF: ${{ github.base_ref }}
run: npm run check:openapi-breaking
docs-sync-strict:
name: Docs Sync (Strict)