From 4822687a64a3c5adc7d608c02c48acc01e33c3a2 Mon Sep 17 00:00:00 2001 From: Hieu Date: Mon, 27 Jul 2026 10:35:42 +0700 Subject: [PATCH] fix: resolve mgmt api specs $refs manually to handle circular error (#48281) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the CONTRIBUTING.md file. YES ## What kind of change does this PR introduce? Bug fix. ## What is the current behavior? `api_v2_openapi.json` has a circular reference (`APIErrorObject.issues` → `APIErrorObject`), which Redocly can't flatten with `--dereferenced` ("Detected circular reference which can't be converted to JSON"). This breaks the [weekly docs update workflow](https://github.com/supabase/supabase/actions/runs/29709444085/job/88251269807). ## What is the new behavior? - Drop `--dereferenced` from `dereference.api.v1` (both v1 and v2, for consistency) - Add a `resolveRefs` helper in `Reference.script.ts` that manually inlines `$refs`, leaving cycles as an unresolved `$ref` instead of expanding infinitely - This also fix the mgmt api update workflow so manual dispatch runs against the selected branch, by changing checkout `ref` from hardcoded `master` to `${{ github.ref }}`. ## Additional context Also fixes `pnpm exec redocly` → `npx --package=@redocly/cli redocly` in the same Makefile, an unrelated pnpm 11 recursive-exec bug hit while debugging this workflow. ## Summary by CodeRabbit * **Chores** * Updated API specification bundling and linting commands to use the current Redocly CLI invocation style. * Improved documentation processing behavior for dereferenced specs, including guidance around circular references. * Preserved existing generated specification outputs and validation settings. * **Chores** * Updated the Mgmt API docs automation workflow formatting (YAML string quoting and schedule/input values). --- .github/workflows/docs-mgmt-api-update.yml | 18 +++--- .../docs/Reference.generated.script.ts | 56 ++++++++++++++++++- apps/docs/spec/Makefile | 16 ++++-- 3 files changed, 74 insertions(+), 16 deletions(-) diff --git a/.github/workflows/docs-mgmt-api-update.yml b/.github/workflows/docs-mgmt-api-update.yml index 951aeec76c9..6b868b1b37b 100644 --- a/.github/workflows/docs-mgmt-api-update.yml +++ b/.github/workflows/docs-mgmt-api-update.yml @@ -3,7 +3,7 @@ name: Update Mgmt Api Docs on: schedule: # Run at 00:00 UTC every Monday - - cron: '0 0 * * 1' + - cron: "0 0 * * 1" workflow_dispatch: permissions: @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - ref: master + ref: ${{ github.ref }} sparse-checkout: | apps/docs patches @@ -32,8 +32,8 @@ jobs: - name: Use Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - node-version-file: '.nvmrc' - cache: 'pnpm' + node-version-file: ".nvmrc" + cache: "pnpm" - name: Install deps run: pnpm install --frozen-lockfile @@ -55,8 +55,8 @@ jobs: uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0 with: token: ${{ steps.app-token.outputs.token }} - commit-message: 'feat: update mgmt api docs' - title: 'feat: update mgmt api docs' - body: 'This PR updates mgmt api docs automatically.' - branch: 'gha/auto-update-mgmt-api-docs' - base: 'master' + commit-message: "feat: update mgmt api docs" + title: "feat: update mgmt api docs" + body: "This PR updates mgmt api docs automatically." + branch: "gha/auto-update-mgmt-api-docs" + base: "master" diff --git a/apps/docs/features/docs/Reference.generated.script.ts b/apps/docs/features/docs/Reference.generated.script.ts index 7a52c883ffb..67c783d320c 100644 --- a/apps/docs/features/docs/Reference.generated.script.ts +++ b/apps/docs/features/docs/Reference.generated.script.ts @@ -258,7 +258,14 @@ async function writeApiReferenceSections() { }, }, } - const endpointsById = mapEndpointsById(mergedSpec) + + // Mgmt api specs bundled without `--dereferenced` + // (v2 has a circular ref in APIErrorObject.issues), + // so we resolve $refs manually here. + // Cycles are left as an unresolved $ref rather than expanded infinitely. + const resolvedSpec = resolveRefs(mergedSpec, mergedSpec) + + const endpointsById = mapEndpointsById(resolvedSpec) const pendingEndpointsByIdWrite = writeFile( join(GENERATED_DIRECTORY, 'api.latest.endpointsById.json'), JSON.stringify(Array.from(endpointsById.entries())) @@ -293,6 +300,53 @@ async function writeApiReferenceSections() { ]) } +function resolveRefs(node: any, root: any, refChain: string[] = []): any { + if (Array.isArray(node)) { + return node.map((item) => resolveRefs(item, root, refChain)) + } + + if (isPlainObject(node)) { + if ('$ref' in node && typeof node.$ref === 'string') { + const refPath = node.$ref + + // Cycle guard: if we're already in the middle of resolving this exact + // ref, inlining further would recurse forever (e.g. APIErrorObject.issues + // -> APIErrorObject). Leave the $ref pointer unresolved at that point + // instead of expanding infinitely. + if (refChain.includes(refPath)) { + return { $ref: refPath } + } + + // Only handle local refs (#/components/...) — this spec doesn't use + // external file refs post-bundling. + if (!refPath.startsWith('#/')) { + return node + } + + const segments = refPath.replace(/^#\//, '').split('/') + let target = root + for (const seg of segments) { + target = target?.[seg] + } + + if (target === undefined) { + console.warn(`Could not resolve $ref: ${refPath}`) + return node + } + + return resolveRefs(target, root, [...refChain, refPath]) + } + + const result: Record = {} + for (const [key, value] of Object.entries(node)) { + result[key] = resolveRefs(value, root, refChain) + } + return result + } + + return node +} + async function writeSelfHostingReferenceSections() { let id = 0 diff --git a/apps/docs/spec/Makefile b/apps/docs/spec/Makefile index 205cdd3d6e6..75515103045 100644 --- a/apps/docs/spec/Makefile +++ b/apps/docs/spec/Makefile @@ -61,18 +61,22 @@ download.analytics.v0: # at build time, so no separate `dereference` / `combine` step is needed. transform: dereference.api.v1 dereference.auth.v1 dereference.storage.v0 +# NOTE: no --dereferenced here — api_v2_openapi.json has a circular ref +# (APIErrorObject.issues -> APIErrorObject) that Redocly can't flatten to +# JSON. v1 uses the same approach for consistency. +# $refs are resolved manually in writeApiReferenceSections dereference.api.v1: - pnpm exec redocly bundle --dereferenced -o $(REPO_DIR)/transforms/api_v1_openapi_deparsed.json $(REPO_DIR)/api_v1_openapi.json - pnpm exec redocly bundle --dereferenced -o $(REPO_DIR)/transforms/api_v2_openapi_deparsed.json $(REPO_DIR)/api_v2_openapi.json + npx --package=@redocly/cli redocly bundle -o $(REPO_DIR)/transforms/api_v1_openapi_deparsed.json $(REPO_DIR)/api_v1_openapi.json + npx --package=@redocly/cli redocly bundle -o $(REPO_DIR)/transforms/api_v2_openapi_deparsed.json $(REPO_DIR)/api_v2_openapi.json dereference.auth.v1: - pnpm exec redocly bundle --dereferenced -o $(REPO_DIR)/transforms/auth_v1_openapi_deparsed.json $(REPO_DIR)/auth_v1_openapi.json + npx --package=@redocly/cli redocly bundle --dereferenced -o $(REPO_DIR)/transforms/auth_v1_openapi_deparsed.json $(REPO_DIR)/auth_v1_openapi.json dereference.storage.v0: - pnpm exec redocly bundle --dereferenced -o $(REPO_DIR)/transforms/storage_v0_openapi_deparsed.json $(REPO_DIR)/storage_v0_openapi.json + npx --package=@redocly/cli redocly bundle --dereferenced -o $(REPO_DIR)/transforms/storage_v0_openapi_deparsed.json $(REPO_DIR)/storage_v0_openapi.json dereference.analytics.v0: - pnpm exec redocly bundle --dereferenced -o $(REPO_DIR)/transforms/analytics_v0_openapi_deparsed.json $(REPO_DIR)/analytics_v0_openapi.json + npx --package=@redocly/cli redocly bundle --dereferenced -o $(REPO_DIR)/transforms/analytics_v0_openapi_deparsed.json $(REPO_DIR)/analytics_v0_openapi.json ############################################################################### # Generate sections from OpenAPI 3.0 @@ -90,7 +94,7 @@ generate.sections.api.v1: ############################################################################### validate.analytics.v0: - pnpm exec redocly lint --extends=minimal $(REPO_DIR)/analytics_v0_openapi.json + npx --package=@redocly/cli redocly lint --extends=minimal $(REPO_DIR)/analytics_v0_openapi.json ############################################################################### # Format everything - easier for git to track changes.