Documentation versions
Each release keeps the manual and appearance shipped with the app.
- Current documentation ' + ''.join(links) + '
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8338c48..4d46a9c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,10 +21,22 @@ jobs: steps: - name: Check out repository uses: actions/checkout@v7 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci --no-audit --no-fund - name: Validate documentation run: npm run check:docs - - name: Build and upload site - uses: withastro/action@v6 + - run: npm run test:archives + - run: npm run build + - name: Assemble frozen release manuals + run: npm run assemble:archives + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/upload-pages-artifact@v3 + with: + path: dist deploy: needs: build diff --git a/README.md b/README.md index bc0ee3b..b175ee2 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,21 @@ npm run check Small corrections can be made with the **Edit page** link on the published site. For larger changes, read [CONTRIBUTING.md](CONTRIBUTING.md) and open a pull request. + +## Manuals shipped with Vizard + +`npm run build:release -- vX.Y.Z` builds a complete manual at +`/vizard-docs/releases/vX.Y.Z/`, with release identity, local search and a hashed +file inventory in `dist/manifest.json`. The app build captures newest docs main +once and keeps the generated bundle for every platform and release retry. + +Current docs continue deploying on main pushes. App releases upload the already +built `manual.zip` and `manual.zip.json` to matching docs releases. Deployment +runs `npm run assemble:archives` after building current docs, copying those +verified files unchanged into `dist/releases`. Never rebuild/restyle old manuals +or replace published release tags/assets. The `/versions/` page links to current +and archived manuals; links from installed manuals are marked online. + +Run `npm run test:archives` to check preservation and failure behavior. To deploy +current docs explicitly: `gh workflow run deploy.yml --repo plmn95/vizard-docs`. +The app repository owns publication credentials and the build/release procedure. diff --git a/astro.config.mjs b/astro.config.mjs index e4dad1b..7f80bb7 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -7,7 +7,7 @@ const isVercel = process.env.VERCEL === '1'; // https://astro.build/config export default defineConfig({ site: isVercel ? 'https://vizard-docs.vercel.app' : 'https://plmn95.github.io', - base: isVercel ? '/' : '/vizard-docs', + base: process.env.VIZARD_DOCS_BASE || (isVercel ? '/' : '/vizard-docs'), integrations: [ starlight({ title: 'Vizard Documentation', @@ -18,6 +18,7 @@ export default defineConfig({ ], components: { SiteTitle: './src/components/SiteTitle.astro', + Footer: './src/components/ManualFooter.astro', }, editLink: { baseUrl: 'https://github.com/plmn95/vizard-docs/edit/main/', diff --git a/package.json b/package.json index 61b296d..afc6fe6 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,10 @@ "check": "npm run check:docs && npm run build", "build": "astro build", "preview": "astro preview", - "astro": "astro" + "astro": "astro", + "build:release": "node scripts/build-release.mjs", + "assemble:archives": "python3 scripts/assemble-archives.py", + "test:archives": "python3 scripts/test-archives.py" }, "dependencies": { "@astrojs/starlight": "^0.41.10", diff --git a/scripts/assemble-archives.py b/scripts/assemble-archives.py new file mode 100644 index 0000000..4090b56 --- /dev/null +++ b/scripts/assemble-archives.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Assemble frozen release assets after building the current site.""" +import hashlib +import html +import json +from pathlib import Path, PurePosixPath +import re +import subprocess +import tempfile +import zipfile + +repo = 'plmn95/vizard-docs' +root = Path('dist') + +def gh(*args): + return subprocess.check_output(['gh', *args], text=True) + +def sha(data): + return hashlib.sha256(data).hexdigest() + +pages = json.loads(gh('api', '--paginate', '--slurp', f'repos/{repo}/releases?per_page=100')) +releases = [r for page in pages for r in page if not r['draft']] +links = [] +for release in releases: + version = release['tag_name'] + if not re.fullmatch(r'v[0-9]+\.[0-9]+\.[0-9]+(?:-[A-Za-z0-9.-]+)?', version): + continue + names = {a['name'] for a in release['assets']} + if not {'manual.zip', 'manual.zip.json'} <= names: + raise RuntimeError(f'Release {version} is missing its manual. Refusing to remove it from the site.') + with tempfile.TemporaryDirectory() as tmp: + for name in ('manual.zip', 'manual.zip.json'): + gh('release', 'download', version, '--repo', repo, '--pattern', name, '--dir', tmp) + archive = Path(tmp, 'manual.zip') + record = json.loads(Path(tmp, 'manual.zip.json').read_text()) + if sha(archive.read_bytes()) != record['sha256'] or record['appVersion'] != version: + raise RuntimeError(f'Archive checksum/identity mismatch: {version}') + destination = root / 'releases' / version + if destination.exists(): + raise RuntimeError(f'Archive path already exists: {destination}; build a clean site first') + with zipfile.ZipFile(archive) as z: + seen = set() + for entry in z.infolist(): + name = entry.filename + if name in seen or name.startswith('/') or any(p in ('', '.', '..') for p in name.split('/')) or '\\' in name or ':' in name or (entry.external_attr >> 16) & 0o170000 == 0o120000: + raise RuntimeError('Unsafe archive entry') + seen.add(name) + data = z.read('manifest.json') + manifest = json.loads(data) + if sha(data) != record['manifestSha256'] or manifest['docsCommit'] != record['docsCommit'] or manifest['appVersion'] != version or manifest.get('localPreview') or manifest['basePath'] != f'/vizard-docs/releases/{version}/': + raise RuntimeError(f'Manifest mismatch: {version}') + if seen != set(manifest['files']) | {'manifest.json'}: + raise RuntimeError('Archive file inventory mismatch') + for name, expected in manifest['files'].items(): + if sha(z.read(name)) != expected: + raise RuntimeError('File checksum mismatch: ' + name) + z.extractall(destination) + links.append(f'
Each release keeps the manual and appearance shipped with the app.
Manual for Vizard {version}. Corrections are made in the latest documentation; check the current source before editing.
} + Documentation versions (online) +