From 13ebc163a19ef2de05550eecd19f43a29cee006b Mon Sep 17 00:00:00 2001 From: ethernet Date: Wed, 23 Sep 2026 17:19:29 -0400 Subject: [PATCH] ci: fold the PowerShell installer job into the tests-os Windows lanes installer-tests.yml predates nothing it still owned. Its pytest step (test_source_launcher_stages.py) is platforms("windows") and already runs in both tests-os Windows lanes, so every installer PR ran it twice. The `installer` lane never gated anything on its own either: every path that set it also sets `python`, which gates tests-os. The two standalone scripts/tests/*.ps1 suites become one platforms("windows") pytest file parametrized over Windows PowerShell 5.1 and pwsh 7, so list_os_marked_tests picks them up with everything else. The `installer` lane goes away from the classifier, detect-changes, ci.yaml and the all-checks-pass gate; the classifier contract now pins that install.ps1 and its suites turn `python` on. --- .github/actions/detect-changes/action.yml | 3 - .github/workflows/bootstrap-installer.yml | 4 +- .github/workflows/ci.yaml | 13 +--- .github/workflows/installer-tests.yml | 59 ------------------- scripts/ci/classify_changes.py | 12 ---- tests/ci/test_classify_changes.py | 16 ++--- .../install/test_install_ps1_script_suites.py | 32 ++++++++++ 7 files changed, 41 insertions(+), 98 deletions(-) delete mode 100644 .github/workflows/installer-tests.yml create mode 100644 tests/scripts/install/test_install_ps1_script_suites.py diff --git a/.github/actions/detect-changes/action.yml b/.github/actions/detect-changes/action.yml index b9ff0f0f7a..c3aaee4061 100644 --- a/.github/actions/detect-changes/action.yml +++ b/.github/actions/detect-changes/action.yml @@ -45,9 +45,6 @@ outputs: npm_lock: description: Post/update the semantic package-lock.json diff PR comment. value: ${{ steps.classify.outputs.npm_lock }} - installer: - description: Run the PowerShell installer tests on a Windows runner. - value: ${{ steps.classify.outputs.installer }} bootstrap: description: Run the bootstrap installer lane (install.sh sandbox + stamp verification). value: ${{ steps.classify.outputs.bootstrap }} diff --git a/.github/workflows/bootstrap-installer.yml b/.github/workflows/bootstrap-installer.yml index cb1087431f..fccf19a2a6 100644 --- a/.github/workflows/bootstrap-installer.yml +++ b/.github/workflows/bootstrap-installer.yml @@ -2,8 +2,8 @@ name: Bootstrap installer # Exercises the bootstrap-installer path on PRs that can affect it: the # POSIX shell installer (scripts/install.sh), its generated pin fragments, -# and the version stamp the `complete` stage ships. The PowerShell installer -# keeps its own Windows-only lane (installer-tests.yml); this lane runs the +# and the version stamp the `complete` stage ships. The PowerShell installer's +# native tests run in the tests-os Windows lanes; this lane runs the # protocol/stamp cross-checks plus a whole current installer against a tiny # application graph. The latter runs real PM/uv and generated launchers, not # every production extra. Tool download/hash tests remain separate. diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8793362235..bb4d0b553e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -72,7 +72,6 @@ jobs: deps: ${{ steps.gate-lanes.outputs.deps }} uv_lock: ${{ steps.gate-lanes.outputs.uv_lock }} npm_lock: ${{ steps.gate-lanes.outputs.npm_lock }} - installer: ${{ steps.gate-lanes.outputs.installer }} bootstrap: ${{ steps.gate-lanes.outputs.bootstrap }} desktop_updater: ${{ steps.gate-lanes.outputs.desktop_updater }} rust: ${{ steps.gate-lanes.outputs.rust }} @@ -147,13 +146,6 @@ jobs: if: needs.detect.outputs.frontend == 'true' uses: ./.github/workflows/js-tests.yml - installer-tests: - name: Installer tests - needs: detect - # Windows-only, and only for PRs that touch install.ps1 or its tests. - if: needs.detect.outputs.installer == 'true' - uses: ./.github/workflows/installer-tests.yml - rust-tests: name: Rust tests needs: detect @@ -166,8 +158,8 @@ jobs: name: Bootstrap installer needs: detect # The bootstrap-installer path: install.sh, the pin fragments it embeds, - # and the version stamp it ships. The PowerShell installer has its own - # Windows-only lane above (installer-tests). + # and the version stamp it ships. The PowerShell installer's native tests + # are platforms("windows") pytest files and run in tests-os. if: needs.detect.outputs.bootstrap == 'true' uses: ./.github/workflows/bootstrap-installer.yml @@ -300,7 +292,6 @@ jobs: - tests-os - lint - js-tests - - installer-tests - rust-tests - bootstrap-installer - e2e-desktop diff --git a/.github/workflows/installer-tests.yml b/.github/workflows/installer-tests.yml deleted file mode 100644 index 1fd485841b..0000000000 --- a/.github/workflows/installer-tests.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Installer tests - -# scripts/install.ps1's PowerShell tests. They exercise the installer as a real -# subprocess, and every path contract they assert (8.3 short-name aliases, -# Git Bash layouts, provider-cmdlet behavior) is Windows-specific — so they need -# a Windows runner. Before this workflow existed the files were in the tree but -# nothing ever ran them. - -on: - workflow_call: - -permissions: - contents: read - -concurrency: - group: installer-tests-${{ github.ref_type == 'tag' && github.run_id || github.ref }} - cancel-in-progress: ${{ github.ref_type != 'tag' }} - -jobs: - powershell: - name: PowerShell installer tests - runs-on: windows-latest - timeout-minutes: 15 - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - # Windows PowerShell 5.1 as well as pwsh 7: install.ps1 is delivered via - # `irm | iex` into whatever shell the user already has, and 5.1 is what - # ships with Windows. A construct that only parses under 7 is a broken - # installer for most of the people hitting it. - - name: 8.3 short-path normalization (pwsh 7) - shell: pwsh - run: pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/tests/test-install-ps1-longpath.ps1 - - - name: 8.3 short-path normalization (Windows PowerShell 5.1) - shell: powershell - run: powershell -NoProfile -ExecutionPolicy Bypass -File scripts/tests/test-install-ps1-longpath.ps1 - - - name: System Node and npm compatibility (pwsh 7) - shell: pwsh - run: pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/tests/test-install-ps1-node-compatibility.ps1 - - - name: System Node and npm compatibility (Windows PowerShell 5.1) - shell: powershell - run: powershell -NoProfile -ExecutionPolicy Bypass -File scripts/tests/test-install-ps1-node-compatibility.ps1 - - - name: Prepare native publication-test tools - uses: ./.github/actions/setup-pm - - - name: Prepare isolated acceptance-test dependencies - shell: bash - run: python -m scripts.ci.python_packages pytest==9.1.1 pytest-asyncio==1.3.0 ruamel.yaml==0.18.17 packaging==26.0 distlib==0.4.3 - - - name: Whole-script launcher publication (PowerShell 5.1 and 7) - shell: bash - env: - HERMES_TEST_FILE_RETRIES: '0' - run: bash scripts/run_tests.sh tests/pm/test_source_launcher_stages.py -k powershell_stage -q -j 1 diff --git a/scripts/ci/classify_changes.py b/scripts/ci/classify_changes.py index f6f48c1f90..0bab2708e4 100644 --- a/scripts/ci/classify_changes.py +++ b/scripts/ci/classify_changes.py @@ -24,7 +24,6 @@ Lanes: PyPI, so a diff that touches neither ``pyproject.toml`` nor ``uv.lock`` must not run it. * ``npm_lock`` — semantic package-lock.json diff PR comment. -* ``installer`` — PowerShell installer tests (Windows runner). * ``bootstrap`` — the bootstrap installer lane: install.sh sandbox install, pin-fragment drift check, and shipped version-stamp verification. * ``desktop_updater`` — the Windows desktop-update hand-off script and the @@ -126,11 +125,6 @@ _SCAN_FILES = {"setup.cfg", "pyproject.toml"} _MCP_CATALOG_PATHS = ("optional-mcps/",) _MCP_CATALOG_FILES = {"hermes_cli/mcp_catalog.py"} -# Windows installer + its PowerShell tests. These only run on a Windows runner, -# so they get their own lane rather than riding along with ``python``. -_INSTALLER_PATHS = ("scripts/tests/",) -_INSTALLER_FILES = {"scripts/install.ps1", "scripts/install.cmd"} - # Bootstrap installer: the POSIX shell installer, the dev-checkout wrapper # that carries the same pin fragment, and the Tauri app's non-Rust sources # (the .rs/Cargo files are the ``rust`` lane's job). Changes here get the @@ -197,10 +191,6 @@ def _is_mcp_catalog(p: str) -> bool: return p.startswith(_MCP_CATALOG_PATHS) or p in _MCP_CATALOG_FILES -def _is_installer(p: str) -> bool: - return p.startswith(_INSTALLER_PATHS) or p in _INSTALLER_FILES - - def _is_desktop_updater(p: str) -> bool: return ( p.startswith(_DESKTOP_UPDATER_PATHS) @@ -256,7 +246,6 @@ def classify(files: list[str]) -> dict[str, bool]: "deps": deps, "uv_lock": any(f in ("pyproject.toml", "uv.lock") for f in files), "npm_lock": npm_lock, - "installer": any(_is_installer(f) for f in files), "bootstrap": any( f.startswith(_BOOTSTRAP_PATHS) or f in _BOOTSTRAP_FILES for f in files ), @@ -277,7 +266,6 @@ def classify(files: list[str]) -> dict[str, bool]: ret["deps"] = True ret["uv_lock"] = True ret["npm_lock"] = True - ret["installer"] = True ret["bootstrap"] = True ret["desktop_updater"] = True ret["rust"] = True diff --git a/tests/ci/test_classify_changes.py b/tests/ci/test_classify_changes.py index 30db63ff22..9e0f8600c0 100644 --- a/tests/ci/test_classify_changes.py +++ b/tests/ci/test_classify_changes.py @@ -40,7 +40,6 @@ DEFAULT = { "deps": True, "uv_lock": True, "npm_lock": True, - "installer": True, "bootstrap": True, "desktop_updater": True, "rust": True, @@ -49,7 +48,7 @@ DEFAULT = { } -def _lanes(python=False, frontend=False, site=False, scan=False, deps=False, uv_lock=False, npm_lock=False, installer=False, bootstrap=False, desktop_updater=False, rust=False, mcp_catalog=False, docker_meta=False, ci_review=False, python_prod=None, nix=None, docker=None) -> dict[str, bool]: +def _lanes(python=False, frontend=False, site=False, scan=False, deps=False, uv_lock=False, npm_lock=False, bootstrap=False, desktop_updater=False, rust=False, mcp_catalog=False, docker_meta=False, ci_review=False, python_prod=None, nix=None, docker=None) -> dict[str, bool]: # python_prod tracks python except for tests-only diffs; default it to # python so the majority of cases don't need to spell it out. # @@ -70,7 +69,6 @@ def _lanes(python=False, frontend=False, site=False, scan=False, deps=False, uv_ "deps": deps, "uv_lock": uv_lock, "npm_lock": npm_lock, - "installer": installer, "bootstrap": bootstrap, "desktop_updater": desktop_updater, "rust": rust, @@ -155,14 +153,10 @@ CASES = { ), # Prose cannot change the closure or the binary. "docs-only → no nix": (["README.md"], _lanes()), - # install.ps1 is a shell script Python never imports, but it's also not - # provably prose, so python stays on (fail-open) alongside the Windows lane. - "install.ps1 → installer": (["scripts/install.ps1"], _lanes(python=True, installer=True)), - "installer test → installer": ( - ["scripts/tests/test-install-ps1-longpath.ps1"], - _lanes(python=True, installer=True), - ), - "python source alone → no installer lane": (["run_agent.py"], _lanes(python=True, scan=True)), + # install.ps1 and its PowerShell suites are exercised by platforms("windows") + # pytest files, so they must turn on python (which gates tests-os). + "install.ps1 → python": (["scripts/install.ps1"], _lanes(python=True)), + "installer suite → python": (["scripts/tests/test-install-ps1-longpath.ps1"], _lanes(python=True)), # The Windows desktop-update hand-off is a PowerShell integration surface: # its tests spawn the real script and poll its loopback server. They run # when the script, the Electron side that launches it, or their own test diff --git a/tests/scripts/install/test_install_ps1_script_suites.py b/tests/scripts/install/test_install_ps1_script_suites.py new file mode 100644 index 0000000000..a98e53b60a --- /dev/null +++ b/tests/scripts/install/test_install_ps1_script_suites.py @@ -0,0 +1,32 @@ +"""install.ps1's self-contained PowerShell suites, under Windows PowerShell 5.1 and pwsh 7. + +``scripts/tests/*.ps1`` drive the real installer as a subprocess and exit +non-zero on any failed assertion. install.ps1 is delivered via ``irm | iex`` +into whatever shell the user already has, and 5.1 is what ships with Windows, +so each suite runs under both: a construct that only parses under 7 is a broken +installer for most of the people hitting it. +""" +import os +from pathlib import Path +import shutil +import subprocess + +import pytest + +pytestmark = pytest.mark.platforms("windows") +SUITES = Path(__file__).resolve().parents[3] / "scripts" / "tests" + + +def _shell(name: str) -> str: + if name == "powershell": + return str(Path(os.environ["SystemRoot"]) / "System32/WindowsPowerShell/v1.0/powershell.exe") + return shutil.which("pwsh") or pytest.fail("native lane requires PowerShell 7") + + +@pytest.mark.parametrize("shell", ["powershell", "pwsh"]) +@pytest.mark.parametrize("suite", sorted(p.name for p in SUITES.glob("test-install-ps1-*.ps1"))) +def test_suite_passes(shell, suite): + result = subprocess.run([_shell(shell), "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", + "-File", str(SUITES / suite)], + capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=300) + assert result.returncode == 0, result.stdout + result.stderr