mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-25 12:55:18 -07:00
[codex] add versioned Pages deployment (#73)
This commit is contained in:
@@ -0,0 +1,76 @@
|
|||||||
|
name: Docs Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
paths:
|
||||||
|
- 'docs-site/**'
|
||||||
|
- 'scripts/docs-versioning.ts'
|
||||||
|
- 'scripts/build-versioned-docs.ts'
|
||||||
|
- '.github/workflows/docs-pages.yml'
|
||||||
|
- 'package.json'
|
||||||
|
- 'bun.lock'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: docs-pages-production
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
if: ${{ github.ref_type != 'tag' || !contains(github.ref_name, '-') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Guard stable docs tag shape
|
||||||
|
id: tag_guard
|
||||||
|
if: github.ref_type == 'tag'
|
||||||
|
run: |
|
||||||
|
if [[ ! "${{ github.ref_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
echo "::notice::Skipping non-stable docs tag ${{ github.ref_name }}"
|
||||||
|
echo "stable_tag=false" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "stable_tag=true" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
if: steps.tag_guard.outputs.stable_tag != 'false'
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.3.5
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: steps.tag_guard.outputs.stable_tag != 'false'
|
||||||
|
run: |
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
cd docs-site && bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Cache versioned docs archives
|
||||||
|
if: steps.tag_guard.outputs.stable_tag != 'false'
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: .tmp/docs-versioned-archive-cache
|
||||||
|
key: docs-versioned-archives-${{ runner.os }}-${{ hashFiles('docs-site/.vitepress/**', 'docs-site/public/assets/fonts/**', 'docs-site/package.json', 'docs-site/bun.lock', 'scripts/build-versioned-docs.ts', 'scripts/docs-versioning.ts') }}
|
||||||
|
|
||||||
|
- name: Test docs
|
||||||
|
if: steps.tag_guard.outputs.stable_tag != 'false'
|
||||||
|
run: bun run docs:test
|
||||||
|
|
||||||
|
- name: Build versioned docs
|
||||||
|
if: steps.tag_guard.outputs.stable_tag != 'false'
|
||||||
|
run: bun run docs:build:versioned
|
||||||
|
|
||||||
|
- name: Deploy docs to Cloudflare Pages
|
||||||
|
if: steps.tag_guard.outputs.stable_tag != 'false'
|
||||||
|
uses: cloudflare/wrangler-action@v3
|
||||||
|
with:
|
||||||
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
command: pages deploy .tmp/docs-versioned-site --project-name "${{ vars.CLOUDFLARE_PAGES_PROJECT_NAME }}" --branch main
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: docs
|
||||||
|
area: docs
|
||||||
|
|
||||||
|
- Published stable docs at the site root with current development docs under `/main/`.
|
||||||
+210
-66
@@ -1,3 +1,7 @@
|
|||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import type { DefaultTheme, HeadConfig, TransformContext, UserConfig } from 'vitepress';
|
||||||
|
|
||||||
const DOCS_HOSTNAME = 'https://docs.subminer.moe';
|
const DOCS_HOSTNAME = 'https://docs.subminer.moe';
|
||||||
const PLAUSIBLE_PROXY_HOSTNAME = 'https://worker.sudacode.com';
|
const PLAUSIBLE_PROXY_HOSTNAME = 'https://worker.sudacode.com';
|
||||||
const PLAUSIBLE_SITE_SCRIPT_PATH = '/js/pa-h28Pn9ppgTJRmiSJlyPT6.js';
|
const PLAUSIBLE_SITE_SCRIPT_PATH = '/js/pa-h28Pn9ppgTJRmiSJlyPT6.js';
|
||||||
@@ -7,20 +11,212 @@ const PLAUSIBLE_INIT_SCRIPT = [
|
|||||||
`plausible.init({ endpoint: '${PLAUSIBLE_ENDPOINT}' });`,
|
`plausible.init({ endpoint: '${PLAUSIBLE_ENDPOINT}' });`,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
function pageToCanonicalHref(page: string): string | null {
|
type DocsChannel = 'stable-root' | 'stable-archive' | 'main';
|
||||||
|
|
||||||
|
type VersionManifest = {
|
||||||
|
latestStable: string;
|
||||||
|
channels: Array<{ label: string; path: string }>;
|
||||||
|
versions: Array<{ version: string; path: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const base = normalizeBase(process.env.SUBMINER_DOCS_BASE ?? '/');
|
||||||
|
const outDir = process.env.SUBMINER_DOCS_OUT_DIR;
|
||||||
|
const channel = normalizeChannel(process.env.SUBMINER_DOCS_CHANNEL);
|
||||||
|
const docsVersion = process.env.SUBMINER_DOCS_VERSION;
|
||||||
|
const latestStable = process.env.SUBMINER_DOCS_LATEST_STABLE ?? 'v0.14.0';
|
||||||
|
const versionManifest = parseVersionManifest(process.env.SUBMINER_DOCS_VERSION_MANIFEST);
|
||||||
|
|
||||||
|
function normalizeBase(value: string): string {
|
||||||
|
if (!value || value === '/') return '/';
|
||||||
|
return `/${value.replace(/^\/+|\/+$/g, '')}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeChannel(value: string | undefined): DocsChannel {
|
||||||
|
if (value === 'main' || value === 'stable-archive') return value;
|
||||||
|
return 'stable-root';
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseVersionManifest(value: string | undefined): VersionManifest {
|
||||||
|
if (!value) {
|
||||||
|
return {
|
||||||
|
latestStable,
|
||||||
|
channels: [
|
||||||
|
{ label: 'Latest stable', path: '/' },
|
||||||
|
{ label: 'main', path: '/main/' },
|
||||||
|
],
|
||||||
|
versions: [{ version: latestStable, path: `/v/${latestStable.replace(/^v/, '')}/` }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(value) as VersionManifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
function withDocsBase(path: string): string {
|
||||||
|
if (/^[a-z]+:\/\//i.test(path)) return path;
|
||||||
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
||||||
|
if (base === '/') return normalizedPath;
|
||||||
|
return `${base.replace(/\/$/, '')}${normalizedPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageToRoute(page: string): string | null {
|
||||||
if (page === '404.md') return null;
|
if (page === '404.md') return null;
|
||||||
|
|
||||||
const route = page
|
const route = page
|
||||||
.replace(/(^|\/)index\.md$/, '')
|
.replace(/(^|\/)index\.md$/, '')
|
||||||
.replace(/\.md$/, '')
|
.replace(/\.md$/, '')
|
||||||
.replace(/\/$/, '');
|
.replace(/\/$/, '');
|
||||||
return route ? `${DOCS_HOSTNAME}/${route}` : `${DOCS_HOSTNAME}/`;
|
return route ? `/${route}` : '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
function pageToCanonicalHref(page: string): string | null {
|
||||||
|
const route = pageToRoute(page);
|
||||||
|
if (!route) return null;
|
||||||
|
|
||||||
|
if (channel === 'main') {
|
||||||
|
return `${DOCS_HOSTNAME}${canonicalRouteWithBase(route)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel === 'stable-archive' && docsVersion !== latestStable) {
|
||||||
|
return `${DOCS_HOSTNAME}${canonicalRouteWithBase(route)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return route === '/' ? `${DOCS_HOSTNAME}/` : `${DOCS_HOSTNAME}${route}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canonicalRouteWithBase(route: string): string {
|
||||||
|
const routeWithBase = withDocsBase(route);
|
||||||
|
return route === '/' ? routeWithBase : routeWithBase.replace(/\/$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformPageHead({ page }: TransformContext): HeadConfig[] {
|
||||||
|
const href = pageToCanonicalHref(page);
|
||||||
|
const head: HeadConfig[] = href ? [['link', { rel: 'canonical', href }]] : [];
|
||||||
|
|
||||||
|
if (channel === 'main') {
|
||||||
|
head.push(['meta', { name: 'robots', content: 'noindex,follow' }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return head;
|
||||||
|
}
|
||||||
|
|
||||||
|
function linkToPagePath(link: string): string | null {
|
||||||
|
if (!link.startsWith('/') || link.startsWith('/v/') || link.startsWith('/main/')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const withoutHash = link.split('#')[0] ?? '/';
|
||||||
|
const withoutQuery = withoutHash.split('?')[0] ?? '/';
|
||||||
|
const route = withoutQuery.replace(/^\/+|\/+$/g, '');
|
||||||
|
return route ? `${route}.md` : 'index.md';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPageForLink(link: string): boolean {
|
||||||
|
const pagePath = linkToPagePath(link);
|
||||||
|
if (!pagePath) return true;
|
||||||
|
return existsSync(join(process.cwd(), pagePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterNav(items: DefaultTheme.NavItem[]): DefaultTheme.NavItem[] {
|
||||||
|
return items
|
||||||
|
.map((item) => {
|
||||||
|
if ('items' in item && item.items) {
|
||||||
|
return { ...item, items: filterNav(item.items as DefaultTheme.NavItem[]) };
|
||||||
|
}
|
||||||
|
if ('link' in item && item.link && !hasPageForLink(item.link)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
.filter((item): item is DefaultTheme.NavItem => Boolean(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterSidebar(items: DefaultTheme.SidebarItem[]): DefaultTheme.SidebarItem[] {
|
||||||
|
return items
|
||||||
|
.map((item) => {
|
||||||
|
const filteredChildren = item.items ? filterSidebar(item.items) : undefined;
|
||||||
|
if (item.link && !hasPageForLink(item.link)) return null;
|
||||||
|
if (item.items && filteredChildren?.length === 0 && !item.link) return null;
|
||||||
|
return { ...item, items: filteredChildren };
|
||||||
|
})
|
||||||
|
.filter((item): item is DefaultTheme.SidebarItem => Boolean(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionItems = [
|
||||||
|
{
|
||||||
|
text: `Latest stable (${versionManifest.latestStable})`,
|
||||||
|
link: '/',
|
||||||
|
},
|
||||||
|
...versionManifest.channels
|
||||||
|
.filter((entry) => entry.label !== 'Latest stable')
|
||||||
|
.map((entry) => ({ text: entry.label, link: entry.path })),
|
||||||
|
...versionManifest.versions.map((entry) => ({
|
||||||
|
text: entry.version,
|
||||||
|
link: entry.path,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
const nav: DefaultTheme.NavItem[] = [
|
||||||
|
{ text: 'Home', link: '/' },
|
||||||
|
{ text: 'Get Started', link: '/installation' },
|
||||||
|
{ text: 'Mining', link: '/mining-workflow' },
|
||||||
|
{ text: 'Configuration', link: '/configuration' },
|
||||||
|
{ text: 'Changelog', link: '/changelog' },
|
||||||
|
{ text: 'Troubleshooting', link: '/troubleshooting' },
|
||||||
|
{ text: docsVersion ?? (channel === 'main' ? 'main' : latestStable), items: versionItems },
|
||||||
|
];
|
||||||
|
|
||||||
|
const sidebar: DefaultTheme.SidebarItem[] = [
|
||||||
|
{
|
||||||
|
text: 'Getting Started',
|
||||||
|
items: [
|
||||||
|
{ text: 'Overview', link: '/' },
|
||||||
|
{ text: 'Installation', link: '/installation' },
|
||||||
|
{ text: 'Usage', link: '/usage' },
|
||||||
|
{ text: 'Mining Workflow', link: '/mining-workflow' },
|
||||||
|
{ text: 'Launcher Script', link: '/launcher-script' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Reference',
|
||||||
|
items: [
|
||||||
|
{ text: 'Configuration', link: '/configuration' },
|
||||||
|
{ text: 'Keyboard Shortcuts', link: '/shortcuts' },
|
||||||
|
{ text: 'Subtitle Annotations', link: '/subtitle-annotations' },
|
||||||
|
{ text: 'Subtitle Sidebar', link: '/subtitle-sidebar' },
|
||||||
|
{ text: 'Immersion Tracking', link: '/immersion-tracking' },
|
||||||
|
{ text: 'Troubleshooting', link: '/troubleshooting' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Integrations',
|
||||||
|
items: [
|
||||||
|
{ text: 'MPV Plugin', link: '/mpv-plugin' },
|
||||||
|
{ text: 'Anki', link: '/anki-integration' },
|
||||||
|
{ text: 'Jellyfin', link: '/jellyfin-integration' },
|
||||||
|
{ text: 'YouTube', link: '/youtube-integration' },
|
||||||
|
{ text: 'Jimaku', link: '/jimaku-integration' },
|
||||||
|
{ text: 'AniList', link: '/anilist-integration' },
|
||||||
|
{ text: 'Character Dictionary', link: '/character-dictionary' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Development',
|
||||||
|
items: [
|
||||||
|
{ text: 'Building & Testing', link: '/development' },
|
||||||
|
{ text: 'Architecture', link: '/architecture' },
|
||||||
|
{ text: 'IPC + Runtime Contracts', link: '/ipc-contracts' },
|
||||||
|
{ text: 'WebSocket + Texthooker API', link: '/websocket-texthooker-api' },
|
||||||
|
{ text: 'Changelog', link: '/changelog' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const config: UserConfig = {
|
||||||
title: 'SubMiner Docs',
|
title: 'SubMiner Docs',
|
||||||
description:
|
description:
|
||||||
'SubMiner: an MPV immersion-mining overlay with Yomitan and AnkiConnect integration.',
|
'SubMiner: an MPV immersion-mining overlay with Yomitan and AnkiConnect integration.',
|
||||||
|
base,
|
||||||
|
...(outDir ? { outDir } : {}),
|
||||||
head: [
|
head: [
|
||||||
['link', { rel: 'preconnect', href: PLAUSIBLE_PROXY_HOSTNAME }],
|
['link', { rel: 'preconnect', href: PLAUSIBLE_PROXY_HOSTNAME }],
|
||||||
[
|
[
|
||||||
@@ -31,13 +227,13 @@ export default {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
['script', {}, PLAUSIBLE_INIT_SCRIPT],
|
['script', {}, PLAUSIBLE_INIT_SCRIPT],
|
||||||
['link', { rel: 'icon', href: '/favicon.ico', sizes: 'any' }],
|
['link', { rel: 'icon', href: withDocsBase('/favicon.ico'), sizes: 'any' }],
|
||||||
[
|
[
|
||||||
'link',
|
'link',
|
||||||
{
|
{
|
||||||
rel: 'icon',
|
rel: 'icon',
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
href: '/favicon-32x32.png',
|
href: withDocsBase('/favicon-32x32.png'),
|
||||||
sizes: '32x32',
|
sizes: '32x32',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -46,7 +242,7 @@ export default {
|
|||||||
{
|
{
|
||||||
rel: 'icon',
|
rel: 'icon',
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
href: '/favicon-16x16.png',
|
href: withDocsBase('/favicon-16x16.png'),
|
||||||
sizes: '16x16',
|
sizes: '16x16',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -54,7 +250,7 @@ export default {
|
|||||||
'link',
|
'link',
|
||||||
{
|
{
|
||||||
rel: 'apple-touch-icon',
|
rel: 'apple-touch-icon',
|
||||||
href: '/apple-touch-icon.png',
|
href: withDocsBase('/apple-touch-icon.png'),
|
||||||
sizes: '180x180',
|
sizes: '180x180',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -70,10 +266,7 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
transformHead({ page }) {
|
transformHead: transformPageHead,
|
||||||
const href = pageToCanonicalHref(page);
|
|
||||||
return href ? [['link', { rel: 'canonical', href }]] : [];
|
|
||||||
},
|
|
||||||
lastUpdated: true,
|
lastUpdated: true,
|
||||||
srcExclude: ['subagents/**'],
|
srcExclude: ['subagents/**'],
|
||||||
markdown: {
|
markdown: {
|
||||||
@@ -84,63 +277,12 @@ export default {
|
|||||||
},
|
},
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
logo: {
|
logo: {
|
||||||
light: '/assets/SubMiner.png',
|
light: withDocsBase('/assets/SubMiner.png'),
|
||||||
dark: '/assets/SubMiner.png',
|
dark: withDocsBase('/assets/SubMiner.png'),
|
||||||
},
|
},
|
||||||
siteTitle: 'SubMiner Docs',
|
siteTitle: 'SubMiner Docs',
|
||||||
nav: [
|
nav: filterNav(nav),
|
||||||
{ text: 'Home', link: '/' },
|
sidebar: filterSidebar(sidebar),
|
||||||
{ text: 'Get Started', link: '/installation' },
|
|
||||||
{ text: 'Mining', link: '/mining-workflow' },
|
|
||||||
{ text: 'Configuration', link: '/configuration' },
|
|
||||||
{ text: 'Changelog', link: '/changelog' },
|
|
||||||
{ text: 'Troubleshooting', link: '/troubleshooting' },
|
|
||||||
],
|
|
||||||
sidebar: [
|
|
||||||
{
|
|
||||||
text: 'Getting Started',
|
|
||||||
items: [
|
|
||||||
{ text: 'Overview', link: '/' },
|
|
||||||
{ text: 'Installation', link: '/installation' },
|
|
||||||
{ text: 'Usage', link: '/usage' },
|
|
||||||
{ text: 'Mining Workflow', link: '/mining-workflow' },
|
|
||||||
{ text: 'Launcher Script', link: '/launcher-script' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Reference',
|
|
||||||
items: [
|
|
||||||
{ text: 'Configuration', link: '/configuration' },
|
|
||||||
{ text: 'Keyboard Shortcuts', link: '/shortcuts' },
|
|
||||||
{ text: 'Subtitle Annotations', link: '/subtitle-annotations' },
|
|
||||||
{ text: 'Subtitle Sidebar', link: '/subtitle-sidebar' },
|
|
||||||
{ text: 'Immersion Tracking', link: '/immersion-tracking' },
|
|
||||||
{ text: 'Troubleshooting', link: '/troubleshooting' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Integrations',
|
|
||||||
items: [
|
|
||||||
{ text: 'MPV Plugin', link: '/mpv-plugin' },
|
|
||||||
{ text: 'Anki', link: '/anki-integration' },
|
|
||||||
{ text: 'Jellyfin', link: '/jellyfin-integration' },
|
|
||||||
{ text: 'YouTube', link: '/youtube-integration' },
|
|
||||||
{ text: 'Jimaku', link: '/jimaku-integration' },
|
|
||||||
{ text: 'AniList', link: '/anilist-integration' },
|
|
||||||
{ text: 'Character Dictionary', link: '/character-dictionary' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Development',
|
|
||||||
items: [
|
|
||||||
{ text: 'Building & Testing', link: '/development' },
|
|
||||||
{ text: 'Architecture', link: '/architecture' },
|
|
||||||
{ text: 'IPC + Runtime Contracts', link: '/ipc-contracts' },
|
|
||||||
{ text: 'WebSocket + Texthooker API', link: '/websocket-texthooker-api' },
|
|
||||||
{ text: 'Changelog', link: '/changelog' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
search: {
|
search: {
|
||||||
provider: 'local',
|
provider: 'local',
|
||||||
},
|
},
|
||||||
@@ -159,3 +301,5 @@ export default {
|
|||||||
socialLinks: [{ icon: 'github', link: 'https://github.com/ksyasuda/SubMiner' }],
|
socialLinks: [{ icon: 'github', link: 'https://github.com/ksyasuda/SubMiner' }],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'M PLUS 1';
|
font-family: 'M PLUS 1';
|
||||||
src: url('/assets/fonts/Mplus1-Medium.ttf') format('truetype');
|
src: url('../../public/assets/fonts/Mplus1-Medium.ttf') format('truetype');
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Manrope Default';
|
font-family: 'Manrope Default';
|
||||||
src: url('/assets/fonts/manrope-latin-600-normal.ttf') format('truetype');
|
src: url('../../public/assets/fonts/manrope-latin-600-normal.ttf') format('truetype');
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
|
|||||||
+12
-5
@@ -30,9 +30,16 @@ bun run docs:dev
|
|||||||
## Cloudflare Pages
|
## Cloudflare Pages
|
||||||
|
|
||||||
- Git repo: `ksyasuda/SubMiner`
|
- Git repo: `ksyasuda/SubMiner`
|
||||||
- Root directory: `docs-site`
|
- Production branch: `main`
|
||||||
- Build command: `bun run docs:build`
|
- Automatic production and preview deployments: disabled
|
||||||
- Build output directory: `.vitepress/dist`
|
- Custom domain: `docs.subminer.moe` attached to Production
|
||||||
- Build watch paths: `docs-site/*`
|
- Deployment path: GitHub Actions direct upload with Wrangler
|
||||||
|
|
||||||
Cloudflare Pages watch paths use a single `*` wildcard for monorepo subdirectories. `docs-site/*` matches nested files under the docs site; `docs-site/**` can cause docs-only pushes to be skipped.
|
The public docs root is stable-only:
|
||||||
|
|
||||||
|
- `/` serves the latest stable release docs.
|
||||||
|
- `/main/` serves development docs from `main` and is marked `noindex,follow`.
|
||||||
|
- `/v/<version>/` serves stable release archives.
|
||||||
|
- Prerelease tags do not update the docs site.
|
||||||
|
|
||||||
|
Keep Cloudflare Git auto-deploy disabled. The production deploy is `.github/workflows/docs-pages.yml`, which uploads `.tmp/docs-versioned-site` with `--branch main` so tag-triggered runs update Production instead of creating preview deployments.
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ outline: [2, 3]
|
|||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { withBase } from 'vitepress';
|
||||||
|
</script>
|
||||||
|
|
||||||
Settings are stored in `$XDG_CONFIG_HOME/SubMiner/config.jsonc` (or `~/.config/SubMiner/config.jsonc` when `XDG_CONFIG_HOME` is unset).
|
Settings are stored in `$XDG_CONFIG_HOME/SubMiner/config.jsonc` (or `~/.config/SubMiner/config.jsonc` when `XDG_CONFIG_HOME` is unset).
|
||||||
On Windows, the default path is `%APPDATA%\SubMiner\config.jsonc`.
|
On Windows, the default path is `%APPDATA%\SubMiner\config.jsonc`.
|
||||||
When both files exist, SubMiner prefers `config.jsonc` over `config.json`.
|
When both files exist, SubMiner prefers `config.jsonc` over `config.json`.
|
||||||
@@ -1044,12 +1048,12 @@ To refresh roughly once per day, set:
|
|||||||
`deleteDuplicateInAuto` controls whether `auto` mode deletes the duplicate after merge (default: `true`). In `manual` mode, the popup asks each time whether to delete the duplicate.
|
`deleteDuplicateInAuto` controls whether `auto` mode deletes the duplicate after merge (default: `true`). In `manual` mode, the popup asks each time whether to delete the duplicate.
|
||||||
When the manual merge popup opens, SubMiner pauses playback and closes any open Yomitan popup first so the merge flow can take focus.
|
When the manual merge popup opens, SubMiner pauses playback and closes any open Yomitan popup first so the merge flow can take focus.
|
||||||
|
|
||||||
<video controls playsinline preload="metadata" poster="/assets/kiku-integration-poster.jpg" style="width: 100%; max-width: 960px;">
|
<video controls playsinline preload="metadata" :poster="withBase('/assets/kiku-integration-poster.jpg')" style="width: 100%; max-width: 960px;">
|
||||||
<source :src="'/assets/kiku-integration.webm'" type="video/webm" />
|
<source :src="withBase('/assets/kiku-integration.webm')" type="video/webm" />
|
||||||
Your browser does not support the video tag.
|
Your browser does not support the video tag.
|
||||||
</video>
|
</video>
|
||||||
|
|
||||||
<a :href="'/assets/kiku-integration.webm'" target="_blank" rel="noreferrer">Open demo in a new tab</a>
|
<a :href="withBase('/assets/kiku-integration.webm')" target="_blank" rel="noreferrer">Open demo in a new tab</a>
|
||||||
|
|
||||||
## External Integrations
|
## External Integrations
|
||||||
|
|
||||||
|
|||||||
+16
-14
@@ -3,6 +3,8 @@
|
|||||||
Short recordings of SubMiner's key features and integrations from real playback sessions.
|
Short recordings of SubMiner's key features and integrations from real playback sessions.
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { withBase } from 'vitepress';
|
||||||
|
|
||||||
const v = '20260301-1';
|
const v = '20260301-1';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -10,11 +12,11 @@ const v = '20260301-1';
|
|||||||
|
|
||||||
Mine vocabulary cards from Yomitan or directly from subtitle lines. SubMiner automatically attaches the sentence, a timing-accurate audio clip, a screenshot, and a translation.
|
Mine vocabulary cards from Yomitan or directly from subtitle lines. SubMiner automatically attaches the sentence, a timing-accurate audio clip, a screenshot, and a translation.
|
||||||
|
|
||||||
<video controls playsinline preload="metadata" :poster="`/assets/minecard-poster.jpg?v=${demoAssetVersion}`">
|
<video controls playsinline preload="metadata" :poster="withBase(`/assets/minecard-poster.jpg?v=${v}`)">
|
||||||
<source :src="`/assets/minecard.webm?v=${demoAssetVersion}`" type="video/webm" />
|
<source :src="withBase(`/assets/minecard.webm?v=${v}`)" type="video/webm" />
|
||||||
<source :src="`/assets/minecard.mp4?v=${demoAssetVersion}`" type="video/mp4" />
|
<source :src="withBase(`/assets/minecard.mp4?v=${v}`)" type="video/mp4" />
|
||||||
<a :href="`/assets/minecard.webm?v=${demoAssetVersion}`" target="_blank" rel="noreferrer">
|
<a :href="withBase(`/assets/minecard.webm?v=${v}`)" target="_blank" rel="noreferrer">
|
||||||
<img :src="`/assets/minecard.webp?v=${demoAssetVersion}`" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />
|
<img :src="withBase(`/assets/minecard.webp?v=${v}`)" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />
|
||||||
</a>
|
</a>
|
||||||
</video>
|
</video>
|
||||||
|
|
||||||
@@ -25,9 +27,9 @@ Mine vocabulary cards from Yomitan or directly from subtitle lines. SubMiner aut
|
|||||||
|
|
||||||
Search and download subtitles from Jimaku, then automatically synchronize them with alass or ffsubsync — all from within SubMiner.
|
Search and download subtitles from Jimaku, then automatically synchronize them with alass or ffsubsync — all from within SubMiner.
|
||||||
|
|
||||||
<!-- <video controls playsinline preload="metadata" :poster="`/assets/demos/subtitle-sync-poster.jpg?v=${v}`">
|
<!-- <video controls playsinline preload="metadata" :poster="withBase(`/assets/demos/subtitle-sync-poster.jpg?v=${v}`)">
|
||||||
<source :src="`/assets/demos/subtitle-sync.webm?v=${v}`" type="video/webm" />
|
<source :src="withBase(`/assets/demos/subtitle-sync.webm?v=${v}`)" type="video/webm" />
|
||||||
<source :src="`/assets/demos/subtitle-sync.mp4?v=${v}`" type="video/mp4" />
|
<source :src="withBase(`/assets/demos/subtitle-sync.mp4?v=${v}`)" type="video/mp4" />
|
||||||
</video> -->
|
</video> -->
|
||||||
|
|
||||||
::: info VIDEO COMING SOON
|
::: info VIDEO COMING SOON
|
||||||
@@ -37,9 +39,9 @@ Search and download subtitles from Jimaku, then automatically synchronize them w
|
|||||||
|
|
||||||
Browse your Jellyfin library, cast to devices, and launch playback directly from SubMiner. Watch progress syncs back to your Jellyfin server.
|
Browse your Jellyfin library, cast to devices, and launch playback directly from SubMiner. Watch progress syncs back to your Jellyfin server.
|
||||||
|
|
||||||
<!-- <video controls playsinline preload="metadata" :poster="`/assets/demos/jellyfin-poster.jpg?v=${v}`">
|
<!-- <video controls playsinline preload="metadata" :poster="withBase(`/assets/demos/jellyfin-poster.jpg?v=${v}`)">
|
||||||
<source :src="`/assets/demos/jellyfin.webm?v=${v}`" type="video/webm" />
|
<source :src="withBase(`/assets/demos/jellyfin.webm?v=${v}`)" type="video/webm" />
|
||||||
<source :src="`/assets/demos/jellyfin.mp4?v=${v}`" type="video/mp4" />
|
<source :src="withBase(`/assets/demos/jellyfin.mp4?v=${v}`)" type="video/mp4" />
|
||||||
</video> -->
|
</video> -->
|
||||||
|
|
||||||
::: info VIDEO COMING SOON
|
::: info VIDEO COMING SOON
|
||||||
@@ -49,9 +51,9 @@ Browse your Jellyfin library, cast to devices, and launch playback directly from
|
|||||||
|
|
||||||
Open subtitles in an external texthooker page for use with browser-based tools and extensions alongside the overlay.
|
Open subtitles in an external texthooker page for use with browser-based tools and extensions alongside the overlay.
|
||||||
|
|
||||||
<!-- <video controls playsinline preload="metadata" :poster="`/assets/demos/texthooker-poster.jpg?v=${v}`">
|
<!-- <video controls playsinline preload="metadata" :poster="withBase(`/assets/demos/texthooker-poster.jpg?v=${v}`)">
|
||||||
<source :src="`/assets/demos/texthooker.webm?v=${v}`" type="video/webm" />
|
<source :src="withBase(`/assets/demos/texthooker.webm?v=${v}`)" type="video/webm" />
|
||||||
<source :src="`/assets/demos/texthooker.mp4?v=${v}`" type="video/mp4" />
|
<source :src="withBase(`/assets/demos/texthooker.mp4?v=${v}`)" type="video/mp4" />
|
||||||
</video> -->
|
</video> -->
|
||||||
|
|
||||||
::: info VIDEO COMING SOON
|
::: info VIDEO COMING SOON
|
||||||
|
|||||||
@@ -113,6 +113,14 @@ bun run docs:test
|
|||||||
bun run docs:build
|
bun run docs:build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For production docs routing, run the versioned build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run docs:build:versioned
|
||||||
|
```
|
||||||
|
|
||||||
|
The versioned build writes `.tmp/docs-versioned-site` with latest stable docs at `/`, development docs at `/main/`, and stable archives under `/v/<version>/`. Prerelease tags are skipped.
|
||||||
|
|
||||||
Focused commands:
|
Focused commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ test('docs reflect current launcher and release surfaces', () => {
|
|||||||
|
|
||||||
expect(mpvPluginContents).toContain('\\\\.\\pipe\\subminer-socket');
|
expect(mpvPluginContents).toContain('\\\\.\\pipe\\subminer-socket');
|
||||||
|
|
||||||
expect(readmeContents).toContain('Root directory: `docs-site`');
|
expect(readmeContents).toContain('Automatic production and preview deployments: disabled');
|
||||||
expect(readmeContents).toContain('Build output directory: `.vitepress/dist`');
|
expect(readmeContents).toContain('/main/');
|
||||||
expect(readmeContents).toContain('Build watch paths: `docs-site/*`');
|
expect(readmeContents).toContain('GitHub Actions direct upload with Wrangler');
|
||||||
expect(developmentContents).not.toContain('../subminer-docs');
|
expect(developmentContents).not.toContain('../subminer-docs');
|
||||||
expect(developmentContents).toContain('bun run docs:build');
|
expect(developmentContents).toContain('bun run docs:build');
|
||||||
expect(developmentContents).toContain('bun run docs:test');
|
expect(developmentContents).toContain('bun run docs:test');
|
||||||
expect(developmentContents).toContain('Build watch paths: `docs-site/*`');
|
expect(developmentContents).toContain('bun run docs:build:versioned');
|
||||||
expect(developmentContents).not.toContain('test:subtitle:dist');
|
expect(developmentContents).not.toContain('test:subtitle:dist');
|
||||||
expect(developmentContents).toContain('bun run build:win');
|
expect(developmentContents).toContain('bun run build:win');
|
||||||
|
|
||||||
|
|||||||
@@ -7,18 +7,18 @@ const docsIndexContents = readFileSync(docsIndexPath, 'utf8');
|
|||||||
test('docs demo media uses shared cache-busting asset version token', () => {
|
test('docs demo media uses shared cache-busting asset version token', () => {
|
||||||
expect(docsIndexContents).toMatch(/const demoAssetVersion = ['"][^'"]+['"]/);
|
expect(docsIndexContents).toMatch(/const demoAssetVersion = ['"][^'"]+['"]/);
|
||||||
expect(docsIndexContents).toContain(
|
expect(docsIndexContents).toContain(
|
||||||
':poster="`/assets/minecard-poster.jpg?v=${demoAssetVersion}`"',
|
':poster="withBase(`/assets/minecard-poster.jpg?v=${demoAssetVersion}`)"',
|
||||||
);
|
);
|
||||||
expect(docsIndexContents).toContain(
|
expect(docsIndexContents).toContain(
|
||||||
'<source :src="`/assets/minecard.webm?v=${demoAssetVersion}`" type="video/webm" />',
|
'<source :src="withBase(`/assets/minecard.webm?v=${demoAssetVersion}`)" type="video/webm" />',
|
||||||
);
|
);
|
||||||
expect(docsIndexContents).toContain(
|
expect(docsIndexContents).toContain(
|
||||||
'<source :src="`/assets/minecard.mp4?v=${demoAssetVersion}`" type="video/mp4" />',
|
'<source :src="withBase(`/assets/minecard.mp4?v=${demoAssetVersion}`)" type="video/mp4" />',
|
||||||
);
|
);
|
||||||
expect(docsIndexContents).toContain(
|
expect(docsIndexContents).toContain(
|
||||||
'<a :href="`/assets/minecard.webm?v=${demoAssetVersion}`" target="_blank" rel="noreferrer">',
|
'<a :href="withBase(`/assets/minecard.webm?v=${demoAssetVersion}`)" target="_blank" rel="noreferrer">',
|
||||||
);
|
);
|
||||||
expect(docsIndexContents).toContain(
|
expect(docsIndexContents).toContain(
|
||||||
'<img :src="`/assets/minecard.webp?v=${demoAssetVersion}`" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />',
|
'<img :src="withBase(`/assets/minecard.webp?v=${demoAssetVersion}`)" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
+7
-5
@@ -86,6 +86,8 @@ features:
|
|||||||
---
|
---
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { withBase } from 'vitepress';
|
||||||
|
|
||||||
const demoAssetVersion = '20260223-2';
|
const demoAssetVersion = '20260223-2';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -135,11 +137,11 @@ const demoAssetVersion = '20260223-2';
|
|||||||
<span class="demo-window__dot"></span>
|
<span class="demo-window__dot"></span>
|
||||||
<span class="demo-window__title">subminer -- playback</span>
|
<span class="demo-window__title">subminer -- playback</span>
|
||||||
</div>
|
</div>
|
||||||
<video controls playsinline preload="metadata" :poster="`/assets/minecard-poster.jpg?v=${demoAssetVersion}`">
|
<video controls playsinline preload="metadata" :poster="withBase(`/assets/minecard-poster.jpg?v=${demoAssetVersion}`)">
|
||||||
<source :src="`/assets/minecard.webm?v=${demoAssetVersion}`" type="video/webm" />
|
<source :src="withBase(`/assets/minecard.webm?v=${demoAssetVersion}`)" type="video/webm" />
|
||||||
<source :src="`/assets/minecard.mp4?v=${demoAssetVersion}`" type="video/mp4" />
|
<source :src="withBase(`/assets/minecard.mp4?v=${demoAssetVersion}`)" type="video/mp4" />
|
||||||
<a :href="`/assets/minecard.webm?v=${demoAssetVersion}`" target="_blank" rel="noreferrer">
|
<a :href="withBase(`/assets/minecard.webm?v=${demoAssetVersion}`)" target="_blank" rel="noreferrer">
|
||||||
<img :src="`/assets/minecard.webp?v=${demoAssetVersion}`" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />
|
<img :src="withBase(`/assets/minecard.webp?v=${demoAssetVersion}`)" alt="SubMiner demo Animated fallback" style="width: 100%; height: auto;" />
|
||||||
</a>
|
</a>
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"docs:dev": "VITE_EXTRA_EXTENSIONS=jsonc vitepress dev --host 0.0.0.0 --port 5173 --strictPort",
|
"docs:dev": "VITE_EXTRA_EXTENSIONS=jsonc vitepress dev --host 0.0.0.0 --port 5173 --strictPort",
|
||||||
"docs:build": "VITE_EXTRA_EXTENSIONS=jsonc vitepress build",
|
"docs:build": "VITE_EXTRA_EXTENSIONS=jsonc vitepress build",
|
||||||
"docs:preview": "VITE_EXTRA_EXTENSIONS=jsonc vitepress preview --host 0.0.0.0 --port 4173 --strictPort",
|
"docs:preview": "VITE_EXTRA_EXTENSIONS=jsonc vitepress preview --host 0.0.0.0 --port 4173 --strictPort",
|
||||||
"test": "bun test plausible.test.ts index.assets.test.ts docs-sync.test.ts seo.test.ts"
|
"test": "bun test plausible.test.ts index.assets.test.ts docs-sync.test.ts seo.test.ts ../scripts/docs-versioning.test.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@catppuccin/vitepress": "^0.1.2",
|
"@catppuccin/vitepress": "^0.1.2",
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import { readFileSync } from 'node:fs';
|
|||||||
const docsConfigPath = new URL('./.vitepress/config.ts', import.meta.url);
|
const docsConfigPath = new URL('./.vitepress/config.ts', import.meta.url);
|
||||||
const docsThemePath = new URL('./.vitepress/theme/index.ts', import.meta.url);
|
const docsThemePath = new URL('./.vitepress/theme/index.ts', import.meta.url);
|
||||||
const docsPackagePath = new URL('./package.json', import.meta.url);
|
const docsPackagePath = new URL('./package.json', import.meta.url);
|
||||||
|
const versionedBuildPath = new URL('../scripts/build-versioned-docs.ts', import.meta.url);
|
||||||
const docsConfigContents = readFileSync(docsConfigPath, 'utf8');
|
const docsConfigContents = readFileSync(docsConfigPath, 'utf8');
|
||||||
const docsThemeContents = readFileSync(docsThemePath, 'utf8');
|
const docsThemeContents = readFileSync(docsThemePath, 'utf8');
|
||||||
const docsPackageContents = readFileSync(docsPackagePath, 'utf8');
|
const docsPackageContents = readFileSync(docsPackagePath, 'utf8');
|
||||||
|
const versionedBuildContents = readFileSync(versionedBuildPath, 'utf8');
|
||||||
|
|
||||||
test('docs site loads the docs.subminer.moe Plausible script through the analytics proxy', () => {
|
test('docs site loads the docs.subminer.moe Plausible script through the analytics proxy', () => {
|
||||||
expect(docsConfigContents).toContain("const DOCS_HOSTNAME = 'https://docs.subminer.moe'");
|
expect(docsConfigContents).toContain("const DOCS_HOSTNAME = 'https://docs.subminer.moe'");
|
||||||
@@ -34,3 +36,8 @@ test('docs site loads the docs.subminer.moe Plausible script through the analyti
|
|||||||
expect(docsThemeContents).not.toContain('initPlausibleTracker');
|
expect(docsThemeContents).not.toContain('initPlausibleTracker');
|
||||||
expect(docsPackageContents).not.toContain('@plausible-analytics/tracker');
|
expect(docsPackageContents).not.toContain('@plausible-analytics/tracker');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('versioned docs reuse current VitePress internals for old page snapshots', () => {
|
||||||
|
expect(versionedBuildContents).toContain("cpSync(join(currentDocsSite, '.vitepress')");
|
||||||
|
expect(versionedBuildContents).toContain('overlayCurrentVitePress(snapshotDocsSite)');
|
||||||
|
});
|
||||||
|
|||||||
@@ -31,6 +31,54 @@ test('docs pages emit stable self-referential canonical URLs', async () => {
|
|||||||
expect(JSON.stringify(rootHead).toLowerCase()).not.toContain('noindex');
|
expect(JSON.stringify(rootHead).toLowerCase()).not.toContain('noindex');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('main docs canonical uses /main/ and emits noindex', async () => {
|
||||||
|
const previousChannel = process.env.SUBMINER_DOCS_CHANNEL;
|
||||||
|
const previousBase = process.env.SUBMINER_DOCS_BASE;
|
||||||
|
process.env.SUBMINER_DOCS_CHANNEL = 'main';
|
||||||
|
process.env.SUBMINER_DOCS_BASE = '/main/';
|
||||||
|
const { default: mainDocsConfig } = await import('./.vitepress/config?main-docs');
|
||||||
|
|
||||||
|
const head = await mainDocsConfig.transformHead?.(makeTransformContext('usage.md'));
|
||||||
|
const rootHead = await mainDocsConfig.transformHead?.(makeTransformContext('index.md'));
|
||||||
|
|
||||||
|
expect(head).toContainEqual([
|
||||||
|
'link',
|
||||||
|
{ rel: 'canonical', href: 'https://docs.subminer.moe/main/usage' },
|
||||||
|
]);
|
||||||
|
expect(rootHead).toContainEqual([
|
||||||
|
'link',
|
||||||
|
{ rel: 'canonical', href: 'https://docs.subminer.moe/main/' },
|
||||||
|
]);
|
||||||
|
expect(head).toContainEqual(['meta', { name: 'robots', content: 'noindex,follow' }]);
|
||||||
|
|
||||||
|
process.env.SUBMINER_DOCS_CHANNEL = previousChannel;
|
||||||
|
process.env.SUBMINER_DOCS_BASE = previousBase;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('latest stable archive canonical points to root equivalent', async () => {
|
||||||
|
const previousChannel = process.env.SUBMINER_DOCS_CHANNEL;
|
||||||
|
const previousBase = process.env.SUBMINER_DOCS_BASE;
|
||||||
|
const previousVersion = process.env.SUBMINER_DOCS_VERSION;
|
||||||
|
const previousLatest = process.env.SUBMINER_DOCS_LATEST_STABLE;
|
||||||
|
process.env.SUBMINER_DOCS_CHANNEL = 'stable-archive';
|
||||||
|
process.env.SUBMINER_DOCS_BASE = '/v/0.14.0/';
|
||||||
|
process.env.SUBMINER_DOCS_VERSION = 'v0.14.0';
|
||||||
|
process.env.SUBMINER_DOCS_LATEST_STABLE = 'v0.14.0';
|
||||||
|
const { default: latestArchiveConfig } = await import('./.vitepress/config?latest-archive');
|
||||||
|
|
||||||
|
const head = await latestArchiveConfig.transformHead?.(makeTransformContext('usage.md'));
|
||||||
|
|
||||||
|
expect(head).toContainEqual([
|
||||||
|
'link',
|
||||||
|
{ rel: 'canonical', href: 'https://docs.subminer.moe/usage' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
process.env.SUBMINER_DOCS_CHANNEL = previousChannel;
|
||||||
|
process.env.SUBMINER_DOCS_BASE = previousBase;
|
||||||
|
process.env.SUBMINER_DOCS_VERSION = previousVersion;
|
||||||
|
process.env.SUBMINER_DOCS_LATEST_STABLE = previousLatest;
|
||||||
|
});
|
||||||
|
|
||||||
test('docs sitemap excludes duplicate README page from indexable URLs', async () => {
|
test('docs sitemap excludes duplicate README page from indexable URLs', async () => {
|
||||||
const items = [{ url: '' }, { url: 'README' }, { url: 'usage' }];
|
const items = [{ url: '' }, { url: 'README' }, { url: 'usage' }];
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
8. If `docs-site/` changed, also run:
|
8. If `docs-site/` changed, also run:
|
||||||
`bun run docs:test`
|
`bun run docs:test`
|
||||||
`bun run docs:build`
|
`bun run docs:build`
|
||||||
|
`bun run docs:build:versioned`
|
||||||
9. Commit release prep.
|
9. Commit release prep.
|
||||||
10. Tag the commit: `git tag v<version>`.
|
10. Tag the commit: `git tag v<version>`.
|
||||||
11. Push commit + tag.
|
11. Push commit + tag.
|
||||||
@@ -66,6 +67,7 @@
|
|||||||
7. Push commit + tag.
|
7. Push commit + tag.
|
||||||
|
|
||||||
Prerelease tags publish a GitHub prerelease only. They do not update `CHANGELOG.md`, `docs-site/changelog.md`, or the AUR package, and they do not consume `changes/*.md` fragments. The final stable release is still the point where `bun run changelog:build` consumes fragments into `CHANGELOG.md` and regenerates stable release notes.
|
Prerelease tags publish a GitHub prerelease only. They do not update `CHANGELOG.md`, `docs-site/changelog.md`, or the AUR package, and they do not consume `changes/*.md` fragments. The final stable release is still the point where `bun run changelog:build` consumes fragments into `CHANGELOG.md` and regenerates stable release notes.
|
||||||
|
Prerelease tags also do not update `https://docs.subminer.moe/`.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
@@ -81,6 +83,8 @@ Notes:
|
|||||||
- If you need to repair a published release body (for example, a prior version’s section was omitted), regenerate notes from `CHANGELOG.md` and re-edit the release with `gh release edit --notes-file`.
|
- If you need to repair a published release body (for example, a prior version’s section was omitted), regenerate notes from `CHANGELOG.md` and re-edit the release with `gh release edit --notes-file`.
|
||||||
- Prerelease tags are handled by `.github/workflows/prerelease.yml`, which always publishes a GitHub prerelease with all current release platforms and never runs the AUR sync job.
|
- Prerelease tags are handled by `.github/workflows/prerelease.yml`, which always publishes a GitHub prerelease with all current release platforms and never runs the AUR sync job.
|
||||||
- Tagged release workflow now also attempts to update `subminer-bin` on the AUR after GitHub Release publication.
|
- Tagged release workflow now also attempts to update `subminer-bin` on the AUR after GitHub Release publication.
|
||||||
|
- Stable release tags update `https://docs.subminer.moe/` and `https://docs.subminer.moe/v/<version>/` through `.github/workflows/docs-pages.yml`; `/main/` continues to show development docs from `main`.
|
||||||
|
- Keep Cloudflare Pages Git auto-deploy disabled for `docs.subminer.moe`. Production docs are direct-uploaded by Wrangler from GitHub Actions with `--branch main`.
|
||||||
- AUR publish is best-effort: the workflow retries transient SSH clone/push failures, then warns and leaves the GitHub Release green if AUR still fails. Follow up with a manual `git push aur master` from the AUR checkout when needed.
|
- AUR publish is best-effort: the workflow retries transient SSH clone/push failures, then warns and leaves the GitHub Release green if AUR still fails. Follow up with a manual `git push aur master` from the AUR checkout when needed.
|
||||||
- Required GitHub Actions secret: `AUR_SSH_PRIVATE_KEY`. Add the matching public key to your AUR account before relying on the automation.
|
- Required GitHub Actions secret: `AUR_SSH_PRIVATE_KEY`. Add the matching public key to your AUR account before relying on the automation.
|
||||||
- Release and prerelease workflows upload updater metadata (`*.yml`) and blockmaps (`*.blockmap`) alongside platform artifacts. Do not remove those files while `electron-updater` is enabled.
|
- Release and prerelease workflows upload updater metadata (`*.yml`) and blockmaps (`*.blockmap`) alongside platform artifacts. Do not remove those files while `electron-updater` is enabled.
|
||||||
|
|||||||
+2
-1
@@ -40,6 +40,7 @@
|
|||||||
"lint": "bun run lint:stats",
|
"lint": "bun run lint:stats",
|
||||||
"docs:dev": "bun run --cwd docs-site docs:dev",
|
"docs:dev": "bun run --cwd docs-site docs:dev",
|
||||||
"docs:build": "bun run --cwd docs-site docs:build",
|
"docs:build": "bun run --cwd docs-site docs:build",
|
||||||
|
"docs:build:versioned": "bun run scripts/build-versioned-docs.ts",
|
||||||
"docs:preview": "bun run --cwd docs-site docs:preview",
|
"docs:preview": "bun run --cwd docs-site docs:preview",
|
||||||
"docs:test": "bun run --cwd docs-site test",
|
"docs:test": "bun run --cwd docs-site test",
|
||||||
"test:docs:kb": "bun test scripts/docs-knowledge-base.test.ts",
|
"test:docs:kb": "bun test scripts/docs-knowledge-base.test.ts",
|
||||||
@@ -71,7 +72,7 @@
|
|||||||
"test:launcher": "bun run test:launcher:src",
|
"test:launcher": "bun run test:launcher:src",
|
||||||
"test:core": "bun run test:core:src",
|
"test:core": "bun run test:core:src",
|
||||||
"test:subtitle": "bun run test:subtitle:src",
|
"test:subtitle": "bun run test:subtitle:src",
|
||||||
"test:fast": "bun run test:config:src && bun run test:core:src && bun run test:docs:kb && bun test src/main-entry-runtime.test.ts src/anki-integration.test.ts src/anki-integration/card-creation-manual-update.test.ts src/anki-integration/anki-connect-proxy.test.ts src/anki-integration/field-grouping-workflow.test.ts src/anki-integration/field-grouping.test.ts src/anki-integration/field-grouping-merge.test.ts src/release-workflow.test.ts src/prerelease-workflow.test.ts src/ci-workflow.test.ts scripts/build-changelog.test.ts scripts/electron-builder-after-pack.test.ts scripts/get-mpv-window-macos.test.ts scripts/mkv-to-readme-video.test.ts scripts/run-coverage-lane.test.ts scripts/update-aur-package.test.ts && bun test src/core/services/immersion-tracker/__tests__/query.test.ts src/core/services/immersion-tracker/__tests__/query-split-modules.test.ts && bun run tsc && bun test dist/main/runtime/registry.test.js",
|
"test:fast": "bun run test:config:src && bun run test:core:src && bun run test:docs:kb && bun test src/main-entry-runtime.test.ts src/anki-integration.test.ts src/anki-integration/card-creation-manual-update.test.ts src/anki-integration/anki-connect-proxy.test.ts src/anki-integration/field-grouping-workflow.test.ts src/anki-integration/field-grouping.test.ts src/anki-integration/field-grouping-merge.test.ts src/release-workflow.test.ts src/prerelease-workflow.test.ts src/ci-workflow.test.ts scripts/docs-versioning.test.ts scripts/build-changelog.test.ts scripts/electron-builder-after-pack.test.ts scripts/get-mpv-window-macos.test.ts scripts/mkv-to-readme-video.test.ts scripts/run-coverage-lane.test.ts scripts/update-aur-package.test.ts && bun test src/core/services/immersion-tracker/__tests__/query.test.ts src/core/services/immersion-tracker/__tests__/query-split-modules.test.ts && bun run tsc && bun test dist/main/runtime/registry.test.js",
|
||||||
"generate:config-example": "bun run src/generate-config-example.ts",
|
"generate:config-example": "bun run src/generate-config-example.ts",
|
||||||
"verify:config-example": "bun run src/verify-config-example.ts",
|
"verify:config-example": "bun run src/verify-config-example.ts",
|
||||||
"start": "bun run build && electron . --start",
|
"start": "bun run build && electron . --start",
|
||||||
|
|||||||
@@ -0,0 +1,337 @@
|
|||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
||||||
|
import { join, resolve } from 'node:path';
|
||||||
|
import {
|
||||||
|
buildVersionManifest,
|
||||||
|
stableTagsWithDocs,
|
||||||
|
versionArchiveCacheName,
|
||||||
|
versionOutputPath,
|
||||||
|
versionPath,
|
||||||
|
} from './docs-versioning';
|
||||||
|
|
||||||
|
const repoRoot = resolve(__dirname, '..');
|
||||||
|
const currentDocsSite = join(repoRoot, 'docs-site');
|
||||||
|
const buildRoot = join(repoRoot, '.tmp/docs-versioned-build');
|
||||||
|
const aggregateOutDir = join(repoRoot, '.tmp/docs-versioned-site');
|
||||||
|
const archiveCacheRoot = join(repoRoot, '.tmp/docs-versioned-archive-cache');
|
||||||
|
const maxCloudflareFiles = 20_000;
|
||||||
|
const maxCloudflareFileBytes = 25 * 1024 * 1024;
|
||||||
|
|
||||||
|
function run(command: string, args: string[], options: { cwd?: string; env?: NodeJS.ProcessEnv } = {}) {
|
||||||
|
const result = spawnSync(command, args, {
|
||||||
|
cwd: options.cwd ?? repoRoot,
|
||||||
|
env: options.env ?? process.env,
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error(`Command failed: ${command} ${args.join(' ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function capture(command: string, args: string[]): string {
|
||||||
|
const result = spawnSync(command, args, {
|
||||||
|
cwd: repoRoot,
|
||||||
|
encoding: 'utf8',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error(result.stderr || `Command failed: ${command} ${args.join(' ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.stdout;
|
||||||
|
}
|
||||||
|
|
||||||
|
function archiveDocsSite(ref: string, targetDir: string) {
|
||||||
|
mkdirSync(targetDir, { recursive: true });
|
||||||
|
const archive = spawnSync('git', ['archive', '--format=tar', ref, 'docs-site'], {
|
||||||
|
cwd: repoRoot,
|
||||||
|
encoding: 'buffer',
|
||||||
|
maxBuffer: 1024 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (archive.status !== 0 || !archive.stdout) {
|
||||||
|
throw new Error(`Unable to archive docs-site from ${ref}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const extract = spawnSync('tar', ['-x', '-C', targetDir], {
|
||||||
|
input: archive.stdout,
|
||||||
|
stdio: ['pipe', 'inherit', 'inherit'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (extract.status !== 0) {
|
||||||
|
throw new Error(`Unable to extract docs-site archive from ${ref}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyCurrentDocsSite(targetDir: string) {
|
||||||
|
mkdirSync(targetDir, { recursive: true });
|
||||||
|
cpSync(currentDocsSite, join(targetDir, 'docs-site'), {
|
||||||
|
recursive: true,
|
||||||
|
dereference: false,
|
||||||
|
filter: (source) =>
|
||||||
|
!/[\\/]node_modules([\\/]|$)/.test(source) &&
|
||||||
|
!/[\\/]\\.vitepress[\\/]dist([\\/]|$)/.test(source),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function overlayCurrentVitePress(snapshotDocsSite: string) {
|
||||||
|
const targetVitePress = join(snapshotDocsSite, '.vitepress');
|
||||||
|
rmSync(targetVitePress, { recursive: true, force: true });
|
||||||
|
cpSync(join(currentDocsSite, '.vitepress'), targetVitePress, {
|
||||||
|
recursive: true,
|
||||||
|
filter: (source) => !isGeneratedVitePressPath(source),
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentThemeFonts = join(currentDocsSite, 'public/assets/fonts');
|
||||||
|
if (existsSync(currentThemeFonts)) {
|
||||||
|
cpSync(currentThemeFonts, join(snapshotDocsSite, 'public/assets/fonts'), {
|
||||||
|
recursive: true,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function linkDocsDependencies(snapshotDocsSite: string) {
|
||||||
|
const currentNodeModules = join(currentDocsSite, 'node_modules');
|
||||||
|
const targetNodeModules = join(snapshotDocsSite, 'node_modules');
|
||||||
|
|
||||||
|
if (!existsSync(currentNodeModules) || existsSync(targetNodeModules)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
symlinkSync(currentNodeModules, targetNodeModules, 'dir');
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareSnapshot(name: string, ref?: string): string {
|
||||||
|
const snapshotRoot = join(buildRoot, name);
|
||||||
|
rmSync(snapshotRoot, { recursive: true, force: true });
|
||||||
|
|
||||||
|
if (ref) {
|
||||||
|
archiveDocsSite(ref, snapshotRoot);
|
||||||
|
} else {
|
||||||
|
copyCurrentDocsSite(snapshotRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshotDocsSite = join(snapshotRoot, 'docs-site');
|
||||||
|
overlayCurrentVitePress(snapshotDocsSite);
|
||||||
|
linkDocsDependencies(snapshotDocsSite);
|
||||||
|
return snapshotDocsSite;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tagHasDocsSite(tag: string): boolean {
|
||||||
|
const result = spawnSync('git', ['cat-file', '-e', `${tag}:docs-site/package.json`], {
|
||||||
|
cwd: repoRoot,
|
||||||
|
});
|
||||||
|
return result.status === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStableVersions(): string[] {
|
||||||
|
const tags = capture('git', ['tag', '--list', 'v*'])
|
||||||
|
.split('\n')
|
||||||
|
.map((tag) => tag.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
return stableTagsWithDocs(tags, tagHasDocsSite);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDocs(options: {
|
||||||
|
snapshotDocsSite: string;
|
||||||
|
base: string;
|
||||||
|
outDir: string;
|
||||||
|
channel: string;
|
||||||
|
version?: string;
|
||||||
|
latestStable: string;
|
||||||
|
manifestJson: string;
|
||||||
|
}) {
|
||||||
|
run('bun', ['run', '--cwd', currentDocsSite, 'vitepress', 'build', options.snapshotDocsSite], {
|
||||||
|
cwd: repoRoot,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
SUBMINER_DOCS_BASE: options.base,
|
||||||
|
SUBMINER_DOCS_OUT_DIR: options.outDir,
|
||||||
|
SUBMINER_DOCS_CHANNEL: options.channel,
|
||||||
|
SUBMINER_DOCS_VERSION: options.version ?? '',
|
||||||
|
SUBMINER_DOCS_LATEST_STABLE: options.latestStable,
|
||||||
|
SUBMINER_DOCS_VERSION_MANIFEST: options.manifestJson,
|
||||||
|
VITE_EXTRA_EXTENSIONS: 'jsonc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHashWithPath(hash: ReturnType<typeof createHash>, path: string) {
|
||||||
|
if (isGeneratedVitePressPath(path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stat = lstatSync(path);
|
||||||
|
hash.update(path.replace(repoRoot, ''));
|
||||||
|
hash.update(String(stat.mode));
|
||||||
|
|
||||||
|
if (stat.isSymbolicLink()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
for (const entry of readdirSync(path).sort()) {
|
||||||
|
updateHashWithPath(hash, join(path, entry));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hash.update(readFileSync(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGeneratedVitePressPath(path: string): boolean {
|
||||||
|
return /[\\/]\\.vitepress[\\/](cache|dist)([\\/]|$)/.test(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeSharedInternalsHash(): string {
|
||||||
|
const hash = createHash('sha256');
|
||||||
|
const paths = [
|
||||||
|
join(currentDocsSite, '.vitepress'),
|
||||||
|
join(currentDocsSite, 'public/assets/fonts'),
|
||||||
|
join(currentDocsSite, 'package.json'),
|
||||||
|
join(currentDocsSite, 'bun.lock'),
|
||||||
|
join(repoRoot, 'scripts/build-versioned-docs.ts'),
|
||||||
|
join(repoRoot, 'scripts/docs-versioning.ts'),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const path of paths) {
|
||||||
|
if (existsSync(path)) {
|
||||||
|
updateHashWithPath(hash, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash.digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function archiveCachePath(version: string, sharedInternalsHash: string): string {
|
||||||
|
return join(archiveCacheRoot, versionArchiveCacheName(version, sharedInternalsHash));
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreCachedArchive(version: string, sharedInternalsHash: string): boolean {
|
||||||
|
const cachedArchive = archiveCachePath(version, sharedInternalsHash);
|
||||||
|
if (!existsSync(cachedArchive)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cpSync(cachedArchive, join(aggregateOutDir, versionOutputPath(version)), {
|
||||||
|
recursive: true,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveArchiveCache(version: string, sharedInternalsHash: string) {
|
||||||
|
const outputPath = join(aggregateOutDir, versionOutputPath(version));
|
||||||
|
if (!existsSync(outputPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedArchive = archiveCachePath(version, sharedInternalsHash);
|
||||||
|
rmSync(cachedArchive, { recursive: true, force: true });
|
||||||
|
mkdirSync(archiveCacheRoot, { recursive: true });
|
||||||
|
cpSync(outputPath, cachedArchive, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertCloudflarePagesLimits(root: string) {
|
||||||
|
let fileCount = 0;
|
||||||
|
const oversizedFiles: string[] = [];
|
||||||
|
|
||||||
|
function walk(dir: string) {
|
||||||
|
for (const entry of readdirSync(dir)) {
|
||||||
|
const path = join(dir, entry);
|
||||||
|
const stat = lstatSync(path);
|
||||||
|
if (stat.isSymbolicLink()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
walk(path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileCount += 1;
|
||||||
|
if (stat.size > maxCloudflareFileBytes) {
|
||||||
|
oversizedFiles.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walk(root);
|
||||||
|
|
||||||
|
if (fileCount > maxCloudflareFiles) {
|
||||||
|
throw new Error(
|
||||||
|
`Versioned docs output has ${fileCount} files; Cloudflare Pages free plan limit is ${maxCloudflareFiles}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oversizedFiles.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Versioned docs output has files over 25 MiB:\n${oversizedFiles.join('\n')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const stableVersions = getStableVersions();
|
||||||
|
const latestStable = stableVersions[0];
|
||||||
|
|
||||||
|
if (!latestStable) {
|
||||||
|
throw new Error('No stable release tags with docs-site/package.json found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const manifest = buildVersionManifest({ latestStable, stableVersions });
|
||||||
|
const manifestJson = JSON.stringify(manifest);
|
||||||
|
const sharedInternalsHash = computeSharedInternalsHash();
|
||||||
|
|
||||||
|
rmSync(buildRoot, { recursive: true, force: true });
|
||||||
|
rmSync(aggregateOutDir, { recursive: true, force: true });
|
||||||
|
mkdirSync(buildRoot, { recursive: true });
|
||||||
|
mkdirSync(aggregateOutDir, { recursive: true });
|
||||||
|
|
||||||
|
const latestStableSnapshot = prepareSnapshot(latestStable, latestStable);
|
||||||
|
buildDocs({
|
||||||
|
snapshotDocsSite: latestStableSnapshot,
|
||||||
|
base: '/',
|
||||||
|
outDir: aggregateOutDir,
|
||||||
|
channel: 'stable-root',
|
||||||
|
version: latestStable,
|
||||||
|
latestStable,
|
||||||
|
manifestJson,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const version of stableVersions) {
|
||||||
|
if (restoreCachedArchive(version, sharedInternalsHash)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshot = version === latestStable ? latestStableSnapshot : prepareSnapshot(version, version);
|
||||||
|
buildDocs({
|
||||||
|
snapshotDocsSite: snapshot,
|
||||||
|
base: versionPath(version),
|
||||||
|
outDir: join(aggregateOutDir, versionOutputPath(version)),
|
||||||
|
channel: 'stable-archive',
|
||||||
|
version,
|
||||||
|
latestStable,
|
||||||
|
manifestJson,
|
||||||
|
});
|
||||||
|
saveArchiveCache(version, sharedInternalsHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainSnapshot = prepareSnapshot('main');
|
||||||
|
buildDocs({
|
||||||
|
snapshotDocsSite: mainSnapshot,
|
||||||
|
base: '/main/',
|
||||||
|
outDir: join(aggregateOutDir, 'main'),
|
||||||
|
channel: 'main',
|
||||||
|
version: 'main',
|
||||||
|
latestStable,
|
||||||
|
manifestJson,
|
||||||
|
});
|
||||||
|
|
||||||
|
writeFileSync(join(aggregateOutDir, 'versions.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
||||||
|
assertCloudflarePagesLimits(aggregateOutDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { describe, expect, test } from 'bun:test';
|
||||||
|
import {
|
||||||
|
buildVersionManifest,
|
||||||
|
compareStableVersionsDesc,
|
||||||
|
isStableReleaseTag,
|
||||||
|
stableTagsWithDocs,
|
||||||
|
versionArchiveCacheName,
|
||||||
|
versionOutputPath,
|
||||||
|
versionPath,
|
||||||
|
} from './docs-versioning';
|
||||||
|
|
||||||
|
describe('docs versioning helpers', () => {
|
||||||
|
test('stable tag filtering excludes beta and rc tags', () => {
|
||||||
|
expect(isStableReleaseTag('v0.14.0')).toBe(true);
|
||||||
|
expect(isStableReleaseTag('v0.15.0-beta.3')).toBe(false);
|
||||||
|
expect(isStableReleaseTag('v0.15.0-rc.1')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('latest stable resolves to v0.14.0 when beta tags are present', () => {
|
||||||
|
const tags = ['v0.13.0', 'v0.15.0-beta.3', 'v0.14.0'].sort(compareStableVersionsDesc);
|
||||||
|
|
||||||
|
expect(tags[0]).toBe('v0.14.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tags before docs-site are skipped', () => {
|
||||||
|
const tags = ['v0.12.0', 'v0.13.0', 'v0.14.0'];
|
||||||
|
const hasDocsSite = (tag: string) => tag !== 'v0.12.0';
|
||||||
|
|
||||||
|
expect(stableTagsWithDocs(tags, hasDocsSite)).toEqual(['v0.14.0', 'v0.13.0']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('version manifest paths are normalized', () => {
|
||||||
|
expect(versionPath('v0.14.0')).toBe('/v/0.14.0/');
|
||||||
|
expect(
|
||||||
|
buildVersionManifest({
|
||||||
|
latestStable: 'v0.14.0',
|
||||||
|
stableVersions: ['v0.14.0'],
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
latestStable: 'v0.14.0',
|
||||||
|
channels: [
|
||||||
|
{ label: 'Latest stable', path: '/' },
|
||||||
|
{ label: 'main', path: '/main/' },
|
||||||
|
],
|
||||||
|
versions: [{ version: 'v0.14.0', path: '/v/0.14.0/' }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('archive cache names are normalized by version and shared internals hash', () => {
|
||||||
|
expect(versionArchiveCacheName('v0.14.0', 'abcdef1234567890')).toBe('abcdef123456-v0.14.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('archive output paths stay relative for filesystem joins', () => {
|
||||||
|
expect(versionOutputPath('v0.14.0')).toBe('v/0.14.0');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
export type DocsVersionEntry = {
|
||||||
|
version: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DocsChannelEntry = {
|
||||||
|
label: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DocsVersionManifest = {
|
||||||
|
latestStable: string;
|
||||||
|
channels: DocsChannelEntry[];
|
||||||
|
versions: DocsVersionEntry[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const STABLE_TAG_PATTERN = /^v\d+\.\d+\.\d+$/;
|
||||||
|
|
||||||
|
export function isStableReleaseTag(tag: string): boolean {
|
||||||
|
return STABLE_TAG_PATTERN.test(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseStableVersion(tag: string): [number, number, number] {
|
||||||
|
const match = /^v(\d+)\.(\d+)\.(\d+)$/.exec(tag);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`Invalid stable SubMiner version tag: ${tag}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compareStableVersionsDesc(a: string, b: string): number {
|
||||||
|
if (!isStableReleaseTag(a) && !isStableReleaseTag(b)) return a.localeCompare(b);
|
||||||
|
if (!isStableReleaseTag(a)) return 1;
|
||||||
|
if (!isStableReleaseTag(b)) return -1;
|
||||||
|
|
||||||
|
const parsedA = parseStableVersion(a);
|
||||||
|
const parsedB = parseStableVersion(b);
|
||||||
|
|
||||||
|
for (let index = 0; index < parsedA.length; index += 1) {
|
||||||
|
const difference = parsedB[index]! - parsedA[index]!;
|
||||||
|
if (difference !== 0) return difference;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function versionPath(version: string): string {
|
||||||
|
return `/v/${version.replace(/^v/, '')}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function versionOutputPath(version: string): string {
|
||||||
|
return `v/${version.replace(/^v/, '')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function versionArchiveCacheName(version: string, sharedInternalsHash: string): string {
|
||||||
|
return `${sharedInternalsHash.slice(0, 12)}-${version}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stableTagsWithDocs(
|
||||||
|
tags: string[],
|
||||||
|
hasDocsSite: (tag: string) => boolean,
|
||||||
|
): string[] {
|
||||||
|
return tags
|
||||||
|
.filter(isStableReleaseTag)
|
||||||
|
.filter(hasDocsSite)
|
||||||
|
.sort(compareStableVersionsDesc);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildVersionManifest(options: {
|
||||||
|
latestStable: string;
|
||||||
|
stableVersions: string[];
|
||||||
|
}): DocsVersionManifest {
|
||||||
|
return {
|
||||||
|
latestStable: options.latestStable,
|
||||||
|
channels: [
|
||||||
|
{ label: 'Latest stable', path: '/' },
|
||||||
|
{ label: 'main', path: '/main/' },
|
||||||
|
],
|
||||||
|
versions: options.stableVersions.map((version) => ({
|
||||||
|
version,
|
||||||
|
path: versionPath(version),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import { resolve } from 'node:path';
|
|||||||
|
|
||||||
const ciWorkflowPath = resolve(__dirname, '../.github/workflows/ci.yml');
|
const ciWorkflowPath = resolve(__dirname, '../.github/workflows/ci.yml');
|
||||||
const ciWorkflow = readFileSync(ciWorkflowPath, 'utf8');
|
const ciWorkflow = readFileSync(ciWorkflowPath, 'utf8');
|
||||||
|
const docsPagesWorkflowPath = resolve(__dirname, '../.github/workflows/docs-pages.yml');
|
||||||
|
const docsPagesWorkflow = readFileSync(docsPagesWorkflowPath, 'utf8');
|
||||||
const packageJsonPath = resolve(__dirname, '../package.json');
|
const packageJsonPath = resolve(__dirname, '../package.json');
|
||||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as {
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as {
|
||||||
scripts: Record<string, string>;
|
scripts: Record<string, string>;
|
||||||
@@ -36,3 +38,28 @@ test('ci workflow runs the maintained source coverage lane and uploads lcov outp
|
|||||||
assert.match(ciWorkflow, /name: Upload coverage artifact/);
|
assert.match(ciWorkflow, /name: Upload coverage artifact/);
|
||||||
assert.match(ciWorkflow, /path: coverage\/test-src\/lcov\.info/);
|
assert.match(ciWorkflow, /path: coverage\/test-src\/lcov\.info/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('main docs deploy exists, serializes deploys, and uses Cloudflare credentials', () => {
|
||||||
|
assert.match(docsPagesWorkflow, /name: Docs Pages/);
|
||||||
|
assert.match(docsPagesWorkflow, /branches:\s*\n\s*-\s*main/);
|
||||||
|
assert.match(docsPagesWorkflow, /group:\s*docs-pages-production/);
|
||||||
|
assert.match(docsPagesWorkflow, /CLOUDFLARE_API_TOKEN/);
|
||||||
|
assert.match(docsPagesWorkflow, /CLOUDFLARE_ACCOUNT_ID/);
|
||||||
|
assert.match(docsPagesWorkflow, /CLOUDFLARE_PAGES_PROJECT_NAME/);
|
||||||
|
assert.match(docsPagesWorkflow, /pages deploy \.tmp\/docs-versioned-site/);
|
||||||
|
assert.match(docsPagesWorkflow, /--branch main/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('docs deploy caches stable archive builds between runs', () => {
|
||||||
|
assert.match(docsPagesWorkflow, /actions\/cache@v4/);
|
||||||
|
assert.match(docsPagesWorkflow, /\.tmp\/docs-versioned-archive-cache/);
|
||||||
|
assert.match(docsPagesWorkflow, /docs-versioned-archives-/);
|
||||||
|
assert.match(docsPagesWorkflow, /docs-site\/\.vitepress\/\*\*/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('docs deploy skips invalid release tags without failing the workflow', () => {
|
||||||
|
assert.match(docsPagesWorkflow, /id:\s*tag_guard/);
|
||||||
|
assert.match(docsPagesWorkflow, /stable_tag=false/);
|
||||||
|
assert.doesNotMatch(docsPagesWorkflow, /exit 78/);
|
||||||
|
assert.match(docsPagesWorkflow, /if:\s*steps\.tag_guard\.outputs\.stable_tag != 'false'/);
|
||||||
|
});
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { resolve } from 'node:path';
|
|||||||
|
|
||||||
const releaseWorkflowPath = resolve(__dirname, '../.github/workflows/release.yml');
|
const releaseWorkflowPath = resolve(__dirname, '../.github/workflows/release.yml');
|
||||||
const releaseWorkflow = readFileSync(releaseWorkflowPath, 'utf8');
|
const releaseWorkflow = readFileSync(releaseWorkflowPath, 'utf8');
|
||||||
|
const docsPagesWorkflowPath = resolve(__dirname, '../.github/workflows/docs-pages.yml');
|
||||||
|
const docsPagesWorkflow = readFileSync(docsPagesWorkflowPath, 'utf8');
|
||||||
const makefilePath = resolve(__dirname, '../Makefile');
|
const makefilePath = resolve(__dirname, '../Makefile');
|
||||||
const makefile = readFileSync(makefilePath, 'utf8');
|
const makefile = readFileSync(makefilePath, 'utf8');
|
||||||
const packageJsonPath = resolve(__dirname, '../package.json');
|
const packageJsonPath = resolve(__dirname, '../package.json');
|
||||||
@@ -38,6 +40,14 @@ test('stable release workflow excludes prerelease beta and rc tags', () => {
|
|||||||
assert.match(releaseWorkflow, /tags:\s*\n(?:.*\n)*\s*-\s*'!v\*-rc\.\*'/);
|
assert.match(releaseWorkflow, /tags:\s*\n(?:.*\n)*\s*-\s*'!v\*-rc\.\*'/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('stable release tags publish docs and prereleases do not update stable docs', () => {
|
||||||
|
assert.match(docsPagesWorkflow, /tags:\s*\n\s*-\s*'v\*'/);
|
||||||
|
assert.match(docsPagesWorkflow, /github\.ref_name/);
|
||||||
|
assert.match(docsPagesWorkflow, /\^v\[0-9\]\+\\\.\[0-9\]\+\\\.\[0-9\]\+\$/);
|
||||||
|
assert.match(docsPagesWorkflow, /bun run docs:build:versioned/);
|
||||||
|
assert.doesNotMatch(docsPagesWorkflow, /beta/);
|
||||||
|
});
|
||||||
|
|
||||||
test('publish release forces an existing draft tag release to become public', () => {
|
test('publish release forces an existing draft tag release to become public', () => {
|
||||||
assert.ok(releaseWorkflow.includes('--draft=false'));
|
assert.ok(releaseWorkflow.includes('--draft=false'));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
"sourceMap": false
|
"sourceMap": false
|
||||||
},
|
},
|
||||||
"include": ["src/**/*", "launcher/**/*.ts", "scripts/*.ts"],
|
"include": ["src/**/*", "launcher/**/*.ts", "scripts/*.ts"],
|
||||||
"exclude": ["node_modules", "dist", "vendor"]
|
"exclude": ["node_modules", "dist", "vendor", "scripts/*.test.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user