feat(api): render and publish the openapi 3.0 documents (#514)

Closes #37.

Gitea generates an OpenAPI 3.0 document since 1.27 (`templates/swagger/v1_openapi3_json.tmpl`, renamed to `templates/swagger/v1-openapi3.generated.json` on main, served by an instance at `/openapi3.v1.json`), but this site never picked it up and still published Swagger 2.0 only. That is what the issue is about: code generators such as `openapi-python-client` reject the document with "You may be trying to use a Swagger document; this is not supported by this project".

**What this does**

- `update_api_docs.sh` downloads the OpenAPI 3.0 document next to the Swagger 2.0 one, rewrites the same placeholders (they sit in `servers` instead of `basePath`) and writes `static/openapi3-latest.json` and `static/openapi3-<minor>.json`. Versions that do not have one (1.26 and older) are skipped with a message and keep their Swagger 2.0 document.
- The OpenAPI 3.0 document is rendered where it exists (`/api/` and `/api/next/`), the older versions keep rendering Swagger 2.0. Both documents describe the same api and carry the same operation ids, so no url of an operation page changes; the `basePath` normalization is now only applied to the Swagger 2.0 documents, since a full url in `servers` is valid.
- The overview page of each version links both documents for download, which is what the issue actually needs: `https://docs.gitea.com/openapi3-27.json` for the current release, `https://docs.gitea.com/openapi3-latest.json` for gitea main.
- The scheduled workflow now refreshes and proposes both documents of gitea main.

**Verification**

- `pnpm dlx @redocly/cli lint static/openapi3-latest.json`: valid, only content warnings that come from upstream (missing 4xx responses, ambiguous paths).
- `openapi-python-client generate --path static/openapi3-latest.json`, the tool from the issue, generates a client. The only warning left is `POST /markdown/raw`, whose request body is `text/plain`.
- Operation ids of `static/swagger-27.json` and `static/openapi3-27.json` are identical (481 on both sides), so the operation pages keep their urls.
- Built the api product and compared a rendered operation page against the Swagger 2.0 one: same sections, the request body is now marked required where the document says so.

The released Swagger 2.0 documents are kept: they are the only description 1.26 and older ever produced, and some tooling still expects them.

<!-- cloudflare-preview --> Preview: https://pr-514.docs-gitea-com.pages.dev

Reviewed-on: https://gitea.com/gitea/docs/pulls/514
Reviewed-by: bircni <[email protected]>
This commit is contained in:
Lunny Xiao
2026-08-14 18:31:00 +00:00
parent 5c78ad533d
commit 4e38ace168
10 changed files with 71941 additions and 47 deletions
+19 -9
View File
@@ -7,8 +7,8 @@ deployment and are linked from the header.
The content directories are the ones the site has always used — `docs/`,
`versioned_docs/`, `i18n/`, `runner-docs/`, `runner-docs_versioned_docs/` and
`static/swagger-*.json` — so the release scripts and the translation workflow
stay unchanged.
`static/swagger-*.json` (plus `static/openapi3-*.json` since gitea 1.27) — so
the release scripts and the translation workflow stay unchanged.
## Running it
@@ -40,8 +40,9 @@ pickers, the version banner, the search facets and the api schemas.
| `i18n/zh-cn/docusaurus-plugin-content-docs/version-1.27/` | `/zh-cn/` |
| `runner-docs_versioned_docs/version-3/` | `/runner/` |
| `runner-docs/` | `/runner/develop/` |
| `static/swagger-27.json` | `/api/` |
| `static/swagger-latest.json` | `/api/next/` |
| `static/openapi3-27.json` | `/api/` |
| `static/openapi3-latest.json` | `/api/next/` |
| `static/swagger-26.json` | `/api/1.26/` |
The loader reads the markdown in two passes. The first one works out the route
of every file, the second normalizes and renders it:
@@ -81,12 +82,21 @@ and `/api/1.27/` are redirected onto them in `cloudflare/_redirects`.
## Api reference
`starlight-openapi` generates a page per operation from the seven swagger
documents `update_api_docs.sh` maintains. Two adjustments happen at build time:
`starlight-openapi` generates a page per operation from the documents
`update_api_docs.sh` maintains: the openapi 3.0 document for the versions that
have one (gitea >= 1.27), the swagger 2.0 one for the older versions. Both
describe the same api and use the same operation ids, so the urls of the
operation pages do not depend on which one is rendered. Three adjustments
happen at build time:
- the documents set `basePath` to the full `https://gitea.com/api/v1` url, which
redoc accepted but is not valid swagger 2.0. A normalized copy is written to
`.cache/openapi/` instead of touching the sources.
- the swagger 2.0 documents set `basePath` to the full
`https://gitea.com/api/v1` url, which redoc accepted but is not valid swagger
2.0. A normalized copy is written to `.cache/openapi/` instead of touching the
sources. The openapi 3.0 documents keep that url in `servers`, where it is
valid, and only get the operation ids and the tag descriptions adjusted.
- both documents of a version are linked for download from its overview page,
see `specDownloads()` in `src/config/api.ts` and
`src/components/ApiOverviewTags.astro`.
- the plugin slugifies the base path, so `/api/1.26/` is generated as
`/api/126/`. `src/integrations/postbuild.ts` renames the directories and
rewrites the links once the build is done.
@@ -1,5 +1,6 @@
---
import { Card, CardGrid } from '@astrojs/starlight/components';
import { specDownloads } from '../config/api';
/**
* Replaces the "Operations" section of the api overview, which lists every
@@ -9,6 +10,9 @@ import { Card, CardGrid } from '@astrojs/starlight/components';
* Substituted for `starlight-openapi/components/overview/OverviewNavigationLinks.astro`
* by the `gitea-openapi-overview` vite plugin in `astro.config.mjs`, so the
* props are the ones that component receives.
*
* The documents of the version are offered for download above the cards, the
* rendered pages are of no use to a code generator.
*/
interface NavigationGroup {
label: string;
@@ -20,6 +24,9 @@ interface NavigationGroup {
const { groups } = Astro.props as { groups: NavigationGroup[] };
const visible = groups.filter((group) => group.links.length > 0);
const meta = Astro.locals.starlightRoute.entry.data.gitea;
const downloads = meta ? specDownloads(meta.version) : [];
/**
* Page of a tag, `/api/operations/tags/repository/`. Derived from the first
* operation link of the group, which is `<base>/operations/<operation>/`, so it
@@ -43,6 +50,21 @@ function count(group: NavigationGroup): string {
}
---
{
downloads.length > 0 && (
<p class="gitea-api-downloads">
Download the specification:
{downloads.map((download, index) => (
<>
{index > 0 && ', '}
<a href={download.href} download>
{download.label}
</a>
</>
))}
</p>
)
}
{
visible.length > 0 && (
<div class="gitea-api-overview">
@@ -73,4 +95,12 @@ function count(group: NavigationGroup): string {
font-size: var(--sl-text-sm);
color: var(--sl-color-gray-3);
}
.gitea-api-downloads {
margin-top: 1.5rem;
}
.gitea-api-downloads a {
margin-inline-start: 0.35rem;
}
</style>
+35 -1
View File
@@ -28,6 +28,7 @@ interface Operation {
}
interface SwaggerDocument {
openapi?: string;
basePath?: string;
host?: string;
schemes?: string[];
@@ -75,6 +76,9 @@ function hyphenateOperationIds(document: SwaggerDocument): void {
* `https://gitea.com/api/v1` url, which redoc accepted but is not valid swagger
* 2.0: the host belongs into `host` and the scheme into `schemes`. Normalize a
* copy instead of touching the sources, which the docusaurus site still reads.
*
* The openapi 3.0 documents keep the url in `servers`, where a full url is
* valid, so only the operation ids and the tags are touched for those.
*/
function normalizeSchema(source: string, name: string): string {
const document = JSON.parse(readFileSync(source, 'utf-8')) as SwaggerDocument;
@@ -129,7 +133,12 @@ export const apiSchemas = getProduct('api')
.map((version) => ({
base: ['api', version.path].filter(Boolean).join('/'),
label: `API ${version.label}`,
schema: normalizeSchema(path.join(repoRoot, version.schema!), `${version.id}.json`),
// gitea generates an openapi 3.0 document since 1.27, render it where it
// exists: it is the same api, described in the format the tooling expects
schema: normalizeSchema(
path.join(repoRoot, version.openapi3 ?? version.schema!),
`${version.id}.json`,
),
// the schema group is lifted to the top level of the sidebar in
// src/middleware/api.ts, so the tag groups below it start collapsed and
// starlight opens the one holding the current operation
@@ -140,3 +149,28 @@ export const apiSchemas = getProduct('api')
operations: { badges: true },
},
}));
export interface SpecDownload {
label: string;
href: string;
}
/**
* The documents of a version, as they are served from `static/`. Shown on the
* overview page of the version: the rendered reference is not something a code
* generator can read, the documents are.
*/
export function specDownloads(versionId: string): SpecDownload[] {
const version = getProduct('api').versions.find((candidate) => candidate.id === versionId);
if (!version) return [];
const downloads: SpecDownload[] = [];
if (version.openapi3) downloads.push({ label: 'OpenAPI 3.0', href: staticHref(version.openapi3) });
if (version.schema) downloads.push({ label: 'Swagger 2.0', href: staticHref(version.schema) });
return downloads;
}
/** `static/openapi3-27.json` is published as `/openapi3-27.json`. */
function staticHref(file: string): string {
return `/${file.replace(/^static\//, '')}`;
}