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)
|
# * 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;
|
# 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
|
# * manually, where you can pick the route (a preset or specific legs) and how many
|
||||||
# releases to sample.
|
# releases to sample;
|
||||||
#
|
# * pull_request, path-filtered to the install/update surface, runs ONLY the
|
||||||
# Deliberately NOT on pull_request: a leg takes ~11 minutes of real toolchain
|
# `pr` route (generate-e2e-matrix.mjs PR_SUBSET): installer-script ->
|
||||||
# installation, and the matrix multiplies that. Updating is release-shaped work,
|
# hermes-update on linux (newest release -> PR, and PR -> NEXT), windows
|
||||||
# so it is gated on releases and the clock instead.
|
# (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:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -116,6 +119,41 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
# Every 12 hours, off the hour to avoid the top-of-hour runner crunch.
|
# Every 12 hours, off the hour to avoid the top-of-hour runner crunch.
|
||||||
- cron: '20 7,19 * * *'
|
- 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:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -159,7 +197,7 @@ jobs:
|
|||||||
# stable release the candidate tag itself is the newest release
|
# stable release the candidate tag itself is the newest release
|
||||||
# tag, and sampling it as an OLD version would produce a no-change
|
# tag, and sampling it as an OLD version would produce a no-change
|
||||||
# update leg — the candidate must never update from itself.
|
# 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 }}
|
INSTALL_REF: ${{ inputs.install-ref }}
|
||||||
EXCLUDE_REF: ${{ inputs.exclude-ref }}
|
EXCLUDE_REF: ${{ inputs.exclude-ref }}
|
||||||
RELEASE_MODE: ${{ inputs.release }}
|
RELEASE_MODE: ${{ inputs.release }}
|
||||||
@@ -211,8 +249,8 @@ jobs:
|
|||||||
- id: gen
|
- id: gen
|
||||||
env:
|
env:
|
||||||
# Free text on dispatch, so it reaches the script as data, never as script.
|
# Free text on dispatch, so it reaches the script as data, never as script.
|
||||||
# Scheduled runs have no inputs: the default route.
|
# Scheduled runs have no inputs: the default route. PRs run the pr subset.
|
||||||
ROUTE: ${{ inputs.route || 'all' }}
|
ROUTE: ${{ github.event_name == 'pull_request' && 'pr' || inputs.route || 'all' }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
matrices="$(node scripts/sandbox/generate-e2e-matrix.mjs \
|
matrices="$(node scripts/sandbox/generate-e2e-matrix.mjs \
|
||||||
@@ -230,6 +268,8 @@ jobs:
|
|||||||
|
|
||||||
bundle-inputs:
|
bundle-inputs:
|
||||||
name: Validate pinned bundle transitions
|
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
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
outputs:
|
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
|
* The pull_request subset: the script install updated by `hermes update`, on every OS,
|
||||||
* legs by name, so a leg name, a fragment of one, or a pasted job name ("<leg> / e2e") runs
|
* from the two starts that catch a regression before it ships -- the newest release
|
||||||
* just those legs. A route that selects nothing throws rather than yielding a green empty run.
|
* 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 {Record<Os, {include: MatrixEntry[]}>} matrices
|
||||||
* @param {string} route
|
* @param {string} route
|
||||||
@@ -293,9 +315,11 @@ const ROUTE_OSES = /** @type {Record<string, Os[]>} */ ({
|
|||||||
export function selectRoute(matrices, route) {
|
export function selectRoute(matrices, route) {
|
||||||
const oses = ROUTE_OSES[route];
|
const oses = ROUTE_OSES[route];
|
||||||
/** @type {(os: Os, entry: MatrixEntry) => boolean} */
|
/** @type {(os: Os, entry: MatrixEntry) => boolean} */
|
||||||
const keep = oses
|
const keep = route === 'pr'
|
||||||
? (os) => oses.includes(os)
|
? inPrSubset
|
||||||
: (_os, entry) => entry.name.includes(route) || route.startsWith(`${entry.name} /`);
|
: oses
|
||||||
|
? (os) => oses.includes(os)
|
||||||
|
: (_os, entry) => entry.name.includes(route) || route.startsWith(`${entry.name} /`);
|
||||||
/** @type {Record<Os, {include: MatrixEntry[]}>} */
|
/** @type {Record<Os, {include: MatrixEntry[]}>} */
|
||||||
const picked = { linux: { include: [] }, windows: { include: [] }, macos: { include: [] } };
|
const picked = { linux: { include: [] }, windows: { include: [] }, macos: { include: [] } };
|
||||||
for (const os of /** @type {Os[]} */ (Object.keys(picked))) {
|
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', () => {
|
test('a route that selects nothing fails instead of running an empty green matrix', () => {
|
||||||
expect(() => generate('windows: no-such-method')).toThrow()
|
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