-
Notifications
You must be signed in to change notification settings - Fork 0
Publish a per-tenant OpenAPI spec, off #36 and onto main #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
c86c7b6
feat(openapi): publish a per-tenant spec and generate clients from it
blarghmatey fffa05c
fix(openapi): diff the union of base/head specs, pin oasdiff, describ…
blarghmatey 03b770c
feat(openapi): publish the learner-records spec and refresh the dashb…
blarghmatey 43f343e
fix(openapi): declare list query params as arrays, not optional arrays
blarghmatey fba8ee5
docs: record the list-query-param rule and which learner-records spec…
blarghmatey e0c8b31
test(openapi): pin that array query params never publish a nullable v…
blarghmatey 41679a1
fix(openapi): keep the breaking-change check running on fork PRs
blarghmatey File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| name: OpenAPI Diff | ||
|
|
||
| # The committed spec is meant to be what a future Concourse client pipeline | ||
| # generates a published TypeScript package from (see README.md), so a diff | ||
| # here is a preview of a change to somebody else's build. This surfaces that | ||
| # change as a comment and fails the PR on a breaking one, rather than leaving | ||
| # it to whoever reads 1500 lines of YAML. | ||
|
|
||
| on: | ||
| pull_request: | ||
| paths: | ||
| - "openapi/specs/**" | ||
|
|
||
| permissions: {} | ||
|
|
||
| jobs: | ||
| openapi-diff: | ||
| runs-on: ubuntu-24.04 | ||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
| steps: | ||
| - name: Checkout HEAD | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| # The exact commit under review, not the branch name: a push while | ||
| # this runs would otherwise diff a commit nobody reviewed. | ||
| ref: ${{ github.event.pull_request.head.sha }} | ||
| path: head | ||
| persist-credentials: false | ||
| - name: Checkout BASE | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| ref: ${{ github.event.pull_request.base.sha }} | ||
| path: base | ||
| persist-credentials: false | ||
| - name: Generate oasdiff changelog | ||
| run: | # Write the comment body to a file rather than a step output. | ||
| # A large changelog interpolated into a JS action's `body:` input becomes a | ||
| # huge INPUT_BODY env var, which can blow past the OS argv+envp size limit | ||
| # and crash the action with "Argument list too long". Writing straight to a | ||
| # file and using `body-path` avoids that entirely. | ||
| # | ||
| # The spec list is the union of base and head filenames, not just base's: | ||
| # a base-only loop silently drops both a spec added in this PR (never in | ||
| # base, so never iterated) and a spec removed in this PR (caught by the | ||
| # -f guard below, so skipped instead of reported as a removal). | ||
| specs=$( | ||
| { | ||
| [ -d base/openapi/specs ] && (cd base/openapi/specs && ls -1 ./*.yaml) | ||
| [ -d head/openapi/specs ] && (cd head/openapi/specs && ls -1 ./*.yaml) | ||
| } 2>/dev/null | xargs -n1 basename | sort -u | ||
| ) | ||
| { | ||
| echo "## OpenAPI Changes" | ||
| echo "" | ||
| echo "<details>" | ||
| echo "<summary>Show/hide changes</summary>" | ||
| echo "" | ||
| echo '```' | ||
| for name in $specs; do | ||
| base_spec="base/openapi/specs/$name" | ||
| head_spec="head/openapi/specs/$name" | ||
| if [ -f "$base_spec" ] && [ -f "$head_spec" ]; then | ||
| echo "## Changes for $name:" | ||
| docker run --rm \ | ||
| --workdir "$GITHUB_WORKSPACE" \ | ||
| --volume "$GITHUB_WORKSPACE:$GITHUB_WORKSPACE:ro" \ | ||
| tufin/oasdiff@sha256:6065c16a4c9ce12504752f444d4981091e58c2a35436fac90b649be47d833db3 \ | ||
| changelog "$base_spec" "$head_spec" | ||
| echo "" | ||
| elif [ -f "$head_spec" ]; then | ||
| echo "## $name: added" | ||
| echo "" | ||
| elif [ -f "$base_spec" ]; then | ||
| echo "## $name: removed" | ||
| echo "" | ||
| fi | ||
| done | ||
| echo '```' | ||
| echo "" | ||
| echo "Unexpected changes? Ensure your branch is up-to-date with \`main\` (consider rebasing)." | ||
| echo "</details>" | ||
| } > comment_body.md | ||
| # A fork's GITHUB_TOKEN is read-only whatever this job asks for, so both | ||
| # comment steps would fail and take the breaking-change check below down | ||
| # with them. The check is the gate; the comment is a convenience. Skip | ||
| # the convenience rather than lose the gate. | ||
| - name: Find existing comment | ||
| id: find_comment | ||
| if: github.event.pull_request.head.repo.full_name == github.repository | ||
| uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4 | ||
| with: | ||
| token: ${{ secrets.GITHUB_TOKEN }} | ||
| repository: ${{ github.repository }} | ||
| issue-number: ${{ github.event.pull_request.number }} | ||
| body-includes: "## OpenAPI Changes" | ||
| - name: Post changes as comment | ||
| uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5 | ||
| # Even with no changes, update the old comment if one was found. | ||
| if: github.event.pull_request.head.repo.full_name == github.repository | ||
| with: | ||
| token: ${{ secrets.GITHUB_TOKEN }} | ||
| edit-mode: "replace" | ||
| repository: ${{ github.repository }} | ||
| issue-number: ${{ github.event.pull_request.number }} | ||
| comment-id: ${{ steps.find_comment.outputs.comment-id }} | ||
| body-path: comment_body.md | ||
| - name: Check for breaking changes | ||
| run: | | ||
| # Breaking here means breaking a client someone else already | ||
| # generated and shipped, so this fails the PR rather than warning. | ||
| # A spec removed outright is the most breaking change there is — | ||
| # deleting the whole published API for a tenant — so it's checked | ||
| # explicitly rather than relying on the -f guard to skip it. | ||
| specs=$( | ||
| { | ||
| [ -d base/openapi/specs ] && (cd base/openapi/specs && ls -1 ./*.yaml) | ||
| [ -d head/openapi/specs ] && (cd head/openapi/specs && ls -1 ./*.yaml) | ||
| } 2>/dev/null | xargs -n1 basename | sort -u | ||
| ) | ||
| for name in $specs; do | ||
| base_spec="base/openapi/specs/$name" | ||
| head_spec="head/openapi/specs/$name" | ||
| if [ -f "$base_spec" ] && [ -f "$head_spec" ]; then | ||
| echo "Checking $name for breaking changes..." | ||
| docker run --rm \ | ||
| --workdir "$GITHUB_WORKSPACE" \ | ||
| --volume "$GITHUB_WORKSPACE:$GITHUB_WORKSPACE:ro" \ | ||
| tufin/oasdiff@sha256:6065c16a4c9ce12504752f444d4981091e58c2a35436fac90b649be47d833db3 \ | ||
| breaking \ | ||
| --fail-on ERR \ | ||
| --format githubactions \ | ||
| "$base_spec" "$head_spec" | ||
| elif [ -f "$base_spec" ]; then | ||
| echo "::error::$name was removed — deleting a published spec is a breaking change." | ||
| exit 1 | ||
| fi | ||
| done | ||
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| #!/usr/bin/env python3 | ||
| """Write each mounted tenant's OpenAPI document to openapi/specs/<tenant>.yaml. | ||
|
|
||
| Run as `uv run bin/generate-openapi-spec`. | ||
|
|
||
| The output is committed, and that is the point: a materialized view gaining or | ||
| renaming a column changes a response model, which changes this file, which | ||
| shows up in review as an interface diff instead of silently drifting away from | ||
| the clients generated off it. `tests/test_openapi_spec.py` fails when the | ||
| committed file no longer matches what the code produces. | ||
|
|
||
| The intended consumer is a Concourse pipeline in ol-infrastructure | ||
| (`ol_concourse/pipelines/libraries/api_clients_pipeline.py`), mirroring the one | ||
| mitxonline and mit-learn already use: watch `openapi/specs/*.yaml` on a | ||
| release branch and regenerate the published TypeScript client from it. That | ||
| pipeline isn't wired up for this repo yet (no `PIPELINE_CONFIGS` entry, no | ||
| `release` branch) — this file exists so the spec is ready to publish once it | ||
| is. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| import cyclopts | ||
|
|
||
| from ol_analytics_api.openapi import render, tenant_specs | ||
|
|
||
| DEFAULT_DIRECTORY = Path("openapi/specs") | ||
|
|
||
| app = cyclopts.App(name="generate-openapi-spec", help=__doc__) | ||
|
|
||
|
|
||
| @app.default | ||
| def generate(*, directory: Path = DEFAULT_DIRECTORY, check: bool = False) -> None: | ||
| """Write (or, with --check, verify) the per-tenant OpenAPI documents. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| directory | ||
| Where the <tenant>.yaml files are written. | ||
| check | ||
| Compare against what is already on disk and exit non-zero on any | ||
| difference, without writing anything. | ||
| """ | ||
| stale = [] | ||
| for tenant_name, spec in tenant_specs().items(): | ||
| path = directory / f"{tenant_name}.yaml" | ||
| rendered = render(spec) | ||
| if check: | ||
| if not path.exists() or path.read_text() != rendered: | ||
| stale.append(path) | ||
| continue | ||
| path.parent.mkdir(parents=True, exist_ok=True) | ||
| path.write_text(rendered) | ||
| sys.stdout.write(f"wrote {path}\n") | ||
| if stale: | ||
| names = ", ".join(str(path) for path in stale) | ||
| sys.stderr.write( | ||
| f"OpenAPI spec is out of date: {names}. " | ||
| "Regenerate with `uv run bin/generate-openapi-spec`.\n" | ||
| ) | ||
| raise SystemExit(1) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| app() |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.