fix(website): ignore release-only screenshot metadata drift

This commit is contained in:
Bailey Dixon
2026-09-12 19:53:48 -04:00
parent 269a2b5b36
commit 1c59f44ad7
21 changed files with 120 additions and 7 deletions
+18
View File
@@ -8,6 +8,15 @@ on:
- "assets/screenshots/03_voice.png"
- "assets/screenshots/06_manage.png"
- "docs/media/screenshots.json"
- "docs/media/desktop-ui-screenshots.json"
- "assets/screenshots/desktop-ui/**"
- "desktop/tray/ui/**"
- "desktop/tray/scripts/*.mjs"
- "desktop/tray/package-lock.json"
- "desktop/tray/index.html"
- "desktop/tray/icons/icon-256.png"
- "desktop/src/endpoint.ts"
- "desktop/src/transportSecurity.ts"
- ".github/workflows/ci-website.yml"
push:
branches: [main, dev]
@@ -17,6 +26,15 @@ on:
- "assets/screenshots/03_voice.png"
- "assets/screenshots/06_manage.png"
- "docs/media/screenshots.json"
- "docs/media/desktop-ui-screenshots.json"
- "assets/screenshots/desktop-ui/**"
- "desktop/tray/ui/**"
- "desktop/tray/scripts/*.mjs"
- "desktop/tray/package-lock.json"
- "desktop/tray/index.html"
- "desktop/tray/icons/icon-256.png"
- "desktop/src/endpoint.ts"
- "desktop/src/transportSecurity.ts"
- ".github/workflows/ci-website.yml"
permissions:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 81 KiB

