Files
hermes-agent/.github/workflows/install-e2e-macos-run.yml
ethernet b1c9d1279a test(install-e2e): every combination also installs HEAD and updates it to a synthetic NEXT
Every leg installed a release tag and updated to HEAD, so nothing ran
HEAD's installer on an empty machine (where all four 2026-09-23
install.ps1 breaks lived) and nothing exercised the updater we ship
today -- tag legs run the OLD build's updater handing off to HEAD.

The generator appends a HEAD -> NEXT start after the sampled tags, so the
column runs wherever the update legs run (dispatch and stable-release).
NEXT is a reserved update ref: the drivers mint a child of the install
commit that adds one marker file, written to the object store only, which
the local bare clone carries into serve.git.

On Windows the HEAD leg takes every git.exe dir off PATH and installs no
remote get-url shim: Get-PinnedGit returns any git on PATH, so either one
skipped pinned-git staging. launch-from-spec's HEAD observer now uses the
driver's real git so it cannot poll '' forever on that leg.
2026-09-23 23:06:28 -04:00

286 lines
12 KiB
YAML

# Reusable runner for ONE macOS install/update combination.
#
# Two driver arms, one runs per dispatch (the other natively skips):
#
# e2e (script arms) tests/install/installer-script-e2e.sh - the
# OS-agnostic git-redirect driver shared with
# linux. installer-script(+desktop) installs,
# script/updater/hermes-desktop-app-update
# updates.
# gui-e2e (desktop arm) tests/install/macos-desktop-e2e.sh - the
# published Hermes-Setup.dmg, mounted and run,
# then the app driven by Playwright for the
# app-update methods.
#
# Method pairs without a driver arm yet NATIVELY SKIP (grey check, no
# runner): the capability knowledge lives here, next to the drivers.
name: install-e2e macos leg
on:
workflow_call:
inputs:
install-method:
description: 'How OLD gets installed. Supported: installer-script, installer-script+desktop (curl | bash one-liner, optionally with --include-desktop) and desktop-installer@latest (the published Hermes-Setup.dmg).'
required: true
type: string
update-method:
description: 'How the install updates to HEAD. Script installs support hermes-update / installer-script / installer-script+desktop / hermes-desktop-app-update; dmg installs support open-app-update / hermes-desktop-app-update.'
required: true
type: string
install-ref:
description: 'What to install before updating: a branch, a tag, or a SHA reachable from main.'
required: false
type: string
default: refs/heads/main
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
dmg-url:
description: 'Bootstrap dmg 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.dmg
bundle-manifest-url:
description: 'URL of the JSON schema-1 bundle manifest for the packaged-app arm. Empty (default) means the bundled arm does not run — native packaged coverage only runs when the caller pins a real signed OLD/NEW pair.'
required: false
type: string
default: ''
bundle-manifest-sha256:
description: Pinned transition manifest digest for stable acceptance
type: string
default: ''
bundle-arch:
description: 'Architecture of the bundled pair: arm64 (macos-15) or x64 (macos-15-intel).'
required: false
type: string
default: arm64
timeout-minutes:
description: 'Job timeout. App-update legs do a full Electron build.'
required: false
type: number
default: 60
permissions:
contents: read
jobs:
# ---- arm 1: script installs (the shared OS-agnostic driver) --------------
e2e:
name: install & update
if: >-
(inputs.install-method == 'installer-script'
|| (inputs.install-method == 'installer-script+desktop' && inputs.tag-has-desktop))
&& (contains(fromJSON('["hermes-update", "installer-script"]'), inputs.update-method)
|| (contains(fromJSON('["installer-script+desktop", "hermes-desktop-app-update"]'), inputs.update-method) && inputs.tag-has-desktop))
uses: ./.github/workflows/install-e2e-run.yml
with:
install-method: ${{ inputs.install-method }}
update-method: ${{ inputs.update-method }}
install-ref: ${{ inputs.install-ref }}
update-ref: ${{ inputs.update-ref }}
tag-has-desktop: ${{ inputs.tag-has-desktop }}
leg-id: ${{ inputs.leg-id }}
runner: macos-latest
timeout-minutes: ${{ inputs.timeout-minutes }}
# ---- arm 2: the published dmg, then Playwright drives the app ------------
gui-e2e:
# Short static name on purpose: name expressions render UNEXPANDED on
# skipped jobs.
name: Hermes-Setup.dmg
if: >-
inputs.install-method == 'desktop-installer@latest' && inputs.tag-has-desktop
&& contains(fromJSON('["open-app-update", "hermes-desktop-app-update", "hermes-update", "installer-script", "installer-script+desktop"]'), inputs.update-method)
runs-on: macos-latest
timeout-minutes: ${{ inputs.timeout-minutes }}
steps:
# Full history: the driver bare-clones this checkout as the repo the
# installer/updater talk to.
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.sha }}
fetch-depth: 0
- 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: Start screen recording
uses: ./.github/actions/e2e-screen-record
with:
mode: start
output: ${{ runner.temp }}/e2e-logs/recording.mkv
- name: Stage serve repo (main -> ${{ inputs.install-ref }})
run: |
set -euo pipefail
tests/install/macos-desktop-e2e.sh --phase stage \
--update-method '${{ inputs.update-method }}' \
--install-ref '${{ inputs.install-ref }}' \
--update-ref '${{ inputs.update-ref }}' \
--dmg-url '${{ inputs.dmg-url }}'
env:
HERMES_E2E_LOG_DIR: ${{ runner.temp }}/e2e-logs
- name: Install ${{ inputs.install-ref }} via Hermes-Setup.dmg
run: |
set -euo pipefail
tests/install/macos-desktop-e2e.sh --phase install \
--update-method '${{ inputs.update-method }}' \
--install-ref '${{ inputs.install-ref }}' \
--dmg-url '${{ inputs.dmg-url }}'
env:
HERMES_E2E_LOG_DIR: ${{ runner.temp }}/e2e-logs
- name: Update ${{ inputs.install-ref }} -> ${{ inputs.update-ref }} (${{ inputs.update-method }})
run: |
set -euo pipefail
tests/install/macos-desktop-e2e.sh --phase update \
--update-method '${{ inputs.update-method }}' \
--install-ref '${{ inputs.install-ref }}' \
--dmg-url '${{ inputs.dmg-url }}'
env:
HERMES_E2E_LOG_DIR: ${{ runner.temp }}/e2e-logs
- name: Stop screen recording
if: always()
uses: ./.github/actions/e2e-screen-record
with:
mode: stop
output: ${{ runner.temp }}/e2e-logs/recording.mkv
- name: Remux recording for browser playback
if: always()
run: |
set -euo pipefail
if [ -f "${{ runner.temp }}/e2e-logs/recording.mkv" ]; then
ffmpeg -y -hide_banner -loglevel error -i "${{ runner.temp }}/e2e-logs/recording.mkv" \
-c copy "${{ runner.temp }}/e2e-logs/recording.mp4"
fi
# Artifact names cannot contain '/'; install-ref may be a full ref.
- name: Build artifact name
id: artifact
if: always()
run: |
echo "name=install-e2e-logs-${{ inputs.leg-id }}" >> "$GITHUB_OUTPUT"
- name: Upload logs
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ steps.artifact.outputs.name }}
path: ${{ runner.temp }}/e2e-logs
retention-days: 14
if-no-files-found: ignore
# ---- arm 3: the real signed bundle pair, in-app update ------------------
# Runs ONLY when the caller passes a bundle-manifest-url: the parent's
# matrix job owns resolving + pinning the OLD/NEW signed release pair.
# The driver itself re-guards on Darwin + GITHUB_ACTIONS and refuses to
# run anywhere native coverage would be fake.
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 == 'x64' && 'macos-15-intel' || 'macos-15' }}
timeout-minutes: ${{ inputs.timeout-minutes }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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: Start screen recording
uses: ./.github/actions/e2e-screen-record
with:
mode: start
output: ${{ runner.temp }}/e2e-logs/recording.mkv
- name: Install OLD signed bundle (${{ inputs.install-ref }})
run: |
set -euo pipefail
bash tests/install/macos-bundled-e2e.sh --phase install \
--manifest-url "$BUNDLE_MANIFEST" --arch "$BUNDLE_ARCH"
env:
BUNDLE_MANIFEST: ${{ inputs.bundle-manifest-url }}
BUNDLE_MANIFEST_SHA256: ${{ inputs.bundle-manifest-sha256 }}
BUNDLE_ARCH: ${{ inputs.bundle-arch }}
HERMES_E2E_LOG_DIR: ${{ runner.temp }}/e2e-logs
- name: Update to NEW via About -> Update now (${{ inputs.bundle-arch }})
run: |
set -euo pipefail
bash tests/install/macos-bundled-e2e.sh --phase update \
--manifest-url "$BUNDLE_MANIFEST" --arch "$BUNDLE_ARCH"
env:
BUNDLE_MANIFEST: ${{ inputs.bundle-manifest-url }}
BUNDLE_MANIFEST_SHA256: ${{ inputs.bundle-manifest-sha256 }}
BUNDLE_ARCH: ${{ inputs.bundle-arch }}
HERMES_E2E_LOG_DIR: ${{ runner.temp }}/e2e-logs
- name: Stop screen recording
if: always()
uses: ./.github/actions/e2e-screen-record
with:
mode: stop
output: ${{ runner.temp }}/e2e-logs/recording.mkv
- name: Remux recording for browser playback
if: always()
run: |
set -euo pipefail
if [ -f "${{ runner.temp }}/e2e-logs/recording.mkv" ]; then
ffmpeg -y -hide_banner -loglevel error -i "${{ runner.temp }}/e2e-logs/recording.mkv" \
-c copy "${{ runner.temp }}/e2e-logs/recording.mp4"
fi
- name: Build artifact name
id: artifact
if: always()
run: |
echo "name=install-e2e-logs-${{ inputs.leg-id }}-bundle-${{ inputs.bundle-arch }}" >> "$GITHUB_OUTPUT"
- name: Upload logs
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ steps.artifact.outputs.name }}
path: ${{ runner.temp }}/e2e-logs
retention-days: 14
if-no-files-found: ignore