From cbffbeecf4ebb7f24e17241f8738923bfa069fd2 Mon Sep 17 00:00:00 2001 From: alt-glitch Date: Wed, 7 Oct 2026 05:48:02 +0530 Subject: [PATCH] ci: package __init__ files on the update path start the update E2Es Importing a/b/c.py runs a/__init__.py and a/b/__init__.py first. The update-lane routing listed hermes_cli/local_runtime/processes.py (the v2026.9.24 updater imports it after the pull) but not the package init that import runs. #134116 changed what that init loads, its PR never ran e2e-upgrade, and main went red until #134242. classify_changes now routes the package inits above every module the e2e_upgrade and e2e_desktop_update lanes own, and above each shared hub, because a hub's inits run whenever the hub is imported. An init that is itself a hub stays out. The routing guard's import walker counts the package inits a module's imports run, so the next unrouted one fails it. This routes eight inits that were missing: local_runtime, gateway, cron, tools, tools/environments, plugins, hermes_platform, hermes_cli/observability. --- scripts/ci/classify_changes.py | 16 ++++++++++++++++ tests/ci/test_update_ci_routing.py | 2 ++ tests/ci/test_update_transitive_routing.py | 3 +++ 3 files changed, 21 insertions(+) diff --git a/scripts/ci/classify_changes.py b/scripts/ci/classify_changes.py index 70d69c5553..7200475a2e 100644 --- a/scripts/ci/classify_changes.py +++ b/scripts/ci/classify_changes.py @@ -451,6 +451,22 @@ _E2E_LANES: dict[str, tuple[str, ...]] = { "hermes_cli/web_routers/status.py", ), } + + +def _with_package_inits(paths: tuple[str, ...]) -> tuple[str, ...]: + """Importing a/b/c.py runs a/__init__.py and a/b/__init__.py first, so a routed module + routes the package inits above it. A shared hub's inits run whenever the hub is imported, so + they are routed too; only an init that is itself a hub stays out.""" + root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + inits = [f"{'/'.join(parts[:i])}/__init__.py" + for p in (*paths, *sorted(_SHARED_HUBS)) if p.endswith(".py") + for parts in [p.split("/")[:-1]] for i in range(1, len(parts) + 1)] + return tuple(dict.fromkeys([*paths, *(i for i in inits if i not in _SHARED_HUBS + and os.path.isfile(os.path.join(root, i)))])) + + +for _lane in ("e2e_upgrade", "e2e_desktop_update"): + _E2E_LANES[_lane] = _with_package_inits(_E2E_LANES[_lane]) # The upgrade journeys are their own lane; editing one does not start ``e2e``. # The update suite shares apps/desktop/e2e/ with the core suite but not its specs. _E2E_LANE_EXCLUDES = { diff --git a/tests/ci/test_update_ci_routing.py b/tests/ci/test_update_ci_routing.py index 1fa3e06180..7863d9a79a 100644 --- a/tests/ci/test_update_ci_routing.py +++ b/tests/ci/test_update_ci_routing.py @@ -650,6 +650,8 @@ def _imports(path: Path) -> frozenset[str]: base = ".".join([*parts, base] if base else parts) names.add(base) names.update(f"{base}.{a.name}" for a in node.names) + # Importing a.b.c runs a/__init__.py and a/b/__init__.py first. + names |= {".".join(n.split(".")[:i]) for n in names for i in range(1, n.count(".") + 1)} found = set() for name in names: target = _module_file(name) diff --git a/tests/ci/test_update_transitive_routing.py b/tests/ci/test_update_transitive_routing.py index 6aeab38360..e2d21445c0 100644 --- a/tests/ci/test_update_transitive_routing.py +++ b/tests/ci/test_update_transitive_routing.py @@ -17,6 +17,9 @@ from tests.ci.test_update_ci_routing import _REPO, _ci_run, _consumers_reached, # backup is the update transaction's pre-build step, not a build dependency. @pytest.mark.parametrize("path", [ "hermes_cli/local_runtime/processes.py", + # Importing processes runs the package init: v2026.9.24's update died there on a name + # the init's eager imports needed from an already-loaded module. + "hermes_cli/local_runtime/__init__.py", "agent/deadline.py", "agent/memory_provider.py", "pm/plugins_state.py",