feat: PowerShell 7 preflight and launcher - preflight.ps1, wikitool.ps1, doctor policy and Mark of the Web checks, pwsh CI job (#151, B)
CI / verify (push) Successful in 2m16s
CI / pwsh (push) Successful in 1m24s
Release / release (push) Successful in 37s

Files changed:
- .gitea/workflows/ci.yml
- CHANGES.md
- INSTALL.md
- README.md
- VERSION
- docs/why-gates-are-code.md
- instructions/bootstrap.md
- instructions/bug-report.md
- instructions/preflight.md
- instructions/setup-instance.md
- instructions/upgrade-instance.md
- tools/CONTRACT.md
- tools/README.md
- tools/bugreport.py
- tools/chemenu/cli.py
- tools/chemenu/commands/dist_cmd.py
- tools/chemenu/commands/doctor.py
- tools/chemenu/prerequisites.py
- tools/chemenu/tests/conftest.py
- tools/chemenu/tests/test_bugreport.py
- tools/chemenu/tests/test_doctor.py
- tools/chemenu/tests/test_preflight.py
- tools/chemenu/tests/test_preflight_pwsh.py
- tools/chemenu/toolpaths.py
- tools/preflight.ps1
- tools/wikitool
- tools/wikitool.ps1
This commit is contained in:
torben committed 2026-10-01 09:52:05 +02:00
1 parent 4035b1ba12
commit 210e0c8286
27 files changed
+1513 -32

No files matched your search

+2
View File
@@ -35,6 +35,8 @@ _WIKITOOL_ENV = (
"WIKITOOL_TASKS_CONFIG",
"CHEMENU_PREFLIGHT_PLATFORM",
"CHEMENU_PREFLIGHT_LONGPATHS",
"CHEMENU_PREFLIGHT_POLICY",
"CHEMENU_PREFLIGHT_MARKED",
) + tuple(var for var, _harness in HARNESS_ENV_VARS)
# Environment git reads for identity or for where its repo lives. A stray
+3 -2
View File
@@ -61,7 +61,7 @@ def checkout(tmp_path: Path, monkeypatch) -> Path:
json.dumps({"schema": 1, "api_token": TOKEN, "enabled": True})
)
(root / ".wikitool-tools.json").write_text(
json.dumps({"schema": 1, "tools": {"python": {"path": "/definitely/not/here/python"}}})
json.dumps({"schema": 1, "complete": True, "tools": {"python": "/definitely/not/here/python"}})
)
launcher = root / "tools" / "wikitool"
launcher.write_text(LAUNCHER.format(python=sys.executable, title=TITLE))
@@ -271,8 +271,9 @@ def test_the_tools_config_path_check_reports_what_is_missing(checkout, tmp_path)
bundle = collect(checkout, tmp_path, "--no-trace")
config = json.loads((bundle / "environment.json").read_text())["tools_config"]
assert config["status"] == "present"
assert config["content"]["complete"] is True
assert config["path_checks"] == [
{"at": "tools.python.path", "path": "/definitely/not/here/python", "exists": False}
{"at": "tools.python", "path": "/definitely/not/here/python", "exists": False}
]
+71
View File
@@ -103,6 +103,10 @@ def _detail(checks, name) -> str:
return next(c.detail for c in checks if c.name == name)
def _fix(checks, name) -> str:
return next(c.fix or "" for c in checks if c.name == name)
def test_healthy_instance_has_no_fail(instance):
checks = doctor.run_doctor()
assert not any(c.status == "FAIL" for c in checks)
@@ -640,3 +644,70 @@ def test_install_dir_is_not_limited_off_windows(instance, monkeypatch):
monkeypatch.setenv(prerequisites.ENV_PLATFORM, "linux")
monkeypatch.setattr(prerequisites, "install_dir", lambda: "/" + "x" * 300)
assert _status(doctor.run_doctor(), "install-dir") == "OK"
# --- PowerShell: execution policy and Mark of the Web (Gitea #151) -----------------
OPEN_POLICY = "MachinePolicy=Undefined,UserPolicy=Undefined,Process=Undefined,CurrentUser=Undefined,LocalMachine=RemoteSigned"
def _windows(monkeypatch, policy=None, marked=None):
monkeypatch.setenv(prerequisites.ENV_PLATFORM, "windows")
if policy is not None:
monkeypatch.setenv(prerequisites.ENV_POLICY, policy)
if marked is not None:
monkeypatch.setenv(prerequisites.ENV_MARKED, marked)
@pytest.mark.parametrize("check", ["execution-policy", "script-marks"])
def test_powershell_checks_do_not_apply_off_windows(instance, monkeypatch, check):
monkeypatch.setenv(prerequisites.ENV_PLATFORM, "linux")
monkeypatch.setenv(prerequisites.ENV_POLICY, "CurrentUser=AllSigned")
monkeypatch.setenv(prerequisites.ENV_MARKED, "wikitool.ps1")
assert _status(doctor.run_doctor(), check) == "OK"
@pytest.mark.parametrize("policy, expected", [
(OPEN_POLICY, "OK"),
("CurrentUser=Unrestricted,LocalMachine=Restricted", "OK"),
("Process=Restricted,LocalMachine=RemoteSigned", "OK"),
("CurrentUser=Undefined,LocalMachine=Undefined", "OK"),
("CurrentUser=Restricted,LocalMachine=RemoteSigned", "FAIL"),
("LocalMachine=AllSigned", "FAIL"),
])
def test_execution_policy_blocks_only_restricted_and_allsigned(instance, monkeypatch, policy, expected):
_windows(monkeypatch, policy=policy)
assert _status(doctor.run_doctor(), "execution-policy") == expected
def test_execution_policy_fix_names_the_one_line_command(instance, monkeypatch):
_windows(monkeypatch, policy="CurrentUser=Restricted")
fix = _fix(doctor.run_doctor(), "execution-policy")
assert "Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned" in fix
@pytest.mark.parametrize("scope", ["MachinePolicy", "UserPolicy"])
def test_a_group_policy_gets_no_command_it_could_not_obey(instance, monkeypatch, scope):
_windows(monkeypatch, policy=f"{scope}=AllSigned,LocalMachine=RemoteSigned")
checks = doctor.run_doctor()
assert _status(checks, "execution-policy") == "FAIL"
assert "Set-ExecutionPolicy" not in _fix(checks, "execution-policy")
assert "Git Bash" in _fix(checks, "execution-policy")
def test_execution_policy_that_cannot_be_read_is_a_warning(instance, monkeypatch):
monkeypatch.setenv(prerequisites.ENV_PLATFORM, "windows")
assert _status(doctor.run_doctor(), "execution-policy") == "WARN"
def test_marked_scripts_fail_with_the_unblock_command(instance, monkeypatch):
_windows(monkeypatch, marked="wikitool.ps1,preflight.ps1")
checks = doctor.run_doctor()
assert _status(checks, "script-marks") == "FAIL"
assert "wikitool.ps1" in _detail(checks, "script-marks")
assert "Unblock-File" in _fix(checks, "script-marks")
def test_unmarked_scripts_are_ok(instance, monkeypatch):
_windows(monkeypatch)
assert _status(doctor.run_doctor(), "script-marks") == "OK"
+6 -2
View File
@@ -94,7 +94,8 @@ class Machine:
self.root = base / root_name
self.tools = self.root / "tools"
self.tools.mkdir(parents=True)
for name in ("preflight.sh", "prerequisites.txt", "wikitool", "run_wikitool.py", "trace-hook"):
for name in ("preflight.sh", "preflight.ps1", "prerequisites.txt", "wikitool", "wikitool.ps1",
"run_wikitool.py", "trace-hook"):
shutil.copy2(TOOLS / name, self.tools / name)
(self.tools / "requirements.txt").write_text("PyYAML\n", encoding="utf-8")
self.sysbin = base / "sysbin"
@@ -392,8 +393,11 @@ def test_launcher_runs_the_entry_script_with_either_venv_layout(machine, layout)
assert result.stdout.splitlines() == [str(machine.tools / "run_wikitool.py"), "doctor", "--json"]
def test_there_is_a_powershell_launcher_slot_but_no_cmd():
def test_there_is_a_powershell_launcher_and_no_cmd():
"""pwsh resolves `tools/wikitool` to `wikitool.ps1` before the sh launcher, and
cmd.exe is not a supported shell, so there is no `.cmd`."""
assert (TOOLS / "wikitool").is_file()
assert (TOOLS / "wikitool.ps1").is_file()
assert not (TOOLS / "wikitool.cmd").exists()
+304
View File
@@ -0,0 +1,304 @@
"""tools/preflight.ps1 and tools/wikitool.ps1 (Gitea #151, section B).
The PowerShell twin of `test_preflight.py`, run against the same stub machine: a
`PATH` the test builds, stub tools in it, and a fake Python that answers the probe
and fakes `-m venv`/`-m pip`. The scripts need PowerShell 7, so these tests skip
where there is none and run in CI's `pwsh` job, whose image carries it; everything
Windows-specific (policy, Mark of the Web, path limit, Store alias) is driven by the
same kind of environment hook the shell script has.
What these add to the shell tests is the one thing only the twin can break: the two
scripts must record the same file. `test_both_preflights_record_the_same_file`
compares them byte for byte.
"""
from __future__ import annotations
import json
import os
import shutil
import subprocess
from pathlib import Path
import pytest
from chemenu import prerequisites
from chemenu.tests.test_preflight import (
ECHO_PYTHON, SHELLS, TOOLS, Machine, _machine_with_root_of, assert_guidance,
)
PWSH = shutil.which("pwsh")
pytestmark = pytest.mark.skipif(PWSH is None, reason="PowerShell 7 (pwsh) is not installed")
WINDOWS = {"CHEMENU_PREFLIGHT_PLATFORM": "windows", "CHEMENU_PREFLIGHT_LONGPATHS": "1"}
def _pwsh(machine: Machine, script: str, *args: str) -> subprocess.CompletedProcess:
env = {
"PATH": os.pathsep.join(str(d) for d in machine.path_dirs),
"HOME": str(machine.base),
"PIP_LOG": str(machine.pip_log),
"DOTNET_CLI_TELEMETRY_OPTOUT": "1",
"POWERSHELL_TELEMETRY_OPTOUT": "1",
**machine.extra_env,
}
return subprocess.run(
[PWSH, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(machine.tools / script), *args],
capture_output=True, text=True, env=env, timeout=120,
)
def _preflight(machine: Machine, *args: str) -> subprocess.CompletedProcess:
return _pwsh(machine, "preflight.ps1", *args)
@pytest.fixture
def machine(tmp_path: Path) -> Machine:
return Machine(tmp_path.resolve()).standard()
# --- the happy path ---------------------------------------------------------------
def test_complete_path_records_absolute_paths_and_sets_up_the_venv(machine):
result = _preflight(machine)
assert result.returncode == 0, result.stdout + result.stderr
data = machine.recorded()
assert data["schema"] == 1 and data["complete"] is True
assert set(data["tools"]) == {"python", "git", "rg"}
assert data["tools"]["rg"] == str(machine.stubs / "rg")
assert (machine.tools / ".venv" / "bin" / "python").is_file()
assert machine.pip_log.read_text(encoding="utf-8").count("install") == 1
assert "Preflight passed" in result.stdout
def test_second_run_changes_nothing(machine):
assert _preflight(machine).returncode == 0
before = machine.tools_file.read_text(encoding="utf-8")
again = _preflight(machine)
assert again.returncode == 0, again.stdout
assert machine.tools_file.read_text(encoding="utf-8") == before
assert machine.pip_log.read_text(encoding="utf-8").count("install") == 1
@pytest.mark.skipif(not SHELLS, reason="no POSIX shell on this machine")
def test_both_preflights_record_the_same_file(machine):
assert _preflight(machine).returncode == 0
from_pwsh = machine.tools_file.read_bytes()
machine.tools_file.unlink()
assert machine.run(shell=SHELLS[0]).returncode == 0
assert machine.tools_file.read_bytes() == from_pwsh
def test_help_prints_the_usage_and_exits_0(machine):
result = _preflight(machine, "--help")
assert result.returncode == 0
assert "--set" in result.stdout
assert not machine.tools_file.exists()
# --- stop cases -------------------------------------------------------------------
def test_missing_rg_stops_with_42_and_a_missing_line(tmp_path):
machine = Machine(tmp_path.resolve()).standard(rg=False)
result = _preflight(machine)
assert result.returncode == 42
assert any(line.split()[:2] == ["MISSING", "rg"] for line in result.stdout.splitlines())
assert_guidance(result.stdout, "ripgrep (rg) was not found", "--set rg=")
data = machine.recorded()
assert data["complete"] is False and "rg" not in data["tools"]
assert not (machine.tools / ".venv").exists()
def test_python_too_old_stops(machine):
machine.extra_env["FAKE_PY_VERSION"] = "3.9"
result = _preflight(machine)
assert result.returncode == 42
assert "TOO_OLD" in result.stdout
assert_guidance(result.stdout, "Python 3.9 is too old")
def test_invalid_set_path_writes_nothing(machine):
result = _preflight(machine, "--set", f"rg={machine.base / 'nowhere' / 'rg'}")
assert result.returncode == 42
assert_guidance(result.stdout, "The path given for ripgrep (rg) does not work")
assert not machine.tools_file.exists()
def test_valid_set_path_is_recorded_and_kept(machine):
elsewhere = machine.stub("rg", "#!/bin/sh\necho 'ripgrep 14.1.1'\n", machine.base / "opt")
machine.stub("rg", "#!/bin/sh\nexit 1\n")
result = _preflight(machine, f"--set=rg={elsewhere}")
assert result.returncode == 0, result.stdout
assert machine.recorded()["tools"]["rg"] == str(elsewhere)
assert _preflight(machine).returncode == 0
assert machine.recorded()["tools"]["rg"] == str(elsewhere)
def test_set_for_an_unknown_tool_is_a_usage_error(machine):
result = _preflight(machine, "--set", "foo=/bin/sh")
assert result.returncode == 1
assert not machine.tools_file.exists()
def test_venv_that_cannot_be_created_stops(machine):
machine.extra_env["FAKE_VENV_FAIL"] = "1"
result = _preflight(machine)
assert result.returncode == 42
assert_guidance(result.stdout, "ensurepip is not available")
def test_pip_failure_stops_and_is_retried_next_time(machine):
machine.extra_env["FAKE_PIP_FAIL"] = "1"
result = _preflight(machine)
assert result.returncode == 42
assert_guidance(result.stdout, "network unreachable")
assert machine.recorded()["complete"] is False
del machine.extra_env["FAKE_PIP_FAIL"]
assert _preflight(machine).returncode == 0
assert machine.recorded()["complete"] is True
@pytest.mark.parametrize("platform, alias", [("linux", "python3"), ("windows", "python")])
def test_store_alias_is_never_run_and_never_recorded(machine, platform, alias):
marker = machine.base / "alias-was-run"
apps = machine.base / "Users" / "u" / "AppData" / "Local" / "Microsoft" / "WindowsApps"
machine.stub(alias, f"#!/bin/sh\necho ran > '{marker}'\nexit 9009\n", apps)
machine.path_dirs.insert(0, apps)
machine.extra_env.update({"CHEMENU_PREFLIGHT_PLATFORM": platform, "CHEMENU_PREFLIGHT_LONGPATHS": "1"})
if platform == "windows":
machine.python("python")
result = _preflight(machine)
assert result.returncode == 0, result.stdout
recorded = machine.recorded()["tools"]["python"]
assert "WindowsApps" not in recorded
assert recorded.startswith(str(machine.stubs))
assert not marker.exists()
# --- install folder length (D32) --------------------------------------------------
@pytest.mark.parametrize("length, longpaths, expected", [
(95, "0", 0),
(96, "0", 42),
(96, "1", 0),
])
def test_install_folder_limit_at_the_boundary(tmp_path, length, longpaths, expected):
machine = _machine_with_root_of(tmp_path, length)
machine.extra_env["CHEMENU_PREFLIGHT_LONGPATHS"] = longpaths
result = _preflight(machine)
assert result.returncode == expected, result.stdout
if expected == 42:
assert_guidance(result.stdout, f"too long ({length} characters, at most 95)", "C:\\Chemenu")
assert not (machine.tools / ".venv").exists()
# --- execution policy and Mark of the Web (D16, T6) --------------------------------
OPEN = "MachinePolicy=Undefined,UserPolicy=Undefined,Process=Undefined,CurrentUser=Undefined,LocalMachine=RemoteSigned"
@pytest.mark.parametrize("policy", [
OPEN,
"CurrentUser=Unrestricted,LocalMachine=Restricted",
"Process=Restricted,LocalMachine=RemoteSigned",
"CurrentUser=Undefined,LocalMachine=Undefined",
])
def test_a_policy_that_lets_scripts_run_passes(machine, policy):
machine.extra_env.update({**WINDOWS, "CHEMENU_PREFLIGHT_POLICY": policy})
result = _preflight(machine)
assert result.returncode == 0, result.stdout
@pytest.mark.parametrize("policy", [
"CurrentUser=Restricted,LocalMachine=RemoteSigned",
"CurrentUser=AllSigned",
"CurrentUser=Undefined,LocalMachine=Restricted",
"Process=Bypass,LocalMachine=AllSigned",
])
def test_a_policy_that_blocks_scripts_stops_with_the_one_line_fix(machine, policy):
machine.extra_env.update({**WINDOWS, "CHEMENU_PREFLIGHT_POLICY": policy})
result = _preflight(machine)
assert result.returncode == 42
assert_guidance(result.stdout, "Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned")
assert not (machine.tools / ".venv").exists()
@pytest.mark.parametrize("scope", ["MachinePolicy", "UserPolicy"])
def test_a_group_policy_is_a_stop_that_names_the_administrator(machine, scope):
machine.extra_env.update({**WINDOWS, "CHEMENU_PREFLIGHT_POLICY": f"{scope}=AllSigned,LocalMachine=RemoteSigned"})
result = _preflight(machine)
assert result.returncode == 42
assert_guidance(result.stdout, "group policy", "Git Bash")
assert "Set-ExecutionPolicy" not in result.stdout
def test_policy_and_marks_are_not_checked_off_windows(machine):
machine.extra_env.update({
"CHEMENU_PREFLIGHT_PLATFORM": "linux",
"CHEMENU_PREFLIGHT_POLICY": "CurrentUser=AllSigned",
"CHEMENU_PREFLIGHT_MARKED": "preflight.ps1",
})
assert _preflight(machine).returncode == 0
def test_a_marked_script_stops_with_the_unblock_command(machine):
machine.extra_env.update({**WINDOWS, "CHEMENU_PREFLIGHT_MARKED": "wikitool.ps1,preflight.ps1"})
result = _preflight(machine)
assert result.returncode == 42
assert_guidance(result.stdout, "wikitool.ps1", "Unblock-File")
assert not (machine.tools / ".venv").exists()
# --- the launcher -----------------------------------------------------------------
def _launch(machine: Machine, *args: str) -> subprocess.CompletedProcess:
return _pwsh(machine, "wikitool.ps1", *args)
def _complete_file(machine: Machine, complete: bool = True) -> None:
machine.tools_file.write_text(json.dumps(
{"schema": 1, "complete": complete, "tools": {"git": "/usr/bin/git"}}, indent=2),
encoding="utf-8")
@pytest.mark.parametrize("state", ["no file", "no venv", "incomplete"])
def test_launcher_stops_with_42_until_the_preflight_has_passed(machine, state):
if state != "no file":
_complete_file(machine, complete=(state != "incomplete"))
if state != "no venv":
machine.stub("python", ECHO_PYTHON, machine.tools / ".venv" / "bin")
result = _launch(machine, "doctor")
assert result.returncode == 42
assert "tools/preflight.ps1" in result.stderr
assert "ExecutionPolicy Bypass" in result.stderr
@pytest.mark.parametrize("layout", [("bin", "python"), ("Scripts", "python.exe")])
def test_launcher_runs_the_entry_script_with_either_venv_layout(machine, layout):
_complete_file(machine)
machine.stub(layout[1], ECHO_PYTHON, machine.tools / ".venv" / layout[0])
result = _launch(machine, "doctor", "--json")
assert result.returncode == 0, result.stderr
assert result.stdout.splitlines() == [str(machine.tools / "run_wikitool.py"), "doctor", "--json"]
def test_launcher_passes_the_exit_code_of_the_cli_through(machine):
_complete_file(machine)
machine.stub("python", "#!/bin/sh\nexit 7\n", machine.tools / ".venv" / "bin")
assert _launch(machine, "lint").returncode == 7
def test_both_launchers_exist_for_pwsh_to_resolve():
assert (TOOLS / "wikitool.ps1").is_file() and (TOOLS / "wikitool").is_file()
def test_the_python_side_reads_what_the_hooks_say(monkeypatch):
"""The doctor checks use the same hook values the script does; the two
parsers must agree on what a policy list means."""
monkeypatch.setenv(prerequisites.ENV_POLICY, "MachinePolicy=AllSigned,LocalMachine=RemoteSigned")
policy = prerequisites.execution_policy()
assert policy is not None and policy.blocks and policy.group_policy