Skip to content

Keep the generated API client in sync with freeshard, automatically - #35

Merged
max-tet merged 4 commits into
mainfrom
feature/clayde/openapi-drift-check
Oct 11, 2026
Merged

max-tet merged 4 commits into
mainfrom
feature/clayde/openapi-drift-check

Conversation

@ClaydeCode

@ClaydeCode ClaydeCode commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

What & why

js/api/openapi.json is a one-time offline dump from scaffold — never refreshed since, nothing watches it, and nothing resyncs it. That's exactly how the /public/meta/version follow-up (sundial#34, already merged) ended up hand-rolling a second fetch path instead of using the generated client: the instruction to skip codegen traced back to an unverified assumption, and the real reason it felt necessary was that regenerating wasn't a one-command thing.

freeshard already solves the equivalent problem for its vendored Python models from freeshard-controller: a just get-types recipe plus a types-drift.yml CI check that fails if the vendored copy and a fresh resync disagree. This PR builds the same two pieces here.

What's new

  • just get-openapi — sibling-clones freeshard (override path via --set SOURCE_DIR), dumps a fresh spec via tools/dump_openapi.py (create_app().openapi() — no server boot, no DB needed, confirmed empirically) into js/api/openapi.json. Pair with the existing just gen-client.
  • .github/workflows/openapi-drift.yml — on every PR: checkout freeshard main, run both recipes, fail the build if anything differs from what's committed. Same shape as types-drift.yml, no secrets needed (both repos are public).
  • tools/dump_openapi.py also fixes a real non-determinism bug I found while testing this: three catch-all passthrough routes (call_backend, call_peer, call_management) share one operationId across every HTTP method on their path, and which method's name lands in that shared id depends on a set()'s per-process hash-seed iteration order — same commit, different dump every run. Confirmed by diffing repeated dumps; fixed by sorting each path's methods into a fixed order and rewriting each operation's id to match its own method. Checked every other path in the spec — only these three ever mismatched. Verified deterministic across four consecutive runs (identical checksums).

What's deliberately not in this PR

I initially also committed a one-time catch-up (freeshard has grown an OIDC provider surface and a user-settings surface since scaffold that Sundial's client has no wrappers for). Reverted it: I'd dumped that catch-up against the freeshard#246 feature branch, not freeshard's actual main — #246 is still open. Against true main, a fresh regen removes getVersion(), which the already-merged sundial#34 depends on in appstore.js. Shipping a "fix" that breaks working code to make a new check pass would be backwards, so openapi.json/client.js in this PR are byte-identical to current main — this PR adds the tool only.

Consequence worth knowing: drift-check will start red on main once this merges, and stay red until freeshard#246 merges and someone reruns just get-openapi && just gen-client. That's accurate — sundial#34 merged slightly ahead of its own backing endpoint — not a bug in the check. Whoever resyncs after #246 lands should expect the OIDC/user-settings catch-up to land then too, now that the dump is deterministic.

Verification

Tested get-openapi and gen-client against a real sibling freeshard checkout at both the (wrong) feature branch and true main; diffed every exported function name before/after to check for breakage before ever trusting a regen; ran the full test suite after each attempt. 83 unit tests pass (e2e unaffected since these two files are unchanged from main).

🤖 Generated with Claude Code

ClaydeCode and others added 4 commits October 9, 2026 18:18
…ally

Added a just get-openapi recipe (sibling-clone freeshard, dump a fresh
OpenAPI spec from create_app().openapi(), no server boot needed) plus
openapi-drift.yml, a PR-time CI check that re-dumps the spec, regenerates
the client, and fails on any diff — the same pattern types-drift.yml
already uses for freeshard's vendored Python models.

js/api/openapi.json was a one-time offline dump from scaffold, never
refreshed since, so this also catches it up: a user-settings/email surface
and an OIDC provider surface that exist in freeshard today were entirely
missing. Checked the regenerated client for breaking renames — four
callManagementProtectedManagementRest* variants got reordered suffixes,
none of them called anywhere outside client.js itself, so nothing breaks.
83 unit + 45 e2e tests pass unchanged.
…eshard main

Two problems found testing the previous commit:

1. The dump wasn't reproducible. Three catch-all passthrough routes
   (call_backend, call_peer, call_management) register every HTTP method on
   one path; FastAPI derives operationId from that shared declaration rather
   than per-method, and which method's name lands in the (duplicated) id
   depends on a Python set()'s per-process hash-seed iteration order.
   Confirmed by diffing repeated dumps of the same commit. dump_openapi.py
   now sorts each path's methods into a fixed order and rewrites each
   operation's operationId to match its own method — deterministic across
   four consecutive runs, checksums identical.

2. The previous commit's 'catch-up' was dumped against the feature branch
   for freeshard#246, not freeshard's actual main — freeshard#246 is still
   open, unmerged. Against true main, a fresh regen removes getVersion(),
   which sundial#34 (already merged to this repo's main) already depends on
   in appstore.js's fetchShardVersion(). Shipping that removal would break
   currently-working code to make a brand-new check pass, which is backwards.

openapi.json and client.js are therefore reverted to exactly match main in
this PR — this adds the tool only. The drift-check job will correctly start
red on main until freeshard#246 merges and someone reruns the resync; that's
accurate, not a bug, and the PR description says so.
Catches up the client to everything merged on freeshard main since the
last regen on 2026-10-09, including the minimum_freeshard_version field
from freeshard#246 and the OIDC/user-model surface.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Without a pin, uv picks the newest Python satisfying freeshard's
">=3.13", which just started breaking: CPython 3.15.0 has no
psycopg-binary wheel yet. freeshard's own CI (test.yml) already pins
3.13; match it here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@max-tet
max-tet merged commit 26fe016 into main Oct 11, 2026
3 checks passed
@max-tet
max-tet deleted the feature/clayde/openapi-drift-check branch October 11, 2026 15:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants