check:shots compares bundle content, not dates; retake the stale shots
Some checks are pending
build-gate / build (push) Waiting to run
A retake that came out byte-identical left the PNG unchanged, git dated it by
its old commit, and the gate called a correct, just-taken picture stale. No
clock can see that case. readme-shots now writes shot-bundles.json (shot ->
sha256 of the bundle it was taken from), and check:shots compares that hash
with the bundle built now; dates are only the fallback for unrecorded shots.
Negative-tested: one byte appended to the doxa-simple-map bundle fails exactly
the three cap2 shots; restoring it passes.
Also retook the eight shots a rebuild had made stale since e2beec9.
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1,018 KiB |
42
documentation/readme-images/shot-bundles.json
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"cap1-single-element": {
|
||||
"bundle": "all-people-groups",
|
||||
"sha256": "d48c17dc369294301d17a4d29fc517cc7c2c2b18561b6d416ae02a9fbfb60c33"
|
||||
},
|
||||
"cap2-adoption": {
|
||||
"bundle": "doxa-simple-map",
|
||||
"sha256": "a94db2c47d598dbc59daec6903afaac93862add9c0b7a532891b9c3c0608ce02"
|
||||
},
|
||||
"cap2-engagement": {
|
||||
"bundle": "doxa-simple-map",
|
||||
"sha256": "a94db2c47d598dbc59daec6903afaac93862add9c0b7a532891b9c3c0608ce02"
|
||||
},
|
||||
"cap2-prayer": {
|
||||
"bundle": "doxa-simple-map",
|
||||
"sha256": "a94db2c47d598dbc59daec6903afaac93862add9c0b7a532891b9c3c0608ce02"
|
||||
},
|
||||
"cap3-monolithic-research": {
|
||||
"bundle": "doxa-research-map",
|
||||
"sha256": "eb305958dffd4a067de6f00155c3f396452adfc6d20e842cc9ae4f921e54f44a"
|
||||
},
|
||||
"cap4-upg100-list": {
|
||||
"bundle": "my-upg-100-list",
|
||||
"sha256": "ff98986714934370a76742e3aa492a15ce67b959888f03092a453595103313d2"
|
||||
},
|
||||
"cap4-upg100-mfe": {
|
||||
"bundle": "my-upg-100-list",
|
||||
"sha256": "ff98986714934370a76742e3aa492a15ce67b959888f03092a453595103313d2"
|
||||
},
|
||||
"cap4-upg100-poster-wall": {
|
||||
"bundle": "my-upg-100-list",
|
||||
"sha256": "ff98986714934370a76742e3aa492a15ce67b959888f03092a453595103313d2"
|
||||
},
|
||||
"cap5-doxa-maps-spa": {
|
||||
"bundle": "doxa-maps-spa",
|
||||
"sha256": "c7b1aad1e3bd4bbe936858f2159c566c1ef2e24fc9636a3446b1e10723d86502"
|
||||
},
|
||||
"cap5-plugin-studio": {
|
||||
"bundle": "plugin-studio",
|
||||
"sha256": "c3d32d94d2df407fc35cea91faf0d46670abf52a63d37000486760361a588a5c"
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,11 @@
|
|||
* Shots whose path is not a /doxa-maps/ leaf (the synthetic iframe host) are REPORTED as skipped
|
||||
* rather than dropped — an unchecked shot must be visible, not invisible.
|
||||
*
|
||||
* CONTENT FIRST, CLOCKS ONLY AS A FALLBACK. readme-shots writes shot-bundles.json: for each shot,
|
||||
* the sha256 of the bundle it was taken from. A matching hash means the picture shows exactly the
|
||||
* bundle built now; a different one means it is stale. No date is involved, so a clone, a rebase
|
||||
* or a retake that came out byte-identical cannot fool it. Shots with no record yet fall back to:
|
||||
*
|
||||
* WHICH CLOCK: GIT, NOT THE FILESYSTEM, for any file that is committed and unmodified.
|
||||
* This compared mtimes, and a clone (or a new worktree) writes files in whatever order git checks
|
||||
* them out — so every fresh checkout failed, pristine main included (11 shots "2s behind" their
|
||||
|
|
@ -33,11 +38,12 @@
|
|||
* tarball) falls back to its mtime, because then the working copy IS the newest version.
|
||||
*
|
||||
* NEGATIVE-TESTED: appending a byte to public/doxa-maps/doxa-simple-map/doxa-simple-map.js makes
|
||||
* this FAIL naming the three cap2-* shots (a dirty file is dated by mtime); reverting it returns it
|
||||
* to PASS. A bare `touch` no longer fails it — a touch changes no content, so nothing is stale.
|
||||
* this FAIL naming exactly the three cap2-* shots (their recorded hash no longer matches);
|
||||
* reverting it returns it to PASS. A bare `touch` does not fail it: it changes no content.
|
||||
*/
|
||||
import { readFileSync, existsSync, statSync } from 'node:fs'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
|
|
@ -51,6 +57,9 @@ if (!existsSync(SHOTS_SRC) || !existsSync(OUT)) {
|
|||
process.exit(0)
|
||||
}
|
||||
|
||||
const SHOT_BUNDLES = join(OUT, 'shot-bundles.json')
|
||||
const recorded = existsSync(SHOT_BUNDLES) ? JSON.parse(readFileSync(SHOT_BUNDLES, 'utf8')) : {}
|
||||
|
||||
// Dirty = modified or untracked under the two trees compared. null = git unavailable -> mtimes only.
|
||||
const git = (...a) => execFileSync('git', ['-C', ROOT, ...a], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
|
||||
let dirty = null
|
||||
|
|
@ -86,6 +95,18 @@ for (const { name, path } of shots) {
|
|||
if (!existsSync(bundle)) { skipped.push(`${name} (bundle ${m[1]} not built)`); continue }
|
||||
|
||||
checked++
|
||||
// FIRST CHOICE: content, not clocks. readme-shots records the sha256 of the bundle each picture
|
||||
// was taken from. Same hash = the picture shows exactly this bundle, however old either file's
|
||||
// date. A different hash = the bundle changed since, so the picture is stale. This also covers
|
||||
// a retake that came out byte-identical, which no clock can see: git dates an unchanged PNG by
|
||||
// its old commit, and the gate called a correct, just-taken picture stale.
|
||||
const rec = recorded[name]
|
||||
if (rec && rec.bundle === m[1]) {
|
||||
const now = createHash('sha256').update(readFileSync(bundle)).digest('hex')
|
||||
if (now !== rec.sha256) stale.push(`${name}.png was shot from a different ${m[1]} bundle than the one built now`)
|
||||
continue
|
||||
}
|
||||
// FALLBACK, for a shot with no record yet: clocks.
|
||||
const b = stamp(bundle), p = stamp(png)
|
||||
if (b.ms > p.ms) {
|
||||
stale.push(`${name}.png is OLDER than the bundle it shows (${m[1]}) — ` +
|
||||
|
|
@ -109,4 +130,4 @@ if (missing.length || stale.length) {
|
|||
console.error(' A stale screenshot looks exactly like a current one — that is why this is a gate.')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('SHOT-FRESHNESS: PASS — every screenshot is newer than the bundle it shows.')
|
||||
console.log('SHOT-FRESHNESS: PASS — every screenshot shows the bundle that is built now.')
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@
|
|||
*/
|
||||
import { chromium } from 'playwright'
|
||||
import { createServer } from 'node:http'
|
||||
import { readFile, stat, mkdir } from 'node:fs/promises'
|
||||
import { readFile, stat, mkdir, writeFile } from 'node:fs/promises'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { join, extname, resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
|
|
@ -86,6 +88,10 @@ const browser = await chromium.launch({ args: [
|
|||
|
||||
// ★ A SHOT THAT FAILS MUST NOT SILENTLY PRODUCE A README IMAGE. Collected and asserted at the end.
|
||||
const failures = []
|
||||
// documentation/readme-images/shot-bundles.json: shot name -> the sha256 of the bundle it was shot
|
||||
// from. Merged, so an ONLY= run updates just the shots it took.
|
||||
const SHOT_BUNDLES = resolve(HERE, '..', 'documentation', 'readme-images', 'shot-bundles.json')
|
||||
const shotBundles = existsSync(SHOT_BUNDLES) ? JSON.parse(readFileSync(SHOT_BUNDLES, 'utf8')) : {}
|
||||
const ONLY = process.env.ONLY ? process.env.ONLY.split(',') : null
|
||||
|
||||
// mapbox canvas selector to wait on (leaf pages mount a custom element with a canvas)
|
||||
|
|
@ -183,6 +189,13 @@ async function shot(name, path, {
|
|||
if (clip) opts.clip = clip
|
||||
await page.screenshot(opts)
|
||||
console.log(' OK', name + '.png', `<- ${path} (${ready} elements)`)
|
||||
// Record WHICH BUNDLE this picture is of, by content hash, so check:shots can tell a current
|
||||
// picture from a stale one without trusting any clock (see SHOT_BUNDLES below).
|
||||
const lm = path.match(/^\/doxa-maps\/([^/]+)\//)
|
||||
const bundleJs = lm && join(ROOT, 'doxa-maps', lm[1], `${lm[1]}.js`)
|
||||
if (bundleJs && existsSync(bundleJs)) {
|
||||
shotBundles[name] = { bundle: lm[1], sha256: createHash('sha256').update(readFileSync(bundleJs)).digest('hex') }
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(' ERR', name, e.message.split('\n')[0])
|
||||
failures.push(name + ': ' + e.message.split('\n')[0])
|
||||
|
|
@ -361,6 +374,7 @@ if (process.env.DXLIFE_URL) {
|
|||
|
||||
await browser.close()
|
||||
server.close()
|
||||
await writeFile(SHOT_BUNDLES, JSON.stringify(Object.fromEntries(Object.entries(shotBundles).sort()), null, 1) + '\n')
|
||||
|
||||
// ★ EXIT NON-ZERO ON ANY FAILED SHOT. Previously this printed DONE unconditionally, so a run
|
||||
// where every map came back blank still reported success and the stale PNGs stayed in documentation/.
|
||||
|
|
|
|||