chore: move API readiness reporting to terminal output (#36433)

This commit is contained in:
Stephen Zhou
2026-05-20 15:23:35 +08:00
committed by GitHub
parent 5cdf4e405b
commit b64d4b53ca
5 changed files with 25 additions and 163 deletions

View File

@@ -1,40 +0,0 @@
# Contracts
## API OpenAPI Readiness
<!-- api-openapi-readiness:start -->
<!-- This section is auto-generated by scripts/generate-api-readiness-readme.mjs. Do not edit between the markers. -->
Snapshot generated from `packages/contracts/generated/api/readiness.json` after running `pnpm -C packages/contracts gen-api-contract-from-openapi`.
Are we OpenAPI ready? **No.** Current generated API contracts are **35.4% ready**.
| Surface | Ready | Not ready | Total | Ready % |
| --------- | ------: | --------: | ------: | --------: |
| console | 205 | 383 | 588 | 34.9% |
| service | 28 | 60 | 88 | 31.8% |
| web | 21 | 20 | 41 | 51.2% |
| **total** | **254** | **463** | **717** | **35.4%** |
Readiness here means the generated contract operation is not marked with:
> Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.
Operations marked with that warning should not be migrated to blindly. Prefer fixing backend OpenAPI annotations first so the generated contract has accurate request and response types, then migrate callers endpoint by endpoint.
The current heuristic marks an operation as not ready when a request body or success response that should have a body contains a loose object type, when a mutating controller reads a JSON body that is not documented as a request body, or when an operation has no documented 2xx response. 204, 205, and 304 responses are treated as bodyless when the request type is otherwise accurate.
<!-- api-openapi-readiness:end -->
## How to Improve Readiness
Improve the ready percentage by fixing the backend annotations that produce loose generated types, then regenerating the contracts.
- Add accurate request body schemas for endpoints that currently generate loose object types.
- Add accurate 2xx response schemas for endpoints that return JSON payloads.
- Use 204 responses for endpoints that intentionally return no body.
- Avoid untyped dictionaries, raw objects, or `additionalProperties: true` responses unless the API really returns an arbitrary object.
- Regenerate with `pnpm -C packages/contracts gen-api-contract` and use this README to verify the updated percentage.
Do not remove the generated warning just to increase the number. The warning should disappear because the backend OpenAPI output became accurate enough for callers to migrate safely.

View File

@@ -1,17 +0,0 @@
{
"surfaces": {
"console": {
"notReady": 383,
"total": 588
},
"service": {
"notReady": 60,
"total": 88
},
"web": {
"notReady": 20,
"total": 41
}
},
"warning": "Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate."
}

View File

@@ -90,7 +90,6 @@ type ApiOperationContext = {
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const apiOpenApiDir = path.resolve(currentDir, 'openapi')
const apiReadinessStatsPath = path.resolve(currentDir, 'generated/api/readiness.json')
const apiControllersDir = path.resolve(currentDir, '../../api/controllers')
const operationMethods = new Set(['delete', 'get', 'patch', 'post', 'put'])
@@ -750,6 +749,10 @@ const recordApiReadiness = (surface: string, isReady: boolean) => {
stats.notReady += 1
}
const formatPercent = (ready: number, total: number) => {
return total === 0 ? '0.0%' : `${((ready / total) * 100).toFixed(1)}%`
}
const normalizeOperations = (document: SwaggerDocument, surface: string) => {
const definitions = document.definitions ??= {}
@@ -792,18 +795,29 @@ const normalizeApiSwagger = (document: SwaggerDocument, surface: string) => {
return document
}
const writeApiReadinessStats = () => {
const printApiReadinessStats = () => {
const sortedSurfaces = Object.entries(apiReadinessStats)
.sort(([left], [right]) => left.localeCompare(right))
fs.mkdirSync(path.dirname(apiReadinessStatsPath), { recursive: true })
fs.writeFileSync(
apiReadinessStatsPath,
`${JSON.stringify({
surfaces: Object.fromEntries(sortedSurfaces),
warning: inaccurateGeneratedContractDescription,
}, null, 2)}\n`,
const totals = sortedSurfaces.reduce(
(summary, [, stats]) => {
summary.notReady += stats.notReady
summary.total += stats.total
return summary
},
{ notReady: 0, total: 0 },
)
const totalReady = totals.total - totals.notReady
const rows = sortedSurfaces.map(([surface, stats]) => {
const ready = stats.total - stats.notReady
return ` ${surface}: ${ready}/${stats.total} ready (${formatPercent(ready, stats.total)}), ${stats.notReady} not ready`
})
console.log([
'API OpenAPI readiness:',
...rows,
` total: ${totalReady}/${totals.total} ready (${formatPercent(totalReady, totals.total)}), ${totals.notReady} not ready`,
].join('\n'))
}
const topLevelPathSegment = (routePath: string) => {
@@ -933,7 +947,7 @@ const createApiJobs = (spec: ApiSpec): ApiJob[] => {
}
const apiJobs = apiSpecs.flatMap(createApiJobs)
writeApiReadinessStats()
printApiReadinessStats()
const createApiConfig = (job: ApiJob): UserConfig => ({
input: job.document,

View File

@@ -15,9 +15,8 @@
},
"scripts": {
"gen-api-contract": "pnpm gen-api-openapi && pnpm gen-api-contract-from-openapi",
"gen-api-contract-from-openapi": "node -e \"fs.rmSync('generated/api', { recursive: true, force: true })\" && openapi-ts -f openapi-ts.api.config.ts && vp fmt generated/api && eslint --fix generated/api && pnpm gen-api-readiness-readme",
"gen-api-contract-from-openapi": "node -e \"fs.rmSync('generated/api', { recursive: true, force: true })\" && openapi-ts -f openapi-ts.api.config.ts && vp fmt generated/api && eslint --fix generated/api",
"gen-api-openapi": "uv run --project ../../api ../../api/dev/generate_swagger_specs.py --output-dir openapi",
"gen-api-readiness-readme": "node scripts/generate-api-readiness-readme.mjs && eslint --fix README.md",
"gen-enterprise-contract": "openapi-ts -f openapi-ts.enterprise.config.ts",
"type-check": "tsgo"
},

View File

@@ -1,94 +0,0 @@
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const packageDir = path.resolve(currentDir, '..')
const readinessStatsPath = path.resolve(packageDir, 'generated/api/readiness.json')
const readmePath = path.resolve(packageDir, 'README.md')
const readinessStartMarker = '<!-- api-openapi-readiness:start -->'
const readinessEndMarker = '<!-- api-openapi-readiness:end -->'
const formatPercent = (ready, total) => {
return total === 0 ? '0.0%' : `${((ready / total) * 100).toFixed(1)}%`
}
const collectStats = () => {
if (!fs.existsSync(readinessStatsPath)) {
throw new Error(
`Missing API readiness stats: ${readinessStatsPath}. Run "pnpm -C packages/contracts gen-api-contract-from-openapi" first.`,
)
}
return JSON.parse(fs.readFileSync(readinessStatsPath, 'utf8'))
}
const tableRow = (surface, ready, notReady, total) => {
return `| ${surface} | ${ready} | ${notReady} | ${total} | ${formatPercent(ready, total)} |`
}
const renderReadinessSection = (stats) => {
const rows = Object.entries(stats.surfaces)
.sort(([left], [right]) => left.localeCompare(right))
.map(([surface, stat]) => tableRow(surface, stat.total - stat.notReady, stat.notReady, stat.total))
const totals = Object.values(stats.surfaces).reduce(
(summary, stat) => {
summary.notReady += stat.notReady
summary.total += stat.total
return summary
},
{ notReady: 0, total: 0 },
)
const totalReady = totals.total - totals.notReady
if (totals.total === 0)
throw new Error(`No API readiness stats found in ${readinessStatsPath}`)
return `${readinessStartMarker}
<!-- This section is auto-generated by scripts/generate-api-readiness-readme.mjs. Do not edit between the markers. -->
Snapshot generated from \`packages/contracts/generated/api/readiness.json\` after running \`pnpm -C packages/contracts gen-api-contract-from-openapi\`.
Are we OpenAPI ready? **No.** Current generated API contracts are **${formatPercent(totalReady, totals.total)} ready**.
| Surface | Ready | Not ready | Total | Ready % |
| --- | ---: | ---: | ---: | ---: |
${rows.join('\n')}
| **total** | **${totalReady}** | **${totals.notReady}** | **${totals.total}** | **${formatPercent(totalReady, totals.total)}** |
Readiness here means the generated contract operation is not marked with:
> ${stats.warning}
Operations marked with that warning should not be migrated to blindly. Prefer fixing backend OpenAPI annotations first so the generated contract has accurate request and response types, then migrate callers endpoint by endpoint.
The current heuristic marks an operation as not ready when a request body or success response that should have a body contains a loose object type, when a mutating controller reads a JSON body that is not documented as a request body, or when an operation has no documented 2xx response. 204, 205, and 304 responses are treated as bodyless when the request type is otherwise accurate.
${readinessEndMarker}
`
}
const updateReadme = (readinessSection) => {
const readme = fs.readFileSync(readmePath, 'utf8')
const startIndex = readme.indexOf(readinessStartMarker)
const endIndex = readme.indexOf(readinessEndMarker)
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
throw new Error(
`Missing readiness markers in ${readmePath}. Expected ${readinessStartMarker} and ${readinessEndMarker}.`,
)
}
const nextReadme = [
readme.slice(0, startIndex),
readinessSection,
readme.slice(endIndex + readinessEndMarker.length),
].join('')
fs.writeFileSync(readmePath, nextReadme)
}
updateReadme(renderReadinessSection(collectStats()))