ci(install-e2e): back to the generator - workflows stay generic, names carry everything

Revert the hardcoded 16-job experiment: the combination spec belongs
in scripts/sandbox/generate-e2e-matrix.mjs (restored), not copy-pasted
YAML blocks. What survives from the experiment:

* leg names carry everything - 'os: install -> update (tag -> HEAD)' -
  generated per entry, since slash-joined names are all the graph
  renders;
* pick-releases annotates each tag ({ref, desktop}) and the generator
  threads tag_has_desktop onto windows entries, so the windows run
  workflow still gates pre-desktop tags without a probe job;
* the per-OS run workflows are untouched: single job, static 'e2e'
  name, native skip gates own all capability knowledge.

install-e2e.yml is one generate job + three per-OS matrix fanouts.
Generator shape (4/16/12 legs for 2 tags), annotation threading, and
all six error paths verified; all four workflows pass actionlint;
driver parses clean pure-ASCII.
This commit is contained in:
ethernet
2026-08-11 16:54:54 -04:00
parent 4e886166ac
commit 1c60bdc30d
5 changed files with 309 additions and 258 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ name: Install & Update E2E — macOS (reusable)
# next to where the driver will be, and implementing a method flips this
# workflow's job-level `if`.
#
# Declared methods (see install-e2e.yml's macos-* jobs):
# Declared methods (see the generator's SPEC):
# install: curl-bash, packaged-app
# update: curl-bash, hermes-update, app-update
+1 -1
View File
@@ -10,7 +10,7 @@ name: Install & Update E2E (reusable)
# is independent: its own sandbox, its own install, nothing rewound or
# shared.
#
# Method ids come from install-e2e.yml's combination jobs. Supported
# Method ids come from scripts/sandbox/generate-e2e-matrix.mjs. Supported
# today: install via curl-bash, update via hermes-update or curl-bash.
# Anything else NATIVELY SKIPS (grey check, no runner): capability
# knowledge lives here, next to the driver, so the caller can dispatch
+80 -253
View File
@@ -2,28 +2,26 @@ name: Install & Update E2E
# Can a user on a released version get to this commit?
#
# One matrix job per {os, install-method -> update-method} combination a
# user could be on, hardcoded HERE so the workflow is the support matrix
# and the Actions graph draws one box per combination. Each box fans out
# over the release tags picked at runtime, so every leg reads
# "<tag> -> HEAD": install that tag the combo's way, update to this commit
# the combo's way.
# The support matrix -- every {os, install-method, update-method} combination
# a user could be on -- lives in scripts/sandbox/generate-e2e-matrix.mjs.
# generate-matrix expands it against the picked release tags into one leg
# per {combination, tag}, split into one matrix job per OS:
#
# Matrix: linux-* a real curl|bash install in the bubblewrap sandbox
# (install-e2e-run.yml)
# Matrix: windows-* the real desktop user flow: website Hermes-Setup.exe
# clicked by AutoHotkey, update via the app,
# Playwright clicking "Update now"
# (install-e2e-windows-run.yml)
# Matrix: macos-* no driver yet (install-e2e-macos-run.yml)
# Matrix: linux a real curl|bash install in the bubblewrap sandbox
# (install-e2e-run.yml)
# Matrix: windows the real desktop user flow: website Hermes-Setup.exe
# clicked by AutoHotkey, update via the app, Playwright
# clicking "Update now" (install-e2e-windows-run.yml)
# Matrix: macos no driver yet (install-e2e-macos-run.yml)
#
# Every combination is dispatched to its OS's run workflow; the run
# workflow natively skips (grey) what its driver cannot run yet -- an
# unimplemented method pair, or a starting tag that predates the surface
# under test (e.g. windows desktop legs from releases before the desktop
# app existed). Capability knowledge lives next to each driver, never
# here: adding a method = adding one job below; implementing one = flipping
# the run workflow's gate.
# under test (pick-releases annotates each tag with what its tree ships,
# e.g. whether the desktop app exists yet). Capability knowledge lives
# next to each driver, never here and never in the generator: declaring a
# method is a spec edit, implementing one is flipping the run workflow's
# gate.
#
# The starting versions are chosen at runtime from the repo's release tags
# (scripts/sandbox/pick-release-tags.sh): newest, oldest, and a spread
@@ -72,8 +70,9 @@ concurrency:
cancel-in-progress: true
jobs:
# Which released versions do we test updating FROM? Resolved once and
# shared by every combination's matrix, so all combos cover the same set.
# Which released versions do we test updating FROM? Resolved once,
# annotated with what each tag's own tree supports, and shared by every
# OS's matrix so all combos cover the same set.
pick-releases:
name: Pick release tags
runs-on: ubuntu-latest
@@ -81,9 +80,10 @@ jobs:
outputs:
tags: ${{ steps.pick.outputs.tags }}
steps:
# This job only reads tag names and runs one script, so take the cheap
# checkout: no blobs (filter), no other files (sparse), but DO fetch tags
# -- they are the whole input, and the default shallow checkout has none.
# This job only reads tag names and trees, so take the cheap
# checkout: no blobs (filter), no other files (sparse), but DO fetch
# tags -- they are the whole input, and the default shallow checkout
# has none.
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
filter: blob:none
@@ -95,11 +95,11 @@ jobs:
set -euo pipefail
tags="$(scripts/sandbox/pick-release-tags.sh --count '${{ inputs.tag-count || 5 }}')"
echo "Testing updates from: $tags"
# Annotate each tag with what its own tree supports, so legs can
# natively skip surfaces the starting version does not have.
# Today: does the release ship the desktop app (apps/desktop,
# #20059)? Cheaper and simpler here -- the tags are already
# checked out -- than a probe job per windows leg.
# Annotate each tag with what its own tree supports, so run
# workflows can natively skip surfaces the starting version does
# not have. Today: does the release ship the desktop app
# (apps/desktop, #20059)? Cheaper here -- the tags are already
# fetched -- than a probe job per leg.
enriched="$(for t in $(echo "$tags" | jq -r '.[]'); do
if git ls-tree -d "$t" apps/desktop | grep -q .; then d=true; else d=false; fi
echo "{\"ref\":\"$t\",\"desktop\":$d}"
@@ -107,248 +107,75 @@ jobs:
echo "Annotated: $enriched"
echo "tags=$enriched" >> "$GITHUB_OUTPUT"
# ---- linux: install via curl-bash ---------------------------------------
linux-curl-bash-to-hermes-update:
name: "linux: curl-bash -> hermes-update (${{ matrix.tag.ref }} -> HEAD)"
if: github.event_name != 'workflow_dispatch' || contains(fromJSON('["all", "both", "update"]'), inputs.route)
# Expand the support matrix against the picked tags: one leg per
# {os, install-method, update-method, tag}, split into a matrix per OS.
generate-matrix:
name: Expand combinations
needs: pick-releases
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
linux: ${{ steps.gen.outputs.linux }}
windows: ${{ steps.gen.outputs.windows }}
macos: ${{ steps.gen.outputs.macos }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
sparse-checkout: scripts/sandbox/generate-e2e-matrix.mjs
sparse-checkout-cone-mode: false
- id: gen
run: |
set -euo pipefail
matrices="$(node scripts/sandbox/generate-e2e-matrix.mjs \
--tags '${{ needs.pick-releases.outputs.tags }}' \
--route '${{ inputs.route || 'all' }}')"
echo "$matrices"
for key in linux windows macos; do
echo "$key=$(echo "$matrices" | node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>console.log(JSON.stringify(JSON.parse(d)[process.argv[1]])))' "$key")" >> "$GITHUB_OUTPUT"
done
linux:
name: ${{ matrix.name }}
if: fromJSON(needs.generate-matrix.outputs.linux).include[0] != null
needs: generate-matrix
strategy:
# One leg breaking is worth knowing about even if another already
# failed, so let every leg report.
fail-fast: false
matrix:
tag: ${{ fromJSON(needs.pick-releases.outputs.tags) }}
max-parallel: 4
matrix: ${{ fromJSON(needs.generate-matrix.outputs.linux) }}
uses: ./.github/workflows/install-e2e-run.yml
with:
install-method: curl-bash
update-method: hermes-update
install-ref: ${{ matrix.tag.ref }}
install-method: ${{ matrix.install_method }}
update-method: ${{ matrix.update_method }}
install-ref: ${{ matrix.install_ref }}
linux-curl-bash-to-curl-bash:
name: "linux: curl-bash -> curl-bash (${{ matrix.tag.ref }} -> HEAD)"
if: github.event_name != 'workflow_dispatch' || contains(fromJSON('["all", "both", "installer"]'), inputs.route)
needs: pick-releases
strategy:
fail-fast: false
max-parallel: 3
matrix:
tag: ${{ fromJSON(needs.pick-releases.outputs.tags) }}
uses: ./.github/workflows/install-e2e-run.yml
with:
install-method: curl-bash
update-method: curl-bash
install-ref: ${{ matrix.tag.ref }}
# ---- windows: install via the website's Hermes-Setup.exe ----------------
windows-desktop-installer-to-desktop-app:
name: "windows: desktop-installer@latest -> desktop-app (${{ matrix.tag.ref }} -> HEAD)"
if: github.event_name != 'workflow_dispatch' || contains(fromJSON('["all", "windows-desktop"]'), inputs.route)
needs: pick-releases
windows:
name: ${{ matrix.name }}
if: fromJSON(needs.generate-matrix.outputs.windows).include[0] != null
needs: generate-matrix
strategy:
fail-fast: false
# 2x-cost runners at ~14 min a leg: two at a time is throughput
# enough for a 5-tag axis without hogging the Windows pool.
# enough without hogging the Windows pool.
max-parallel: 2
matrix:
tag: ${{ fromJSON(needs.pick-releases.outputs.tags) }}
matrix: ${{ fromJSON(needs.generate-matrix.outputs.windows) }}
uses: ./.github/workflows/install-e2e-windows-run.yml
with:
tag-has-desktop: ${{ matrix.tag.desktop }}
install-method: desktop-installer@latest
update-method: desktop-app
install-ref: ${{ matrix.tag.ref }}
install-method: ${{ matrix.install_method }}
update-method: ${{ matrix.update_method }}
install-ref: ${{ matrix.install_ref }}
tag-has-desktop: ${{ matrix.tag_has_desktop }}
windows-desktop-installer-to-installer-rerun:
name: "windows: desktop-installer@latest -> desktop-installer-rerun@latest (${{ matrix.tag.ref }} -> HEAD)"
if: github.event_name != 'workflow_dispatch' || contains(fromJSON('["all", "windows-desktop"]'), inputs.route)
needs: pick-releases
macos:
name: ${{ matrix.name }}
if: fromJSON(needs.generate-matrix.outputs.macos).include[0] != null
needs: generate-matrix
strategy:
fail-fast: false
matrix:
tag: ${{ fromJSON(needs.pick-releases.outputs.tags) }}
uses: ./.github/workflows/install-e2e-windows-run.yml
with:
tag-has-desktop: ${{ matrix.tag.desktop }}
install-method: desktop-installer@latest
update-method: desktop-installer-rerun@latest
install-ref: ${{ matrix.tag.ref }}
windows-desktop-installer-to-hermes-update:
name: "windows: desktop-installer@latest -> hermes-update (${{ matrix.tag.ref }} -> HEAD)"
if: github.event_name != 'workflow_dispatch' || contains(fromJSON('["all", "windows-desktop"]'), inputs.route)
needs: pick-releases
strategy:
fail-fast: false
matrix:
tag: ${{ fromJSON(needs.pick-releases.outputs.tags) }}
uses: ./.github/workflows/install-e2e-windows-run.yml
with:
tag-has-desktop: ${{ matrix.tag.desktop }}
install-method: desktop-installer@latest
update-method: hermes-update
install-ref: ${{ matrix.tag.ref }}
windows-desktop-installer-to-irm-iex:
name: "windows: desktop-installer@latest -> irm-iex (${{ matrix.tag.ref }} -> HEAD)"
if: github.event_name != 'workflow_dispatch' || contains(fromJSON('["all", "windows-desktop"]'), inputs.route)
needs: pick-releases
strategy:
fail-fast: false
matrix:
tag: ${{ fromJSON(needs.pick-releases.outputs.tags) }}
uses: ./.github/workflows/install-e2e-windows-run.yml
with:
tag-has-desktop: ${{ matrix.tag.desktop }}
install-method: desktop-installer@latest
update-method: irm-iex
install-ref: ${{ matrix.tag.ref }}
# ---- windows: install via irm | iex --------------------------------------
windows-irm-iex-to-desktop-app:
name: "windows: irm-iex -> desktop-app (${{ matrix.tag.ref }} -> HEAD)"
if: github.event_name != 'workflow_dispatch' || contains(fromJSON('["all", "windows-desktop"]'), inputs.route)
needs: pick-releases
strategy:
fail-fast: false
matrix:
tag: ${{ fromJSON(needs.pick-releases.outputs.tags) }}
uses: ./.github/workflows/install-e2e-windows-run.yml
with:
tag-has-desktop: ${{ matrix.tag.desktop }}
install-method: irm-iex
update-method: desktop-app
install-ref: ${{ matrix.tag.ref }}
windows-irm-iex-to-installer-rerun:
name: "windows: irm-iex -> desktop-installer-rerun@latest (${{ matrix.tag.ref }} -> HEAD)"
if: github.event_name != 'workflow_dispatch' || contains(fromJSON('["all", "windows-desktop"]'), inputs.route)
needs: pick-releases
strategy:
fail-fast: false
matrix:
tag: ${{ fromJSON(needs.pick-releases.outputs.tags) }}
uses: ./.github/workflows/install-e2e-windows-run.yml
with:
tag-has-desktop: ${{ matrix.tag.desktop }}
install-method: irm-iex
update-method: desktop-installer-rerun@latest
install-ref: ${{ matrix.tag.ref }}
windows-irm-iex-to-hermes-update:
name: "windows: irm-iex -> hermes-update (${{ matrix.tag.ref }} -> HEAD)"
if: github.event_name != 'workflow_dispatch' || contains(fromJSON('["all", "windows-desktop"]'), inputs.route)
needs: pick-releases
strategy:
fail-fast: false
matrix:
tag: ${{ fromJSON(needs.pick-releases.outputs.tags) }}
uses: ./.github/workflows/install-e2e-windows-run.yml
with:
tag-has-desktop: ${{ matrix.tag.desktop }}
install-method: irm-iex
update-method: hermes-update
install-ref: ${{ matrix.tag.ref }}
windows-irm-iex-to-irm-iex:
name: "windows: irm-iex -> irm-iex (${{ matrix.tag.ref }} -> HEAD)"
if: github.event_name != 'workflow_dispatch' || contains(fromJSON('["all", "windows-desktop"]'), inputs.route)
needs: pick-releases
strategy:
fail-fast: false
matrix:
tag: ${{ fromJSON(needs.pick-releases.outputs.tags) }}
uses: ./.github/workflows/install-e2e-windows-run.yml
with:
tag-has-desktop: ${{ matrix.tag.desktop }}
install-method: irm-iex
update-method: irm-iex
install-ref: ${{ matrix.tag.ref }}
# ---- macos: no driver yet (every leg natively skips in the run workflow) -
macos-curl-bash-to-curl-bash:
name: "macos: curl-bash -> curl-bash (${{ matrix.tag.ref }} -> HEAD)"
if: github.event_name != 'workflow_dispatch' || inputs.route == 'all'
needs: pick-releases
strategy:
fail-fast: false
matrix:
tag: ${{ fromJSON(needs.pick-releases.outputs.tags) }}
matrix: ${{ fromJSON(needs.generate-matrix.outputs.macos) }}
uses: ./.github/workflows/install-e2e-macos-run.yml
with:
install-method: curl-bash
update-method: curl-bash
install-ref: ${{ matrix.tag.ref }}
macos-curl-bash-to-hermes-update:
name: "macos: curl-bash -> hermes-update (${{ matrix.tag.ref }} -> HEAD)"
if: github.event_name != 'workflow_dispatch' || inputs.route == 'all'
needs: pick-releases
strategy:
fail-fast: false
matrix:
tag: ${{ fromJSON(needs.pick-releases.outputs.tags) }}
uses: ./.github/workflows/install-e2e-macos-run.yml
with:
install-method: curl-bash
update-method: hermes-update
install-ref: ${{ matrix.tag.ref }}
macos-curl-bash-to-app-update:
name: "macos: curl-bash -> app-update (${{ matrix.tag.ref }} -> HEAD)"
if: github.event_name != 'workflow_dispatch' || inputs.route == 'all'
needs: pick-releases
strategy:
fail-fast: false
matrix:
tag: ${{ fromJSON(needs.pick-releases.outputs.tags) }}
uses: ./.github/workflows/install-e2e-macos-run.yml
with:
install-method: curl-bash
update-method: app-update
install-ref: ${{ matrix.tag.ref }}
macos-packaged-app-to-curl-bash:
name: "macos: packaged-app -> curl-bash (${{ matrix.tag.ref }} -> HEAD)"
if: github.event_name != 'workflow_dispatch' || inputs.route == 'all'
needs: pick-releases
strategy:
fail-fast: false
matrix:
tag: ${{ fromJSON(needs.pick-releases.outputs.tags) }}
uses: ./.github/workflows/install-e2e-macos-run.yml
with:
install-method: packaged-app
update-method: curl-bash
install-ref: ${{ matrix.tag.ref }}
macos-packaged-app-to-hermes-update:
name: "macos: packaged-app -> hermes-update (${{ matrix.tag.ref }} -> HEAD)"
if: github.event_name != 'workflow_dispatch' || inputs.route == 'all'
needs: pick-releases
strategy:
fail-fast: false
matrix:
tag: ${{ fromJSON(needs.pick-releases.outputs.tags) }}
uses: ./.github/workflows/install-e2e-macos-run.yml
with:
install-method: packaged-app
update-method: hermes-update
install-ref: ${{ matrix.tag.ref }}
macos-packaged-app-to-app-update:
name: "macos: packaged-app -> app-update (${{ matrix.tag.ref }} -> HEAD)"
if: github.event_name != 'workflow_dispatch' || inputs.route == 'all'
needs: pick-releases
strategy:
fail-fast: false
matrix:
tag: ${{ fromJSON(needs.pick-releases.outputs.tags) }}
uses: ./.github/workflows/install-e2e-macos-run.yml
with:
install-method: packaged-app
update-method: app-update
install-ref: ${{ matrix.tag.ref }}
install-method: ${{ matrix.install_method }}
update-method: ${{ matrix.update_method }}
install-ref: ${{ matrix.install_ref }}
+223
View File
@@ -0,0 +1,223 @@
#!/usr/bin/env node
/**
* Expand the install/update support matrix into concrete E2E combinations.
*
* One source of truth for every {os, install-method, update-method} pair a
* user could be on. This file only DECLARES and EXPANDS: it knows nothing
* about which combinations CI can drive. Every combination is dispatched to
* its OS's run workflow, and THAT workflow natively skips the method pairs
* its driver cannot run yet -- capability knowledge lives next to each
* driver (install-e2e-run.yml, install-e2e-windows-run.yml,
* install-e2e-macos-run.yml).
*
* Used by .github/workflows/install-e2e.yml, which runs it with the picked
* release tags (annotated at pick time with what each tag's tree ships):
*
* node scripts/sandbox/generate-e2e-matrix.mjs \
* --tags '[{"ref":"v2026.8.3","desktop":true}]' --route all
*
* Prints JSON: { linux: {include:[...]}, windows: {include:[...]},
* macos: {include:[...]} } -- every entry is {name, install_method,
* update_method, install_ref}, and windows entries add tag_has_desktop
* (from the tag annotation) so the run workflow can natively skip
* desktop-surface legs from releases that predate the desktop app.
*/
import path from 'node:path';
import { parseArgs } from 'node:util';
import { fileURLToPath } from 'node:url';
/**
* Method ids are strict machine strings -- workflows and the IMPLEMENTED
* table key off them, so an unknown id must fail loudly (see KNOWN_METHODS).
*
* `versions` on an entry expands it into one combination per version. Only
* "latest" is allowed today: it means "the artifact published on the website
* right now" (Hermes-Setup.exe has no versioned archive yet). When archived
* installer versions exist, widen ALLOWED_VERSIONS.
*/
export const SPEC = {
windows: {
install: [
// irm https://hermes.nousresearch.com/install.ps1 | iex
{ method: 'irm-iex' },
// Website Hermes-Setup.exe, clicked through the GUI.
{ method: 'desktop-installer', versions: ['latest'] },
],
update: [
{ method: 'irm-iex' },
// Run the bootstrap exe again over an existing install (--update flow).
{ method: 'desktop-installer-rerun', versions: ['latest'] },
{ method: 'hermes-update' },
// Settings -> About -> "Update now" inside the running desktop app.
{ method: 'desktop-app' },
],
},
macos: {
install: [
{ method: 'curl-bash' },
{ method: 'packaged-app' },
],
update: [
{ method: 'curl-bash' },
{ method: 'hermes-update' },
{ method: 'app-update' },
],
},
linux: {
install: [
{ method: 'curl-bash' },
],
update: [
{ method: 'curl-bash' },
{ method: 'hermes-update' },
],
},
};
const KNOWN_METHODS = new Set([
'irm-iex',
'desktop-installer',
'desktop-installer-rerun',
'hermes-update',
'desktop-app',
'curl-bash',
'packaged-app',
'app-update',
]);
const ALLOWED_VERSIONS = new Set(['latest']);
function validateEntry(os, kind, entry) {
if (typeof entry.method !== 'string' || !KNOWN_METHODS.has(entry.method)) {
throw new Error(`${os}.${kind}: unknown method id ${JSON.stringify(entry.method)} -- add it to KNOWN_METHODS if intentional`);
}
if ('versions' in entry) {
if (!Array.isArray(entry.versions) || entry.versions.length === 0) {
throw new Error(`${os}.${kind}.${entry.method}: versions must be a non-empty array`);
}
for (const v of entry.versions) {
if (!ALLOWED_VERSIONS.has(v)) {
throw new Error(`${os}.${kind}.${entry.method}: version ${JSON.stringify(v)} not allowed -- only ${[...ALLOWED_VERSIONS].join(', ')} until versioned installer archives exist`);
}
}
}
const unknown = Object.keys(entry).filter((k) => k !== 'method' && k !== 'versions');
if (unknown.length) {
throw new Error(`${os}.${kind}.${entry.method}: unknown keys ${unknown.join(', ')}`);
}
}
/** Expand one method entry into concrete ids ("desktop-installer@latest"). */
export function expandMethod(os, kind, entry) {
validateEntry(os, kind, entry);
if (!entry.versions) return [entry.method];
return entry.versions.map((v) => `${entry.method}@${v}`);
}
/** Every {os, install, update, secondUpdate} combination in SPEC. */
export function generateEnvironments(spec) {
const envs = [];
for (const [os, osSpec] of Object.entries(spec)) {
const { install, update, secondUpdate = [], ...unknown } = osSpec;
if (Object.keys(unknown).length) {
throw new Error(`${os}: unknown spec keys ${Object.keys(unknown).join(', ')}`);
}
// Chained second updates (install -> update -> update again) are a real
// axis -- the updater that RESULTS from an update must itself update --
// but nothing implements them yet. Refuse a spec that declares them so
// the first implementation is forced to come through here.
if (!Array.isArray(secondUpdate) || secondUpdate.length !== 0) {
throw new Error(`${os}: secondUpdate must be empty until a second-update leg is implemented`);
}
const installs = install.flatMap((e) => expandMethod(os, 'install', e));
const updates = update.flatMap((e) => expandMethod(os, 'update', e));
for (const i of installs) {
for (const u of updates) {
envs.push({ os, install: i, update: u, secondUpdate: '' });
}
}
}
return envs;
}
/** Mirror of install-e2e.yml's dispatch `route` choice. */
function routeWants(route, env) {
switch (route) {
case 'all':
return true;
case 'both':
return env.os === 'linux';
case 'update':
return env.os === 'linux' && env.update === 'hermes-update';
case 'installer':
return env.os === 'linux' && env.update === 'curl-bash';
case 'windows-desktop':
return env.os === 'windows';
default:
throw new Error(`unknown route filter: ${JSON.stringify(route)}`);
}
}
/**
* Split the combinations into one matrix per OS.
*
* `tags` (the released versions we test updating FROM, as {ref, desktop}
* annotation objects) is the OUTER axis: for each tag, for each
* combination, one dispatch that installs the tag and updates to HEAD.
* No capability filtering happens here -- every declared combination is
* dispatched, and the OS's run workflow natively skips what its driver
* cannot run yet. Entry names carry everything (os, method pair, tag
* transition) because slash-joined leg names are all the graph renders.
*/
export function buildMatrices(envs, { tags = [], route = 'all' } = {}) {
for (const t of tags) {
if (typeof t?.ref !== 'string' || typeof t?.desktop !== 'boolean') {
throw new Error(`tags must be {ref, desktop} annotation objects, got ${JSON.stringify(t)}`);
}
}
const byOs = { linux: [], windows: [], macos: [] };
for (const env of envs) {
if (!routeWants(route, env)) continue;
const bucket = byOs[env.os];
if (!bucket) {
throw new Error(`no matrix bucket for os ${JSON.stringify(env.os)}`);
}
if (tags.length === 0) {
throw new Error('combinations were selected but no --tags given');
}
for (const tag of tags) {
const entry = {
name: `${env.os}: ${env.install} -> ${env.update} (${tag.ref} -> HEAD)`,
install_method: env.install,
update_method: env.update,
install_ref: tag.ref,
};
if (env.os === 'windows') entry.tag_has_desktop = tag.desktop;
bucket.push(entry);
}
}
return Object.fromEntries(
Object.entries(byOs).map(([os, include]) => [os, { include }]),
);
}
function main() {
const { values } = parseArgs({
options: {
tags: { type: 'string', default: '[]' },
route: { type: 'string', default: 'all' },
},
});
const tags = JSON.parse(values.tags);
if (!Array.isArray(tags)) {
throw new Error('--tags must be a JSON array of {ref, desktop} annotation objects');
}
const envs = generateEnvironments(SPEC);
const matrices = buildMatrices(envs, { tags, route: values.route });
process.stdout.write(`${JSON.stringify(matrices, null, 2)}\n`);
}
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
main();
}
+4 -3
View File
@@ -72,9 +72,10 @@ param(
[string]$Phase = "all",
# Update method to exercise in the update-gui phase, named by the same
# ids install-e2e.yml's combination jobs use. Only "desktop-app" (the
# app's own Update button) is implemented; the others are declared arms
# so the surface is stable when they land.
# ids the combination generator (scripts/sandbox/generate-e2e-matrix
# .mjs) declares. Only "desktop-app" (the app's own Update button) is
# implemented; the others are declared arms so the surface is stable
# when they land.
[ValidateSet("desktop-app", "hermes-update", "desktop-installer-rerun@latest", "irm-iex")]
[string]$Route = "desktop-app",