docs: prefix legacy V1 API paths in generated OpenAPI (#12491)

# Which Problems Are Solved

- Generated V1 API reference docs are missing their base path prefixes
(e.g. `/admin/v1`, `/auth/v1`, `/management/v1`, `/system/v1`).
- Endpoint pages show routes like `/healthz` instead of
`/admin/v1/healthz`.

This happens because V1 protos still define `base_path` via legacy
OpenAPI v2 swagger annotations, while docs generation uses
`protoc-gen-connect-openapi`, which ignores those annotations and emits
the bare `google.api.http` paths.

# How the Problems Are Solved

- After OpenAPI generation in
`apps/docs/scripts/generate-proto-docs.mjs`, apply a small hardcoded map
of the four legacy V1 services and prepend the correct prefix to each
path in the generated specs.
- The rewrite is idempotent (`startsWith(prefix)`), so already-prefixed
paths are left alone.
- Because the prefixes are written into the generated OpenAPI files,
Fumadocs MDX generation and runtime page rendering stay in sync.

# Additional Changes

- None.

# Additional Context

- V1 APIs are legacy and frozen; hardcoding the four known prefixes is
simpler and more reliable than dynamically scraping `.proto` files.
- Changing the protos or dual-generating with `openapiv2` would be much
more invasive for a docs-only issue.
- Verified locally for Admin, Auth, Management, and System pages (e.g.
`/admin/v1/healthz`, `/management/v1/users/{id}`).

---------

Co-authored-by: Livio Spring <[email protected]>
Co-authored-by: Cursor <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
This commit is contained in:
Federico Coppede
2026-07-27 14:59:02 +00:00
committed by GitHub
co-authored by Livio Spring Cursor Copilot Autofix powered by AI
parent ca6595f8c5
commit 87538f29e1
+44 -2
View File
@@ -1,6 +1,6 @@
import fs from 'fs';
import { glob } from 'glob';
import { join, dirname, resolve } from 'path';
import { join, dirname, resolve, basename } from 'path';
import { spawn, spawnSync } from 'child_process';
import { fileURLToPath } from 'url';
@@ -13,6 +13,42 @@ const VERSIONS_FILE = join(ROOT_DIR, 'content/versions.json');
const REPO_URL = 'https://github.com/zitadel/zitadel.git';
const PNPM_COMMAND = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
// Legacy V1 APIs declare base_path via OpenAPI v2 swagger options that
// protoc-gen-connect-openapi ignores. Prefix paths so fumadocs shows the real URLs.
const V1_PATH_PREFIXES = {
admin: '/admin/v1',
auth: '/auth/v1',
management: '/management/v1',
system: '/system/v1',
};
function applyV1PathPrefixes(outputPath) {
for (const [service, prefix] of Object.entries(V1_PATH_PREFIXES)) {
const files = glob.sync(`**/${service}.openapi.json`, { cwd: outputPath, absolute: true, nodir: true });
for (const file of files) {
try {
const doc = JSON.parse(fs.readFileSync(file, 'utf8'));
if (!doc.paths || Object.keys(doc.paths).length === 0) continue;
const newPaths = {};
let changed = false;
for (const [route, operations] of Object.entries(doc.paths)) {
const newRoute = route.startsWith(prefix) ? route : `${prefix}${route}`;
if (newRoute !== route) changed = true;
newPaths[newRoute] = operations;
}
if (!changed) continue;
doc.paths = newPaths;
fs.writeFileSync(file, JSON.stringify(doc, null, 2));
console.log(`Applied ${prefix} to ${basename(file)}`);
} catch (e) {
console.warn(`Failed to apply ${prefix} to ${basename(file)}`, e);
}
}
}
}
async function run() {
if (!fs.existsSync(VERSIONS_FILE)) {
console.error('versions.json not found. Run fetch-docs.mjs first.');
@@ -182,7 +218,13 @@ async function run() {
} catch (e) {
console.warn(`Failed to clean up OpenAPI specs for ${label}`, e);
}
try {
applyV1PathPrefixes(outputPath);
} catch (e) {
console.warn(`Failed to apply V1 path prefixes for ${label}`, e);
}
resolvePromise();
}
});