diff --git a/.agents/skills/herdr-pre-release-audit/references/pre-release-audit.md b/.agents/skills/herdr-pre-release-audit/references/pre-release-audit.md index 68f7de19..8666f644 100644 --- a/.agents/skills/herdr-pre-release-audit/references/pre-release-audit.md +++ b/.agents/skills/herdr-pre-release-audit/references/pre-release-audit.md @@ -61,7 +61,7 @@ Process: - Preserve the existing changelog style and sections: `Added`, `Changed`, `Fixed`, `Removed`, and `Breaking Changes` when applicable. 7. Audit next-release public docs. - - Treat root `README.md` and the current stable docs source as the latest released public docs. While `docs/versions/manifest.json` has `stable_source: legacy`, that source is `website/src/content/docs/`; afterward it is the current immutable snapshot under `docs/versions//website/src/content/docs/`. + - Treat root `README.md` and the version selected by `docs/versions/manifest.json` under `docs/versions//website/src/content/docs/` as the latest released public docs. Published version docs may contain factual corrections made after the release tag. - Treat `docs/next/README.md` as the next-release root README and `docs/next/website/src/content/docs/` as the complete unpublished website-doc draft. - Treat `docs/preview/website/` as bot-owned output for the active preview release. Never edit it during release review and never use it as the stable release source. - Compare meaningful user-facing changes in the range against next-release docs first. diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 4c1be29c..b437c61f 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -42,7 +42,7 @@ jobs: with: bun-version: 1.3.14 - - name: Validate published snapshots + - name: Validate published documentation run: | node website/scripts/docs-versions.mjs check node website/scripts/docs-preview.mjs check diff --git a/AGENTS.md b/AGENTS.md index 82f89868..edd38bcb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -163,9 +163,9 @@ Unreleased docs live in `docs/next/website/src/content/docs/`. Update those when The active preview release docs live in `docs/preview/website/`. Preview CI owns this mutable snapshot and commits it atomically with `website/preview.json`; never edit it manually. Validate it with `node website/scripts/docs-preview.mjs check`. -Immutable stable release snapshots live in `docs/versions/`. The website build generates `/docs/preview/` from the active preview snapshot and `/docs//` from stable snapshots. The current stable `/docs/` temporarily uses the tracked legacy `website/src/content/docs/` tree; the next stable release switches it to the current immutable snapshot and removes the legacy copy. Do not edit generated preview, version, or snapshot-backed stable files under `website/src/content/docs/`. +Published stable-release documentation lives in `docs/versions/`. Release CI seeds each version from the tagged `docs/next` tree, and maintainers may correct factual documentation errors in a published version afterward. Apply a correction separately to `docs/next` when it also applies to future releases; never replace a published tree with the current draft. The website build generates `/docs/preview/` from the active preview snapshot, `/docs//` from the maintained version directories, and `/docs/` from the version selected by `docs/versions/manifest.json`. Do not edit generated files under `website/src/content/docs/`. -During release review, finalize `docs/next` and run `just release-docs-check`. Do not copy draft docs into preview or stable manually. Preview CI snapshots the selected commit. After a stable GitHub Release succeeds, release CI snapshots the exact tag, updates `latest.json`, and deploys them together. Normal feature/fix work should not edit root `README.md`, root `CHANGELOG.md`, legacy stable website docs, or `website/latest.json` unless explicitly requested. +During release review, finalize `docs/next` and run `just release-docs-check`. Do not copy draft docs into preview or published versions manually. Preview CI snapshots the selected commit. After a stable GitHub Release succeeds, release CI seeds a new version from the exact tag, updates `latest.json`, and deploys them together. Normal feature/fix work should not edit root `README.md`, root `CHANGELOG.md`, published version docs, or `website/latest.json` unless it is a focused correction to already-published documentation or explicitly requested. Put local PRDs, planning notes, and exploratory specs under `.local/prd/`; `.local/` is ignored and locally controlled. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2553c811..90fdb0a5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,9 +65,9 @@ If your proposal changes the visual language, interaction model, workflow, persi ## Documentation for unreleased changes -The root `README.md`, root `CHANGELOG.md`, and public website docs describe released Herdr builds. Do not update root `README.md`, root `CHANGELOG.md`, `docs/preview/`, `docs/versions/`, or `website/src/content/docs/` for normal PRs. +The root `README.md`, root `CHANGELOG.md`, and public website docs describe released Herdr builds. Do not update root `README.md`, root `CHANGELOG.md`, `docs/preview/`, `docs/versions/`, or `website/src/content/docs/` for normal code PRs. A focused correction to already-published documentation may update the affected `docs/versions//` files and should make the same correction under `docs/next` when it remains relevant to future releases. -If your PR changes user-facing behavior, mention the needed public-doc update in the PR. Update `docs/next/README.md` only when the root README needs to change for the next stable release. Update the draft under `docs/next/website/src/content/docs/` when website docs need to change. Draft changes stay unpublished until preview CI snapshots a selected commit or stable release CI snapshots a tag; contributors and maintainers do not copy them into public docs manually. +If your PR changes user-facing behavior, mention the needed public-doc update in the PR. Update `docs/next/README.md` only when the root README needs to change for the next stable release. Update the draft under `docs/next/website/src/content/docs/` when website docs need to change. Draft changes stay unpublished until preview CI snapshots a selected commit or stable release CI seeds a new version from a tag; contributors and maintainers do not copy an entire draft tree into published docs manually. You do not need to edit the changelog for normal PRs. Maintainers prepare `docs/next/CHANGELOG.md` during release review. diff --git a/docs/next/website/src/content/docs/configuration.mdx b/docs/next/website/src/content/docs/configuration.mdx index 92f5efa0..f1831d95 100644 --- a/docs/next/website/src/content/docs/configuration.mdx +++ b/docs/next/website/src/content/docs/configuration.mdx @@ -373,6 +373,8 @@ position = "bottom-right" Choose `herdr` for an in-app toast, `terminal` for an outer-terminal notification that works well over SSH, `system` for the local OS notification service, or `off` to disable popups. Herdr suppresses popups for the active tab. Search `ui.toast` in the [Config reference](/docs/config-reference/) for positions, delay behavior, and clipboard feedback settings. +On macOS, `system` tries `terminal-notifier` first and falls back to `/usr/bin/osascript` when it is unavailable or fails. The fallback appears as Script Editor in Notification Center and cannot activate the hosting terminal. Install `terminal-notifier` with `brew install terminal-notifier`. For a supported, detected terminal, it can activate the terminal app when you click the notification. Alternatively, choose `terminal` to let a supported outer terminal own the notification. + ## Sound Sound notifications play through the local Herdr client. Custom sounds must be mp3 files; relative paths are resolved from the config file's directory. diff --git a/docs/next/website/src/content/docs/ja/configuration.mdx b/docs/next/website/src/content/docs/ja/configuration.mdx index e2b5ea5e..3069ebef 100644 --- a/docs/next/website/src/content/docs/ja/configuration.mdx +++ b/docs/next/website/src/content/docs/ja/configuration.mdx @@ -367,6 +367,8 @@ position = "bottom-right" アプリ内トーストには `herdr`、SSH 越しでも使いやすい外側のターミナル通知には `terminal`、ローカル OS の通知サービスには `system`、ポップアップを無効にするには `off` を選びます。Herdr はアクティブなタブのポップアップを抑制します。位置、遅延の挙動、クリップボードのフィードバック設定については、[設定リファレンス](/docs/config-reference/)で `ui.toast` を検索してください。 +macOS では、`system` は最初に `terminal-notifier` を試し、利用できない場合や失敗した場合は `/usr/bin/osascript` にフォールバックします。このフォールバックは通知センターで Script Editor として表示され、ホストターミナルをアクティブにできません。`brew install terminal-notifier` で `terminal-notifier` をインストールできます。対応するターミナルを Herdr が検出できた場合、通知のクリック時にそのターミナルアプリをアクティブにできます。または、`terminal` を選んで対応する外側のターミナルに通知を送信させてください。 + ## サウンド サウンド通知はローカルの Herdr クライアントで再生されます。カスタムサウンドは mp3 ファイルでなければなりません。相対パスは設定ファイルのディレクトリから解決されます。 diff --git a/docs/next/website/src/content/docs/zh-cn/configuration.mdx b/docs/next/website/src/content/docs/zh-cn/configuration.mdx index 8b8f6c4d..257cb127 100644 --- a/docs/next/website/src/content/docs/zh-cn/configuration.mdx +++ b/docs/next/website/src/content/docs/zh-cn/configuration.mdx @@ -367,6 +367,8 @@ position = "bottom-right" 选择 `herdr` 可使用应用内 toast,选择 `terminal` 可使用适合 SSH 场景的外层终端通知,选择 `system` 可使用本地操作系统通知服务,选择 `off` 可禁用弹出通知。Herdr 会抑制活动标签页的弹出通知。在[配置参考](/docs/config-reference/)中搜索 `ui.toast`,可查看位置、延迟行为和剪贴板反馈设置。 +在 macOS 上,`system` 会先尝试 `terminal-notifier`;如果它不可用或执行失败,则回退到 `/usr/bin/osascript`。该回退方式会在通知中心显示为 Script Editor,并且无法激活承载 Herdr 的终端。可运行 `brew install terminal-notifier` 安装 `terminal-notifier`。如果 Herdr 检测到受支持的终端,点击通知时它可以激活该终端应用。也可以选择 `terminal`,让受支持的外层终端负责发送通知。 + ## 声音 声音通知通过本地 Herdr 客户端播放。自定义声音必须是 mp3 文件;相对路径从配置文件所在目录解析。 diff --git a/docs/versions/0.8.0/website/src/content/docs/configuration.mdx b/docs/versions/0.8.0/website/src/content/docs/configuration.mdx index 92f5efa0..f1831d95 100644 --- a/docs/versions/0.8.0/website/src/content/docs/configuration.mdx +++ b/docs/versions/0.8.0/website/src/content/docs/configuration.mdx @@ -373,6 +373,8 @@ position = "bottom-right" Choose `herdr` for an in-app toast, `terminal` for an outer-terminal notification that works well over SSH, `system` for the local OS notification service, or `off` to disable popups. Herdr suppresses popups for the active tab. Search `ui.toast` in the [Config reference](/docs/config-reference/) for positions, delay behavior, and clipboard feedback settings. +On macOS, `system` tries `terminal-notifier` first and falls back to `/usr/bin/osascript` when it is unavailable or fails. The fallback appears as Script Editor in Notification Center and cannot activate the hosting terminal. Install `terminal-notifier` with `brew install terminal-notifier`. For a supported, detected terminal, it can activate the terminal app when you click the notification. Alternatively, choose `terminal` to let a supported outer terminal own the notification. + ## Sound Sound notifications play through the local Herdr client. Custom sounds must be mp3 files; relative paths are resolved from the config file's directory. diff --git a/docs/versions/0.8.0/website/src/content/docs/ja/configuration.mdx b/docs/versions/0.8.0/website/src/content/docs/ja/configuration.mdx index e2b5ea5e..3069ebef 100644 --- a/docs/versions/0.8.0/website/src/content/docs/ja/configuration.mdx +++ b/docs/versions/0.8.0/website/src/content/docs/ja/configuration.mdx @@ -367,6 +367,8 @@ position = "bottom-right" アプリ内トーストには `herdr`、SSH 越しでも使いやすい外側のターミナル通知には `terminal`、ローカル OS の通知サービスには `system`、ポップアップを無効にするには `off` を選びます。Herdr はアクティブなタブのポップアップを抑制します。位置、遅延の挙動、クリップボードのフィードバック設定については、[設定リファレンス](/docs/config-reference/)で `ui.toast` を検索してください。 +macOS では、`system` は最初に `terminal-notifier` を試し、利用できない場合や失敗した場合は `/usr/bin/osascript` にフォールバックします。このフォールバックは通知センターで Script Editor として表示され、ホストターミナルをアクティブにできません。`brew install terminal-notifier` で `terminal-notifier` をインストールできます。対応するターミナルを Herdr が検出できた場合、通知のクリック時にそのターミナルアプリをアクティブにできます。または、`terminal` を選んで対応する外側のターミナルに通知を送信させてください。 + ## サウンド サウンド通知はローカルの Herdr クライアントで再生されます。カスタムサウンドは mp3 ファイルでなければなりません。相対パスは設定ファイルのディレクトリから解決されます。 diff --git a/docs/versions/0.8.0/website/src/content/docs/zh-cn/configuration.mdx b/docs/versions/0.8.0/website/src/content/docs/zh-cn/configuration.mdx index 8b8f6c4d..257cb127 100644 --- a/docs/versions/0.8.0/website/src/content/docs/zh-cn/configuration.mdx +++ b/docs/versions/0.8.0/website/src/content/docs/zh-cn/configuration.mdx @@ -367,6 +367,8 @@ position = "bottom-right" 选择 `herdr` 可使用应用内 toast,选择 `terminal` 可使用适合 SSH 场景的外层终端通知,选择 `system` 可使用本地操作系统通知服务,选择 `off` 可禁用弹出通知。Herdr 会抑制活动标签页的弹出通知。在[配置参考](/docs/config-reference/)中搜索 `ui.toast`,可查看位置、延迟行为和剪贴板反馈设置。 +在 macOS 上,`system` 会先尝试 `terminal-notifier`;如果它不可用或执行失败,则回退到 `/usr/bin/osascript`。该回退方式会在通知中心显示为 Script Editor,并且无法激活承载 Herdr 的终端。可运行 `brew install terminal-notifier` 安装 `terminal-notifier`。如果 Herdr 检测到受支持的终端,点击通知时它可以激活该终端应用。也可以选择 `terminal`,让受支持的外层终端负责发送通知。 + ## 声音 声音通知通过本地 Herdr 客户端播放。自定义声音必须是 mp3 文件;相对路径从配置文件所在目录解析。 diff --git a/docs/versions/README.md b/docs/versions/README.md index dbfd01f6..42ca1c69 100644 --- a/docs/versions/README.md +++ b/docs/versions/README.md @@ -1,13 +1,18 @@ # Versioned documentation -This directory contains immutable documentation snapshots for stable Herdr releases. +This directory contains the published documentation for stable Herdr releases. -Do not edit snapshot files manually. They must match the release tag recorded in `manifest.json`. Validate them with: +Release CI creates each version from the tagged `docs/next` tree after the GitHub Release succeeds. Maintainers can correct published documentation in its version directory afterward. When a correction also applies to future releases, make the same focused change under `docs/next`; do not replace a published tree with the current draft. + +Validate the manifest and build every published version with: ```bash node website/scripts/docs-versions.mjs check +cd website && bun run build ``` -Release CI creates a new snapshot from the tagged `docs/next` tree after the GitHub Release succeeds. `website/scripts/prepare-docs.mjs` renders these snapshots at `/docs//` and uses the current snapshot for `/docs/` after the legacy stable-doc migration completes. `/docs/preview/` comes only from the active preview release snapshot in `docs/preview/`, never directly from `docs/next/`. +`website/scripts/prepare-docs.mjs` renders each maintained version at `/docs//` and uses the version selected by `manifest.json` for `/docs/`. Generated files under `website/src/content/docs/` are not editable sources. `/docs/preview/` comes only from the active preview release snapshot in `docs/preview/`, never directly from `docs/next/`. + +The `tag`, `commit`, and `source` fields in `manifest.json` record where release CI initially published a version. Git history records later documentation corrections. The historical backfill starts at v0.5.11, the first release that included the Astro/Starlight documentation site. diff --git a/scripts/test_preview.py b/scripts/test_preview.py index c280f572..137a13f0 100644 --- a/scripts/test_preview.py +++ b/scripts/test_preview.py @@ -211,7 +211,7 @@ file: ../../../public/assets/logo.svg self.assertIn("[Install](/docs/0.7.4/install/)", output) self.assertIn("file: ../../../../../public/assets/logo.svg", output) self.assertIn("from '../../../../components/ConfigReference.astro'", output) - self.assertIn("blob/v0.7.4/docs/next/website/src/content/docs/index.mdx", output) + self.assertIn("blob/master/docs/versions/0.7.4/website/src/content/docs/index.mdx", output) self.assertIn("blob/v0.7.4/SKILL.md", output) diff --git a/website/README.md b/website/README.md index a511a861..ceb872af 100644 --- a/website/README.md +++ b/website/README.md @@ -15,9 +15,9 @@ Documentation has three lifecycle states: - `../docs/next/website/` is the committed, author-edited draft. Production builds never read it. - `../docs/preview/website/` is the latest preview release snapshot, rendered at `/docs/preview/`. -- `../docs/versions//website/` contains immutable stable release snapshots, rendered at `/docs//`. +- `../docs/versions//website/` contains maintained stable-release documentation, rendered at `/docs//`. Release CI seeds a new version from its tag; later factual corrections are made directly in that version directory and mirrored to `docs/next` when they also apply to future releases. -The current stable site temporarily remains tracked under `src/content/docs/` because it contains post-v0.7.5 documentation corrections. The next stable release switches `docs/versions/manifest.json` to snapshot-backed stable docs and removes that legacy copy. From then on, `src/content/docs/` is entirely generated and ignored. +The version selected by `docs/versions/manifest.json` is also rendered at `/docs/`. `src/content/docs/` is entirely generated and ignored. Preview CI snapshots the selected commit and updates `preview.json` in one commit: @@ -26,7 +26,7 @@ node website/scripts/docs-preview.mjs snapshot node website/scripts/docs-preview.mjs check ``` -Stable release CI snapshots the exact tag after the GitHub Release succeeds: +Stable release CI seeds a new maintained version from the exact tag after the GitHub Release succeeds: ```bash node website/scripts/docs-versions.mjs publish diff --git a/website/scripts/check-built-docs.mjs b/website/scripts/check-built-docs.mjs index 8841bf74..66e904e4 100644 --- a/website/scripts/check-built-docs.mjs +++ b/website/scripts/check-built-docs.mjs @@ -48,7 +48,7 @@ assertIncludes(preview, 'name="robots" content="noindex, nofollow"'); assertIncludes(archived, `data-pagefind-filter="version[content]" content="${versions.current}"`); assertIncludes(archived, 'name="robots" content="noindex, nofollow"'); if (archived.includes(`This page documents Herdr ${versions.current}`)) { - throw new Error('the current immutable snapshot must not be labeled as outdated'); + throw new Error('the current documentation version must not be labeled as outdated'); } const versionSelect = stable.match(/]*aria-label="Documentation version"[^>]*>([\s\S]*?)<\/select>/)?.[1]; if (!versionSelect) throw new Error('stable docs are missing the version selector'); @@ -68,7 +68,7 @@ if (previous) { const sitemap = await readFile(resolve(distDir, 'sitemap-0.xml'), 'utf8'); assertIncludes(sitemap, 'https://herdr.dev/docs/'); if (nonCanonicalDocsUrl.test(sitemap)) { - throw new Error('preview or immutable documentation URLs must not appear in the sitemap'); + throw new Error('preview or versioned documentation URLs must not appear in the sitemap'); } const build = await inspectFiles(distDir); diff --git a/website/scripts/docs-snapshot.mjs b/website/scripts/docs-snapshot.mjs index b85557a1..e9eaf92c 100644 --- a/website/scripts/docs-snapshot.mjs +++ b/website/scripts/docs-snapshot.mjs @@ -73,7 +73,7 @@ export async function compareGitTree(git, ref, sourceRoot, snapshotRoot) { assertRelativePath(relativePath, entry.path); expected.set(relativePath, entry); } - const actualPaths = await listSnapshotPaths(snapshotRoot); + const actualPaths = await listDocumentationPaths(snapshotRoot); const expectedPaths = [...expected.keys()].sort(); if (JSON.stringify(actualPaths) !== JSON.stringify(expectedPaths)) { throw new Error(`${snapshotRoot} file list differs from ${ref}:${sourceRoot}`); @@ -89,7 +89,7 @@ export async function compareGitTree(git, ref, sourceRoot, snapshotRoot) { } } -async function listSnapshotPaths(root) { +export async function listDocumentationPaths(root) { const paths = []; async function walk(directory) { for (const entry of await readdir(directory, { withFileTypes: true })) { diff --git a/website/scripts/docs-versions.integration.test.ts b/website/scripts/docs-versions.integration.test.ts index 5e2d36a5..052d9ca2 100644 --- a/website/scripts/docs-versions.integration.test.ts +++ b/website/scripts/docs-versions.integration.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, test } from 'bun:test'; import { execFileSync } from 'node:child_process'; -import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises'; import { join, resolve } from 'node:path'; import { tmpdir } from 'node:os'; @@ -21,7 +21,8 @@ describe('documentation release publishing', () => { await write(root, 'website/latest.json', '{"version":"0.9.0"}\n'); await write(root, 'README.md', 'stable readme\n'); await write(root, 'README.zh-CN.md', 'stable readme zh-cn\n'); - await write(root, 'docs/next/website/src/content/docs/index.mdx', 'next docs\n'); + const nextDocs = '---\ntitle: Documentation\n---\n\nnext docs\n'; + await write(root, 'docs/next/website/src/content/docs/index.mdx', nextDocs); await write(root, 'docs/next/website/src/data/config-reference.json', '{"next":true}\n'); await write(root, 'docs/next/README.md', 'next readme\n'); await write(root, 'docs/next/README.zh-CN.md', 'next readme zh-cn\n'); @@ -40,7 +41,7 @@ describe('documentation release publishing', () => { await expect(read(root, 'website/src/data/config-reference.json')).rejects.toThrow(); expect(await read(root, 'README.md')).toBe('next readme\n'); expect(await read(root, 'README.zh-CN.md')).toBe('next readme zh-cn\n'); - expect(await read(root, 'docs/versions/1.0.0/website/src/content/docs/index.mdx')).toBe('next docs\n'); + expect(await read(root, 'docs/versions/1.0.0/website/src/content/docs/index.mdx')).toBe(nextDocs); const manifest = JSON.parse(await read(root, 'docs/versions/manifest.json')); expect(manifest.current).toBe('1.0.0'); @@ -65,7 +66,19 @@ describe('documentation release publishing', () => { `${JSON.stringify({ build_id: 'preview-test', commit: previewCommit })}\n`, ); runPrepare(root); - expect(await read(root, 'website/src/content/docs/index.mdx')).toBe('next docs\n'); + const stableDocs = await read(root, 'website/src/content/docs/index.mdx'); + expect(stableDocs).toContain('next docs'); + expect(stableDocs).toContain( + 'editUrl: https://github.com/herdrdev/herdr/blob/master/docs/versions/1.0.0/website/src/content/docs/index.mdx', + ); + const versionedDocs = await read( + root, + 'website/src/content/docs/_versions/1.0.0/index.mdx', + ); + expect(versionedDocs).toContain('next docs'); + expect(versionedDocs).toContain( + 'editUrl: https://github.com/herdrdev/herdr/blob/master/docs/versions/1.0.0/website/src/content/docs/index.mdx', + ); expect(await read(root, 'website/src/content/docs/preview/index.mdx')).toContain( 'Preview build `preview-test`', ); @@ -74,10 +87,36 @@ describe('documentation release publishing', () => { await write(root, 'website/latest.json', '{"version":"1.0.0"}\n'); runScript(root, ['check']); + const correctedDocs = '---\ntitle: Documentation\n---\n\ncorrected docs\n'; + await write( + root, + 'docs/versions/1.0.0/website/src/content/docs/index.mdx', + correctedDocs, + ); + await write( + root, + 'docs/versions/1.0.0/website/src/data/config-reference.json', + '{"corrected":true}\n', + ); + runScript(root, ['check']); + await write(root, 'README.md', 'post-release correction\n'); runScript(root, ['publish', 'v1.0.0']); expect(await read(root, 'README.md')).toBe('post-release correction\n'); + runPrepare(root); + expect(await read(root, 'website/src/content/docs/index.mdx')).toContain('corrected docs'); + expect(await read(root, 'website/src/content/docs/_versions/1.0.0/index.mdx')).toContain( + 'corrected docs', + ); + expect(await read(root, 'website/src/data/config-reference.json')).toBe( + '{"corrected":true}\n', + ); + const versionReferences = JSON.parse( + await read(root, 'website/src/data/config-reference-versions.json'), + ); + expect(versionReferences['1.0.0']).toEqual({ corrected: true }); + runScript(root, ['publish', 'v0.9.0']); const archivedManifest = JSON.parse(await read(root, 'docs/versions/manifest.json')); expect(archivedManifest.current).toBe('1.0.0'); @@ -85,6 +124,34 @@ describe('documentation release publishing', () => { expect(await read(root, 'README.md')).toBe('post-release correction\n'); runScript(root, ['check']); + const correctedArchivedDocs = '---\ntitle: Documentation\n---\n\ncorrected archived docs\n'; + await write( + root, + 'docs/versions/0.9.0/website/src/content/docs/index.mdx', + correctedArchivedDocs, + ); + runScript(root, ['publish', 'v0.9.0']); + expect(await read(root, 'docs/versions/0.9.0/website/src/content/docs/index.mdx')).toBe( + correctedArchivedDocs, + ); + + const archivedDocsRoot = resolve( + root, + 'docs/versions/0.9.0/website/src/content/docs', + ); + await rm(archivedDocsRoot, { recursive: true, force: true }); + expect(() => runScript(root, ['check'])).toThrow(); + await write( + root, + 'docs/versions/0.9.0/website/src/content/docs/index.mdx', + correctedArchivedDocs, + ); + const linkedPage = resolve(archivedDocsRoot, 'linked.mdx'); + await symlink('index.mdx', linkedPage); + expect(() => runScript(root, ['check'])).toThrow(); + await rm(linkedPage); + runScript(root, ['check']); + delete archivedManifest.versions[0].commit; await write(root, 'docs/versions/manifest.json', `${JSON.stringify(archivedManifest)}\n`); expect(() => runScript(root, ['check'])).toThrow(); diff --git a/website/scripts/docs-versions.mjs b/website/scripts/docs-versions.mjs index 30cb099d..0923d683 100644 --- a/website/scripts/docs-versions.mjs +++ b/website/scripts/docs-versions.mjs @@ -2,11 +2,11 @@ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { - compareGitTree, createGit, extractGitTree, gitPathExists, gitTreesEqual, + listDocumentationPaths, resolveCommit, } from './docs-snapshot.mjs'; @@ -167,38 +167,26 @@ export async function checkVersions() { if (!['website/src/content/docs', 'docs/next/website/src/content/docs'].includes(entry.source)) { throw new Error(`docs version ${entry.version} has unsupported source ${entry.source}`); } - const sourceRoot = entry.source; - const snapshotRoot = resolve(versionsDir, entry.version, 'website/src/content/docs'); - await compareGitTree(git, entry.tag, sourceRoot, snapshotRoot); - const referenceSource = sourceRoot.includes('docs/next/') - ? 'docs/next/website/src/data/config-reference.json' - : 'website/src/data/config-reference.json'; - const referenceSnapshot = resolve( - versionsDir, - entry.version, - 'website/src/data/config-reference.json', + const versionRoot = resolve(versionsDir, entry.version, 'website'); + const documentationPaths = await listDocumentationPaths( + resolve(versionRoot, 'src/content/docs'), ); - if (gitPathExists(git, entry.tag, referenceSource)) { - const actualReference = await readFile(referenceSnapshot); - const taggedReference = git(['show', `${entry.tag}:${referenceSource}`], { binary: true }); - if (!actualReference.equals(taggedReference)) { - throw new Error(`${entry.version} config reference differs from ${entry.tag}:${referenceSource}`); - } - } else { - try { - await readFile(referenceSnapshot); - throw new Error(`${entry.version} has an unexpected config reference snapshot`); - } catch (error) { - if (error.code !== 'ENOENT') throw error; - } + if (!documentationPaths.includes('index.md') && !documentationPaths.includes('index.mdx')) { + throw new Error(`docs version ${entry.version} has no root index`); + } + + try { + JSON.parse(await readFile(resolve(versionRoot, 'src/data/config-reference.json'), 'utf8')); + } catch (error) { + if (error.code !== 'ENOENT') throw error; } } if (!seen.has(manifest.current)) { throw new Error(`current docs version ${manifest.current} has no snapshot`); } - process.stdout.write(`validated ${manifest.versions.length} documentation snapshots\n`); + process.stdout.write(`validated ${manifest.versions.length} documentation versions\n`); } export async function publishVersion(tag) { @@ -217,8 +205,7 @@ export async function publishVersion(tag) { if (!existing?.commit || existing.commit !== release.commit) { throw new Error(`published documentation ${release.version} has mismatched commit provenance`); } - await compareGitTree(git, release.commit, existing.source, resolve(versionsDir, release.version, 'website/src/content/docs')); - process.stdout.write(`documentation snapshot ${release.tag} is already published\n`); + process.stdout.write(`documentation ${release.tag} is already published\n`); return; } if (manifest.current && compareVersions(release.version, manifest.current) < 0) { @@ -229,13 +216,7 @@ export async function publishVersion(tag) { if (existing.commit && existing.commit !== release.commit) { throw new Error(`version ${release.version} has mismatched commit provenance`); } - await compareGitTree( - git, - release.commit, - existing.source, - resolve(versionsDir, release.version, 'website/src/content/docs'), - ); - process.stdout.write(`documentation snapshot ${release.tag} is already archived\n`); + process.stdout.write(`documentation ${release.tag} is already archived\n`); return; } const archived = await snapshotTag(tag, 'docs/next/website/src/content/docs'); @@ -251,7 +232,7 @@ export async function publishVersion(tag) { return; } if (existing) { - throw new Error(`version ${release.version} already has a documentation snapshot`); + throw new Error(`version ${release.version} already has published documentation`); } const metadata = await snapshotTag(tag, 'docs/next/website/src/content/docs'); diff --git a/website/scripts/prepare-docs.mjs b/website/scripts/prepare-docs.mjs index a96e2c41..074ef36d 100644 --- a/website/scripts/prepare-docs.mjs +++ b/website/scripts/prepare-docs.mjs @@ -44,7 +44,6 @@ if (process.argv[2] === '--rewrite-preview-doc-fixture') { rewriteVersionDocContent(Buffer.concat(chunks).toString('utf8'), { version: process.argv[3] ?? '0.7.5', tag: `v${process.argv[3] ?? '0.7.5'}`, - sourceRoot: 'docs/next/website/src/content/docs', relativePath: 'index.mdx', }), ); @@ -118,8 +117,8 @@ async function prepareDocs({ draft }) { stableDocsDir, (content, relativePath) => rewriteStableDocContent(content, { + version: currentEntry.version, tag: currentEntry.tag, - sourceRoot: currentEntry.source, relativePath, }), ); @@ -162,7 +161,6 @@ async function prepareDocs({ draft }) { rewriteVersionDocContent(content, { version, tag: entry.tag, - sourceRoot: entry.source, relativePath, }), ); @@ -269,24 +267,22 @@ export function rewritePreviewDocContent( return insertPreviewNotice(withSourceLink, relativePath, { buildId, commit }); } -export function rewriteStableDocContent(content, { tag, sourceRoot, relativePath }) { +export function rewriteStableDocContent(content, { version, tag, relativePath }) { const taggedContent = rewriteRepositoryLinks(content, tag); - return setGeneratedEditUrl( - taggedContent, - `https://github.com/herdrdev/herdr/blob/${tag}/${sourceRoot}/${relativePath}`, - ); + return setGeneratedEditUrl(taggedContent, versionedDocSourceUrl(version, relativePath)); } -export function rewriteVersionDocContent(content, { version, tag, sourceRoot, relativePath }) { +export function rewriteVersionDocContent(content, { version, tag, relativePath }) { const taggedContent = rewriteRepositoryLinks( content.replaceAll('/docs/', `/docs/${version}/`), tag, ); const rewritten = rewriteRelativeDocPaths(taggedContent, 2); - return setGeneratedEditUrl( - rewritten, - `https://github.com/herdrdev/herdr/blob/${tag}/${sourceRoot}/${relativePath}`, - ); + return setGeneratedEditUrl(rewritten, versionedDocSourceUrl(version, relativePath)); +} + +function versionedDocSourceUrl(version, relativePath) { + return `https://github.com/herdrdev/herdr/blob/master/docs/versions/${version}/website/src/content/docs/${relativePath}`; } function rewriteRepositoryLinks(content, ref) {