Skip to content

Update OpenAPI spec (a07d8e7) #189

Update OpenAPI spec (a07d8e7)

Update OpenAPI spec (a07d8e7) #189

Workflow file for this run

name: Validate SDKs
permissions:
contents: write
issues: write
pull-requests: write
statuses: write
on:
pull_request:
branches: [main]
workflow_dispatch:
concurrency:
group: sdk-validation-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
load-matrix:
runs-on: ubuntu-latest
outputs:
spec-changed: ${{ steps.filter.outputs.spec }}
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: filter
with:
filters: |
spec:
- 'spec/**'
- name: Load SDK matrix
id: set-matrix
run: echo "matrix=$(jq -c . .github/sdk-matrix.json)" >> "$GITHUB_OUTPUT"
sdk_build:
name: sdk_build (${{ matrix.language }})
needs: load-matrix
if: needs.load-matrix.outputs.spec-changed == 'true'
# Per-language runner from sdk-matrix.json — ios needs macOS because the
# SDK's hand-maintained code imports Apple-only frameworks (CryptoKit).
runs-on: ${{ matrix.runner || 'ubuntu-latest' }}
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.load-matrix.outputs.matrix) }}
steps:
- name: Resolve SDK checkout path
run: echo "SDK_CHECKOUT_PATH=backend/$(basename ${{ matrix.sdk_repo }})" >> "$GITHUB_ENV"
- name: Checkout openapi-spec
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
path: openapi-spec
- name: Checkout live SDK
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: ${{ matrix.sdk_repo }}
path: ${{ env.SDK_CHECKOUT_PATH }}
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '24'
cache: 'npm'
cache-dependency-path: openapi-spec/package-lock.json
- name: Install dependencies
working-directory: openapi-spec
run: npm ci
- name: Setup SDK runtime
uses: ./openapi-spec/.github/actions/setup-sdk-runtime
with:
language: ${{ matrix.language }}
sdk-path: ${{ env.SDK_CHECKOUT_PATH }}
- name: Extract baseline snapshot
working-directory: openapi-spec
run: |
npm run sdk:compat-extract -- \
--lang ${{ matrix.language }} \
--sdk-root "$GITHUB_WORKSPACE/${{ env.SDK_CHECKOUT_PATH }}"
- name: Generate SDK
working-directory: openapi-spec
run: npm run sdk:generate -- --lang ${{ matrix.language }} --output "$GITHUB_WORKSPACE/${{ env.SDK_CHECKOUT_PATH }}"
- name: Run SDK CI
working-directory: ${{ env.SDK_CHECKOUT_PATH }}
run: |
if [ -x script/ci ]; then
script/ci
elif [ -x scripts/ci ]; then
scripts/ci
else
echo "No script/ci or scripts/ci found, skipping"
fi
- name: Capture SDK code diff
if: always()
working-directory: ${{ env.SDK_CHECKOUT_PATH }}
run: |
mkdir -p "$GITHUB_WORKSPACE/openapi-spec/.oagen/${{ matrix.language }}"
# intent-to-add so untracked generated files appear in the diff
git add -AN
git -c core.quotepath=false diff HEAD \
--src-prefix="a/${{ matrix.language }}/" \
--dst-prefix="b/${{ matrix.language }}/" \
> "$GITHUB_WORKSPACE/openapi-spec/.oagen/${{ matrix.language }}/sdk-code.diff" || true
- name: Extract candidate snapshot
working-directory: openapi-spec
run: |
npm run sdk:compat-extract -- \
--lang ${{ matrix.language }} \
--sdk-root "$GITHUB_WORKSPACE/${{ env.SDK_CHECKOUT_PATH }}" \
--output .oagen/${{ matrix.language }}/sdk
- name: Copy manifest for diagnostics
if: always()
working-directory: openapi-spec
run: |
mkdir -p .oagen/${{ matrix.language }}/sdk
cp "$GITHUB_WORKSPACE/${{ env.SDK_CHECKOUT_PATH }}/.oagen-manifest.json" \
.oagen/${{ matrix.language }}/sdk/ 2>/dev/null || true
- name: Compat diff
id: compat-diff
continue-on-error: true
working-directory: openapi-spec
run: npm run sdk:compat-diff -- --lang ${{ matrix.language }}
- name: Compat summary
if: always() && steps.compat-diff.outcome != 'skipped'
working-directory: openapi-spec
run: |
REPORT=".oagen/${{ matrix.language }}/compat-report.json"
if [ -f "$REPORT" ]; then
npm run sdk:compat-summary -- --report "$REPORT" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload diagnostics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: oagen-diagnostics-${{ matrix.language }}
include-hidden-files: true
path: openapi-spec/.oagen/${{ matrix.language }}/
retention-days: 14
sdk-validation:
runs-on: ubuntu-latest
needs: [load-matrix, sdk_build]
if: always()
steps:
- name: Checkout openapi-spec
if: needs.load-matrix.outputs.spec-changed == 'true'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node
if: needs.load-matrix.outputs.spec-changed == 'true'
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '24'
cache: 'npm'
- name: Download diagnostics
if: needs.load-matrix.outputs.spec-changed == 'true'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: oagen-diagnostics-*
path: sdk-diagnostics
- name: Install dependencies
if: needs.load-matrix.outputs.spec-changed == 'true'
run: npm ci
- name: Validate changelog scopes
id: scope-check
if: needs.load-matrix.outputs.spec-changed == 'true' && github.event_name == 'pull_request'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
# Enforce the changelog scope-mapping invariant on the PR instead of
# only after merge in generate-prs.yml. Reconstruct the same inputs
# (spec diff + old/new IR + compat reports) the post-merge classifier
# uses, then run it with --strict-scopes. Records the result as an
# output and never hard-fails here, so the PR comment still renders;
# "Check result" turns a failure into a red check.
if ! git fetch --depth=1 origin "$BASE_SHA" 2>/dev/null; then
echo "::warning::could not fetch base $BASE_SHA; skipping changelog scope validation"
echo "scopes-valid=true" >> "$GITHUB_OUTPUT"
exit 0
fi
git show "$BASE_SHA:spec/open-api-spec.yaml" > /tmp/previous-open-api-spec.yaml
npx oagen diff --old /tmp/previous-open-api-spec.yaml --new spec/open-api-spec.yaml \
> /tmp/diff-report.json || DIFF_EXIT=$?
# exit 2 means breaking changes were detected — not a failure
if [ "${DIFF_EXIT:-0}" -ne 0 ] && [ "${DIFF_EXIT:-0}" -ne 2 ]; then
echo "::warning::oagen diff failed (exit ${DIFF_EXIT}); skipping changelog scope validation"
echo "scopes-valid=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if ! jq -e '.summary' /tmp/diff-report.json > /dev/null 2>&1; then
echo "::warning::oagen diff produced invalid JSON; skipping changelog scope validation"
echo "scopes-valid=true" >> "$GITHUB_OUTPUT"
exit 0
fi
npx oagen parse --spec /tmp/previous-open-api-spec.yaml > /tmp/previous-ir.json
npx oagen parse --spec spec/open-api-spec.yaml > /tmp/current-ir.json
# Merge every language's breaking compat changes into one report so a
# single run scope-checks both spec-level changes and SDK-surface
# breaks (e.g. a removed *Params type) — the latter only appear in the
# generated SDKs' compat reports, not in the spec diff.
jq -s '{changes: (map(.changes // []) | add)}' \
sdk-diagnostics/oagen-diagnostics-*/compat-report.json > /tmp/merged-compat.json 2>/dev/null \
|| echo '{"changes":[]}' > /tmp/merged-compat.json
out=$(node scripts/sdk-release-metadata.mjs \
--diff-report /tmp/diff-report.json \
--old-ir /tmp/previous-ir.json \
--new-ir /tmp/current-ir.json \
--compat-report /tmp/merged-compat.json \
--strict-scopes \
--output /dev/null 2>&1) && rc=0 || rc=$?
if [ "$rc" -ne 0 ]; then
echo "scopes-valid=false" >> "$GITHUB_OUTPUT"
echo "$out"
{
echo "### ❌ Changelog scope validation failed"
echo ""
echo "A changelog entry resolved to an unmapped or undocumented scope."
echo "Map it in \`scripts/sdk-release-metadata.mjs\` (\`SERVICE_SCOPE_OVERRIDES\` / \`scopeFromName\`) and add \`SCOPE_DOC_URLS\` + \`SCOPE_LABELS\`."
echo ""
echo '```'
echo "$out"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
echo "::error::Changelog scope validation failed — see job summary"
else
echo "scopes-valid=true" >> "$GITHUB_OUTPUT"
fi
exit 0
- name: Build code diff report
if: needs.load-matrix.outputs.spec-changed == 'true'
id: diff-report
run: |
languages="$(jq -r 'map(.language) | join(",")' .github/sdk-matrix.json)"
node scripts/build-sdk-diff-report.mjs \
--artifacts-root sdk-diagnostics \
--languages "$languages" \
--output /tmp/sdk-diff-report.html
- name: Upload code diff report
if: steps.diff-report.outputs.report-exists == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sdk-code-diff-report
path: /tmp/sdk-diff-report.html
retention-days: 14
- name: Publish report to GitHub Pages
id: publish-pages
if: >-
steps.diff-report.outputs.report-exists == 'true'
&& github.event_name == 'pull_request'
&& github.event.pull_request.head.repo.full_name == github.repository
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
worktree=$(mktemp -d)
if git ls-remote --exit-code --heads origin gh-pages >/dev/null 2>&1; then
git fetch --depth=1 origin gh-pages
git worktree add "$worktree" origin/gh-pages
(cd "$worktree" && git checkout -B gh-pages)
else
git worktree add --orphan -b gh-pages "$worktree"
: > "$worktree/.nojekyll"
fi
target_dir="$worktree/pr-${PR_NUMBER}"
mkdir -p "$target_dir"
cp /tmp/sdk-diff-report.html "$target_dir/sdk-diff-report.html"
(
cd "$worktree"
git add -A
if git diff --cached --quiet; then
echo "No changes to publish"
else
git commit -m "Update PR #${PR_NUMBER} SDK diff preview"
git push origin gh-pages
fi
)
owner="${REPO%%/*}"
name="${REPO#*/}"
echo "pages-url=https://${owner}.github.io/${name}/pr-${PR_NUMBER}/sdk-diff-report.html" >> "$GITHUB_OUTPUT"
- name: Render PR comment
if: needs.load-matrix.outputs.spec-changed == 'true'
run: |
node scripts/sdk-compat-pr-comment.mjs \
--artifacts-root sdk-diagnostics \
--build-result "${{ needs.sdk_build.result }}" \
--run-id "${{ github.run_id }}" \
--repo "${{ github.repository }}" \
--code-diff-available "${{ steps.diff-report.outputs.report-exists }}" \
--pages-url "${{ steps.publish-pages.outputs.pages-url }}" \
--output /tmp/sdk-compat-comment.md
- name: Upsert PR comment
if: github.event_name == 'pull_request' && needs.load-matrix.outputs.spec-changed == 'true'
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
jq -n --rawfile body /tmp/sdk-compat-comment.md '{body: $body}' > /tmp/sdk-compat-comment.json
COMMENT_ID="$(gh api "repos/$REPO/issues/$PR_NUMBER/comments" --paginate \
--jq '.[] | select(.user.login == "github-actions[bot]") | select(.body | contains("<!-- sdk-validation-comment -->")) | .id' \
| head -n 1)"
if [[ -n "$COMMENT_ID" ]]; then
gh api \
--method PATCH \
"repos/$REPO/issues/comments/$COMMENT_ID" \
--input /tmp/sdk-compat-comment.json
else
gh api \
--method POST \
"repos/$REPO/issues/$PR_NUMBER/comments" \
--input /tmp/sdk-compat-comment.json
fi
- name: Report SDK compatibility status
# Publish a dedicated `sdk-compat` commit status on the PR head so the
# SDK automation bot can read a clean breaking/non-breaking signal
# (success = no breaking changes; failure = breaking) instead of
# scraping the PR comment. The build/test gate (this job's conclusion)
# is a separate signal; this status only encodes compatibility.
#
# Skip on fork PRs: `pull_request` events from forks get a read-only
# `github.token` regardless of the `permissions:` block, so the
# `statuses` write below would 403 and fail an otherwise-green job.
if: >-
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
RUN_ID: ${{ github.run_id }}
run: |
set -euo pipefail
shopt -s nullglob
# A PR is breaking if any language's compat report contains a
# breaking-severity change. Missing reports (no spec change, or the
# build failed before producing them) count as non-breaking here —
# a failed build is already caught by the job conclusion below.
reports=(sdk-diagnostics/oagen-diagnostics-*/compat-report.json)
breaking=0
if [ ${#reports[@]} -gt 0 ]; then
breaking=$(jq -s '[.[].changes[]? | select(.severity == "breaking")] | length' "${reports[@]}")
fi
if [ "$breaking" -gt 0 ]; then
state="failure"
description="$breaking breaking SDK change(s) detected"
else
state="success"
description="No breaking SDK changes detected"
fi
echo "sdk-compat: $state ($description)"
gh api "repos/$REPO/statuses/$HEAD_SHA" \
-f state="$state" \
-f context="sdk-compat" \
-f description="$description" \
-f target_url="https://github.com/$REPO/actions/runs/$RUN_ID"
- name: Check result
run: |
if [[ "${{ needs.load-matrix.outputs.spec-changed }}" != "true" ]]; then
echo "No spec changes, skipping SDK validation"
exit 0
fi
if [[ "${{ needs.sdk_build.result }}" != "success" ]]; then
echo "SDK build failed"
exit 1
fi
if [[ "${{ steps.scope-check.outputs.scopes-valid }}" == "false" ]]; then
echo "Changelog scope validation failed — a changelog entry resolved to an unmapped or undocumented scope."
echo "See the 'Validate changelog scopes' step and job summary for the offending entries."
exit 1
fi