Skip to content

feat(openapi-zod-ts): add --check-drift CI gate for generated output - #400

Merged
benjamineckstein merged 1 commit into
mainfrom
feat/cli-drift-check
Jul 4, 2026
Merged

feat(openapi-zod-ts): add --check-drift CI gate for generated output#400
benjamineckstein merged 1 commit into
mainfrom
feat/cli-drift-check

Conversation

@benjamineckstein

@benjamineckstein benjamineckstein commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

What

Adds npx openapi-zod-ts --check-drift, a read-only CI gate for consumers who commit generated output. It regenerates in memory, compares against the committed files, writes nothing, and exits non-zero with per-file diagnostics when any file is stale, missing, or extra. Under GitHub Actions it also emits ::error file=...:: annotations (pinned to the PR diff) and a $GITHUB_STEP_SUMMARY panel, so a drift failure is actionable at a glance.

This is distinct from the existing --check, which validates hand-written input_schema Zod coverage. --check-drift validates the generated files themselves.

Consumer usage

```yaml

  • run: npm ci
  • run: npx openapi-zod-ts --check-drift
    ```

Reusable API

New openapi-zod-ts/drift-check subpath export (`compareOutput`, `reportDrift`, `DriftReport`), so `openapi-server`, `openapi-react-query`, and `openapi-msw` can adopt the same gate in fast-follow PRs.

Correctness notes

  • Zod-integration mode is handled: when `input_schema` is set and the schema file exists, the expected output matches the schema-enhanced `models.ts`/`client.ts` the write path produces, so there is no spurious drift.
  • Content is normalized for CRLF and trailing newlines, so a Windows checkout does not false-positive.
  • A single-file `output` path now fails with a clear message instead of a raw system error.

Docs

New guide at `guides/drift-detection` covering one-command usage, a copy-paste GitHub Actions snippet, and how this differs from `--check`.

Follow-ups (not in this PR)

  • Adopt the shared `drift-check` helper in `openapi-server`, `openapi-react-query`, and `openapi-msw`.
  • Extract a single "final output map" function shared by the write path and the drift path, so the schema-enhancement logic lives in exactly one place.

Testing

921 tests pass. `drift-check.ts` at 100% branch coverage. `fallow:audit` exit 0, lint clean.

https://claude.ai/code/session_01SZmVypA9rE5H3HTreJgyE2

Summary by CodeRabbit

  • New Features

    • Added a new --check-drift mode to validate generated output against the committed filesystem, with CI-friendly failure behavior.
    • Documented a “Drift detection in CI” guide, including GitHub Actions error annotations and optional step summaries.
    • Exposed the drift-check utility via a new package subpath export.
  • Bug Fixes

    • Improved drift detection by normalizing CRLF→LF and enforcing a consistent trailing newline.
    • Better handling of missing/extra files and output path edge cases.
  • Tests

    • Added/updated coverage for CLI parsing, drift reporting, and generator behavior in --check-drift mode.

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 60b724b8-2237-4357-9516-aab75a86e5de

📥 Commits

Reviewing files that changed from the base of the PR and between 141db78 and b891208.

📒 Files selected for processing (10)
  • docs/astro.config.mjs
  • docs/src/content/docs/guides/drift-detection.mdx
  • packages/openapi-zod-ts/package.json
  • packages/openapi-zod-ts/src/__tests__/cli-args.test.ts
  • packages/openapi-zod-ts/src/__tests__/drift-check.test.ts
  • packages/openapi-zod-ts/src/__tests__/generator-schema.test.ts
  • packages/openapi-zod-ts/src/cli-args.ts
  • packages/openapi-zod-ts/src/cli.ts
  • packages/openapi-zod-ts/src/drift-check.ts
  • packages/openapi-zod-ts/src/generator.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/astro.config.mjs
