330 lines
17 KiB
YAML
330 lines
17 KiB
YAML
# Reusable runner for ONE Windows install/update combination.
|
|
#
|
|
# One job, two orthogonal axes: tests/install/windows-e2e.ps1 dispatches its
|
|
# install phase on install-method and its update phase on update-method, so
|
|
# implementing a new pair is a driver function + a gate edit here - never a
|
|
# new job. The driver's phases share state via the workroot, and every leg
|
|
# runs the REAL user surface for its methods:
|
|
#
|
|
# desktop-installer@latest the website's Hermes-Setup.exe, downloaded and
|
|
# run headed, AutoHotkey clicks Install ->
|
|
# Launch, the real Electron window must appear.
|
|
# installer-script the irm | iex one-liner: the install.ps1
|
|
# shipped AT the OLD ref, headless.
|
|
# installer-script+desktop the same one-liner with -IncludeDesktop:
|
|
# builds Hermes.exe AND registers Start Menu /
|
|
# Desktop shortcuts.
|
|
# hermes-update venv hermes.exe update.
|
|
# open-app-update the app's own Update button, app launched
|
|
# from the installed exe under Playwright's
|
|
# Electron driver (Settings -> About ->
|
|
# "Update now"); the production hand-off chain
|
|
# runs untouched.
|
|
# hermes-desktop-app-update the same button, app launched via `hermes
|
|
# desktop`: the driver captures the product's
|
|
# own spawn (argv/cwd/env) and re-executes it
|
|
# under Playwright.
|
|
#
|
|
# Method pairs without a driver arm yet NATIVELY SKIP (grey check, no
|
|
# runner): the capability knowledge lives here, next to the driver, so the
|
|
# caller can dispatch every declared combination without knowing which ones
|
|
# work.
|
|
#
|
|
# Call it:
|
|
#
|
|
# jobs:
|
|
# windows:
|
|
# uses: ./.github/workflows/install-e2e-windows-run.yml
|
|
# with:
|
|
# install-method: desktop-installer@latest
|
|
# update-method: open-app-update
|
|
# install-ref: v2026.8.3
|
|
|
|
name: install-e2e windows leg
|
|
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
install-method:
|
|
description: 'How OLD gets installed. Supported: desktop-installer@latest (website exe, AHK-clicked), installer-script (irm | iex install.ps1) and installer-script+desktop (the same with -IncludeDesktop). Declared-but-TODO methods skip.'
|
|
required: true
|
|
type: string
|
|
update-method:
|
|
description: 'How the install updates to HEAD. Supported: open-app-update (Update button under Playwright, from a desktop-bearing install), hermes-desktop-app-update (same button, app launched via hermes desktop), hermes-update, installer-script, installer-script+desktop, desktop-installer@latest (re-download Hermes-Setup.exe, AHK clicks Install over the existing install).'
|
|
required: true
|
|
type: string
|
|
install-ref:
|
|
description: 'Ref to install as OLD (served as main while the installer runs). auto = the newest release tag in the checkout.'
|
|
required: false
|
|
type: string
|
|
default: auto
|
|
update-ref:
|
|
description: "What to update TO: HEAD (the commit under test), or NEXT -- a synthetic child of install-ref that the driver mints, so a leg that installs HEAD still has an update to take (HEAD's own updater)."
|
|
required: false
|
|
type: string
|
|
default: HEAD
|
|
tag-has-desktop:
|
|
description: "Whether install-ref ships the desktop app (apps/desktop). The caller annotates this from the tag's own tree; desktop-method legs from pre-desktop releases natively skip."
|
|
required: false
|
|
type: boolean
|
|
default: true
|
|
leg-id:
|
|
description: "Artifact-safe matrix leg id (from generate-e2e-matrix.mjs legId). Names this leg's logs + player artifacts so the report job can link a row to its zip."
|
|
required: true
|
|
type: string
|
|
setup-exe-url:
|
|
description: 'Bootstrap installer to install OLD with. Default: the latest published one — what a user downloads today.'
|
|
required: false
|
|
type: string
|
|
default: https://hermes-assets.nousresearch.com/Hermes-Setup.exe
|
|
timeout-minutes:
|
|
description: 'Job timeout. The install leg does real toolchain work and the update leg a full Electron rebuild.'
|
|
required: false
|
|
type: number
|
|
# Green legs take up to ~40 min; from v2026.9.24 the update alone runs the venv->PM
|
|
# takeover plus npm ci and a Desktop rebuild (24 min in 108542725043, 11 of them one
|
|
# npm ci), and the app-update driver may wait 30 min. pty-run.py's deadline and the
|
|
# per-step budgets bound real hangs, so the job cap only has to clear a slow runner.
|
|
default: 90
|
|
bundle-manifest-url:
|
|
type: string
|
|
default: ''
|
|
bundle-manifest-sha256:
|
|
description: Pinned transition manifest digest for stable acceptance
|
|
type: string
|
|
default: ''
|
|
bundle-arch:
|
|
type: string
|
|
default: x64
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
bundled-e2e:
|
|
name: packaged bundle (open-app-update)
|
|
if: inputs.install-method == 'packaged-app' && inputs.update-method == 'open-app-update'
|
|
runs-on: ${{ inputs.bundle-arch == 'arm64' && 'windows-latest-32-arm-core' || 'windows-latest-32-core' }}
|
|
timeout-minutes: ${{ inputs.timeout-minutes }}
|
|
steps:
|
|
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7.0.1
|
|
with:
|
|
ref: ${{ github.sha }}
|
|
- name: Set up driver tools only
|
|
uses: ./.github/actions/setup-pm
|
|
with:
|
|
toolchain: all
|
|
packages: ffmpeg
|
|
cache: false
|
|
cache-python: false
|
|
cache-node: false
|
|
- name: Install locked chat driver dependencies
|
|
shell: bash
|
|
run: npm ci --workspace tests-js --include-workspace-root --omit=dev --ignore-scripts --no-audit --no-fund
|
|
- name: Verify native helpers
|
|
shell: pwsh
|
|
run: |
|
|
node --test tests/install/e2e-assets/windows-bundled-helpers.test.mjs
|
|
if ($LASTEXITCODE) { exit $LASTEXITCODE }
|
|
- name: Start recording
|
|
uses: ./.github/actions/e2e-screen-record
|
|
with:
|
|
mode: start
|
|
output: ${{ runner.temp }}/hermes-bundled-update-recording.mkv
|
|
- name: Install, update and observe native relaunch
|
|
shell: pwsh
|
|
env:
|
|
BUNDLE_MANIFEST: ${{ inputs.bundle-manifest-url }}
|
|
BUNDLE_MANIFEST_SHA256: ${{ inputs.bundle-manifest-sha256 }}
|
|
BUNDLE_ARCH: ${{ inputs.bundle-arch }}
|
|
run: |
|
|
powershell -NoProfile -ExecutionPolicy Bypass -File tests/install/windows-bundled-e2e.ps1 -ManifestUrl $env:BUNDLE_MANIFEST -Arch $env:BUNDLE_ARCH
|
|
if ($LASTEXITCODE) { exit $LASTEXITCODE }
|
|
- name: Stop recording
|
|
if: always()
|
|
uses: ./.github/actions/e2e-screen-record
|
|
with:
|
|
mode: stop
|
|
output: ${{ runner.temp }}/hermes-bundled-update-recording.mkv
|
|
- name: Upload native acceptance evidence
|
|
if: always()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: install-e2e-logs-${{ inputs.leg-id }}-bundle-${{ inputs.bundle-arch }}
|
|
path: |
|
|
${{ runner.temp }}/hermes-bundled-update/proof/
|
|
${{ runner.temp }}/hermes-bundled-update/home/logs/
|
|
${{ runner.temp }}/hermes-bundled-update-recording.mkv
|
|
if-no-files-found: error
|
|
retention-days: 14
|
|
e2e:
|
|
# Short static name on purpose: name expressions render UNEXPANDED on
|
|
# skipped jobs.
|
|
name: e2e
|
|
# The implemented {install x update} pairs. Two rules feed the table:
|
|
# * every desktop-surface method needs the starting tag to ship
|
|
# apps/desktop (pre-desktop releases have no window to launch, no
|
|
# Update button to click, no -IncludeDesktop to pass);
|
|
# * open-app-update needs an OS entry point, which only the
|
|
# desktop-bearing installs create.
|
|
if: >-
|
|
(inputs.install-method == 'installer-script'
|
|
|| (contains(fromJSON('["installer-script+desktop", "desktop-installer@latest"]'), inputs.install-method) && inputs.tag-has-desktop))
|
|
&& (contains(fromJSON('["hermes-update", "installer-script"]'), inputs.update-method)
|
|
|| (contains(fromJSON('["installer-script+desktop", "hermes-desktop-app-update", "desktop-installer@latest"]'), inputs.update-method) && inputs.tag-has-desktop)
|
|
|| (inputs.update-method == 'open-app-update' && inputs.tag-has-desktop
|
|
&& contains(fromJSON('["desktop-installer@latest", "installer-script+desktop"]'), inputs.install-method)))
|
|
runs-on: windows-latest-32-core
|
|
timeout-minutes: ${{ inputs.timeout-minutes }}
|
|
|
|
env:
|
|
# Sibling of the checkout (D:\a\hermes-agent\hermes-desktop-gui-e2e):
|
|
# outside the repo so the staged bare clone and the install never
|
|
# collide with the checkout itself. NOTE: ${{ runner.temp }} is NOT
|
|
# available in job-level env (only github/inputs/matrix/needs/
|
|
# secrets/strategy/vars).
|
|
HERMES_E2E_WORKROOT: ${{ github.workspace }}\..\hermes-desktop-gui-e2e
|
|
|
|
steps:
|
|
# Full history: the driver bare-clones this checkout as the repo the
|
|
# installer/updater talk to, and both OLD and HEAD must be reachable
|
|
# in that clone. A shallow checkout cannot serve either need.
|
|
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7.0.1
|
|
with:
|
|
ref: ${{ github.sha }}
|
|
fetch-depth: 0
|
|
|
|
# Old installers refresh PATH after winget installs ripgrep, dropping the
|
|
# process-only uv path. Supply the tool before running those shipped scripts.
|
|
- name: Set up driver tools only
|
|
uses: ./.github/actions/setup-pm
|
|
with:
|
|
toolchain: all
|
|
packages: ffmpeg,ripgrep
|
|
cache: false
|
|
cache-python: false
|
|
cache-node: false
|
|
|
|
- name: Install locked chat driver dependencies
|
|
shell: bash
|
|
run: npm ci --workspace tests-js --include-workspace-root --omit=dev --ignore-scripts --no-audit --no-fund
|
|
|
|
# A released tag prints its one-shot chat through prompt_toolkit, whose Windows
|
|
# output object needs a console screen buffer -- piping the CLI's stdout into the
|
|
# evidence log takes that away and the turn dies with NoConsoleScreenBufferError.
|
|
# The driver therefore runs that turn under e2e-assets/pty-run.py, which uses
|
|
# pywinpty: the same ConPTY spawner the product uses on Windows, pinned to the
|
|
# version pyproject.toml requires. Install it with uv (the toolchain python ships
|
|
# without pip) out of HERMES_RUNTIME_DIR, which is the tools directory itself and
|
|
# is not on PATH for workflow steps; the import check proves it landed.
|
|
# py-spy lands beside it: the driver's hang watchdog dumps the stack of
|
|
# every Python process of a stuck install or update before stopping it.
|
|
- name: Install the pseudoconsole runner's dependency
|
|
shell: pwsh
|
|
run: |
|
|
$uvDir = Get-ChildItem $env:HERMES_RUNTIME_DIR -Filter 'uv-*' -Directory | Select-Object -First 1
|
|
if (-not $uvDir) { throw "uv not found under $env:HERMES_RUNTIME_DIR" }
|
|
& (Join-Path $uvDir.FullName 'uv.exe') pip install --system --python $env:HERMES_PYTHON --quiet "pywinpty==3.0.5" "py-spy==0.4.2"
|
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
python -c "import winpty; print('pty-run dependency ready')"
|
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
& (Join-Path (Split-Path $env:HERMES_PYTHON) 'py-spy.exe') --version
|
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
|
|
# One recording mechanism on every OS: the composite action verifies
|
|
# ffmpeg from the PM toolchain (packages: ffmpeg above), starts the
|
|
# capture, and record-stop fails on a zero-frame file so a silently
|
|
# missing recording cannot go green.
|
|
- name: Start screen recording
|
|
uses: ./.github/actions/e2e-screen-record
|
|
with:
|
|
mode: start
|
|
output: ${{ github.workspace }}\gui-e2e-proof\recording.mkv
|
|
|
|
# Product steps below never see setup-pm's HERMES_RUNTIME_DIR or
|
|
# HERMES_PYTHON: windows-e2e.ps1 drops both on entry, so the installer,
|
|
# the app, `hermes update` and every chat turn share the leg's one store
|
|
# (<HERMES_HOME>\tools), as on a user's machine. setup-pm reaches the
|
|
# driver's own tooling only through HERMES_PYTHON (and PATH for older
|
|
# installers' uv/ripgrep).
|
|
- name: Stage serve repo (main -> ${{ inputs.install-ref }})
|
|
shell: powershell
|
|
run: powershell -NoProfile -ExecutionPolicy Bypass -File tests\install\windows-e2e.ps1 -Phase stage -InstallMethod "${{ inputs.install-method }}" -Route "${{ inputs.update-method }}" -InstallRef "${{ inputs.install-ref }}" -UpdateRef "${{ inputs.update-ref }}" -SetupExeUrl ${{ inputs.setup-exe-url }}
|
|
|
|
- name: Install ${{ inputs.install-ref }} (${{ inputs.install-method }})
|
|
shell: powershell
|
|
run: powershell -NoProfile -ExecutionPolicy Bypass -File tests\install\windows-e2e.ps1 -Phase install -InstallMethod "${{ inputs.install-method }}" -Route "${{ inputs.update-method }}" -InstallRef "${{ inputs.install-ref }}" -SetupExeUrl ${{ inputs.setup-exe-url }}
|
|
|
|
- name: Update ${{ inputs.install-ref }} -> ${{ inputs.update-ref }} (${{ inputs.update-method }})
|
|
id: update
|
|
shell: powershell
|
|
run: powershell -NoProfile -ExecutionPolicy Bypass -File tests\install\windows-e2e.ps1 -Phase update -InstallMethod "${{ inputs.install-method }}" -Route "${{ inputs.update-method }}" -InstallRef "${{ inputs.install-ref }}" -SetupExeUrl ${{ inputs.setup-exe-url }}
|
|
|
|
# After everything, including the new runtime's launch and its smoke
|
|
# checks: the bootstrap marker can complete on a later run than the
|
|
# install, so stamp truthfulness is asserted here — not in the install
|
|
# phase. Known-failure legs legitimately never reach the new HEAD.
|
|
- name: Verify install stamps describe the updated checkout
|
|
if: steps.update.outputs.known_failure == ''
|
|
shell: powershell
|
|
run: powershell -NoProfile -ExecutionPolicy Bypass -File tests\install\windows-e2e.ps1 -Phase verify-stamp -InstallMethod "${{ inputs.install-method }}" -Route "${{ inputs.update-method }}" -InstallRef "${{ inputs.install-ref }}" -SetupExeUrl ${{ inputs.setup-exe-url }}
|
|
|
|
- name: Stage known-failure receipt
|
|
if: steps.update.outputs.known_failure != ''
|
|
shell: pwsh
|
|
run: |
|
|
New-Item -ItemType Directory -Path gui-e2e-proof -Force | Out-Null
|
|
Copy-Item -LiteralPath (Join-Path $env:HERMES_E2E_WORKROOT 'known-failure.json') -Destination gui-e2e-proof/known-failure.json
|
|
|
|
- name: Upload known-failure receipt
|
|
if: steps.update.outputs.known_failure != ''
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: install-e2e-known-${{ steps.update.outputs.known_failure }}--${{ inputs.leg-id }}
|
|
path: gui-e2e-proof/known-failure.json
|
|
if-no-files-found: error
|
|
retention-days: 14
|
|
|
|
- name: Stop screen recording
|
|
if: always()
|
|
uses: ./.github/actions/e2e-screen-record
|
|
with:
|
|
mode: stop
|
|
output: ${{ github.workspace }}\gui-e2e-proof\recording.mkv
|
|
|
|
- name: Remux recording for browser playback
|
|
if: always()
|
|
shell: pwsh
|
|
run: |
|
|
$mkv = "$env:GITHUB_WORKSPACE\gui-e2e-proof\recording.mkv"
|
|
if (Test-Path -LiteralPath $mkv) {
|
|
& ffmpeg -y -hide_banner -loglevel error -i $mkv -c copy "$env:GITHUB_WORKSPACE\gui-e2e-proof\recording.mp4"
|
|
}
|
|
|
|
- name: Collect proof + logs
|
|
if: always()
|
|
shell: powershell
|
|
run: |
|
|
$out = "gui-e2e-proof"
|
|
New-Item -ItemType Directory -Path $out -Force | Out-Null
|
|
$work = $env:HERMES_E2E_WORKROOT
|
|
$home_ = Join-Path $work "hermes-home"
|
|
foreach ($pair in @(
|
|
@{ src = (Join-Path $work "proof"); dst = "proof" },
|
|
@{ src = (Join-Path $work "logs"); dst = "driver-logs" },
|
|
@{ src = (Join-Path $work "shas.json"); dst = "shas.json" },
|
|
@{ src = (Join-Path $home_ "logs"); dst = "logs" },
|
|
@{ src = (Join-Path $home_ ".hermes-update-result.json"); dst = ".hermes-update-result.json" }
|
|
)) {
|
|
if (Test-Path $pair.src) { Copy-Item $pair.src (Join-Path $out $pair.dst) -Recurse -Force }
|
|
}
|
|
|
|
- name: Upload proof + logs
|
|
if: always()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: install-e2e-logs-${{ inputs.leg-id }}
|
|
path: gui-e2e-proof
|
|
retention-days: 14
|
|
if-no-files-found: ignore
|