-
-
Notifications
You must be signed in to change notification settings - Fork 1
feat(openapi-zod-ts): add --check-drift CI gate for generated output #400
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
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,129 @@ | ||
| --- | ||
| title: Drift detection in CI | ||
| description: Use --check-drift to gate CI on stale, missing, or extra generated files and catch regeneration lapses before they reach production. | ||
| --- | ||
|
|
||
| import { Steps, Code, Aside } from '@astrojs/starlight/components' | ||
|
|
||
| ## The problem | ||
|
|
||
| Your OpenAPI spec evolves. A new endpoint is added, a response shape changes, a field is renamed. The generated output (`models.ts`, `client.ts`, and friends) must be regenerated and committed whenever the spec changes. Without a CI gate, a team member can update the spec, forget to regenerate, and push. The discrepancy is invisible until a type error or a runtime mismatch surfaces downstream. | ||
|
|
||
| `--check-drift` solves this with a single command that fits in any CI pipeline. | ||
|
|
||
| ## One-command usage | ||
|
|
||
| ```sh | ||
| npx openapi-zod-ts --check-drift | ||
| ``` | ||
|
|
||
| The command: | ||
|
|
||
| 1. Loads your config file (`openapi-zod-ts.config.json` by default, or the path you pass with `--config`). | ||
| 2. Runs the full generator pipeline in memory. No files are written. | ||
| 3. Applies the same Prettier formatting the write path uses, so the comparison is exact. | ||
| 4. Reads the committed files from your configured `output` directory. | ||
| 5. Exits 0 when everything matches. Exits 1 and prints per-file diagnostics when anything is stale, missing, or extra. | ||
|
|
||
| Pass `--config` when your config file is not in the current directory: | ||
|
|
||
| ```sh | ||
| npx openapi-zod-ts --config packages/api/openapi-zod-ts.config.json --check-drift | ||
| ``` | ||
|
|
||
| ## What it checks | ||
|
|
||
| | Status | Meaning | | ||
| | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| | **STALE** | The file exists on disk but its content differs from what the generator produces today. Usually caused by a spec change that was not followed by a regeneration commit. | | ||
| | **MISSING** | The generator would produce this file but it is not on disk. Could happen after a first-time setup or after a config change that adds a new output file. | | ||
| | **EXTRA** | A file is in the output directory that the generator does not produce. Often a stale artifact from a removed endpoint or a file that was manually added to the generated output directory (which is not recommended). | | ||
|
|
||
| Example output when drift is detected: | ||
|
|
||
| ``` | ||
| Drift check failed: generated output does not match committed files. | ||
|
|
||
| STALE models.ts (content differs from what the generator produces today) | ||
| MISSING client-config.ts (expected by the generator but not found on disk) | ||
| EXTRA old-models.ts (on disk but not produced by the generator; delete it or regenerate) | ||
|
|
||
| Fix: openapi-zod-ts | ||
| ``` | ||
|
|
||
| When the check passes: | ||
|
|
||
| ``` | ||
| Drift check passed: all generated files are up to date. | ||
| ``` | ||
|
|
||
| ## GitHub Actions integration | ||
|
|
||
| Add a dedicated drift check step to your workflow. When `GITHUB_ACTIONS=true`, the command also emits `::error file=...::` workflow commands so GitHub renders inline annotations on the PR, and appends a summary table to the step summary panel. | ||
|
|
||
| ```yaml | ||
| name: CI | ||
|
|
||
| on: | ||
| pull_request: | ||
| push: | ||
| branches: [main] | ||
|
|
||
| jobs: | ||
| drift-check: | ||
| name: Generated output drift check | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 22 | ||
|
|
||
| - name: Install dependencies | ||
| run: npm ci | ||
|
|
||
| - name: Check generated output is up to date | ||
| run: npx openapi-zod-ts --check-drift | ||
| ``` | ||
|
|
||
| For a monorepo where each package has its own config: | ||
|
|
||
| ```yaml | ||
| - name: Check generated output (packages/api) | ||
| run: npx openapi-zod-ts --config packages/api/openapi-zod-ts.config.json --check-drift | ||
| ``` | ||
|
|
||
| ## How this differs from --check | ||
|
|
||
| The two drift flags guard against different problems: | ||
|
|
||
| | Flag | What it checks | When it applies | | ||
| | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------- | | ||
| | `--check` | Schema drift: your `input_schema` Zod file vs. what a fresh bootstrap from the spec would produce. Ensures your hand-written Zod schemas stay aligned with the contract. | Only when `input_schema` is configured. | | ||
| | `--check-drift` | Output file drift: the committed generated files (`models.ts`, `client.ts`, etc.) vs. what the generator would produce today from the current spec and config. | Always, for every generator run. | | ||
|
|
||
| A project can have `--check` pass (the Zod schema is aligned with the spec) and `--check-drift` fail (the TypeScript client is stale because regeneration was not committed). Both flags can be run together in the same CI step: | ||
|
|
||
| ```sh | ||
| npx openapi-zod-ts --check --check-drift | ||
| ``` | ||
|
|
||
| ## What is NOT checked | ||
|
|
||
| The user-owned `input_schema` file (your hand-written Zod schemas, typically `zod.ts`) is intentionally excluded from `--check-drift` scope. The generator bootstraps this file once and never overwrites it: it belongs to you. Use `--check` to verify that your schema file stays aligned with the OpenAPI contract. | ||
|
|
||
| ## Fixing drift | ||
|
|
||
| When the command reports drift, run the generator to regenerate and commit the result: | ||
|
|
||
| ```sh | ||
| # Regenerate | ||
| npx openapi-zod-ts | ||
|
|
||
| # Commit the updated generated files | ||
| git add src/api/ | ||
| git commit -m "chore: regenerate api client" | ||
| ``` | ||
|
|
||
| For more on the CLI options, see the [Types and fetch client reference](/openapi-zod-ts). | ||
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 |
|---|---|---|
|
|
@@ -36,6 +36,7 @@ describe('parseCliArgs', () => { | |
| cwd: dirname(resolve(fakeCwd, 'config.json')), | ||
| watch: false, | ||
| check: false, | ||
| checkDrift: false, | ||
| resetSchema: false, | ||
| }) | ||
| }) | ||
|
|
@@ -48,6 +49,7 @@ describe('parseCliArgs', () => { | |
| cwd: '/abs/path', | ||
| watch: false, | ||
| check: false, | ||
| checkDrift: false, | ||
| resetSchema: false, | ||
| }) | ||
| }) | ||
|
|
@@ -79,6 +81,7 @@ describe('parseCliArgs', () => { | |
| cwd: fakeCwd, | ||
| watch: false, | ||
| check: false, | ||
| checkDrift: false, | ||
| resetSchema: false, | ||
| }) | ||
| }) | ||
|
|
@@ -285,6 +288,59 @@ describe('parseCliArgs', () => { | |
| }) | ||
| }) | ||
|
|
||
| describe('--check-drift', () => { | ||
| it('sets checkDrift to true when --check-drift is given', () => { | ||
| const result = parseCliArgs([...baseArgv, '--check-drift'], fakeCwd) | ||
| expect(result.action).toBe('run') | ||
| if (result.action === 'run') { | ||
| expect(result.checkDrift).toBe(true) | ||
| } | ||
| }) | ||
|
|
||
| it('sets checkDrift to false when --check-drift is not given', () => { | ||
| const result = parseCliArgs([...baseArgv], fakeCwd) | ||
| expect(result.action).toBe('run') | ||
| if (result.action === 'run') { | ||
| expect(result.checkDrift).toBe(false) | ||
| } | ||
| }) | ||
|
|
||
| it('returns error action when --check-drift and --watch are combined', () => { | ||
| const result = parseCliArgs([...baseArgv, '--check-drift', '--watch'], fakeCwd) | ||
| expect(result.action).toBe('error') | ||
| }) | ||
|
|
||
| it('error message for --check-drift --watch mentions both flags', () => { | ||
| const result = parseCliArgs([...baseArgv, '--check-drift', '--watch'], fakeCwd) | ||
| if (result.action === 'error') { | ||
| expect(result.message).toContain('--check-drift') | ||
| expect(result.message).toContain('--watch') | ||
| } | ||
| }) | ||
|
|
||
| it('combines --check-drift with --input and --output', () => { | ||
| const result = parseCliArgs( | ||
| [...baseArgv, '--check-drift', '--input', 'spec.json', '--output', 'out/'], | ||
| fakeCwd | ||
| ) | ||
| expect(result.action).toBe('run') | ||
| if (result.action === 'run') { | ||
| expect(result.checkDrift).toBe(true) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. warn warn |
||
| expect(result.inputOverride).toBe(resolve(fakeCwd, 'spec.json')) | ||
| expect(result.outputOverride).toBe(resolve(fakeCwd, 'out/')) | ||
| } | ||
| }) | ||
|
|
||
| it('combines --check-drift with --check (both are allowed independently)', () => { | ||
| const result = parseCliArgs([...baseArgv, '--check-drift', '--check'], fakeCwd) | ||
| expect(result.action).toBe('run') | ||
| if (result.action === 'run') { | ||
| expect(result.checkDrift).toBe(true) | ||
| expect(result.check).toBe(true) | ||
| } | ||
| }) | ||
| }) | ||
|
|
||
| describe('--reset-schema', () => { | ||
| it('sets resetSchema to true when --reset-schema is given', () => { | ||
| const result = parseCliArgs([...baseArgv, '--reset-schema'], fakeCwd) | ||
|
|
||
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Qualify the GitHub Actions side effects as failure-only.
reportDrift()emits::errorannotations and writes$GITHUB_STEP_SUMMARYonly when drift is detected; clean runs return after logging the success message. Wording this as unconditional forGITHUB_ACTIONS=trueoverstates the behavior and will mislead users looking for annotations on passing runs.🤖 Prompt for AI Agents