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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
|
||||
Reference in New Issue
Block a user