From daf16505d7750c78aae302f6e438fa7d2c7a8384 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Martineau Date: Sat, 17 Oct 2026 10:06:27 -0400 Subject: [PATCH] Each release makes the demo video again, with the screenshots A new website-media skill makes the screenshots and the website's demo video, says what to look at, and commits them: the screenshots in MeshTerm, the video in the site checkout. The release skill runs it after the bump and pushes the site before the tag, so the website job builds the page with the new video. The release preflight also reports the site checkout. Co-Authored-By: Claude Opus 5.5 (1M context) --- .claude/skills/release/SKILL.md | 46 +++---- .claude/skills/release/release_check.py | 32 ++++- .claude/skills/website-media/SKILL.md | 168 ++++++++++++++++++++++++ scripts/README.md | 6 +- 4 files changed, 222 insertions(+), 30 deletions(-) create mode 100644 .claude/skills/website-media/SKILL.md diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md index 1e16a7a..cedb0f8 100644 --- a/.claude/skills/release/SKILL.md +++ b/.claude/skills/release/SKILL.md @@ -135,30 +135,26 @@ alias to the gather step instead. What *does* still carry the number: `docs/cli/README.md` has a sample document with a `"version"` field, which is the live `{version}` placeholder and must show the new one. -### The screenshots — made again, after the bump +### The screenshots and the video — made again, after the bump Every release refreshes the screenshots (JP, 2026-10-16: "make it so the images are -automatically updated when we release"). They show the version in their header, so this -runs **after** `__version__` changes and before the commit: +automatically updated when we release") and the website's demo video (JP, 2026-10-17). +Both show the version in their header, so they run **after** `__version__` changes and +before the commit: follow the **`website-media` skill in release mode**. It runs +`scripts/screenshots.py` (~1 min) then `scripts/video.py` (~4 min), and says what to look +at before anything is committed. -``` -python scripts/screenshots.py -git status --short docs/screenshots -``` +The two land in different repositories: -It drives `--demo` headless and rewrites `docs/screenshots/{terminal,cardputer-zero,picocalc-lyra}/` -(the README's four captures, and the website's three sets) and `docs/screenshots/splash/` -(the website's splash), about a minute in all. The demo is deterministic, so the only -changes should be the version in each header plus whatever the release changed on those -screens. Open one image of each set and look at it before committing: a screen that -changed in a way the changelog doesn't explain is a bug to understand, not a picture to -ship. The images ride in the release commit, so the tag carries them and the `website` -job builds meshterm.net from them. +- The screenshots (`docs/screenshots/`) ride in the release commit, so the tag carries + them and the `website` job builds meshterm.net from them. +- The video, its poster, and its play card go to the site checkout beside this one + (`../meshterm.net/src/`) and get a commit there ("The demo video of MeshTerm X.Y.Z"). The + `website` job builds from the site's `main`, so that commit is **pushed before the tag** + (step 5). -It needs the `dev` extras (Pillow, fontTools, time-machine), the emulator's Terminus font -(`meshterm emulate --fetch-fonts`, once), Windows' Cascadia Mono and Segoe UI fonts, and -the network for the map's tiles the first time. If it fails, stop and fix it, or ask JP -whether to release with last release's images — don't commit a half-written set. +If either fails, stop and fix it, or ask JP whether to release with last release's +images and video — don't commit a half-written set. ## 5. Commit, tag, push @@ -178,12 +174,17 @@ hardcoded one. Seven release commits (0.3.1 through 0.10.0) went out without it this line used to omit it. Commit on `main` — never a topic branch. Then **confirm with JP before pushing**, because -the push is the act that builds and publishes the release: +the push is the act that builds and publishes the release. The site goes first, so the +`website` job finds the new video on its `main`: ``` +git -C ../meshterm.net push origin main git push origin main && git push origin vX.Y.Z ``` +The one confirmation covers both pushes: say that the site push carries the new video +(and any other site commits that are ahead of origin, by subject) when you ask. + This is the one workflow where pushing is expected. It is still asked for, once, in that moment — the standing rule against unasked pushes is not suspended by the skill, it is satisfied by the answer. @@ -196,8 +197,9 @@ gh release view vX.Y.Z ``` Five platform builds, the release job, and `website` — which rebuilds meshterm.net from the -tag so the site shows the new version and the screenshots of step 4 — so it takes a few -minutes. `website` runs the site repo's own `build.py`, so a change to the site's sources +tag so the site shows the new version, the screenshots, and the video of step 4 — so it +takes a few minutes. Once `website` has pushed its "MeshTerm X.Y.Z" commit, bring the +local site up to date: `git -C ../meshterm.net pull --ff-only`. `website` runs the site repo's own `build.py`, so a change to the site's sources must be pushed to `meshterm.net` before the tag, or the job builds with the old ones. If only `website` fails, the release itself is fine (it runs after publishing); the usual cause is the `SITE_DEPLOY_KEY` secret, and once that is fixed `gh run rerun --failed` finishes diff --git a/.claude/skills/release/release_check.py b/.claude/skills/release/release_check.py index 3070f1d..fbf8bbd 100644 --- a/.claude/skills/release/release_check.py +++ b/.claude/skills/release/release_check.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 """Preflight for a MeshTerm release, and the answer to "what's in the next one?". -Reports the six things worth knowing before a version number is written down, and says +Reports the seven things worth knowing before a version number is written down, and says nothing else: the current version and the tag it belongs to, the commits since, whether -there is an `[Unreleased]` section to rename, which changelog link refs are missing, and -whether the working tree is clean and level with `origin/main`. +there is an `[Unreleased]` section to rename, which changelog link refs are missing, +whether the working tree is clean and level with `origin/main`, and the same for the site +checkout beside it, where the release's demo video goes (the `website-media` skill). It changes nothing. Every finding is a line a person acts on, not a gate that blocks. """ @@ -19,13 +20,14 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[3] INIT = ROOT / "meshterm" / "__init__.py" CHANGELOG = ROOT / "CHANGELOG.md" +SITE = ROOT.parent / "meshterm.net" OK, WARN, INFO = "ok ", "warn", " " -def git(*args: str) -> str: - """Run a git command in the repo, returning stripped stdout ('' on failure).""" - done = subprocess.run(["git", "-C", str(ROOT), *args], capture_output=True, text=True) +def git(*args: str, repo: Path = ROOT) -> str: + """Run a git command in ``repo``, returning stripped stdout ('' on failure).""" + done = subprocess.run(["git", "-C", str(repo), *args], capture_output=True, text=True) return done.stdout.strip() if done.returncode == 0 else "" @@ -115,6 +117,24 @@ def main() -> int: behind, ahead = counts.split() say(OK if behind == "0" else WARN, f"{ahead} ahead / {behind} behind origin/main") + print(f"\nSite ({SITE.name}, where the demo video goes)") + if not (SITE / ".git").exists(): + say(WARN, f"no checkout at {SITE}: the video cannot be made") + else: + dirty = git("status", "--porcelain", "--", "src", repo=SITE) + count = len(dirty.splitlines()) + say(OK if not dirty else WARN, "src/ clean" if not dirty else f"src/: {count} change(s)") + git("fetch", "--quiet", "origin", repo=SITE) + counts = git("rev-list", "--left-right", "--count", "origin/main...HEAD", repo=SITE) + if counts: + behind, ahead = counts.split() + say(OK if behind == "0" else WARN, f"{ahead} ahead / {behind} behind origin/main") + if behind != "0": + say(INFO, "pull --ff-only before making the video") + # These go out with the release's site push, before the tag. + for subject in git("log", "--format=%s", "origin/main..HEAD", repo=SITE).splitlines(): + say(INFO, f"ahead: {subject}") + print("\nRun the gates before tagging:") print(" python -m pytest -q && ruff check . && ruff format --check .\n") return 0 diff --git a/.claude/skills/website-media/SKILL.md b/.claude/skills/website-media/SKILL.md new file mode 100644 index 0000000..77abe53 --- /dev/null +++ b/.claude/skills/website-media/SKILL.md @@ -0,0 +1,168 @@ +--- +name: website-media +description: Make the website's demo video and screenshots again from the demo mesh — the terminal video on meshterm.net (scripts/video.py) and the twelve screenshots of the README and the website (scripts/screenshots.py) — check them, and commit them in both repositories. Use when JP asks to make, redo, refresh, or update the video, the demo recording, the screenshots, the website's images, or the website after a visible change; the release skill also runs it after each version bump. +--- + +# Making the website's video and screenshots + +The two always go together (JP, 2026-10-17: "include it with the screenshot updates when we +update the website, manually or automatically when we release"). Both come from the demo +mesh (`meshterm --demo`), headless, and both show the version in their header, so a set +made before a bump is already stale. + +| What | Script | Where it goes | Time | +|---|---|---|---| +| 12 screenshots + 2 splash wordmarks | `scripts/screenshots.py` | MeshTerm `docs/screenshots/` | ~1 min | +| The demo video, its poster, its play card | `scripts/video.py` | meshterm.net `src/demo.mp4`, `src/demo-poster.png`, `src/demo-play.png` | ~4 min | + +**The video is terminal-only, on purpose** (JP, 2026-10-17: "it's marketing after all"). +Don't offer handheld videos again. + +**Why the video lives in the site repository and not in the tag:** it is ~4 MB, and it +changes at every release (the version is on screen). In MeshTerm, that would grow every +clone of the code by 4 MB a release. The site repository already keeps the copy it serves. +The cost is one more push at release time (see *Release mode*). + +## Before you start + +- The site checkout must sit beside this one, at `../meshterm.net`. The video script + writes there by default and stops if the folder is missing. +- The site must be in sync and clean where the video goes: + + ``` + git -C ../meshterm.net fetch -q + git -C ../meshterm.net status -sb + ``` + + If it is behind (the release workflow's `website` job pushes a "MeshTerm X.Y.Z" commit + each release), `git -C ../meshterm.net pull --ff-only`. If `src/demo*` has uncommitted + changes you didn't make, stop and ask. +- Needs: the `dev` extras (Pillow, fontTools, time-machine), the emulator's Terminus font + (`meshterm emulate --fetch-fonts`, once), Windows' Cascadia Mono and Segoe UI fonts, the + network for the map's tiles the first time, and ffmpeg with libx264 on the `PATH`. + +## 1. Run both, one after the other, in the background + +``` +.venv/Scripts/python scripts/screenshots.py +.venv/Scripts/python scripts/video.py +``` + +One background command for both, in this order. **Never in parallel**, and don't run the +test suite beside them: the video's tour runs on the real clock and its painter uses every +core. Both are driven by waits on screen text, so a slow machine only makes them slower. + +The video prints its length and size at the end (`recorded 118.9 s, 189 screens`, then +`demo.mp4 (4001 KB)`). + +## 2. Look at what you made + +A green run is not a correct picture. Check both: + +- **Screenshots:** `git status --short docs/screenshots`, then open one image of each set. + Only the version in the header should change, plus whatever this change set out to + change on those screens. Anything else is a bug to understand, not a picture to ship. +- **Video length:** under **2:00**. The site's caption says "Under two minutes". If it + went over, shorten holds in `_tour` (`p.hold(...)`), never the scenes JP asked for. +- **Video content:** make a contact sheet and look at it: + + ``` + ffmpeg -v error -y -i ../meshterm.net/src/demo.mp4 -vf "fps=1/4,scale=400:-1,tile=6x5" -frames:v 1 /sheet.png + ``` + + The tour, in order (JP chose each part; keep them all): + 1. the main menu; + 2. Chat → Public → the paths of Gabriel's message from Beloeil, selected 1, 2, 1, 2; + 3. Chloé's message arrives live; we type "Hey, I wonder if @[Dave-Lachine] is coming + Saturday?" — the mention turns into a chip when the `]` is typed — and Dave answers + with a mention of us, so his message carries our white ★; + 4. Trace target → Brossard, 2 bytes per hop (our node uses 3; a trace collapses it to + 2), the "5 new records" dialog, then the per-hop table; + 5. the map: pan, ^L to our node, zoom 10 → 16 until street names and buildings show; + 6. Repeater admin on Mont-Royal: Read settings (the video jumps over most of it), then + Send advert; + 7. the live feed, one packet opened, and back to a clean main menu. + + Also open `src/demo-poster.png`: it must show the paths dialog with the **second** path + selected. + +## 3. When it fails + +`video.py` prints `the recording failed (1)` after a traceback. Read the traceback's last +line: a `TimeoutError` carries the whole screen as it was. + +- **A wait never matched** (`'…' did not appear`): a UX string the tour waits for changed, + or a dialog the tour doesn't expect is in the way. The video's own history: the trace + once got a records dialog when the path width changed, and a menu filter "live" matched + Courier first. Fix the tour in `scripts/video.py`, not the app — unless the screen shows + an app bug, which then gets fixed and reported to JP. +- **The trace says "no reply":** the demo's route to that target includes a link the + world doesn't have. Brossard is the target because its route always works; see the + `_tour` comment before changing it. +- **To iterate on painting or encoding** without recording again, pass + `--recording /rec.pickle`: the first run keeps the recording there, a later + run with the same path only paints and encodes. Never keep a recording across a change + to the app. + +If you cannot make it work, stop and ask JP whether to keep last time's video. Never ship +half a set. + +## 4. Commit + +MeshTerm, by name (other sessions share the tree): + +``` +git add docs/screenshots +``` + +The site, the three source files only: + +``` +git -C ../meshterm.net add src/demo.mp4 src/demo-poster.png src/demo-play.png +git -C ../meshterm.net commit -m "The demo video of MeshTerm X.Y.Z" -m "Co-Authored-By: " +``` + +Then one of the two modes. + +### Manual mode — JP asked for the website now + +Commit the screenshots on their own in MeshTerm ("The screenshots show …", what changed). +Then rebuild the site so its `docs/` serves the new files: + +``` +cd ../meshterm.net && ../MeshTerm/.venv/Scripts/python build.py +``` + +`build.py` writes seven derived images again with new bytes even when nothing changed. +Unless the splash art changed (`docs/screenshots/splash/` in MeshTerm), restore them: + +``` +git -C ../meshterm.net checkout -- docs/assets/apple-touch-icon.png docs/assets/logo-96.png docs/assets/og.png docs/assets/social-preview.png docs/assets/splash-narrow.png docs/assets/splash.png docs/favicon.ico +``` + +Commit the built `docs/` with the source files (one commit). **Don't push**: tell JP the +site is committed and ahead of origin. The live site changes only when he pushes it. Note +that the screenshots the site shows come from the MeshTerm checkout, so they are the +working tree's, not a release's. + +### Release mode — called from the release skill, after the bump + +Don't build the site locally: the release workflow's `website` job builds it from the tag +and from the site's `main`. Commit the site's three source files as above, and the +screenshots ride in the release commit (the release skill stages `docs/screenshots`). + +**The site's commit must be pushed before the tag**, or the job builds with last +release's video. The release skill's push step does it, in the same confirmed push. After +the job has run, `git -C ../meshterm.net pull --ff-only` picks up its "MeshTerm X.Y.Z" +commit. + +## The one step no script can do + +MeshTerm's README plays its own copy of the video: a `user-attachments` upload, because +GitHub plays nothing else in a README. GitHub has no API for that upload. After a new +video, say in one line that the README still plays the previous upload; never try to +upload it yourself. If JP drops `../meshterm.net/src/demo.mp4` into a GitHub comment box +and gives you the new URL, swap it in `README.md` (the bare `user-attachments` line near +the top). Its caption on the next line must then say the video comes from the demo mesh: +the one written for the first upload says "made on a real mesh", which is true only of +that upload. diff --git a/scripts/README.md b/scripts/README.md index 4834bf6..7230ab0 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -103,5 +103,7 @@ something slow: the tiles of the map, and most of the read of the repeater's set Thus the video jumps over the wait. Each run gives the same video to the eye, but not the same file, because the camera copies -the screen on the real clock. After a change to the screens, run the script, then rebuild -the website (refer to the README of the `meshterm.net` repository). +the screen on the real clock. Each release makes the video again, after the screenshots, +and pushes it to the website repository before the tag. After a change to the screens +between two releases, run the two scripts, then rebuild the website (refer to the README of +the `meshterm.net` repository).