name: Windows install + update E2E # PR-time native Windows install/update journey (tests/e2e/core/windows_update): # the real scripts/install.ps1 on a fresh fake user profile (git redirected to a local # bare clone, no git/python/uv on PATH), then the real `hermes update` HEAD -> NEXT, # a running gateway across that update, and a turn against the loopback provider. # Each file walks one journey (one install) and asserts several user-visible cells; # tracked bugs are message-gated xfails (tests/e2e/core/_pending_fixes.py). # # Called from tests-os.yml when a Python PR touches the install/update code # (the classifier's e2e_upgrade lane) and on every push to main. Pushes to # wine2e-install/** run it alone, for sabotage proofs and iteration without the # rest of CI. on: workflow_call: push: branches: ['wine2e-install/**'] permissions: contents: read concurrency: group: windows-install-update-e2e-${{ github.event_name }}-${{ github.ref }} cancel-in-progress: true jobs: install-update: name: Windows install.ps1 + hermes update E2E runs-on: windows-latest-32-core # A typical run is ~17 min; one slow runner took 28 min for the same work (run 36295230146). timeout-minutes: 50 steps: - name: Checkout code uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7.0.1 with: # serve.git is a bare clone of this checkout: the installer's treeless clone and # `hermes update` need real history and release tags (version identity), as on GitHub. fetch-depth: 0 - name: Set up locked Python and test dependencies uses: ./.github/actions/setup-pm with: packages: git,ripgrep extras: '["all", "telegram", "anthropic", "mistral", "fal", "modal", "daytona", "parallel-web"]' test-environment: 'true' prune-python-cache: true - name: Run Windows install + update E2E shell: bash run: | scripts/run_tests.sh tests/e2e/core/windows_update/ -- \ -m "platforms and integration" -v --tb=short -rA env: HERMES_E2E_WINDOWS_INSTALL: '1' # Machine work dirs (serve.git, transcripts) stay short under runner.temp. The fake # user profiles live in C:\Users itself, where a real one does: the checkout carries # 159-character paths, so a deeper profile hits MAX_PATH where no user would (the # runner is disposable, so writing C:\Users is fine). HERMES_E2E_MACHINE_ROOT: ${{ runner.temp }}\wm HERMES_E2E_PROFILES_ROOT: C:\Users HERMES_E2E_ARTIFACTS: ${{ runner.temp }}\win-update-e2e # One journey per file, all in parallel (each is mostly network + subprocess wait). HERMES_TEST_WORKERS: '6' HERMES_TEST_FILE_TIMEOUT: '2400' HERMES_TEST_FILE_RETRIES: '0' OPENROUTER_API_KEY: '' OPENAI_API_KEY: '' NOUS_API_KEY: '' - name: Upload transcripts, logs and receipts if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: windows-install-update-e2e-${{ github.run_attempt }} path: ${{ runner.temp }}/win-update-e2e if-no-files-found: ignore retention-days: 14 - name: No leftover Python / Hermes processes # Backstop for each journey's teardown: nothing a machine started may outlive it. if: always() shell: pwsh run: | $names = 'python.exe', 'pythonw.exe', 'hermes.exe', 'node.exe' # Work dirs live under runner.temp\wm; profiles are C:\Users\*hermes-e2e-. $root = "${{ runner.temp }}\wm".ToLower() $profileMark = 'hermes-e2e-' $deadline = (Get-Date).AddSeconds(20) do { $left = @(Get-CimInstance Win32_Process | Where-Object { $blob = "$($_.CommandLine) $($_.ExecutablePath)".ToLower() ($names -contains $_.Name.ToLower()) -and ($blob.Contains($root) -or $blob.Contains($profileMark)) }) if ($left.Count -eq 0) { 'no leftover processes from the E2E machines'; exit 0 } Start-Sleep -Milliseconds 500 } while ((Get-Date) -lt $deadline) foreach ($p in $left) { "::error::leftover pid=$($p.ProcessId) ppid=$($p.ParentProcessId): $($p.CommandLine)" } exit 1