@@ -22,13 +22,24 @@ export const desktopUiScreenshotSourceFiles = Object.freeze([
const binaryExtensions = new Set(['.png'])
function normalizedSource(relativePath, bytes) {
if (binaryExtensions.has(extname(relativePath))) return bytes
const text = bytes.toString('utf8').replace(/\r\n?/g, '\n')
if (relativePath !== 'desktop/tray/package-lock.json') return Buffer.from(text, 'utf8')
// The screenshot fixture has fixed display versions. A release-only bump of
// the root package does not change it; dependency versions still affect rendering.
const lockfile = JSON.parse(text)
delete lockfile.version
if (lockfile.packages?.['']) delete lockfile.packages[''].version
return Buffer.from(JSON.stringify(lockfile), 'utf8')
}
export async function computeDesktopUiSourceFingerprint(repositoryRoot = defaultRepositoryRoot) {
const hash = createHash('sha256')
for (const relativePath of desktopUiScreenshotSourceFiles) {
const bytes = await readFile(resolve(repositoryRoot, relativePath))
const normalized = binaryExtensions.has(extname(relativePath))
? bytes
: Buffer.from(bytes.toString('utf8').replace(/\r\n?/g, '\n'), 'utf8')
const normalized = normalizedSource(relativePath, bytes)
hash.update(relativePath)
hash.update('\0')
hash.update(normalized)
@@ -36,7 +47,7 @@ export async function computeDesktopUiSourceFingerprint(repositoryRoot = default
}
return {
algorithm: 'sha256',
normalization: 'text-lf-v1',
normalization: 'text-lf-lockfile-root-version-v2',
digest: hash.digest('hex'),
files: [...desktopUiScreenshotSourceFiles]
}
@@ -0,0 +1,68 @@
import assert from 'node:assert/strict'
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, dirname, join, resolve } from 'node:path'
import test from 'node:test'
import { computeDesktopUiSourceFingerprint, desktopUiScreenshotSourceFiles } from './desktop-ui-source-fingerprint.mjs'
async function fixture(t) {
const root = await mkdtemp(join(tmpdir(), 'hermes-desktop-fingerprint-'))
t.after(async () => {
assert.equal(dirname(resolve(root)), resolve(tmpdir()))
assert.ok(basename(root).startsWith('hermes-desktop-fingerprint-'))
await rm(root, { recursive: true, force: true })
})
for (const file of desktopUiScreenshotSourceFiles) {
const destination = join(root, file)
await mkdir(dirname(destination), { recursive: true })
await writeFile(destination, 'fixture\n')
}
const lockfile = {
name: '@hermes-relay/tray-ui', version: '0.4.0-beta.6', lockfileVersion: 3,
packages: {
'': { name: '@hermes-relay/tray-ui', version: '0.4.0-beta.6', dependencies: { react: '^19.0.0' } },
'node_modules/react': { version: '19.0.0', integrity: 'example-integrity' },
},
}
const saveLock = () => writeFile(join(root, 'desktop/tray/package-lock.json'), JSON.stringify(lockfile, null, 2))
await saveLock()
return { root, lockfile, saveLock, fingerprint: () => computeDesktopUiSourceFingerprint(root) }
}
test('root release versions do not invalidate fixed-version screenshots', async t => {
const f = await fixture(t)
const before = await f.fingerprint()
f.lockfile.version = '0.4.0-beta.7'
f.lockfile.packages[''].version = '0.4.0-beta.7'
await f.saveLock()
assert.deepEqual(await f.fingerprint(), before)
})
test('dependency versions and integrity remain fingerprinted', async t => {
const f = await fixture(t)
const before = (await f.fingerprint()).digest
f.lockfile.packages['node_modules/react'].version = '19.1.0'
await f.saveLock()
const changedVersion = (await f.fingerprint()).digest
assert.notEqual(changedVersion, before)
f.lockfile.packages['node_modules/react'].integrity = 'changed-integrity'
await f.saveLock()
assert.notEqual((await f.fingerprint()).digest, changedVersion)
})
test('production UI changes still invalidate screenshots', async t => {
const f = await fixture(t)
const before = (await f.fingerprint()).digest
await writeFile(join(f.root, 'desktop/tray/ui/App.tsx'), 'changed UI\n')
assert.notEqual((await f.fingerprint()).digest, before)
})
test('text line endings do not cause platform-only drift', async t => {
const f = await fixture(t)
const before = await f.fingerprint()
for (const file of desktopUiScreenshotSourceFiles.filter(file => !file.endsWith('.png'))) {
const path = join(f.root, file)
await writeFile(path, (await readFile(path, 'utf8')).replace(/\n/g, '\r\n'))
}
assert.deepEqual(await f.fingerprint(), before)
})
+2 -2
View File
@@ -50,8 +50,8 @@
],
"sourceFingerprint": {
"algorithm": "sha256",
"normalization": "text-lf-v1",
"digest": "38253cb9fb1124a629625ea0bc5be09a4f61af91e3fca0103cbb12a5279aae54",
"normalization": "text-lf-lockfile-root-version-v2",
"digest": "ed01f2a0d65cd152c271bc37f9c6b0e555ca0e1306ec09c7638cdcbe283ed5eb",
"files": [
"desktop/tray/ui/App.tsx",
"desktop/tray/ui/main.tsx",
+16
View File
@@ -57,6 +57,22 @@ The same scripts generate and verify smaller WebP derivatives used by the
responsive `srcset` declarations. The full-resolution PNG files remain the
canonical fallback; do not hand-edit the WebP variants.
Desktop UI screenshots are captured from the production React surface with fixed
public fixtures. From the repository root, refresh them with:
```bash
node desktop/tray/scripts/capture-desktop-ui.mjs
node website/scripts/desktop-ui-assets.mjs sync
```
The capture command updates `docs/media/desktop-ui-screenshots.json` only after
the scenes succeed. Its fingerprint includes UI sources, fixtures and dependency
versions/integrity. The tray lockfile's two root-package `version` fields are
excluded because the fixture pins its display versions; a release-only version
bump does not invalidate the images. Changes to fixture versions still invalidate
the fingerprint. `npm run assets:check` tests this distinction before checking
the canonical images and website derivatives.
## Coolify
Deploy the website and VitePress guide with their repository-owned Dockerfile.
+1 -1
View File
@@ -8,7 +8,7 @@
},
"scripts": {
"assets:sync": "node scripts/product-assets.mjs sync && node scripts/responsive-assets.mjs sync && node scripts/desktop-ui-assets.mjs sync",
"assets:check": "node scripts/product-assets.mjs check && node scripts/responsive-assets.mjs check && node scripts/desktop-ui-assets.mjs check",
"assets:check": "node --test ../desktop/tray/scripts/desktop-ui-source-fingerprint.test.mjs && node scripts/product-assets.mjs check && node scripts/responsive-assets.mjs check && node scripts/desktop-ui-assets.mjs check",
"locales:validate": "python ../scripts/check-website-locales.py",
"dev": "npm run assets:check && npm run locales:validate && astro dev",
"build": "npm run assets:check && npm run locales:validate && astro check && astro build && node scripts/check-built-site.mjs",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 81 KiB