Keep unknown failures red, rotate evidence per attempt, and emit receipts for signature-confirmed historical cases. Add CI-only diagnostics and an exact-tag input for the unresolved July hand-off.
The slowest green leg ever recorded is 29 minutes; every cap hit in the
suite's history was a hang, never work. Caps were linux 75 / macos 120 /
windows 240, so a wedged leg burned up to 4 hours of runner time to
report what its log showed in the first minutes. 60 minutes covers the
slowest leg plus cold-cache variance, and every driver-internal bound
(dmg install 45m, AHK 50m, updater wait) still fires before the job cap
in any single-hang scenario, keeping failure diagnostics specific.
The detached-updater wait drops 90m -> 35m on the same evidence: a
working updater finishes far inside 35m; a wedged one never finishes at
any bound, and the longer wait only delayed the report by an hour.
The workflow input descriptions and the skips README still declared
open-app-update and the Setup.exe re-run as driver TODOs; both run now.
Skips have exactly two causes and the prose names them: no OS entry
point for the pair, or the starting release predates the surface. The
chart's TODO label itself stays until the n/a relabel lands with the
known-broken-OLD gate work.
The last declared TODO: a user whose install is stale re-downloads
Hermes-Setup.exe and clicks Install over the existing install, the GUI
twin of re-running the one-liner. Windows shows the full installer UI on
a re-run (the already-installed fast path is macOS-only), so the existing
AHK install drive applies unchanged; install.ps1's repository stage
fetches the existing checkout forward to what main serves, now HEAD.
Invoke-PhaseInstallGui gains an update mode instead of a parallel copy:
the phase label, proof dir, and expected-sha assertion become parameters,
and the update-is-available assert stays install-only. The bootstrap log
rotates before the re-run so the AHK's completion fallback cannot match
the install phase's old completion line.
Windows transcripts were ZERO bytes: ts-prefix.ps1 formatted with
{0:D2}, but Floor() returns a double and the D specifier is
integer-only - it threw per line, and under the driver's relaxed EAP
every line errored into the void. {0:00} fixes it (custom numeric
format works on doubles). Reproduced the exact pipeline locally
(empty file + Format specifier invalid), verified the fix produces
prefixed merged stdout+stderr with exit code intact. That is also
why the log timeline never auto-synced: there was nothing in the
files to sync.
The GitHub artifact URL 307s to /suites/... server-side and strips
the ?zip= query param. The player now reads the zip URL from a
#zip= HASH param (client-side, survives the redirect) with ?zip=
as fallback; the hash path was verified in a real browser against a
real leg zip (auto-fetch + boot).
Per ethie's design, one player artifact for the whole run: new
leg-player job uploads playback.html (archive:false) before the
matrix legs, the report job needs it, and each ran cell gets TWO
links - 📼 to the player with #zip=<that leg's logs zip> and ⬇️ to
the raw zip. Per-leg player uploads removed from all three run
workflows.
Each leg uploads playback.html as a single-file artifact (archive:
false) before the driver runs, so it exists even on failure. The
results chart now links every leg that RAN (pass or fail, not skip)
to its player with ?zip= pointing at that leg's logs artifact.
Leg<->artifact mapping: the generator mints a leg_id per matrix entry
(sanitized matrix name, exported legId()), every run workflow names
its artifacts install-e2e-{player,logs}-<leg-id>, and the report job
feeds the run's artifact name->id list to the results renderer, which
rebuilds the leg id from the parsed job name. GitHub does not link
jobs to artifacts, so the deterministic name is the join key.
Empirical finding: GitHub artifact downloads are auth-gated (the
download URL 307s to /suites/... which is 404 anonymous), so a
locally-opened player page cannot fetch the zip cross-origin. The
player now degrades gracefully: ?zip= fetch failure renders a real
download link for the zip (a normal click carries the user's session)
plus a drag-and-drop / file-picker path, and no-param opens as a pure
drop target. Verified in a real browser against a real artifact URL.
Verified: generator emits leg_id, results renderer emits
✅/❌ [📼](...?zip=...) only on ran cells, npm run check PASS,
install tests 36/36, strict tsc PASS, actionlint x4 PASS.
A static single-file player (tests/install/e2e-assets/playback.html):
?zip=<artifact zip url> unzips in-browser (JSZip), plays the screen
recording with a timer pinned top-left, and renders every *.log with
video<->log sync: the video follows the driver's transcript, clicking
a log line seeks the video. A sync-offset slider aligns the recording
start (ffmpeg comes up first) with the driver's relative clock.
Sync axis: drivers now prefix every transcript line with [+MM:SS]
relative to driver start (ts-prefix.sh / ts-prefix.ps1, pipe-safe
under pipefail / relaxed EAP). Browsers cannot play Matroska, so each
leg remuxes recording.mkv -> recording.mp4 (-c copy, no re-encode)
before the artifact upload, on all three OSes.
Verified end-to-end in a real browser against a generated artifact
zip: zip load, mp4 playback, timer, tab switching, follow-sync at
t=6/t=12, click-to-seek, autoplay policy (expected NotAllowedError on
synthetic play; real clicks fine).
Also fixes the shim fail message's dead variable ( ->
observed_git_url) in both posix drivers.
windows-desktop-gui-e2e.ps1 and windows-installer-script-e2e.ps1 fold
into tests/install/windows-e2e.ps1 with orthogonal -InstallMethod and
-Route axes: the install phase dispatches on one, the update phase on
the other, and shared workroot state carries how OLD landed - so any
implemented update method can follow any implemented install method.
Implementing a new pair is now a driver function plus a gate edit,
never a new job.
The run workflow collapses to ONE inner job whose if: is the
implemented-pairs table. Newly cheap pairs go live with the merge:
desktop-installer@latest -> hermes-update / installer-script /
installer-script+desktop / hermes-desktop-app-update
installer-script(+desktop) -> hermes-desktop-app-update
installer-script+desktop -> open-app-update (the -IncludeDesktop
install registers real Start Menu / Desktop shortcuts)
Only desktop-installer@latest as an UPDATE method stays a declared
TODO. scripts/windows_e2e_harness.ps1 executes the parse/parameter/
dispatch checks under pwsh before any Windows runner spins up.
The composite action .github/actions/e2e-screen-record owns setup and
lifecycle on all three OSes: ffmpeg via apt/brew-verify/winget+cache,
capture via x11grab/gdigrab/avfoundation, mkv at 15fps stopped by 'q'
on live stdin with kill fallback. Linux runners have no display, so
start brings up a dedicated Xvfb :99 and exports DISPLAY - one display
serves both the recorder and any app a later step launches.
Recording moves out of the GUI driver into workflow infrastructure -
that is what makes it uniform - and a missing ffmpeg or a zero-frame
file now FAILS the leg instead of skipping silently: the graceful-skip
path is how the windows leg shipped no recording.mkv while green.
Lifecycle proven locally: start against lavfi testsrc, q-stop, ffprobe
duration check (record-start.sh/record-stop.sh under nix ffmpeg).
The one-liner with its desktop stage opted in (--include-desktop /
-IncludeDesktop) is a real install kind, distinct on both sides:
on windows the stage builds Hermes.exe AND registers Start Menu /
Desktop shortcuts - a second path to a hand-launchable app - while
on linux/macos it builds into the checkout and registers no OS
entry point.
Declared on every OS and driven by both script drivers: the drivers
pass the flag through (hard failure if the ref predates it - the
tag-has-desktop gate already skips pre-desktop tags upstream) and
assert the built app exists under apps/desktop/release afterwards.
The run-workflow gates run +desktop pairs only on desktop-bearing
tags; app-update pairs from +desktop installs stay declared TODOs.
The desktop app has two launch paths, so app-update becomes two
methods. open-app-update starts the app from the OS entry point the
desktop installer created (the installed exe / the .app), so it exists
only where a desktop installer does. hermes-desktop-app-update starts
the app via hermes desktop, which every install method provides on
every OS that ships the desktop app - on linux it is the only app
surface, since no desktop installer or packaged artifact exists there.
Both variants are desktop-surface methods on every OS, so the
tag_has_desktop annotation moves from windows-only to every matrix
entry, install-e2e-run.yml grows the input, and the plan chart marks
pre-desktop cells on all OSes.
The windows GUI arm's implemented pair renames to open-app-update;
every other new combination is a declared TODO that natively skips.
The install.ps1 sibling of installer-script-e2e.sh: stage serve.git
(main parked at OLD, GIT_CONFIG_GLOBAL insteadOf redirect - NOT env
config, which install.ps1 clobbers), run the install.ps1 shipped AT
the OLD ref headless (-SkipSetup -HermesHome/-InstallDir explicit
because the oldest tags predate the HERMES_HOME env override;
-NonInteractive probed from the ref's own script text), assert the
checkout + venv hermes.exe, advance served main, update via
hermes-update (--yes probed) or HEAD's install.ps1, assert HEAD.
install-e2e-windows-run.yml grows a second job for the arm: the
installer-script x {hermes-update, installer-script} pairs flip from
grey to live, app-update from a script install stays a declared TODO.
PS 5.1-safe pure ASCII.
Stage logic verified behaviorally under pwsh (redirect resolves the
canonical URL to serve.git at OLD, per-ref install.ps1 extraction
parses, advance lands HEAD); the install legs themselves need a real
Windows runner - dispatched next.
Per review the unions were overcomplicated. Install methods are now
just: installer-script (the platform one-liner - curl | bash on
linux/macos, irm | iex on windows), desktop-installer, and
packaged-app (declared, unused). Update methods are every install
method (re-run it over the existing install) plus hermes-update and
app-update. desktop-installer-rerun, desktop-app, curl-bash, and
irm-iex are gone as ids; the windows driver's ValidateSet, switch
arms, and both run workflows' gates renamed to match. tsc --checkJs
clean; generator output re-verified (4 linux / 16 windows / 6 macos
legs for 2 tags).
Graph polish + one structural simplification, after the first render
of the combo-box layout:
* Leg names: every combination job's display name is now
'${{ matrix.tag.ref }} -> HEAD' - the box title (job id) already
carries os+methods, so repeating them per leg was noise. The inner
job renders as a short static 'e2e' tail (dynamic names render
unexpanded on skipped jobs, so it must stay static).
* The windows probe job is gone: pick-releases now annotates each
picked tag with whether its tree ships apps/desktop
({ref, desktop} objects in the matrix), and the windows run
workflow gates on the new tag-has-desktop boolean input directly.
One tree listing at pick time replaces N probe jobs, and the
'probe tag' noise disappears from the graph.
Annotation loop verified against the real tag set (pre/post-desktop
split lands exactly at the app's introduction); 16-combo inventory
re-asserted; all four workflows pass actionlint.
Run 31530831547 failed the moment old tags hit the windows leg: the
bootstrap install of v2026.4.30 / v2026.5.29.2 succeeded but no app
window ever appeared - those releases predate the desktop app
(#20059, v2026.5.31), so there is nothing to launch and no Update
button to click. Add a probe job that asks the tag's own tree
(git ls-tree apps/desktop) and gate the run job on it, so
desktop-method legs from pre-desktop tags natively skip instead of
failing. Data-driven - no version cutoff list to rot. Probe logic
verified locally against pre- and post-desktop tags plus the auto
sentinel.
Run 31530831547 showed skipped windows legs as the literal
'${{ inputs.install-method }} -> ...' - GitHub does not evaluate
name expressions for natively skipped jobs. The caller's job name
already carries the method pair, so name the inner job statically.
Two structural changes to the combination fanout:
1. Tags become the OUTER axis, as a sub-graph per starting version:
install-e2e.yml fans a plain matrix over the picked tags into a new
per-tag reusable workflow (install-e2e-tag.yml), which runs the
combination generator for that one tag and fans out one job per
{os, install-method, update-method}. The Actions graph now reads
'from vX -> windows: install -> update' per leg. Nothing is
hardcoded in the workflows: the tag workflow calls the generator
itself.
2. Native skips move to the point that owns the capability knowledge:
macOS combos (no driving workflow exists) grey out in the tag
workflow via install-e2e-skip.yml, untouched by the tag axis; ALL
windows combos dispatch to install-e2e-windows-run.yml, which takes
install-method/update-method inputs and natively skips the pairs
its driver cannot run yet - so implementing a windows method is a
change in the run workflow + driver only. The driver's -Route ids
now match the generator's method ids verbatim.
Generator output shape, route filters, and all error paths re-verified
locally; all four workflows pass actionlint; driver re-parses clean
pure-ASCII.
Run 31523695265 went green but both phases logged '(ffmpeg not on
PATH; skipping screen recording)' and the artifact had no
recording.mkv. Restore the winget install + cache + PATH steps from
the retired axis (same pinned actions/cache SHA those green runs
used), scoped to just ffmpeg since AutoHotkey now comes from the
portable zip inside the driver.
Run 31520267702 died in 3s: 'Missing an argument for parameter
InstallRef'. powershell.exe -File drops a "" argument from the command
line entirely, so the parameter binder saw -InstallRef followed by
-SetupExeUrl. Default both the workflow input and the script parameter
to 'auto' (= newest release tag) instead of empty.
Run 31519103491 failed the 'update genuinely available' assert with
the installer landing on HEAD itself. The staging assumed the website
exe installs a baked release pin, but the bootstrap log shows
Pin { commit: None, branch: main } - the published installer installs
whatever main serves, and serve.git's main was parked at HEAD.
Stage the way the linux axis does: park served main at OLD
(-InstallRef, default newest release tag; threaded through the
reusable workflow as install-ref) for the install phase, assert the
install lands exactly there, then advance main to HEAD in the update
phase - an update becomes available the same way it does for a real
user. allowAnySHA1InWant stays as belt-and-braces for installer builds
that DO bake a pin.
Restructure tek's two-job desktop-windows-e2e.yml into the shape the
linux axis already has: install-e2e.yml keeps its update/installer
routes untouched and windows-desktop returns as a route in the same
family, calling a reusable install-e2e-windows-run.yml.
Behind that route is now ONLY the real user flow - the headless
contract job (install.ps1 at HEAD~1, desktop-update.ps1 -NoUi,
BASE/CURRENT/NEXT ref dance) is gone, along with its driver. Every leg
goes through a surface a user touches: website Hermes-Setup.exe headed
with AutoHotkey clicking Install -> Launch, then the installed
Hermes.exe under Playwright's Electron driver clicking Settings ->
About -> 'Update now', through the detached hand-off to a relaunched
window asserted on HEAD.
The driver drops the synthetic-NEXT staging with the contract job:
serve.git just serves HEAD as main and OLD is the release pin baked
into the website exe - the literal starting point of every real GUI
user, same philosophy as the linux axis's release-tag matrix. The
-Route parameter (desktop today) declares the future update mechanisms
as arms: 'update' (hermes update from the installed venv) and
'installer' (re-run the bootstrap exe) raise until implemented, so the
workflow surface is stable when they land.
The cherry-picked desktop-windows-e2e.yml covers everything the
install-e2e-windows-run.yml axis did and more: the contract job drives
the same desktop-update.ps1 hand-off (plus a CURRENT->NEXT forward
leg), and the GUI job replaces AHK-only driving with the full real
user flow - website Hermes-Setup.exe, clicked Install/Launch, then
Playwright clicking Settings -> About -> 'Update now' in the packaged
app, through the detached hand-off to a relaunched window.
Remove the superseded workflow, its driver, and the AHK/button assets
under tests/install/windows/ (the GUI job's e2e-assets carry the
re-captured templates), and drop the windows-desktop route from
install-e2e.yml's dispatch options.
windows sibling of install-e2e-run.yml. no bubblewrap on windows, so the
git proxying is git's own transport rewrite: an isolated GIT_CONFIG_GLOBAL
with multi-valued url.<file://fake.git>.insteadOf for both hardcoded repo
URLs, so the published Hermes-Setup.exe's install.ps1 clone, hermes update's
fetch, and the desktop's ls-remote all land on a local bare repo whose main
the driver controls - installer and updater run verbatim.
one run: seed fake.git from the checkout, force fake main to the newest
release tag, drive the real published bootstrap installer with AutoHotkey
(GUI, no headless mode), promote fake main to HEAD, then apply the desktop
app's builtin update route (scripts/desktop-update.ps1 -NoUi when the
installed base ships it, staged hermes-setup.exe --update otherwise) and
assert HEAD == target with a working hermes.
TODO routes: bare hermes update, and re-running the bootstrap installer
over the existing checkout.