From fe618c42be5b05cd031672b5fce6b0059b63fdd6 Mon Sep 17 00:00:00 2001 From: bircni Date: Thu, 22 Oct 2026 11:28:15 +0200 Subject: [PATCH] feat: add a cli product for the gt documentation gt, the Gitea CLI, gets its own product next to the runner: the CLI entry in the header, its own sidebars, search scope, crawler start url and redirects, and `make cut-version PRODUCT=cli`. Until a release series is listed, the develop docs are served at /cli/ without the unreleased banner. A directory with an index page and no _category_.json now takes its sidebar label and position from that page, which leads the group, so the generated reference can be grouped by command. Signed-off-by: bircni --- cli-docs_versions.json | 1 + cloudflare/_redirects | 6 ++-- cloudflare/docsearch-crawler.json | 4 ++- packages/content-loader/src/products.ts | 47 +++++++++++++++++++++++-- packages/content-loader/src/sidebar.ts | 25 ++++++++----- scripts/cut-version.mjs | 18 +++++----- sites/docs/README.md | 20 ++++++----- sites/docs/src/config/strings.ts | 6 ++-- sites/docs/src/lib/sidebars.ts | 16 ++++----- sites/docs/src/schema.ts | 2 +- 10 files changed, 104 insertions(+), 41 deletions(-) create mode 100644 cli-docs_versions.json diff --git a/cli-docs_versions.json b/cli-docs_versions.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/cli-docs_versions.json @@ -0,0 +1 @@ +[] diff --git a/cloudflare/_redirects b/cloudflare/_redirects index d758d5ef..d51e913a 100644 --- a/cloudflare/_redirects +++ b/cloudflare/_redirects @@ -1,11 +1,13 @@ # Redirects for legacy URLs and routes the site no longer serves directly. -# The api reference and the runner docs are separate products, published in -# english only, so they have no localized copy. +# The api reference, the runner and the cli docs are separate products, +# published in english only, so they have no localized copy. /zh-cn/api/* /api/:splat 301 /zh-tw/api/* /api/:splat 301 /zh-cn/runner/* /runner/:splat 301 /zh-tw/runner/* /runner/:splat 301 +/zh-cn/cli/* /cli/:splat 301 +/zh-tw/cli/* /cli/:splat 301 # The version served at the root of a product is also reachable under its # number, which is what the release notes and old links use. diff --git a/cloudflare/docsearch-crawler.json b/cloudflare/docsearch-crawler.json index 903d5310..76ff5892 100644 --- a/cloudflare/docsearch-crawler.json +++ b/cloudflare/docsearch-crawler.json @@ -7,6 +7,7 @@ "https://docs.gitea.com/zh-cn/", "https://docs.gitea.com/zh-tw/", "https://docs.gitea.com/runner/", + "https://docs.gitea.com/cli/", "https://docs.gitea.com/api/", "https://docs.gitea.com/enterprise/" ], @@ -15,7 +16,8 @@ "https://docs.gitea.com/1.2*/**", "https://docs.gitea.com/*/1.2*/**", "https://docs.gitea.com/api/1.2*/**", - "https://docs.gitea.com/runner/[0-9]/**" + "https://docs.gitea.com/runner/[0-9]/**", + "https://docs.gitea.com/cli/[0-9]*/**" ], "selectors": { "lvl0": { "selector": "//nav[contains(@aria-labelledby,'starlight__sidebar')]//a[@aria-current='page']/ancestor::details//summary//span", "type": "xpath", "global": true, "default_value": "Documentation" }, diff --git a/packages/content-loader/src/products.ts b/packages/content-loader/src/products.ts index 510eba4f..e2e3160d 100644 --- a/packages/content-loader/src/products.ts +++ b/packages/content-loader/src/products.ts @@ -8,7 +8,7 @@ export type LocaleId = 'en-us' | 'zh-cn' | 'zh-tw'; -export type ProductId = 'docs' | 'api' | 'runner' | 'enterprise'; +export type ProductId = 'docs' | 'api' | 'runner' | 'cli' | 'enterprise'; /** Values substituted for `@name@` placeholders in the markdown sources. */ export interface VersionVariables { @@ -246,6 +246,43 @@ const runnerProduct: ProductDef = { ], }; +/** + * gt, the Gitea CLI: one directory per release series, english only, like the + * runner. The label is the newest release of the series, which is also the tag + * the generated reference pages of that directory come from. + * + * Until the first series is listed here the develop docs are served at the + * product root, without the unreleased banner, since there is no release to + * point to. Listing a series moves them to `/cli/develop/`. + */ +const cliSeries: { id: string; latestRelease: string }[] = []; + +const cliProduct: ProductDef = { + id: 'cli', + label: 'CLI', + base: 'cli', + locales: ['en-us'], + versions: [ + { + id: 'develop', + label: 'develop', + sources: { 'en-us': 'cli-docs' }, + ...(cliSeries.length > 0 + ? { path: 'develop', banner: 'unreleased' as const } + : { path: '', latest: true }), + }, + ...cliSeries.map( + ({ id, latestRelease }, index): VersionDef => ({ + id, + label: latestRelease, + path: index === 0 ? '' : id, + latest: index === 0, + sources: { 'en-us': `cli-docs_versioned_docs/version-${id}` }, + }), + ), + ], +}; + /** API reference, rendered from the swagger documents by starlight-openapi. */ const apiProduct: ProductDef = { id: 'api', @@ -301,7 +338,13 @@ const enterpriseProduct: ProductDef = { externalBaseUrl: 'https://docs.gitea.com/enterprise', }; -export const products: ProductDef[] = [docsProduct, apiProduct, runnerProduct, enterpriseProduct]; +export const products: ProductDef[] = [ + docsProduct, + apiProduct, + runnerProduct, + cliProduct, + enterpriseProduct, +]; export function getProduct(id: ProductId): ProductDef { const product = products.find((candidate) => candidate.id === id); diff --git a/packages/content-loader/src/sidebar.ts b/packages/content-loader/src/sidebar.ts index 2770e4b3..292b1847 100644 --- a/packages/content-loader/src/sidebar.ts +++ b/packages/content-loader/src/sidebar.ts @@ -96,14 +96,23 @@ interface Tree { label(dir: string): string; } -/** Sidebar of a directory tree, ordered by `sidebar_position`. */ +/** + * Sidebar of a directory tree, ordered by `sidebar_position`. A directory + * without `_category_.json` but with an `index` page, as the generated cli + * reference has one per command, takes its label and position from that page, + * which comes first in it. + */ function treeOf(pages: SidebarPage[]): Tree { const categories = new Map(); + const indexes = new Map(); for (const page of pages) { - if (!page.meta.category) continue; - const dir = [page.meta.dir, page.meta.name].filter(Boolean).join('/'); - categories.set(dir, page); + if (page.meta.category) { + categories.set([page.meta.dir, page.meta.name].filter(Boolean).join('/'), page); + } else if (page.meta.name === 'index' && page.meta.dir) { + indexes.set(page.meta.dir, page); + } } + const heading = (dir: string) => categories.get(dir) ?? indexes.get(dir); const childDirs = (dir: string) => { const prefix = dir ? `${dir}/` : ''; @@ -121,7 +130,7 @@ function treeOf(pages: SidebarPage[]): Tree { const entries: { sort: [number, number, string]; entry: SidebarEntry }[] = []; for (const child of childDirs(dir)) { - const category = categories.get(child); + const category = heading(child); const label = category?.title ?? labelFromDirname(child.split('/').at(-1)!); entries.push({ sort: [category?.meta.order ?? last, 0, label], @@ -131,7 +140,8 @@ function treeOf(pages: SidebarPage[]): Tree { for (const page of pages) { if (page.meta.dir !== dir || page.meta.category || page.hidden) continue; - entries.push({ sort: [page.meta.order ?? last, 1, page.title], entry: link(page) }); + const order = indexes.get(dir) === page ? -last : (page.meta.order ?? last); + entries.push({ sort: [order, 1, page.title], entry: link(page) }); } return entries @@ -142,8 +152,7 @@ function treeOf(pages: SidebarPage[]): Tree { .map((item) => item.entry); }; - const label = (dir: string) => - categories.get(dir)?.title ?? labelFromDirname(dir.split('/').at(-1)!); + const label = (dir: string) => heading(dir)?.title ?? labelFromDirname(dir.split('/').at(-1)!); return { build, label }; } diff --git a/scripts/cut-version.mjs b/scripts/cut-version.mjs index f1c205cc..e05ff23e 100644 --- a/scripts/cut-version.mjs +++ b/scripts/cut-version.mjs @@ -4,10 +4,11 @@ * * node scripts/cut-version.mjs docs 1.28 # freezes docs/ as 1.28 * node scripts/cut-version.mjs runner 4 # freezes runner-docs/ as 4 + * node scripts/cut-version.mjs cli 0.1 # freezes cli-docs/ as 0.1 * * For the docs it copies the english tree, every translation and the sidebar - * file; for the runner the english tree and its sidebar. The version list - * (`versions.json`, `runner-docs_versions.json`) is updated too. + * file; for the runner and the cli the english tree and its sidebar. The + * version list (`versions.json`, `-docs_versions.json`) is updated too. * * The label, the release variables (`@version@`, `@dockerVersion@`, ...) and * which version is served at the root live in @@ -21,8 +22,8 @@ import process from 'node:process'; const [product, version] = process.argv.slice(2); -if (!product || !version || !['docs', 'runner'].includes(product)) { - console.error('usage: node scripts/cut-version.mjs '); +if (!product || !version || !['docs', 'runner', 'cli'].includes(product)) { + console.error('usage: node scripts/cut-version.mjs '); process.exit(1); } @@ -52,6 +53,7 @@ async function freeze(from, to) { async function freezeSidebar(from, to) { if (await exists(to)) throw new Error(`${to} already exists`); const sidebar = require(path.join(root, from)); + await mkdir(path.join(root, path.dirname(to)), { recursive: true }); await writeFile(path.join(root, to), `${JSON.stringify(sidebar, null, 2)}\n`); console.log(` ${from} -> ${to}`); } @@ -80,12 +82,12 @@ if (product === 'docs') { await addToVersions('versions.json'); } else { - await freeze('runner-docs', `runner-docs_versioned_docs/version-${version}`); + await freeze(`${product}-docs`, `${product}-docs_versioned_docs/version-${version}`); await freezeSidebar( - 'runner-sidebars.js', - `runner-docs_versioned_sidebars/version-${version}-sidebars.json`, + `${product}-sidebars.js`, + `${product}-docs_versioned_sidebars/version-${version}-sidebars.json`, ); - await addToVersions('runner-docs_versions.json'); + await addToVersions(`${product}-docs_versions.json`); } console.log( diff --git a/sites/docs/README.md b/sites/docs/README.md index 01b7eef6..5622f094 100644 --- a/sites/docs/README.md +++ b/sites/docs/README.md @@ -1,12 +1,13 @@ # Astro + Starlight site Renders the gitea documentation with [Starlight](https://starlight.astro.build): -the docs (6 versions x 3 languages), the runner docs (4 series) and the api -reference (7 swagger documents). The enterprise docs stay in their own +the docs (6 versions x 3 languages), the runner docs (4 series), the gt (Gitea +CLI) docs and the api reference (7 swagger documents). The enterprise docs stay in their own 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 +`versioned_docs/`, `i18n/`, `runner-docs/`, `runner-docs_versioned_docs/`, +`cli-docs/` and `static/swagger-*.json` (plus `static/openapi3-*.json` since gitea 1.27) — so the release scripts and the translation workflow stay unchanged. @@ -40,6 +41,7 @@ pickers, the version banner, the search facets and the api schemas. | `i18n/zh-cn/docs/version-1.27/` | `/zh-cn/` | | `runner-docs_versioned_docs/version-3/` | `/runner/` | | `runner-docs/` | `/runner/develop/` | +| `cli-docs/` | `/cli/`, until a cli series is released | | `static/openapi3-27.json` | `/api/` | | `static/openapi3-latest.json` | `/api/next/` | | `static/swagger-26.json` | `/api/1.26/` | @@ -60,8 +62,10 @@ of every file, the second normalizes and renders it: Sidebars are built per (product, version, language) from `_category_.json` and from the sidebar files (`sidebars.js`, `versioned_sidebars/`, -`runner-sidebars.js`, `runner-docs_versioned_sidebars/`), and selected in -`src/routeData.ts`, since starlight only supports a single static sidebar. +`runner-sidebars.js`, `runner-docs_versioned_sidebars/`, `cli-sidebars.js`), +and selected in `src/routeData.ts`, since starlight only supports a single +static sidebar. A directory without `_category_.json` takes the label and the +position of its `index` page, which the generated cli reference relies on. Set `GITEA_DOCS_STRICT_LINKS=true` to fail the build on a broken internal link instead of warning. @@ -69,9 +73,9 @@ instead of warning. ## Products and languages The docs are the site itself, so their language comes first: `/zh-cn/1.26/`. -The runner and the api are separate products and keep their language inside the -product, `/runner/zh-cn/3/`, which is reserved: both are published in english -only today. Starlight builds a fallback page in every configured language for +The runner, the cli and the api are separate products and keep their language +inside the product, `/runner/zh-cn/3/`, which is reserved: all are published in +english only today. Starlight builds a fallback page in every configured language for every english page, so `/zh-cn/runner/` would exist; the post build integration removes those directories and `cloudflare/_redirects` sends them to the english page. diff --git a/sites/docs/src/config/strings.ts b/sites/docs/src/config/strings.ts index 0e17e67f..e4f0eea4 100644 --- a/sites/docs/src/config/strings.ts +++ b/sites/docs/src/config/strings.ts @@ -23,7 +23,7 @@ type Strings = { const en: Strings = { versionLabel: 'Version', languageLabel: 'Language', - products: { docs: 'Docs', api: 'API', runner: 'Runner', enterprise: 'Enterprise' }, + products: { docs: 'Docs', api: 'API', runner: 'Runner', cli: 'CLI', enterprise: 'Enterprise' }, footer: { Community: 'Community', Code: 'Code', More: 'More' }, signIn: 'Sign In', translationNotice: 'This translation may be behind the english original.', @@ -41,7 +41,7 @@ const strings: Record = { 'zh-cn': { versionLabel: '版本', languageLabel: '语言', - products: { docs: '文档', api: 'API', runner: 'Runner', enterprise: '企业版' }, + products: { docs: '文档', api: 'API', runner: 'Runner', cli: 'CLI', enterprise: '企业版' }, footer: { Community: '社区', Code: '开源代码', More: '更多' }, signIn: '登录', translationNotice: '当前中文文档翻译不是最新版,访问英文版本查看最新内容,或', @@ -56,7 +56,7 @@ const strings: Record = { 'zh-tw': { versionLabel: '版本', languageLabel: '語言', - products: { docs: '文件', api: 'API', runner: 'Runner', enterprise: '企業版' }, + products: { docs: '文件', api: 'API', runner: 'Runner', cli: 'CLI', enterprise: '企業版' }, footer: { Community: '社區', Code: '開源程式碼', More: '更多' }, signIn: '登入', translationNotice: '當前中文文檔翻譯不是最新版,訪問英文版本查看最新內容,或', diff --git a/sites/docs/src/lib/sidebars.ts b/sites/docs/src/lib/sidebars.ts index d01bafa8..70c44446 100644 --- a/sites/docs/src/lib/sidebars.ts +++ b/sites/docs/src/lib/sidebars.ts @@ -12,9 +12,9 @@ import { getCollection } from 'astro:content'; import { repoRoot } from '../config/paths'; /** - * Sidebar files: the top level order of the docs and the - * handwritten sidebars of the runner, one per version. Read as is, so cutting a - * version keeps needing no change here. + * Sidebar files: the top level order of the docs and the handwritten sidebars + * of the runner and the cli, one per version. Read as is, so cutting a version + * keeps needing no change here. */ function sidebarSources(): Map { const sidebars = new Map(); @@ -42,18 +42,18 @@ function sidebarSources(): Map { version.id === 'next' ? 'sidebars.js' : `versioned_sidebars/version-${version.id}-sidebars.json`; - } else if (product.id === 'runner') { - files[`runner@${version.id}`] = + } else if (product.id === 'runner' || product.id === 'cli') { + files[`${product.id}@${version.id}`] = version.id === 'develop' - ? 'runner-sidebars.js' - : `runner-docs_versioned_sidebars/version-${version.id}-sidebars.json`; + ? `${product.id}-sidebars.js` + : `${product.id}-docs_versioned_sidebars/version-${version.id}-sidebars.json`; } } } for (const [key, file] of Object.entries(files)) { const config = read(file); - const items = config && (config.docs ?? config.runner ?? Object.values(config)[0]); + const items = config && (config.docs ?? config.runner ?? config.cli ?? Object.values(config)[0]); if (items) sidebars.set(key, items); } return sidebars; diff --git a/sites/docs/src/schema.ts b/sites/docs/src/schema.ts index 8f920202..cad4d72b 100644 --- a/sites/docs/src/schema.ts +++ b/sites/docs/src/schema.ts @@ -7,7 +7,7 @@ import { z } from 'astro:content'; * per version sidebars, the version and language pickers and the search facets. */ export const giteaMeta = z.object({ - product: z.enum(['docs', 'api', 'runner', 'enterprise']), + product: z.enum(['docs', 'api', 'runner', 'cli', 'enterprise']), version: z.string(), locale: z.string(), prefix: z.string(),