fix(website): ignore release-only screenshot metadata drift
@@ -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:
|
||||
|
||||
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 82 KiB |
|
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)
|
||||
})
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 81 KiB |