🚧 Files skipped from review as they are similar to previous changes (8)
  • packages/openapi-zod-ts/package.json
  • packages/openapi-zod-ts/src/tests/generator-schema.test.ts
  • packages/openapi-zod-ts/src/tests/cli-args.test.ts
  • packages/openapi-zod-ts/src/cli.ts
  • packages/openapi-zod-ts/src/tests/drift-check.test.ts
  • packages/openapi-zod-ts/src/cli-args.ts
  • packages/openapi-zod-ts/src/drift-check.ts
  • packages/openapi-zod-ts/src/generator.ts

📝 Walkthrough

Walkthrough

Adds a --check-drift CLI flag that regenerates output in memory, compares it against committed files on disk, and exits non-zero when files are stale, missing, or extra. Also adds drift reporting helpers, generator integration, tests, a package export, and documentation.

Changes

--check-drift CI gate

Layer / File(s) Summary
DriftReport and reporting helpers
packages/openapi-zod-ts/src/drift-check.ts, packages/openapi-zod-ts/package.json
Defines DriftReport, normalizes file content, classifies stale/missing/extra files, formats diagnostics, emits GitHub Actions annotations and step summaries, and exports ./drift-check.
CLI flag and type wiring
packages/openapi-zod-ts/src/cli-args.ts, packages/openapi-zod-ts/src/cli.ts, packages/openapi-zod-ts/src/generator.ts
Adds checkDrift to the CLI action and generator options, parses --check-drift, rejects it with --watch, and forwards the flag into generation.
Generator drift-check flow
packages/openapi-zod-ts/src/generator.ts
Builds the canonical output map, compares it before any writes when drift checking is enabled, and keeps schema-enhanced output handling aligned with the normal write path.
Drift-check and CLI tests
packages/openapi-zod-ts/src/__tests__/drift-check.test.ts, packages/openapi-zod-ts/src/__tests__/cli-args.test.ts, packages/openapi-zod-ts/src/__tests__/generator-schema.test.ts
Covers drift comparison, reporting behavior, CLI parsing, and generator behavior with and without input_schema.
Drift detection guide
docs/src/content/docs/guides/drift-detection.mdx, docs/astro.config.mjs
Adds the new guide, sidebar entry, usage examples, diagnostics, GitHub Actions workflow snippets, and regeneration steps.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐇 I hopped through CI with a careful thump,
Checked every file from stem to stump.
If drift appears, I sound the bell,
If all is fresh, then all is well.
With --check-drift, the bunny grins—
Clean builds and tidy commits win! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding a read-only --check-drift CI gate for generated output.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cli-drift-check

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Fallow audit report

Found 18 findings.

