Publish a per-tenant OpenAPI spec, off #36 and onto main - #70
Merged
Merged
Conversation
MIT Learn consumes this service through hand-written types and a hand-written axios client that mirror models.py column for column. That was the right call for the first cut - nothing here published a client, and blocking the dashboard on a cross-repo publish pipeline was not worth it - but it means the frontend drifts silently every time a materialized view gains or renames a column. It already has: ol-analytics-api#33 made three engagement totals nullable and added seven columns, and mit-learn's types still say otherwise. Each tenant is a mounted sub-app, so it owns its own /openapi.json and the root app's schema contains none of it. `openapi.py` builds the apps through the same create_app() the server runs and takes each tenant's document from there, with two fixups that exist because the output is for a client generator rather than for the tenant's own /docs: paths are re-prefixed with the mount path, since Starlette strips it before the sub-app sees a request and a client pointed at the service host would otherwise call URLs that do not exist; and the document version is pinned rather than read from the package, so a release that changes no route produces no diff. Every route now names its own operation_id. That string becomes the generated client's method name, and FastAPI's default derives one from the function name and the whole path - `contractUtilizationOrganizationsOrganizationIdContract UtilizationGet`, renamed whenever the path moves. The tag prefix is also what keeps the org and contract routers' identically-named panels apart. Verified rather than assumed, since the org and contract endpoints are registered in a loop over a table of specs with a runtime-parametrized generic: openapi-generator v7.2.0 emits a distinct TypeScript interface per row model and per envelope (no collapse to one untyped OrgAnalyticsResponse), resolves the 3.1 `anyOf: [integer, null]` columns to `number | null`, and the result typechecks clean under `tsc --strict`. A test asserts the non-collapse so a future registration change cannot quietly undo it. The spec is committed because it is a cross-repo interface: ol-infrastructure's api_clients_pipeline watches openapi/specs/*.yaml on release and publishes the TypeScript package from it, the same arrangement behind @mitodl/mitxonline-api-axios. Drift fails CI twice over - as a test, and as a --check run of the generator, which is the only thing that exercises the generator at all. openapi-diff.yml comments the changelog on any PR touching a spec and fails on a breaking change, because breaking one here means breaking a client someone already shipped. Still to land before mit-learn can drop its hand-written client: the ol-analytics-api-clients repo and the PIPELINE_CONFIGS entry pointing at it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AVSXwars1LvgtqV1YWhrB1
…e the pipeline as future The base-only spec loop skipped added specs in the changelog and let the -f guard silently skip deleted specs in the breaking-change check -- deleting a whole published API would have passed CI. Iterate the union of base and head filenames instead, and fail explicitly on a removed spec. Pin oasdiff by digest in both invocations so an upstream image change can't silently alter breaking-change classification. The README, generator script, and workflow comment described the ol-infrastructure client-publishing pipeline as already wired up; none of it exists yet (no PIPELINE_CONFIGS entry, no release branch, MIT Learn still on its hand-written client). Reworded to the intended future state. Added a test asserting every route's operation_id is explicit: the existing uniqueness check still passes for a route that fell back to FastAPI's path-derived default, which is unique but not stable.
…oard's The spec export was written before the b2b_learner_records tenant existed and before the dashboard gained /learner-progress, so this is the first generator run that covers what main actually serves. b2b_learner_records.yaml is new; b2b_dashboard.yaml picks up the response-model and description changes that landed in the meantime. /learner-progress had no explicit operation_id, so FastAPI derived one from the function name and the whole path (learner_progress_organizations__organization_id__contracts__contract_id__learner_progress_get), which renames the generated client method whenever the route moves. test_operation_ids_are_explicit catches exactly this; naming it learners_progress_retrieve follows the tag-prefixed convention the other two routers already use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH3LZ41bD4ZKDnTQcviqb6
`Optional[list[X]] = None` renders as `anyOf: [{type: array}, {type: null}]`.
openapi-generator's typescript-axios cannot reduce that to `Array<X> | null`,
so it falls back to treating the parameter as an arbitrary object and spreads
it with `Object.entries`. The generated call then serializes
`["passed", "certified"]` as `?0=passed&1=certified` instead of the repeated
`?completion_status=passed&completion_status=certified` FastAPI parses, and
the generated file fails `tsc --strict`.
Three parameters are affected: b2b_learner_records' `learner_id` (on
/learners and /enrollments) and `completion_status`, and b2b_dashboard's
`completion_status` on /learner-progress. All three already collapse the two
cases at the call site (`tuple(learner_id or ())`), so an omitted parameter
and an empty list were never distinguishable to the query layer. Declaring
them as plain arrays with `Query(default_factory=list)` makes the schema say
what the endpoints already mean, and gives a fresh list per request rather
than a shared mutable default.
Verified with openapi-generator 7.2.0: both tenants' clients now assign the
array to the parameter's own key, which common.ts's setFlattenedQueryParams
appends once per element, and both typecheck clean under `tsc --strict`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CH3LZ41bD4ZKDnTQcviqb6
… is which The `anyOf: [array, null]` footgun is invisible until someone generates a client, which nothing in this repo does yet, so the constraint needs to be written down next to the other two generator constraints. The tenant now has two YAML files with near-identical names and different jobs: the hand-written partner draft under docs/openapi/ and the generated contract under openapi/specs/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH3LZ41bD4ZKDnTQcviqb6
…ariant The previous commit argued from the call sites that an omitted list filter and an empty one are the same query. Nothing enforced it, and nothing stopped a new route from reintroducing `list[X] | None` and quietly breaking the generated client again. test_array_query_params_are_not_nullable sweeps every published parameter for an `anyOf` containing an array, so it covers routes nobody has written yet rather than the three that exist. The endpoint test pins the behavior half: omitting both filters adds no predicate and binds only the org and paging parameters. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH3LZ41bD4ZKDnTQcviqb6
OpenAPI ChangesShow/hide changesUnexpected changes? Ensure your branch is up-to-date with |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The published error contract is inaccurate, and generator/workflow edge cases can leave obsolete specs or fail fork-based checks.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 2
Open (3)
What changed in this PR
Publishes per-tenant OpenAPI contracts for future generated TypeScript clients and adds CI drift enforcement.
Changes:
- Adds generated specifications and a tenant-aware generator.
- Stabilizes operation IDs and array query schemas.
- Adds contract validation and OpenAPI diff automation.
| File | Description |
|---|---|
.github/workflows/ci.yml |
Checks generated specs in CI. |
.github/workflows/openapi-diff.yml |
Reports and detects breaking API changes. |
README.md |
Documents published contracts. |
bin/generate-openapi-spec |
Generates or validates tenant specs. |
openapi/specs/b2b_dashboard.yaml |
Publishes dashboard API contract. |
openapi/specs/b2b_learner_records.yaml |
Publishes learner-record API contract. |
pyproject.toml |
Adds generator dependencies and lint coverage. |
src/ol_analytics_api/main.py |
Adds stable tenant names. |
src/ol_analytics_api/openapi.py |
Builds and renders tenant specifications. |
src/ol_analytics_api/tenants/b2b_dashboard/routers/admin.py |
Adds explicit operation ID. |
src/ol_analytics_api/tenants/b2b_dashboard/routers/contracts.py |
Adds generated operation IDs. |
src/ol_analytics_api/tenants/b2b_dashboard/routers/learners.py |
Stabilizes operation ID and array schema. |
src/ol_analytics_api/tenants/b2b_dashboard/routers/organizations.py |
Adds generated operation IDs. |
src/ol_analytics_api/tenants/b2b_learner_records/routers/organizations.py |
Makes repeatable filters non-nullable. |
tests/test_learner_records.py |
Tests omitted list filters. |
tests/test_lifespan.py |
Updates tenant fixtures. |
tests/test_openapi_spec.py |
Tests contract integrity and generator compatibility. |
uv.lock |
Locks new development dependencies. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
A fork's GITHUB_TOKEN is read-only no matter what the job requests, so both comment steps fail there. They run before the oasdiff breaking check, so the whole job dies and the one thing that actually gates the PR never runs. Both comment steps now skip unless the PR head is in this repo. Also fixes the "adding a new tenant" example, which still called Tenant with the old positional signature: copying it would have bound the mount path to `name` and then raised for the missing factory. The lifespan argument moved from third to fourth with it. Reported by Copilot on #70. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH3LZ41bD4ZKDnTQcviqb6
blarghmatey
added a commit
that referenced
this pull request
Sep 22, 2026
main picked up openapi-spec-export (#70) after this branch forked, so the committed spec predates completion_status_counts. Regenerated with bin/generate-openapi-spec after rebasing onto main. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N1inM1db2mZzptMmC9VJfs
blarghmatey
added a commit
that referenced
this pull request
Sep 22, 2026
… main Two things #70 changed under this branch. _partner_header became _partner_token here while #70 added a test that calls it. Both sides touched different parts of the file, so the rebase merged them cleanly and the result referenced a helper that no longer exists. #70 also added pyyaml, which was the only reason the error-code test asserted against the ErrorCode enum rather than the contract the enum exists to mirror. It now reads the enum out of docs/openapi/b2b-learner-records-v1.yaml, so a code added on one side and not the other fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qoz9pfQxUU8VLsRxr1tTnm
blarghmatey
added a commit
to mitodl/ol-infrastructure
that referenced
this pull request
Sep 22, 2026
ol-analytics-api publishes per-tenant OpenAPI specs as of mitodl/ol-analytics-api#70, but it cuts releases as bare CalVer tags (2026.9.17.1) and keeps no long-lived release branch, so there is nothing for the existing `release`-branch topology to watch. This adds an optional source_repo_tag_regex: when set, the source resource versions on tags (version_type=tags) instead of on spec changes landing on a branch. Two things to know about that switch, both in the docstring as well. The git resource consults `paths` only when versioning on commits, so under a tag regex every matching tag rebuilds the client whether or not a spec moved -- the "only when the interface changed" property comes from the release cadence instead of the path filter. `branch` stops applying too, so a matching tag anywhere in the repo triggers it. The regex is anchored and rejects hotfix/<sha>, v-prefixed and releases/-prefixed forms; checked against all eight tags the repo currently has. The LoadVarStep fix is not tag-specific. It read .git/refs/heads/<branch>, which does not exist under a detached tag checkout. .git/ref is written for every version_type and is already what this repo does elsewhere (k8s_apps/pipeline.py, simple_pulumi/pipeline.py). The mitxonline and mit-learn pipelines resolve it to the same SHA they read before; regenerating both shows an unchanged `source` and only this file path moving. Requires the version_type support added in ol-concourse. Without it the argument falls through git_repo's **kwargs onto the Resource as a top-level key, leaving it out of `source` entirely and the tag_regex inert -- it generates clean and silently versions on commits, so this cannot merge until that release lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH3LZ41bD4ZKDnTQcviqb6
3 tasks
blarghmatey
added a commit
that referenced
this pull request
Sep 25, 2026
The per-tenant spec from #70 was generated before this branch filled in the activity columns, so generate-openapi-spec --check failed on the stale descriptions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Szp4MpXds7CmAW3h7KGL6W
blarghmatey
added a commit
that referenced
this pull request
Sep 25, 2026
#62) * feat(b2b_learner_records): serve activity from the learner-records MVs ol-data-platform#2693 adds last_active_on, days_active and the three counters to mv_b2b_learner_enrollment, and last_active_on and courses_in_progress to mv_b2b_learner. The tenant projected all of them as NULL. in_progress now also counts activity. mv_b2b_learner.courses_in_progress is defined that way, so /learners and /enrollments disagree unless the completion_status CASE matches it. The counter descriptions said "distinct blocks". The MV sums per-day distinct counts, so a block used on two days counts twice. The spec also says activity doesn't move updated_since: record_updated_on can't carry a day-granular activity date without falling below the partner's cursor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XMuwnLQwHtHuH7aECS7sf3 * fix(b2b_learner_records): regenerate the OpenAPI spec The per-tenant spec from #70 was generated before this branch filled in the activity columns, so generate-openapi-spec --check failed on the stale descriptions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Szp4MpXds7CmAW3h7KGL6W * fix(b2b_learner_records): gate recomputed activity on active enrollments mv_b2b_learner counts last_active_on and courses_in_progress over active enrollments only (ol-data-platform#2693), but the contract_id rollup took them over every enrollment in scope, so ?contract_id= reported activity from reclaimed seats that the default request hides. They now follow enrollment_is_active like courses_enrolled, and include_inactive still widens them to every enrollment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Szp4MpXds7CmAW3h7KGL6W --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
blarghmatey
added a commit
that referenced
this pull request
Sep 25, 2026
main picked up openapi-spec-export (#70) after this branch forked, so the committed spec predates completion_status_counts. Regenerated with bin/generate-openapi-spec after rebasing onto main. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N1inM1db2mZzptMmC9VJfs
blarghmatey
added a commit
that referenced
this pull request
Sep 25, 2026
… envelope (#68) * feat(b2b_dashboard): return per-status counts on the learner-progress envelope MIT Learn's learner directory renders four summary tiles (Enrollments, Not started, In progress, Completed) above the learner table. The other three came from three extra /learner-progress requests with limit=1 and a completion_status filter, each paying for an org-manager check, a page query and a full COUNT(*) scan regardless of limit. This adds completion_status_counts to LearnerProgressResponse, computed as conditional SUMs alongside the outcomes_withheld_count SUM already in the count query, so the breakdown costs no extra scan. not_started, in_progress, passed and certified come from mutually exclusive branches of _COMPLETION_STATUS, so the buckets never overlap, and consent-withheld rows land in none of them: the buckets plus outcomes_withheld_count sum to total_count. The counts share the response's own WHERE clause, so they narrow along with search/status/include_inactive filters rather than staying contract-wide. That's the cheaper option (the count query already has the clause) and keeps the envelope internally consistent; a contract-wide variant would need a second, unfiltered aggregate query. Fixes #67. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N1inM1db2mZzptMmC9VJfs * test(b2b_dashboard): cover CompletionStatusCounts field descriptions Extends the manager-facing-description parametrization to the new CompletionStatusCounts model, so its fields are held to the same no-field-name-references constraint as LearnerProgress and LearnerProgressResponse instead of relying on manual inspection. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N1inM1db2mZzptMmC9VJfs * fix(b2b_dashboard): document bucket precedence and fix a test invariant CompletionStatusCounts field descriptions read as if passed and in_progress could overlap with certified, but _COMPLETION_STATUS checks certificate status first: a certified enrollment counts only there even when it also has a passing grade. Each field's description now says what it excludes, and the fields are reordered to match the CASE precedence. test_completion_status_counts_reported_from_the_count_query used total_count=10 with buckets summing to 10 plus withheld=1, violating the buckets + outcomes_withheld_count == total_count invariant the endpoint promises. Derives total_count from the fixture values instead of a hardcoded one, and asserts the sum invariant directly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N1inM1db2mZzptMmC9VJfs * chore(b2b_dashboard): regenerate OpenAPI spec after rebase main picked up openapi-spec-export (#70) after this branch forked, so the committed spec predates completion_status_counts. Regenerated with bin/generate-openapi-spec after rebasing onto main. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N1inM1db2mZzptMmC9VJfs * docs(b2b_dashboard): note why completion_status_counts skips cohort suppression CompletionStatusCounts is the only b2b_analytics aggregate that never applies a cohort_policy floor. Document that it's intentional, since the learner-progress endpoint already exposes the individual matching rows, so there's nothing left to hide by suppressing the summary. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VfT3zzM5d6LQ5BQvkJhN5N * chore(b2b_dashboard): regenerate OpenAPI spec for docstring update Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VfT3zzM5d6LQ5BQvkJhN5N --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
blarghmatey
added a commit
to mitodl/ol-infrastructure
that referenced
this pull request
Sep 26, 2026
* feat(api-clients): generate ol-analytics-api's client off release tags ol-analytics-api publishes per-tenant OpenAPI specs as of mitodl/ol-analytics-api#70, but it cuts releases as bare CalVer tags (2026.9.17.1) and keeps no long-lived release branch, so there is nothing for the existing `release`-branch topology to watch. This adds an optional source_repo_tag_regex: when set, the source resource versions on tags (version_type=tags) instead of on spec changes landing on a branch. Two things to know about that switch, both in the docstring as well. The git resource consults `paths` only when versioning on commits, so under a tag regex every matching tag rebuilds the client whether or not a spec moved -- the "only when the interface changed" property comes from the release cadence instead of the path filter. `branch` stops applying too, so a matching tag anywhere in the repo triggers it. The regex is anchored and rejects hotfix/<sha>, v-prefixed and releases/-prefixed forms; checked against all eight tags the repo currently has. The LoadVarStep fix is not tag-specific. It read .git/refs/heads/<branch>, which does not exist under a detached tag checkout. .git/ref is written for every version_type and is already what this repo does elsewhere (k8s_apps/pipeline.py, simple_pulumi/pipeline.py). The mitxonline and mit-learn pipelines resolve it to the same SHA they read before; regenerating both shows an unchanged `source` and only this file path moving. Requires the version_type support added in ol-concourse. Without it the argument falls through git_repo's **kwargs onto the Resource as a top-level key, leaving it out of `source` entirely and the tag_regex inert -- it generates clean and silently versions on commits, so this cannot merge until that release lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH3LZ41bD4ZKDnTQcviqb6 * feat(api-clients): publish one npm package per spec ol-analytics-api publishes two specs, one per tenant, and they are independent APIs: an org-manager dashboard behind a user JWT, and a machine-to-machine partner integration. A consumer of one has no reason to pull the other's types in, so they ship as separate packages. client_repo_subpath now takes a list as well as a string, and the publish job emits one task per entry. The packages share the repo-root VERSION the bump step writes, so a release moves them together. Existing variants pass a string and are normalized to a single-element list, so mitxonline and mit-learn still emit exactly one publish task against the same directory as before. Their task name changes from `publish-node` to `publish-node-<package>`, since two tasks in one plan cannot share a name; that is a step label in the Concourse UI, not behavior. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH3LZ41bD4ZKDnTQcviqb6 * fix(api-clients): make the tag regex POSIX, and fail loudly on a stale lib Two problems with the tag support in the preceding commits, both found in review. The regex used `\d`. The resource filters tags with `grep -E`, which is POSIX ERE, not PCRE: GNU grep emits "stray \ before d", drops the escape, and the pattern then requires literal `d` characters. It matched none of the eight tags ol-analytics-api has, so the pipeline would have set up clean and never fired on a release. My earlier check of this pattern used Python's `re`, which is the wrong engine and matched all eight. Verified the replacement by piping every real tag through /usr/bin/grep -E, plus hotfix/<sha>, v- and releases/-prefixed forms, a three-component version and a -rc1 suffix, none of which match. The ordering hazard also had no mechanical guard. An ol-concourse without version_type support does not reject the argument -- git_repo forwards it through **kwargs onto the Resource, so it lands as a top-level key, `source` never gets it, and the resource falls back to versioning commits on `main` with no paths filter, republishing the client on every commit. Merging this before the ol-concourse release, or merging it and forgetting to relock, was a silent failure. Generation now raises if version_type did not reach the source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CH3LZ41bD4ZKDnTQcviqb6 * fix(api-clients): give each npm package its own retryable publish job Sequential publish steps in one job can't recover from a partial failure: if the first `yarn npm publish` succeeds and a later package fails, retrying the job re-runs the first publish, which npm rejects since that name/version already exists. Split the publish job per subpath, each gated on the same generate-clients run, so a failed package retries on its own without touching packages that already published. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ps9LGhZZH9g4D4MLpsoNuC --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
23 tasks done
blarghmatey
added a commit
that referenced
this pull request
Sep 30, 2026
… main Two things #70 changed under this branch. _partner_header became _partner_token here while #70 added a test that calls it. Both sides touched different parts of the file, so the rebase merged them cleanly and the result referenced a helper that no longer exists. against the ErrorCode enum rather than the contract the enum exists to mirror. It now reads the enum out of docs/openapi/b2b-learner-records-v1.yaml, so a code added on one side and not the other fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qoz9pfQxUU8VLsRxr1tTnm
23 tasks
blarghmatey
added a commit
that referenced
this pull request
Sep 30, 2026
… main Two things #70 changed under this branch. _partner_header became _partner_token here while #70 added a test that calls it. Both sides touched different parts of the file, so the rebase merged them cleanly and the result referenced a helper that no longer exists. against the ErrorCode enum rather than the contract the enum exists to mirror. It now reads the enum out of docs/openapi/b2b-learner-records-v1.yaml, so a code added on one side and not the other fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qoz9pfQxUU8VLsRxr1tTnm
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.


What are the relevant tickets?
N/A
Description (What does it do?)
The spec export built in #37 never reached
main. #37 merged intofeat/complement-suppression(#36), which is still open, conflicting and 21commits behind, so nothing it shipped is on
mainand nothing downstream of itcan start. This takes those two commits off that stack and puts them on current
main, unchanged apart from theTenantregistry conflict (mainhas gained asecond tenant since).
The rest of the diff is what falls out of running the generator against a
mainthat has moved on:
openapi/specs/b2b_learner_records.yamlis new. The tenant did not exist whenfeat(openapi): publish a per-tenant spec and generate clients from it #37 was written.
openapi/specs/b2b_dashboard.yamlpicks up the response-model and descriptionchanges that landed in the meantime, plus the new
/learner-progressroute./learner-progresshad no explicitoperation_id, so FastAPI derived one fromthe function name and the whole path.
test_operation_ids_are_explicit(alreadypart of feat(openapi): publish a per-tenant spec and generate clients from it #37) fails on it; it is now
learners_progress_retrieve.One behavior change.
learner_idandcompletion_statuswere declaredlist[X] | None = None, which renders asanyOf: [{type: array}, {type: null}].openapi-generator's typescript-axios cannot reduce that, so it treats the
parameter as an arbitrary object and spreads it with
Object.entries. Thegenerated client sends
?0=passed&1=certifiedrather than the repeated?completion_status=passed&completion_status=certifiedthat FastAPI parses, andthe file does not compile under
tsc --strict. Three parameters hit this, acrossboth tenants.
They are now plain arrays with
Query(default_factory=list). All three alreadydid
tuple(learner_id or ())at the call site, and_shared_predicatesgates onif filters.learner_ids:, so an omitted parameter and an empty list were neverdistinguishable to the query layer. The wire format for callers who pass the
parameter is unchanged; what changes is that the schema stops advertising a
nullthe code never treated as distinct.test_array_query_params_are_not_nullablesweeps every published parameter foran
anyOfcontaining an array, so a route nobody has written yet cannotreintroduce this quietly.
How can this be tested?
Local checks, all passing:
The part CI does not cover is whether the committed spec actually generates a
usable client, since no pipeline consumes it yet. I ran openapi-generator 7.2.0
(
typescript-axios) over both committed specs directly:Before the parameter change, both tenants' clients failed with:
at the
Object.entriesspread. After it, both generate clean and assign thearray to the parameter's own key, which
common.ts'ssetFlattenedQueryParamsappends once per element, giving the repeated form FastAPI expects.
Also confirmed on the generated output: each row model gets its own interface and
each envelope its own (
OrgAnalyticsResponseContractUtilization,LearnerRecordsResponseLearner, and so on), with no collapse to a single untypedenvelope, which was the open question from #37.
maxItems: 100onlearner_idsurvives the move off the optional form.
Additional Context
Still not done, unchanged from #37, and all of it either outward-facing or
needing a decision:
ol-analytics-api-clientsrepo does not exist yet. It now needs twogenerator configs rather than one.
PIPELINE_CONFIGSentry inol-infrastructure(
src/ol_concourse/pipelines/libraries/configuration.py). Still blocked onwhich branch it watches: that field is
releasefor mitxonline and mit-learn,and this repo has no
releasebranch.frontends/api/src/analytics/types.tsand
clients.ts, blocked on 1 and 2.I have not touched #36. Lifting these commits off it leaves it holding only the
anonymization work.
🤖 Generated with Claude Code
https://claude.ai/code/session_01CH3LZ41bD4ZKDnTQcviqb6