Skip to content

D790 — The README hero is re-recorded by CI on an orphan branch

Decision. This reopens the "outside CI" consequence of D330. The README's hero is no longer a file of main: the hero workflow (hero.yml, called by release.yml) records the Atlas of each release that changes web/ and force-pushes it as hero.webp, the single commit of the orphan branch readme-assets; the README points at raw.githubusercontent.com/BeppeTemp/cartographer/readme-assets/hero.webp. The recording is deterministic and headless: hero.spec.ts installs Playwright's clock paused before the page loads and produces each frame by advancing it by 1000/HERO_FPS ms (30 by default), stepping the CSS transitions and animations by the same amount, and taking a screenshot at a device scale factor of 2. make hero runs the same path anywhere, into a gitignored hero.webp.

Why. The hero should always show the Atlas of the latest release, with nobody in the loop. D330's recording filmed the compositor in real time in a headed window on a GPU, which no runner has, and a real-time capture on a software renderer is choppy (headless Chromium hands the screencast under 20 frames/s). Stepping a virtual clock makes the picture independent of how fast the machine paints: a slow renderer only makes the capture take longer. The file is 16-25 MB, so committing a new one per UI release would grow every clone by that much each time; on an orphan branch that is force-pushed, the old ones become unreachable. The cost is that the README depends on a branch outside main's history and on a recording nobody reviews in a PR.

Alternatives rejected. - Keep docs/atlas/hero.webp on main, re-recorded by hand. It needs a GPU machine and a person, goes stale between runs, and costs ~16 MB of every clone per recording (the pack was already 71 MB with five versions). - Record the DevTools screencast on the runner. Headless it delivers under 20 frames/s and SwiftShader is slower still; a headed window under a virtual display has no GPU either. The capture would be choppy. - GitHub Pages or a release asset. camo proxies Pages images and answers 404 for a 17 MB file; releases/latest/download/... is 404 because every release is a prerelease. raw.githubusercontent.com is not proxied and renders at any size (verified with gh api markdown). - Push the hero to main from the workflow. main is protected, and its history is what we are keeping light. - Record on every release, or compare with the previous tag. The first costs a runner for nothing when the UI did not change; the second depends on which tag came before and on every release having run. A Web-Tree: trailer on the branch's commit says which web/ the hero was recorded from, so a release records only when its own tree differs. - page.clock.fastForward to halve the renders. It fires due timers at most once, which is not how the page runs; runFor is the faithful step.

Consequences. The suite run of hero.spec.ts (no HERO_RECORD) is unchanged: the fixture, full speed, the same assertions, so a step of the tour that no longer exists still fails CI (D330's "stale loudly"). In a recording every wait goes through one helper that steps frames until its condition holds and fails with the step's name after 30 virtual seconds. Two traps live next to the code that hits them: the fake clock does not drive CSS transitions (stepAnimations), and headless WebGL needs --enable-unsafe-swiftshader, which the Playwright config already passes and the recording keeps. The workflow holds contents: write on GITHUB_TOKEN and nothing else, never pushes to main, is not awaited by anything, and always records when dispatched by hand (the tag must contain this machinery, since it checks out the tag's UI, tour and scripts). publish-hero.sh builds the commit with plumbing in the clone's own object store, not in a scratch repository: the checkout's credentials belong to that clone's git dir. The branch exists before the README switches to it (it was bootstrapped from the PR branch with publish-hero.sh hero.webp main@<sha>), so the README never points at a missing image; the first release that changes web/ replaces it.

Measured on an Apple-silicon Mac: the tour is 517 frames (17.2 s of film at 30 fps), captured in about 5 minutes, 0.4-0.6 s a frame including the warm-up, and two runs on the same build give the same frame count. The encoded WebP is about 25 MB at quality 92 (HERO_QUALITY: 85 gives about 17 MB, 80 about 14 MB), against D330's 16 MB at the same settings: the frames are lossless screenshots, not a JPEG screencast, so more detail survives to the encoder. The first run on ubuntu-latest has to confirm the budget: the job's timeout is 60 minutes, and if it is exceeded HERO_FPS in hero.yml drops to the highest of 24 or 20 that fits, the measured per-frame time going here.