Dependencies (3)
Severity Rule Location Description
minor fallow/unused-dev-dependency examples/package.json:15 Package '@tanstack/react-query' is in devDependencies but never imported; imported in other workspaces: packages/integration, packages/petstore-fastify
minor fallow/unused-dev-dependency examples/package.json:17 Package 'react' is in devDependencies but never imported; imported in other workspaces: packages/integration, packages/petstore-fastify
minor fallow/unused-dev-dependency packages/integration/package.json:24 Package 'fastify' is in devDependencies but never imported; imported in other workspaces: packages/petstore-contract, packages/petstore-fastify
Duplication (15)
Severity Rule Location Description
minor fallow/code-duplication packages/openapi-zod-ts/src/\_\_tests\_\_/cli-args.test.ts:328 Code clone group 3 (7 lines, 2 instances)
minor fallow/code-duplication packages/openapi-zod-ts/src/\_\_tests\_\_/cli-args.test.ts:328 Code clone group 4 (5 lines, 3 instances)
minor fallow/code-duplication packages/openapi-zod-ts/src/\_\_tests\_\_/drift-check.test.ts:322 Code clone group 6 (7 lines, 3 instances)
minor fallow/code-duplication packages/openapi-zod-ts/src/\_\_tests\_\_/drift-check.test.ts:322 Code clone group 5 (6 lines, 2 instances)
minor fallow/code-duplication packages/openapi-zod-ts/src/\_\_tests\_\_/drift-check.test.ts:322 Code clone group 7 (6 lines, 4 instances)
minor fallow/code-duplication packages/openapi-zod-ts/src/\_\_tests\_\_/drift-check.test.ts:335 Code clone group 7 (6 lines, 4 instances)
minor fallow/code-duplication packages/openapi-zod-ts/src/\_\_tests\_\_/drift-check.test.ts:348 Code clone group 6 (7 lines, 3 instances)
minor fallow/code-duplication packages/openapi-zod-ts/src/\_\_tests\_\_/drift-check.test.ts:348 Code clone group 7 (6 lines, 4 instances)
minor fallow/code-duplication packages/openapi-zod-ts/src/\_\_tests\_\_/drift-check.test.ts:368 Code clone group 8 (5 lines, 2 instances)
minor fallow/code-duplication packages/openapi-zod-ts/src/\_\_tests\_\_/drift-check.test.ts:382 Code clone group 8 (5 lines, 2 instances)
minor fallow/code-duplication packages/openapi-zod-ts/src/\_\_tests\_\_/drift-check.test.ts:383 Code clone group 9 (6 lines, 2 instances)
minor fallow/code-duplication packages/openapi-zod-ts/src/\_\_tests\_\_/drift-check.test.ts:399 Code clone group 6 (7 lines, 3 instances)
minor fallow/code-duplication packages/openapi-zod-ts/src/\_\_tests\_\_/drift-check.test.ts:399 Code clone group 7 (6 lines, 4 instances)
minor fallow/code-duplication packages/openapi-zod-ts/src/\_\_tests\_\_/drift-check.test.ts:399 Code clone group 5 (6 lines, 2 instances)
minor fallow/code-duplication packages/openapi-zod-ts/src/\_\_tests\_\_/drift-check.test.ts:401 Code clone group 9 (6 lines, 2 instances)

Generated by fallow.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fallow audit report

12 inline findings selected for GitHub review.

)
expect(result.action).toBe('run')
if (result.action === 'run') {
expect(result.checkDrift).toBe(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warn fallow/code-duplication: Code clone group 4 (7 lines, 2 instances)

warn fallow/code-duplication: Code clone group 5 (5 lines, 3 instances)

errSpy.mockRestore()
})

it('emits ::error file=<filename>:: annotation when github=true for stale files (no outputDir)', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warn fallow/code-duplication: Code clone group 7 (7 lines, 3 instances)

warn fallow/code-duplication: Code clone group 6 (6 lines, 2 instances)

warn fallow/code-duplication: Code clone group 8 (6 lines, 4 instances)

errSpy.mockRestore()
})

it('emits ::error file=<filename>:: annotation when github=true for missing files (no outputDir)', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warn fallow/code-duplication: Code clone group 8 (6 lines, 4 instances)

errSpy.mockRestore()
})

it('prefixes annotation file path with outputDir when provided', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warn fallow/code-duplication: Code clone group 7 (7 lines, 3 instances)

warn fallow/code-duplication: Code clone group 8 (6 lines, 4 instances)

})

it('does NOT emit ::error annotations when github=false even if GITHUB_ACTIONS is set', () => {
process.env['GITHUB_ACTIONS'] = 'true'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warn fallow/code-duplication: Code clone group 9 (5 lines, 2 instances)

errSpy.mockRestore()
})

