Update OpenAPI spec (a07d8e7) #189
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |