Skip to content

fix: re-pin openapi generator to 3.1-native 7.12.0 and guard codegen in CI#135

Open
Kyzgor wants to merge 1 commit into
permitio:mainfrom
Kyzgor:codegen/repin-generator-3.1
Open

fix: re-pin openapi generator to 3.1-native 7.12.0 and guard codegen in CI#135
Kyzgor wants to merge 1 commit into
permitio:mainfrom
Kyzgor:codegen/repin-generator-3.1

Conversation

@Kyzgor

@Kyzgor Kyzgor commented Jun 30, 2026

Copy link
Copy Markdown
  • What kind of change does this PR introduce?

Bug fix (build tooling). It restores typed regeneration of the OpenAPI client and adds a CI guard so a regression of the generator pin can no longer silently ship an untyped client (a future spec migration past 3.1 is a separately-tracked gap, see "Other information").

  • What is the current behavior? (You can also link to an open issue here)

Fixes #130. yarn generate-openapi-client no longer produces a typed client; it silently emits an all-any, type-erased one and still exits 0.

The generator is pinned to 6.2.1 (openapitools.json), which bundles swagger-core 2.2.4 and only models OpenAPI 3.0. The served spec at https://api.permit.io/v2/openapi.json is now OpenAPI 3.1.0, so 6.2.1 cannot resolve most schemas and degrades them to any. Because the script passes --skip-validate-spec, the run still exits 0. A fresh regeneration today replaces the typed client with any (247 of 319 type files):

// src/openapi/types/role-create.ts regenerated under the pinned 6.2.1 against the 3.1.0 spec
export interface RoleCreate {
    [key: string]: any;
    'key': any;
    'name': any;
    // ...every named property erased to `any`, generation still exits 0
}

This was reported and triaged in #130; the chosen direction there is to model the spec natively (a 3.1-capable generator) plus a CI check so it cannot silently regress.

  • What is the new behavior (if this is a feature change)?

Three changes, scoped to the toolchain. The committed client under src/openapi/ is not regenerated here (see "Other information"):

  1. Re-pin the generator to 7.12.0 (openapitools.json), a 3.1-native generator line that regenerates a typed client (named properties instead of any). The earlier 7.1.0 and 7.5.0 releases abort with a NullPointerException on this spec; 7.12.0 generates cleanly:
// same file, regenerated under 7.12.0 against the same 3.1.0 spec
export interface RoleCreate {
    'key': string;
    'name': string;
    // ...named, typed properties; no `[key: string]: any` index signature
}
  1. Add a CI codegen guard (scripts/check-codegen.mjs plus a committed src/tests/codegen/fixtures/openapi-3.1.0.json snapshot). It regenerates through the same real pipeline as generate-openapi-client (the same openapi-generator-cli wrapper, the version resolved from openapitools.json, and the same --additional-properties flags), differing only in that it reads the pinned fixture instead of the live URL and writes to a throwaway directory it then inspects (so no committed file is touched and no prettier pass is needed). It does not rely on the generator's exit code: it reads the emitted types/*.ts and fails if any collapses to an all-any [key: string]: any; index signature. Positive canaries assert named scalar properties stay typed, catching the partial case where a property degrades to a bare 'prop': any; with no index signature.

  2. Wire it as a separate CI job (codegen-guard in .github/workflows/ci.yaml) with setup-java (Temurin 11), kept off the yarn test path. The generator needs Java and a generation run that does not fit ava's per-test budget, so it runs in its own job.

Check (reproduced locally; upstream PR CI pending) Before (6.2.1) After (7.12.0)
yarn generate-openapi-client output 247 of 319 type files all-any 350 type files: 348 named/typed + 2 known allOf+default residuals (allow-listed)
RoleCreate.key any (under [key: string]: any) string
generate exit code on collapse 0 (silent) guarded; the codegen-guard job goes red
node scripts/check-codegen.mjs (run locally) n/a passes
yarn lint / yarn build / yarn test:unit (48) / yarn test:module-imports (9) (local) pass pass
committed src/openapi/ client (.ts source) unchanged unchanged (byte-identical)

(CI's test-and-lint job runs these on Node 18 and 20. yarn test also includes a test:integration suite, and there are separate test:e2e:* suites; both need live, scoped Permit credentials or a running PDP and are not exercised by a build-tooling change.)

The guard is not green-by-construction; I verified locally that it goes red on both regression modes. First, pinning back: reverting openapitools.json to 6.2.1 (the only file changed) fails with 245 type file(s) collapsed to all-\any`(245, not the live-spec 247, because the guard runs against the pinned fixture and excludes the two allow-listedallOf+defaultresiduals). Second, a partial degrade: leaving the pin at7.12.0 but degrading one covered property in the fixture (TenantObj.idtoany) fails with canary tenant-obj.ts: property 'id' degraded to `any`, which exercises the canary path that the tree-wide index-signature scan alone would miss. On the unmodified fixture the guard passes; its full status line is codegen guard OK - 350 type files fully typed (2 known residual: derived-role-block-edit.ts, derived-role-rule-create.ts; 5 canaries typed)`, i.e. the 348 typed plus 2 allow-listed residuals from the table above.

  • Other information:

Scope. This PR fixes and guards the toolchain only; it does not regenerate and commit the client. src/openapi/ is left byte-identical, so there are no consumer-facing type changes and this is non-breaking. A full regeneration is a larger, breaking re-baseline, and is deliberately not part of this PR. When that follow-up runs, the current spec removes or renames many generated files and drops a few exported types; granted_to typing on the role types changes as well. That churn is driven by roughly three years of spec drift rather than the generator bump — a fresh 6.2.1 regeneration produces the same removals, so it is not introduced by this re-pin — and the exact re-baseline set is enumerated when that follow-up PR is opened. The re-baseline lands as a separate follow-up PR (a semver-major) so it can be reviewed and released deliberately. This PR is the prerequisite: it makes regeneration produce a typed client again and guards it.

Not included, on purpose. The client re-baseline above is one planned follow-up PR that regenerates src/openapi/ and, as part of the same work, resolves the allOf+default residuals the 7.x generator leaves. At least two are currently known (derived-role-block-edit, derived-role-rule-create); the full set is confirmed only when the re-baseline is done, so the guard allow-lists those two rather than asserting a closed count. Live-spec drift detection is also deferred: the guard pins a fixture snapshot, so a spec migration past 3.1 would need the fixture re-armed, and a non-blocking scheduled check for that is better as its own small job.

Coordination. #121 (hand-adding extends to the six role types) touches the frozen src/openapi/; it is independent of this PR, and the eventual regeneration folds it in. #131 also edits .github/workflows/ci.yaml; the two are independent (this PR only appends a separate codegen-guard job), but whichever merges second needs a trivial rebase of the appended job.

Guard exercised in CI, not only locally. The codegen-guard job was validated end-to-end on the fork (Kyzgor/permit-node, Temurin 11, GitHub Actions). It is GREEN under the fix (run 28370619361: codegen guard OK - 350 type files fully typed) and RED under two negative controls: a pin-revert to 6.2.1 (run 28371064327: 245 type file(s) collapsed to all-\any`) and a single covered-property degrade of the fixture (TenantObj.idtoany; run 28371066398: canary tenant-obj.ts: property 'id' degraded to `any`). In both failing runs the test-and-lintjobs (Node 18 and 20) stayed GREEN, isolating the failure tocodegen-guard. The upstream codegen-guard` job will run on this PR once opened.

CI note. security/snyk (permit) errors on fork PRs because it needs an org token the fork cannot access. It is not a code issue, and a maintainer can re-run it.

…in CI

The pinned generator 6.2.1 cannot read the OpenAPI 3.1.0 spec served at
api.permit.io: it regenerates an all-`any`, type-erased client (247 of 319 type
files) yet exits 0, masked by --skip-validate-spec. Running
`yarn generate-openapi-client` today silently replaces the typed client with `any`.

Re-pin the generator to 7.12.0, the first 3.1-native line that regenerates a typed
client (named properties instead of `any`; 2 known allOf+default residuals remain,
allow-listed and tracked for the re-baseline), and add a CI codegen guard that
regenerates from a pinned 3.1.0 fixture through the real pipeline and fails if any
type collapses to all-`any`. The guard runs in its own Java-provisioned job, off
the test suite.

The committed client under src/openapi/ is intentionally left unchanged: this
restores and guards the toolchain. Re-baselining the generated client against the
current spec is a separate, larger follow-up.

Fixes permitio#130
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.

generate-openapi-client silently emits an all-any client: pinned generator 6.2.1 cannot read the now-3.1 spec

1 participant