#!/usr/bin/env bun /** * Generate changelog section in webpage/index.html from src/lib/data/changelog.json * This ensures a single source of truth for release information */ import { readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; const ROOT_DIR = join(import.meta.dir, '..'); const CHANGELOG_PATH = join(ROOT_DIR, 'src/lib/data/changelog.json'); const INDEX_PATH = join(ROOT_DIR, 'webpage/index.html'); interface ChangelogEntry { version: string; date: string; changes: Array<{ type: 'feature' | 'fix'; text: string }>; imageTag: string; } // SVG icons for change types const FEATURE_SVG = ``; const FIX_SVG = ``; const TOGGLE_SVG = ``; const COPY_SVG = ``; function formatDate(dateStr: string): string { const date = new Date(dateStr); return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); } function generateChangeItem(change: { type: 'feature' | 'fix'; text: string }): string { const pillClass = change.type === 'feature' ? 'changelog-pill-feature' : 'changelog-pill-fix'; const svg = change.type === 'feature' ? FEATURE_SVG : FIX_SVG; const label = change.type === 'feature' ? 'New' : 'Fix'; return `
  • ${svg}${label}${change.text}
  • `; } function generateLatestEntry(entry: ChangelogEntry): string { const changes = entry.changes.map(generateChangeItem).join('\n'); const version = entry.version.startsWith('v') ? entry.version : `v${entry.version}`; return `

    ${version}

    Latest
    ${formatDate(entry.date)}
    Docker image: ${entry.imageTag} or fnsys/dockhand:latest
    `; } function generateCollapsibleEntry(entry: ChangelogEntry): string { const changes = entry.changes.map(generateChangeItem).join('\n'); const version = entry.version.startsWith('v') ? entry.version : `v${entry.version}`; return `

    ${version}

    ${TOGGLE_SVG}
    ${formatDate(entry.date)}
    Docker image: ${entry.imageTag}
    `; } function generateChangelogSection(entries: ChangelogEntry[]): string { if (entries.length === 0) { return ''; } const [latest, ...rest] = entries; const latestHtml = generateLatestEntry(latest); const restHtml = rest.map(generateCollapsibleEntry).join('\n'); return `

    Release history

    Track our progress and see what's new in each version. Spoiler: it gets better every time.

    ${latestHtml} ${restHtml}
    `; } // Read changelog.json console.log('Reading changelog from:', CHANGELOG_PATH); const changelog: ChangelogEntry[] = JSON.parse(readFileSync(CHANGELOG_PATH, 'utf-8')); console.log(`Found ${changelog.length} changelog entries`); // Read index.html console.log('Reading index.html from:', INDEX_PATH); let indexHtml = readFileSync(INDEX_PATH, 'utf-8'); // Generate new changelog section const newChangelogSection = generateChangelogSection(changelog); // Replace changelog section using regex // Match from "" to the closing "" before "" const changelogRegex = / [\s\S]*?<\/section>(?=\s*\n\s*)/; if (!changelogRegex.test(indexHtml)) { console.error('ERROR: Could not find changelog section in index.html'); console.error('Looking for pattern: ... followed by '); process.exit(1); } indexHtml = indexHtml.replace(changelogRegex, newChangelogSection); // Also update softwareVersion in JSON-LD schema if (changelog.length > 0) { const latestVersion = changelog[0].version; // Match "softwareVersion": "X.X" or "softwareVersion": "X.X.X" const versionRegex = /"softwareVersion":\s*"[\d.]+"/; if (versionRegex.test(indexHtml)) { indexHtml = indexHtml.replace(versionRegex, `"softwareVersion": "${latestVersion}"`); console.log(`Updated softwareVersion to: ${latestVersion}`); } } // Write back to index.html writeFileSync(INDEX_PATH, indexHtml); console.log(''); console.log('Generated changelog in webpage/index.html'); console.log(` - Latest version: v${changelog[0]?.version || 'unknown'}`); console.log(` - Total entries: ${changelog.length}`);