feat(about): add changelog drawer, latest-version check, and commit hash

Maintain a CHANGELOG.md (Keep a Changelog format) at the repo root and surface
it on the admin About page:

- The page now shows the running build's short commit hash next to the version,
  linked to the GitHub commit. Commit is injected at build time via a new
  resolveAppCommit() (ZPAN_APP_COMMIT -> WORKERS_CI_COMMIT_SHA -> git rev-parse),
  wired through vite/tsup defines, the node entry, Docker, and CI.
- A new admin-only GET /api/system/changelog endpoint fetches CHANGELOG.md from
  master on GitHub, caches it, parses the latest released version, and reports
  whether an update is available (semver compare against the running version).
- The About page renders a "latest version" row with an update-available badge
  and a side drawer that displays the changelog markdown.

Tests cover the semver compare, changelog parse/fetch caching, the API wrapper,
and the route (admin-gated, parsed payload).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
saltbo
2026-06-09 13:38:29 -04:00
co-authored by Claude Opus 4.8
parent 5a5583b4ed
commit e700fd4977
25 changed files with 455 additions and 27 deletions
+16
View File
@@ -1,3 +1,4 @@
import { execFileSync } from 'node:child_process'
import { readFileSync } from 'node:fs'
export function resolveAppVersion() {
@@ -11,3 +12,18 @@ export function resolveAppVersion() {
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
return pkg.version
}
export function resolveAppCommit() {
// Release/Docker builds pass the exact SHA via ZPAN_APP_COMMIT; Cloudflare
// Workers Builds injects WORKERS_CI_COMMIT_SHA. Local dev has neither, so fall
// back to git. Returns the short SHA, or '' when no source is available.
const explicit = process.env.ZPAN_APP_COMMIT ?? process.env.WORKERS_CI_COMMIT_SHA
if (explicit) {
return explicit.slice(0, 7)
}
try {
return execFileSync('git', ['rev-parse', '--short', 'HEAD'], { encoding: 'utf8' }).trim()
} catch {
return ''
}
}