it('does not write GITHUB_STEP_SUMMARY when github=true but env var is not set', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warn fallow/code-duplication: Code clone group 7 (7 lines, 3 instances)

warn fallow/code-duplication: Code clone group 8 (6 lines, 4 instances)

warn fallow/code-duplication: Code clone group 6 (6 lines, 2 instances)


it('does not write GITHUB_STEP_SUMMARY when github=true but env var is not set', () => {
// GITHUB_STEP_SUMMARY is not set (cleared in beforeEach).
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warn fallow/code-duplication: Code clone group 10 (6 lines, 2 instances)

// This runs BEFORE any mkdir/writeFile so the output directory is left untouched on
// failure. zod.ts (the user-owned schema file) is intentionally excluded: the generator
// never overwrites it after the first bootstrap, so it must not be in the expected set.
if (checkDrift) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warn fallow/code-duplication: Code clone group 3 (84 lines, 2 instances)

driftPlan.schemaExists &&
!resetSchema
) {
const schemaPath = resolve(cwd, config.input_schema)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warn fallow/code-duplication: Code clone group 11 (11 lines, 2 instances)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 7b443b1.

console.log(
`${prefix}Skipping ${config.input_schema}: already exists (edit freely, it's yours).`
)
console.log(`${prefix}Skipping ${config.input_schema}: already exists (edit freely, it's yours).`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warn fallow/code-duplication: Code clone group 11 (11 lines, 2 instances)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/src/content/docs/guides/drift-detection.mdx`:
- Around line 62-63: The drift-check documentation is overstating the GitHub
Actions behavior in reportDrift(): the ::error annotations and
$GITHUB_STEP_SUMMARY output happen only on failure when drift is detected, not
on every run with GITHUB_ACTIONS=true. Update the wording in the drift-detection
guide to make those side effects explicitly failure-only, and keep the success
path described as a clean run that only logs the success message.

In `@packages/openapi-zod-ts/src/drift-check.ts`:
- Around line 137-153: The GitHub annotation helper only covers stale and
missing drift, so extra files are not annotated in PRs. Update
emitGithubAnnotations in drift-check.ts to also iterate over report.extra and
emit a matching ::error workflow command using the same prefix handling and the
entry.filename symbol, so all drift types found by reportDrift() produce inline
GitHub Actions annotations.

In `@packages/openapi-zod-ts/src/generator.ts`:
- Around line 204-205: The fix hint in generator.ts only appends --config via
fixCommand, so it drops active --input/--output overrides for the no-config
path. Update the fix command construction in generator.ts to include the same
input/output values used by the generator when opts.configPath is undefined,
alongside --check-drift, so the suggested openapi-zod-ts command matches the
current target; use the existing configDesc/fixCommand logic as the place to
preserve these overrides.
- Around line 148-219: The combined check path in generator.ts is being bypassed
because the earlier check-only return exits before the `checkDrift` branch runs.
Update the control flow in `generate` so `--check --check-drift` still executes
the drift validation logic (including `compareOutput` and `reportDrift`) before
any early exit, while preserving the existing `check`-only behavior and using
the existing `checkDrift`, `reportDrift`, and `generate` symbols to locate the
fix.
- Around line 148-152: The drift check in generator.ts is incorrectly treating
co-located input_schema files as extra because they are removed from expectedMap
but still discovered by compareOutput(). Update the drift comparison flow in the
checkDrift branch so user-owned schema files are excluded from the actual
filesystem scan as well, or otherwise filtered out before reporting. Make sure
the schemas.ts to ./schemas.js path handled by the generator logic (including
the input_schema handling around the schema generation setup) no longer produces
report.extra for the schema file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: acd1dc9c-72e0-4f0d-adb1-a76b7b3757d3

📥 Commits

Reviewing files that changed from the base of the PR and between fb3d7b0 and 141db78.

📒 Files selected for processing (10)
  • docs/astro.config.mjs
  • docs/src/content/docs/guides/drift-detection.mdx
  • packages/openapi-zod-ts/package.json
  • packages/openapi-zod-ts/src/__tests__/cli-args.test.ts
  • packages/openapi-zod-ts/src/__tests__/drift-check.test.ts
  • packages/openapi-zod-ts/src/__tests__/generator-schema.test.ts
  • packages/openapi-zod-ts/src/cli-args.ts
  • packages/openapi-zod-ts/src/cli.ts
  • packages/openapi-zod-ts/src/drift-check.ts
  • packages/openapi-zod-ts/src/generator.ts

Comment on lines +62 to +63
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.

Copy link
Copy Markdown

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 ::error annotations and writes $GITHUB_STEP_SUMMARY only when drift is detected; clean runs return after logging the success message. Wording this as unconditional for GITHUB_ACTIONS=true overstates the behavior and will mislead users looking for annotations on passing runs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/src/content/docs/guides/drift-detection.mdx` around lines 62 - 63, The
drift-check documentation is overstating the GitHub Actions behavior in
reportDrift(): the ::error annotations and $GITHUB_STEP_SUMMARY output happen
only on failure when drift is detected, not on every run with
GITHUB_ACTIONS=true. Update the wording in the drift-detection guide to make
those side effects explicitly failure-only, and keep the success path described
as a clean run that only logs the success message.

Comment on lines +137 to +153
function emitGithubAnnotations(
report: DriftReport,
fixCommand: string,
outputDir: string | undefined
): void {
const prefix = outputDir !== undefined && outputDir !== '' ? `${outputDir}/` : ''
for (const entry of report.stale) {
console.log(
`::error file=${prefix}${entry.filename}::Drift detected: ${entry.filename} is stale. Run '${fixCommand}' and commit the result.`
)
}
for (const entry of report.missing) {
console.log(
`::error file=${prefix}${entry.filename}::Drift detected: ${entry.filename} is missing. Run '${fixCommand}' and commit the result.`
)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Annotate EXTRA files in GitHub Actions too.

reportDrift() fails on extra entries as well, but this helper only emits workflow commands for stale and missing. That leaves one class of drift without inline PR annotations even though the file path is known.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/openapi-zod-ts/src/drift-check.ts` around lines 137 - 153, The
GitHub annotation helper only covers stale and missing drift, so extra files are
not annotated in PRs. Update emitGithubAnnotations in drift-check.ts to also
iterate over report.extra and emit a matching ::error workflow command using the
same prefix handling and the entry.filename symbol, so all drift types found by
reportDrift() produce inline GitHub Actions annotations.

Comment thread packages/openapi-zod-ts/src/generator.ts
Comment thread packages/openapi-zod-ts/src/generator.ts
Comment on lines +204 to +205
const configDesc = opts.configPath !== undefined ? `--config ${opts.configPath}` : ''
const fixCommand = ['openapi-zod-ts', configDesc].filter(Boolean).join(' ')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve active --input/--output overrides in the fix hint.

fixCommand only carries --config. For the supported no-config path (--input ... --output ... --check-drift), the suggested openapi-zod-ts command won't reproduce the same target and can fail outright.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/openapi-zod-ts/src/generator.ts` around lines 204 - 205, The fix
hint in generator.ts only appends --config via fixCommand, so it drops active
--input/--output overrides for the no-config path. Update the fix command
construction in generator.ts to include the same input/output values used by the
generator when opts.configPath is undefined, alongside --check-drift, so the
suggested openapi-zod-ts command matches the current target; use the existing
configDesc/fixCommand logic as the place to preserve these overrides.

Adds a --check-drift flag that regenerates all output in memory, applies
the same Prettier formatting as the write path, and compares against committed
files on disk. Exits non-zero with per-file diagnostics when any file is
stale, missing, or extra. Writes nothing to disk.

When GITHUB_ACTIONS=true, emits ::error file=<name>:: annotations per drifted
file and appends a markdown table to GITHUB_STEP_SUMMARY.

Exports a reusable ./drift-check subpath (compareOutput, reportDrift,
DriftReport) so openapi-server, openapi-react-query, and openapi-msw can
adopt the same gate in follow-up PRs.

Claude-Session: https://claude.ai/code/session_01SZmVypA9rE5H3HTreJgyE2
@benjamineckstein
benjamineckstein merged commit bb9f257 into main Jul 4, 2026
18 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 4, 2026
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.

1 participant