hermes-agent/.github/workflows/e2e-windows.yml
2026-06-18 16:12:14 -04:00

269 lines
11 KiB
YAML

name: E2E — Windows Desktop
on:
push:
branches: [ethie/e2e]
workflow_dispatch:
concurrency:
group: e2e-windows-${{ github.ref }}
cancel-in-progress: true
jobs:
build-installer:
name: Build Hermes-Setup.exe
runs-on: windows-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
# The cache key is the exact installer build fingerprint. A hit means
# this package-lock + bootstrap-installer source combo was already built,
# so we can skip the entire Node/Rust/toolchain dance and just upload it.
- name: Restore installer build cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
id: installer-cache
with:
path: Hermes-Setup.exe
key: hermes-installer-${{ runner.os }}-${{ hashFiles('package-lock.json', 'apps/bootstrap-installer/**', '!apps/bootstrap-installer/src-tauri/target/**') }}
- name: Setup Node.js
if: steps.installer-cache.outputs.cache-hit != 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
cache: npm
- name: Setup Rust
if: steps.installer-cache.outputs.cache-hit != 'true'
uses: dtolnay/rust-toolchain@1.96.0 # stable
- name: Install npm dependencies
if: steps.installer-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Cache Rust targets
if: steps.installer-cache.outputs.cache-hit != 'true'
uses: Swatinem/rust-cache@c106961feec855ef5a58494c5a2a31c9d80429a9
with:
workspaces: apps/bootstrap-installer/src-tauri
- name: Build installer
if: steps.installer-cache.outputs.cache-hit != 'true'
run: npm run tauri:build
working-directory: apps/bootstrap-installer
timeout-minutes: 10
env:
# Pin the installer to the exact commit being tested so that the
# install script checks out this PR/code rather than remote main.
HERMES_BUILD_PIN_COMMIT: ${{ github.sha }}
# Only runs on cache miss. Pick the exe the Tauri build produced and
# normalize its name so downstream jobs always know what to download.
- name: Normalize installer artifact name
if: steps.installer-cache.outputs.cache-hit != 'true'
shell: pwsh
run: |
$candidates = @(
'apps/bootstrap-installer/src-tauri/target/release/bundle/app/Hermes.exe',
'apps/bootstrap-installer/src-tauri/target/release/bundle/app/Hermes_0.0.1_x64.exe',
'apps/bootstrap-installer/src-tauri/target/release/bundle/app/Hermes_0.0.1_x64-setup.exe',
'apps/bootstrap-installer/src-tauri/target/release/Hermes.exe'
)
$installer = $null
foreach ($c in $candidates) {
if (Test-Path $c) {
$installer = Resolve-Path $c
break
}
}
if (-not $installer) {
$installer = Get-ChildItem -Path 'apps/bootstrap-installer/src-tauri/target/release' `
-Recurse -Filter '*.exe' | Where-Object { $_.Name -notlike '*setup*' -or $true } | Select-Object -First 1 -ExpandProperty FullName
}
if (-not $installer) {
throw 'Could not find built Hermes-Setup.exe'
}
Copy-Item -Path $installer -Destination 'Hermes-Setup.exe' -Force
Write-Host "Normalized installer: Hermes-Setup.exe (from $installer)"
- name: Upload installer artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: hermes-installer-${{ runner.os }}-${{ hashFiles('package-lock.json', 'apps/bootstrap-installer/**', '!apps/bootstrap-installer/src-tauri/target/**') }}
path: Hermes-Setup.exe
if-no-files-found: error
e2e:
name: E2E — Windows Desktop
needs: build-installer
runs-on: windows-latest
timeout-minutes: 60
env:
# Isolated install directory so the real install flow doesn't touch the
# runner's user profile. Kept under the workspace for easy cleanup.
HERMES_HOME: ${{ github.workspace }}\.e2e-hermes-home
steps:
- uses: actions/checkout@v4
- name: Download installer artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: hermes-installer-${{ runner.os }}-${{ hashFiles('package-lock.json', 'apps/bootstrap-installer/**', '!apps/bootstrap-installer/src-tauri/target/**') }}
- name: Install AutoHotkey v2
shell: pwsh
run: |
winget install -e --id AutoHotkey.AutoHotkey --silent --accept-source-agreements --accept-package-agreements --disable-interactivity
$ahkDir = "$env:ProgramW6432\AutoHotkey\v2"
Add-Content -Path $env:GITHUB_PATH -Value $ahkDir
- name: Install ffmpeg
shell: pwsh
run: |
winget install -e --id Gyan.FFmpeg --silent --accept-source-agreements --accept-package-agreements --disable-interactivity
$wingetLinks = "$env:LOCALAPPDATA\Microsoft\WinGet\Links"
echo $wingetLinks | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
# ── Stage the real installer binary as a user download ──────────
- name: Stage Hermes-Setup.exe in Downloads
shell: pwsh
run: |
$downloads = [Environment]::GetFolderPath('UserProfile') + '\Downloads'
New-Item -ItemType Directory -Path $downloads -Force | Out-Null
if (-not (Test-Path 'Hermes-Setup.exe')) {
throw 'Hermes-Setup.exe artifact was not downloaded'
}
Copy-Item -Path 'Hermes-Setup.exe' -Destination "$downloads\Hermes-Setup.exe" -Force
Write-Host "Staged installer: $downloads\Hermes-Setup.exe"
# ── Prepare an isolated HERMES_HOME and pre-clone the repo ──────
# This lets install.ps1 use the local checkout as its git origin,
# so the whole flow exercises the real installer without reaching GitHub.
- name: Pre-clone workspace into isolated HERMES_HOME
shell: pwsh
run: |
$installDir = "$env:HERMES_HOME\hermes-agent"
New-Item -ItemType Directory -Path "$installDir\.." -Force | Out-Null
git clone --no-hardlinks "${{ github.workspace }}" "$installDir"
# Make sure the clone has the pinned commit reachable.
Push-Location "$installDir"
git fetch origin ${{ github.sha }} 2>$null || git fetch --depth=1 origin ${{ github.sha }}
Pop-Location
Write-Host "Isolated install dir ready: $installDir"
# ── Screen recording for coordinate discovery ───────────────────
- name: Start screen recording
shell: pwsh
run: |
Start-Process ffmpeg -ArgumentList "-y -f gdigrab -framerate 15 -i desktop -vcodec libx264 -preset ultrafast recording.mp4" `
-RedirectStandardOutput ffmpeg.log -RedirectStandardError ffmpeg.err -PassThru |
ForEach-Object { $_.Id } | Out-File ffmpeg.pid
# ── Run the headed installer + AHK helper ───────────────────────
# For the first run we deliberately do NOT click the "Install Hermes"
# button. AHK waits, captures the window, and exits. A maintainer
# reviews the screenshots/recording, supplies the button coordinates,
# and we then update the AHK script to click them.
- name: Launch Hermes-Setup.exe and capture welcome screen
shell: pwsh
timeout-minutes: 5
run: |
$downloads = [Environment]::GetFolderPath('UserProfile') + '\Downloads'
$installer = "$downloads\Hermes-Setup.exe"
# Run the AHK helper first so it can detect the window.
$ahk = @'
#Requires AutoHotkey v2.0
#SingleInstance Force
; Wait for the Hermes installer window to appear.
winTitle := "Hermes"
if not WinWait(winTitle,, 30)
{
FileAppend("ERROR: Hermes installer window did not appear within 30s`n", "ahk.log")
ExitApp(1)
}
WinGetPos(&x, &y, &w, &h, winTitle)
FileAppend(Format("Window found at x={1} y={2} w={3} h={4}`n", x, y, w, h), "ahk.log")
; TODO: replace with real coords after first CI run.
; clickX := x + w // 2
; clickY := y + h // 2 + 120
; Click(clickX, clickY)
; FileAppend(Format("Clicked at x={1} y={2}`n", clickX, clickY), "ahk.log")
; Take a screenshot and exit. Maintainer reviews this and sends coords.
Sleep(1000)
Send("{PrintScreen}")
FileAppend("Captured screenshot, exiting`n", "ahk.log")
ExitApp(0)
'@
$ahk | Out-File -Encoding utf8 "capture-button.ahk"
$ahkProc = Start-Process AutoHotkey64
-ArgumentList '"capture-button.ahk"' -PassThru -NoNewWindow `
-RedirectStandardOutput ahk-stdout.log -RedirectStandardError ahk-stderr.log
# Launch the real installer (headed).
$proc = Start-Process -FilePath $installer -PassThru -NoNewWindow
$proc.Id | Out-File installer.pid
# Wait for AHK helper to finish (30s window wait + 1s sleep).
$ahkProc | Wait-Process -Timeout 45 -ErrorAction SilentlyContinue
if (-not $ahkProc.HasExited) {
Write-Host "AHK helper is still running; stopping it"
Stop-Process -Id $ahkProc.Id -Force -ErrorAction SilentlyContinue
}
# Leave the installer running briefly for the recording, then stop it.
Start-Sleep -Seconds 5
if ($proc -and -not $proc.HasExited) {
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
}
# ── Run Playwright against the installed binary ─────────────────
# (placeholder: will be enabled once installer completes successfully.)
- name: Launch installed app and run e2e
if: false
working-directory: apps/desktop
run: npx playwright test e2e/ --reporter=list
env:
# Point the e2e spec at the desktop binary that the installer built.
HERMES_E2E_INSTALL_ROOT: ${{ env.HERMES_HOME }}\hermes-agent
# ── Teardown & artifacts ────────────────────────────────────────
- name: Stop screen recording
if: always()
shell: pwsh
run: |
$ffmpegpid = Get-Content ffmpeg.pid -ErrorAction SilentlyContinue
if ($ffmpegpid) {
Stop-Process -Id $ffmpegpid -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
}
- name: Upload screen recording
uses: actions/upload-artifact@v4
if: always()
with:
name: screen-recording
path: recording.mp4
if-no-files-found: ignore
- name: Upload AHK logs and script
uses: actions/upload-artifact@v4
if: always()
with:
name: ahk-discovery
path: |
ahk.log
ahk-stdout.log
ahk-stderr.log
capture-button.ahk
if-no-files-found: ignore