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.
This commit is contained in:
56
.github/workflows/install-e2e.yml
vendored
56
.github/workflows/install-e2e.yml
vendored
@@ -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:
|
||||
|
||||
@@ -282,9 +282,31 @@ const ROUTE_OSES = /** @type {Record<string, Os[]>} */ ({
|
||||
});
|
||||
|
||||
/**
|
||||
* 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 ("<leg> / 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 ("<leg> / e2e") runs just those legs. A route that selects nothing
|
||||
* throws rather than yielding a green empty run.
|
||||
*
|
||||
* @param {Record<Os, {include: MatrixEntry[]}>} matrices
|
||||
* @param {string} route
|
||||
@@ -293,9 +315,11 @@ const ROUTE_OSES = /** @type {Record<string, Os[]>} */ ({
|
||||
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<Os, {include: MatrixEntry[]}>} */
|
||||
const picked = { linux: { include: [] }, windows: { include: [] }, macos: { include: [] } };
|
||||
for (const os of /** @type {Os[]} */ (Object.keys(picked))) {
|
||||
|
||||
@@ -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)',
|
||||
])
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user