fix(docs): match documentation sources to release versions (#11868)

This commit is contained in:
Taesu
2026-10-16 00:11:46 +00:00
committed by GitHub
parent a1f6f0bf19
commit e5b971444a
13 changed files with 157 additions and 30 deletions
+1
View File
@@ -8,6 +8,7 @@ on:
- completed
branches:
# Match editBranch values with a pinned commitSha in `docs/lib/docs-version-sources.json`.
- next
- "v*.*.x"
permissions:
+3 -1
View File
@@ -10,11 +10,12 @@ import type { PageTreesByVersion } from "./provider";
import { DocsProvider } from "./provider";
import { DocsShell } from "./shell";
const [latestVersion, version16] = docsVersions;
const [betaVersion, latestVersion, version16] = docsVersions;
const pageTreesByVersion = {
latest: getSourceFor(latestVersion.id).getPageTree(),
"1.6": getSourceFor(version16.id).getPageTree(),
beta: getSourceFor(betaVersion.id).getPageTree(),
} satisfies PageTreesByVersion;
function getAvailablePaths(version: DocsVersion) {
@@ -27,6 +28,7 @@ function getAvailablePaths(version: DocsVersion) {
const versionAvailability = {
latest: getAvailablePaths(latestVersion),
"1.6": getAvailablePaths(version16),
beta: getAvailablePaths(betaVersion),
} satisfies VersionAvailability;
const resolvedDocsVersions = loadDocsVersions();
export default function Layout({ children }: { children: ReactNode }) {
+1
View File
@@ -49,6 +49,7 @@ function useDismissablePopover(
function getVersionTitle(version: DocsVersion) {
if (version.id === "latest") return "Latest Version";
if (version.id === "beta") return "Beta Version";
return `Version ${version.id}`;
}
+6 -1
View File
@@ -1,6 +1,6 @@
{
"latest": {
"contentDirectory": "docs",
"contentDirectory": "_generated/docs/latest",
"editBranch": "main",
"commitSha": null
},
@@ -8,5 +8,10 @@
"contentDirectory": "_generated/docs/v1-6",
"editBranch": "v1.6.x",
"commitSha": "c11bfa667f9f260092c3d4c3798fe50d74b79718"
},
"beta": {
"contentDirectory": "_generated/docs/beta",
"editBranch": "next",
"commitSha": "be781d5fd26aa4719f5f4fa3ebd767736d4883b1"
}
}
+58 -1
View File
@@ -6,17 +6,29 @@ import {
getVersionFromPathname,
getVersionTargetHref,
resolveVersionFromSlug,
scopeDocsHref,
versionedDocsHref,
} from "./docs-versions";
const [latestVersion, version16] = docsVersions;
const [betaVersion, latestVersion, version16] = docsVersions;
const availability: VersionAvailability = {
latest: [],
"1.6": ["/docs/introduction", "/docs/plugins/scim"],
beta: ["/docs/introduction", "/docs/plugins/scim"],
};
describe("documentation versions", () => {
it("lists beta before stable while keeping stable as the default", () => {
expect(docsVersions.map((version) => version.id)).toEqual([
"beta",
"latest",
"1.6",
]);
expect(getVersionFromPathname("/docs/introduction").id).toBe("latest");
expect(resolveVersionFromSlug([]).version.id).toBe("latest");
});
it("targets maintenance branches for documentation edits", () => {
expect(docsVersionSources[version16.id].editBranch).toBe("v1.6.x");
});
@@ -28,6 +40,51 @@ describe("documentation versions", () => {
);
});
it("pins beta documentation to next and keeps its release line separate", () => {
expect(docsVersionSources[betaVersion.id].editBranch).toBe("next");
expect(docsVersionSources[betaVersion.id].commitSha).toMatch(
/^[0-9a-f]{40}$/,
);
expect(betaVersion.releaseLine).toBe("1.8");
});
it("resolves beta pages and scopes their links", () => {
expect(resolveVersionFromSlug(["beta", "plugins", "scim"])).toEqual({
version: betaVersion,
relSlug: ["plugins", "scim"],
});
expect(getVersionFromPathname("/docs/beta/plugins/scim")).toBe(betaVersion);
expect(getVersionFromPathname("/docs/beta-tutorial")).toBe(latestVersion);
expect(scopeDocsHref("/docs/plugins/scim", betaVersion)).toBe(
"/docs/beta/plugins/scim",
);
expect(scopeDocsHref("/docs/beta/plugins/scim", betaVersion)).toBe(
"/docs/beta/plugins/scim",
);
expect(scopeDocsHref("/docs/1.6/introduction", betaVersion)).toBe(
"/docs/1.6/introduction",
);
});
it("switches between beta and stable without losing the current page", () => {
expect(
getVersionTargetHref(
"/docs/beta/plugins/scim",
betaVersion,
latestVersion,
{ ...availability, latest: ["/docs/plugins/scim"] },
),
).toBe("/docs/plugins/scim");
expect(
getVersionTargetHref(
"/docs/plugins/scim",
latestVersion,
betaVersion,
availability,
),
).toBe("/docs/beta/plugins/scim");
});
it("resolves archived version slugs", () => {
expect(resolveVersionFromSlug(["1.6", "plugins", "scim"])).toEqual({
version: version16,
+7 -1
View File
@@ -18,6 +18,12 @@ export interface DocsVersionConfig {
}
export const docsVersions = [
{
label: "v1.8 (Beta)",
releaseLine: "1.8",
id: "beta",
badge: null,
},
{
label: "v1.7 (Latest)",
releaseLine: "1.7",
@@ -44,7 +50,7 @@ const docsRootPathPattern = /^\/docs\/?([?#].*)?$/;
/**
* The default (latest) version entry.
*/
export const latestVersion = docsVersions[0];
export const latestVersion = docsVersions[1];
/**
* Find a version config by its URL slug.
+4
View File
@@ -29,6 +29,7 @@ describe("LLM routes", () => {
expect(content).toContain("https://better-auth.com/docs/llms.txt");
expect(content).toContain("https://better-auth.com/docs/1.6/llms.txt");
expect(content).toContain("https://better-auth.com/docs/beta/llms.txt");
expect(content).toContain("https://mcp.better-auth.com/mcp");
expect(content).not.toContain("/llms.txt/docs/");
});
@@ -61,6 +62,9 @@ describe("LLM routes", () => {
"/docs/1.6/plugins/scim.md",
);
expect(getLegacyMarkdownTarget(["1.6"])).toBe("/docs/1.6/llms.txt");
expect(getLegacyMarkdownTarget(["beta", "plugins", "scim"])).toBe(
"/docs/beta/plugins/scim.md",
);
expect(getLegacyMarkdownTarget(["docs", "introduction.txt"])).toBeNull();
});
+1 -1
View File
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import { docsVersions } from "./docs-versions";
import { scopeMarkdownLinks } from "./markdown-links";
const version16 = docsVersions[1];
const version16 = docsVersions[2];
describe("versioned Markdown links", () => {
it("scopes unversioned links without duplicating version prefixes", () => {
+9 -1
View File
@@ -1,7 +1,7 @@
import { loader } from "fumadocs-core/source";
import { lucideIconsPlugin } from "fumadocs-core/source/lucide-icons";
import { toFumadocsSource } from "fumadocs-mdx/runtime/server";
import { blogCollection, docs, docsV16 } from "@/.source/server";
import { blogCollection, docs, docsBeta, docsV16 } from "@/.source/server";
import type { DocsVersionId } from "./docs-versions";
import { pageTreePlugin } from "./page-tree";
@@ -21,9 +21,17 @@ export const sourceV16 = loader({
plugins: docsPlugins,
});
export const sourceBeta = loader({
baseUrl: "/docs/beta",
source: docsBeta.toFumadocsSource(),
pageTree: { noRef: true },
plugins: docsPlugins,
});
const docsSources = {
latest: source,
"1.6": sourceV16,
beta: sourceBeta,
} satisfies Record<DocsVersionId, typeof source>;
export function getSourceFor(versionId: DocsVersionId) {
+40 -22
View File
@@ -24,10 +24,21 @@ const releaseVersionsPath = join(
"docs",
"release-versions.json",
);
const localReleaseVersion = await readPackageVersion(
join(repositoryRoot, packagePath),
);
const localVersion = docsVersions.find((version) =>
localReleaseVersion.startsWith(`${version.releaseLine}.`),
);
if (!localVersion) {
throw new Error(
`No documentation version configured for ${localReleaseVersion}`,
);
}
const versionsToSync = docsVersions.flatMap((version) => {
const source = docsVersionSources[version.id];
if (source.commitSha === null) return [];
return [{ ...version, ...source, commitSha: source.commitSha }];
if (version.id === localVersion.id) return [];
return [{ ...version, ...source }];
});
function git(args: string[], cwd?: string) {
@@ -57,7 +68,7 @@ async function readPackageVersion(path: string) {
}
async function prepareWorktree(version: (typeof versionsToSync)[number]) {
const { commitSha } = version;
const revision = `refs/docs-versions/${version.id.replaceAll(".", "-")}`;
const checkoutDirectory = resolve(
tempRoot,
"worktrees",
@@ -71,7 +82,7 @@ async function prepareWorktree(version: (typeof versionsToSync)[number]) {
"--detach",
"--no-checkout",
checkoutDirectory,
commitSha,
revision,
]);
await git(["-C", checkoutDirectory, "sparse-checkout", "init", "--cone"]);
await git([
@@ -88,19 +99,22 @@ async function prepareWorktree(version: (typeof versionsToSync)[number]) {
"checkout",
"--quiet",
"--detach",
commitSha,
revision,
]);
return { checkoutDirectory, commitSha, version };
return { checkoutDirectory, version };
}
async function syncVersion({
checkoutDirectory,
commitSha,
version,
}: Awaited<ReturnType<typeof prepareWorktree>>) {
const destination = join(docsRoot, "content", version.contentDirectory);
const sourceRevision =
checkoutDirectory === repositoryRoot
? "local"
: (version.commitSha?.slice(0, 9) ?? "HEAD");
console.log(
`[sync-versions] ${version.editBranch}@${commitSha.slice(0, 9)}:${remotePath} → ${destination}`,
`[sync-versions] ${version.editBranch}@${sourceRevision}:${remotePath} → ${destination}`,
);
const sourcePath = join(checkoutDirectory, remotePath);
try {
@@ -112,12 +126,17 @@ async function syncVersion({
);
}
await rm(destination, { recursive: true, force: true });
await cp(sourcePath, destination, { recursive: true });
await writeFile(join(destination, ".gitkeep"), "");
const releaseVersion = await readPackageVersion(
join(checkoutDirectory, packagePath),
);
if (!releaseVersion.startsWith(`${version.releaseLine}.`)) {
throw new Error(
`Invalid release version for ${version.id}: expected ${version.releaseLine}.x, received ${releaseVersion}`,
);
}
await rm(destination, { recursive: true, force: true });
await cp(sourcePath, destination, { recursive: true });
await writeFile(join(destination, ".gitkeep"), "");
return [version.id, releaseVersion] as const;
}
@@ -137,11 +156,16 @@ try {
"origin",
...versionsToSync.map(
(version) =>
`+${version.commitSha}:refs/docs-versions/${version.id.replaceAll(".", "-")}`,
`+${version.commitSha ?? version.editBranch}:refs/docs-versions/${version.id.replaceAll(".", "-")}`,
),
]);
const worktrees = [];
const worktrees = [
{
checkoutDirectory: repositoryRoot,
version: { ...localVersion, ...docsVersionSources[localVersion.id] },
},
];
for (const version of versionsToSync) {
worktrees.push(await prepareWorktree(version));
}
@@ -155,17 +179,11 @@ try {
"One or more documentation versions failed to sync",
);
}
const latestVersion = docsVersions.find((version) => version.id === "latest");
if (!latestVersion) throw new Error("Missing latest documentation version");
const releaseVersions = Object.fromEntries([
[
latestVersion.id,
await readPackageVersion(join(repositoryRoot, packagePath)),
],
...results.flatMap((result) =>
const releaseVersions = Object.fromEntries(
results.flatMap((result) =>
result.status === "fulfilled" ? [result.value] : [],
),
]);
);
await writeFile(
releaseVersionsPath,
`${JSON.stringify(releaseVersions, null, 2)}\n`,
+27 -2
View File
@@ -12,6 +12,12 @@ import {
remarkAutoTypeTable,
} from "fumadocs-typescript";
import * as z from "zod";
import packageMetadata from "../packages/better-auth/package.json" with {
type: "json",
};
import { docsVersionSources } from "./lib/docs-version-sources";
import type { DocsVersion } from "./lib/docs-versions";
import { docsVersions } from "./lib/docs-versions";
const docsPageSchema = pageSchema.extend({
sidebarBadge: z.string().min(1).optional(),
@@ -22,8 +28,16 @@ const processedMarkdownOptions = {
mdxAsPlaceholder: ["APIMethod"],
} satisfies LLMsOptions;
function getContentDirectory(version: DocsVersion) {
return packageMetadata.version.startsWith(`${version.releaseLine}.`)
? "./content/docs"
: `./content/${docsVersionSources[version.id].contentDirectory}`;
}
const [betaVersion, latestVersion, version16] = docsVersions;
export const docs = defineDocs({
dir: "./content/docs",
dir: getContentDirectory(latestVersion),
docs: {
schema: docsPageSchema,
postprocess: {
@@ -34,7 +48,18 @@ export const docs = defineDocs({
});
export const docsV16 = defineDocs({
dir: "./content/_generated/docs/v1-6",
dir: getContentDirectory(version16),
docs: {
schema: docsPageSchema,
postprocess: {
includeProcessedMarkdown: processedMarkdownOptions,
},
async: true,
},
});
export const docsBeta = defineDocs({
dir: getContentDirectory(betaVersion),
docs: {
schema: docsPageSchema,
postprocess: {