From 7605349f25e8ca38e10063136d71226c3e92fd94 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sat, 26 Sep 2026 17:51:42 -0700 Subject: [PATCH] ci(install-e2e): run a path-filtered four-leg subset on pull requests install-e2e.yml only ran on the clock, so nothing in front of a merge installed a release and updated it on a real OS. A pull_request trigger, path-filtered to the install/update surface (derived from 60 days of update/install/pm commits), runs the new `pr` route of generate-e2e-matrix.mjs with only the newest release tag sampled: linux installer-script -> hermes-update (newest release -> PR) linux installer-script -> hermes-update (PR -> NEXT) windows installer-script -> hermes-update (PR -> NEXT) macos installer-script -> hermes-update (newest release -> PR) The bundle-manifest validation job is skipped on PRs (bundled legs need dispatch-only manifests). The full matrix stays on schedule and release. --- .github/workflows/install-e2e.yml | 56 +++++++++++++++++++++---- scripts/sandbox/generate-e2e-matrix.mjs | 36 +++++++++++++--- tests-js/e2e-matrix-route.test.ts | 11 +++++ 3 files changed, 89 insertions(+), 14 deletions(-) diff --git a/.github/workflows/install-e2e.yml b/.github/workflows/install-e2e.yml index 47b681560c..864bd8a8e6 100644 --- a/.github/workflows/install-e2e.yml +++ b/.github/workflows/install-e2e.yml @@ -36,11 +36,14 @@ name: Install & Update E2E # * every 12 hours, so upstream drift (a new uv, a Node bump, a PyPI change) # surfaces on a schedule rather than in someone's review cycle; # * manually, where you can pick the route (a preset or specific legs) and how many -# releases to sample. -# -# Deliberately NOT on pull_request: a leg takes ~11 minutes of real toolchain -# installation, and the matrix multiplies that. Updating is release-shaped work, -# so it is gated on releases and the clock instead. +# releases to sample; +# * pull_request, path-filtered to the install/update surface, runs ONLY the +# `pr` route (generate-e2e-matrix.mjs PR_SUBSET): installer-script -> +# hermes-update on linux (newest release -> PR, and PR -> NEXT), windows +# (PR -> NEXT) and macos (newest release -> PR), sampling just the newest tag. +# Four legs in parallel keep a PR's wall clock near one leg (~15 min); +# the full matrix multiplies that, so it stays on the clock and releases. +# A red scheduled run is reported by install-e2e-red.yml. on: workflow_dispatch: @@ -116,6 +119,41 @@ on: schedule: # Every 12 hours, off the hour to avoid the top-of-hour runner crunch. - cron: '20 7,19 * * *' + pull_request: + # The install/update surface: derived from 60 days of commits whose subject + # names update/install/pm/bootstrap/relaunch. Keep it to code a real + # install or `hermes update` executes; hermes_cli/main.py and friends are + # deliberately absent (they change daily and are covered by e2e-upgrade). + paths: + - 'scripts/install*' + - 'scripts/desktop-update/**' + - 'setup-hermes.*' + - 'hermes_bootstrap.py' + - 'pm/**' + - 'hermes_cli/update*' + - 'hermes_cli/*update*' + - 'hermes_cli/pm*' + - 'hermes_cli/*install*' + - 'hermes_cli/_early_recovery.py' + - 'hermes_cli/_launchers.py' + - 'hermes_cli/managed_uv.py' + - 'hermes_cli/venv_sync.py' + - 'hermes_cli/relaunch.py' + - 'hermes_cli/gateway.py' + - 'gateway/status.py' + - 'apps/bootstrap-installer/**' + - 'apps/desktop/electron/*update*' + - 'apps/desktop/electron/updater/**' + - 'pyproject.toml' + - 'uv.lock' + - 'package.json' + - 'package-lock.json' + - 'tests/install/**' + - 'scripts/sandbox/generate-e2e-matrix.mjs' + - 'scripts/sandbox/pick-release-tags.sh' + - 'scripts/ci/setup_toolchain.py' + - '.github/actions/setup-pm/**' + - '.github/workflows/install-e2e*.yml' permissions: contents: read @@ -159,7 +197,7 @@ jobs: # stable release the candidate tag itself is the newest release # tag, and sampling it as an OLD version would produce a no-change # update leg — the candidate must never update from itself. - TAG_COUNT: ${{ inputs.tag-count || 2 }} + TAG_COUNT: ${{ github.event_name == 'pull_request' && '1' || inputs.tag-count || 2 }} INSTALL_REF: ${{ inputs.install-ref }} EXCLUDE_REF: ${{ inputs.exclude-ref }} RELEASE_MODE: ${{ inputs.release }} @@ -211,8 +249,8 @@ jobs: - id: gen env: # Free text on dispatch, so it reaches the script as data, never as script. - # Scheduled runs have no inputs: the default route. - ROUTE: ${{ inputs.route || 'all' }} + # Scheduled runs have no inputs: the default route. PRs run the pr subset. + ROUTE: ${{ github.event_name == 'pull_request' && 'pr' || inputs.route || 'all' }} run: | set -euo pipefail matrices="$(node scripts/sandbox/generate-e2e-matrix.mjs \ @@ -230,6 +268,8 @@ jobs: bundle-inputs: name: Validate pinned bundle transitions + # Bundled legs need dispatch-only manifests; a PR never runs them. + if: github.event_name != 'pull_request' runs-on: ubuntu-latest timeout-minutes: 5 outputs: diff --git a/scripts/sandbox/generate-e2e-matrix.mjs b/scripts/sandbox/generate-e2e-matrix.mjs index d54159b2c8..f9e09c78fe 100644 --- a/scripts/sandbox/generate-e2e-matrix.mjs +++ b/scripts/sandbox/generate-e2e-matrix.mjs @@ -282,9 +282,31 @@ const ROUTE_OSES = /** @type {Record} */ ({ }); /** - * Narrow the matrices to a route: a preset keeps whole OS matrices; anything else selects - * legs by name, so a leg name, a fragment of one, or a pasted job name (" / e2e") runs - * just those legs. A route that selects nothing throws rather than yielding a green empty run. + * The pull_request subset: the script install updated by `hermes update`, on every OS, + * from the two starts that catch a regression before it ships -- the newest release + * updating to the PR (the jump every user makes next) and the PR updating to a synthetic + * NEXT (the updater the PR itself ships). One leg per {os, start} worth its runner: + * linux both starts, windows the PR's own updater, macos the release jump. + * @type {{os: Os, to: UpdateTarget}[]} + */ +const PR_SUBSET = [ + { os: 'linux', to: 'HEAD' }, + { os: 'linux', to: 'NEXT' }, + { os: 'windows', to: 'NEXT' }, + { os: 'macos', to: 'HEAD' }, +]; + +/** @param {Os} os @param {MatrixEntry} entry */ +function inPrSubset(os, entry) { + return entry.install_method === 'installer-script' && entry.update_method === 'hermes-update' && + PR_SUBSET.some((leg) => leg.os === os && leg.to === entry.update_ref); +} + +/** + * Narrow the matrices to a route: a preset keeps whole OS matrices; `pr` keeps the + * PR_SUBSET legs; anything else selects legs by name, so a leg name, a fragment of one, or + * a pasted job name (" / e2e") runs just those legs. A route that selects nothing + * throws rather than yielding a green empty run. * * @param {Record} matrices * @param {string} route @@ -293,9 +315,11 @@ const ROUTE_OSES = /** @type {Record} */ ({ export function selectRoute(matrices, route) { const oses = ROUTE_OSES[route]; /** @type {(os: Os, entry: MatrixEntry) => boolean} */ - const keep = oses - ? (os) => oses.includes(os) - : (_os, entry) => entry.name.includes(route) || route.startsWith(`${entry.name} /`); + const keep = route === 'pr' + ? inPrSubset + : oses + ? (os) => oses.includes(os) + : (_os, entry) => entry.name.includes(route) || route.startsWith(`${entry.name} /`); /** @type {Record} */ const picked = { linux: { include: [] }, windows: { include: [] }, macos: { include: [] } }; for (const os of /** @type {Os[]} */ (Object.keys(picked))) { diff --git a/tests-js/e2e-matrix-route.test.ts b/tests-js/e2e-matrix-route.test.ts index 35a8969ba5..cd4cb7a2b6 100644 --- a/tests-js/e2e-matrix-route.test.ts +++ b/tests-js/e2e-matrix-route.test.ts @@ -42,3 +42,14 @@ test('an OS preset runs that OS whole and no other', () => { test('a route that selects nothing fails instead of running an empty green matrix', () => { expect(() => generate('windows: no-such-method')).toThrow() }) + +test('the pr route runs one script install updated by hermes update per OS start, nothing else', () => { + const pr: Matrices = generate('pr') + + expect(legNames(pr).sort()).toEqual([ + 'linux: installer-script -> hermes-update (HEAD -> NEXT)', + 'linux: installer-script -> hermes-update (v2026.6.19 -> HEAD)', + 'macos: installer-script -> hermes-update (v2026.6.19 -> HEAD)', + 'windows: installer-script -> hermes-update (HEAD -> NEXT)', + ]) +})