forked from molecule-ai/molecule-core
fix(release): drift-gate TOP_LEVEL_MODULES + smoke-import main in publish
Two compounding bugs surfaced when 0.1.16 hit production today: 1. scripts/build_runtime_package.py had a hand-curated TOP_LEVEL_MODULES set listing every workspace/*.py that should get its bare imports rewritten to `molecule_runtime.X`. The set silently went stale: - Missing: transcript_auth (added since #87 phase 1c), runtime_wedge, watcher → unrewritten imports shipped, every workspace startup died with ModuleNotFoundError. - Stale: claude_sdk_executor, cli_executor (both removed in #87), hermes_executor (never existed) → harmless but misleading. 2. publish-runtime.yml's wheel-smoke step asserted on stable invariants (BaseAdapter, AdapterConfig, a2a_client error sentinel) but never imported main. So even though main.py held the broken bare `from transcript_auth import ...`, the smoke check passed. Fixes: - Build script now derives the on-disk module set from workspace/*.py and asserts it matches TOP_LEVEL_MODULES exactly. Drift in either direction fails the build with a specific diff message instead of shipping a broken wheel. Closed-list typo guard preserved (we still edit the set explicitly when a module is added/removed) — the gate just makes drift impossible to ignore. - TOP_LEVEL_MODULES updated to current reality: drop the 3 stale, add the 3 missing. - publish-runtime.yml wheel-smoke now `import molecule_runtime.main` before the invariant asserts. main is the entry point and transitively imports every module — any bare-import bug surfaces as ModuleNotFoundError before PyPI accepts the upload. Tested locally: `python3 scripts/build_runtime_package.py --version 0.1.99 --out /tmp/build-test` succeeds, and /tmp/build-test/molecule_runtime/main.py contains the rewritten `from molecule_runtime.transcript_auth import ...`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0a455b7d71
commit
c68dc1877f
8
.github/workflows/publish-runtime.yml
vendored
8
.github/workflows/publish-runtime.yml
vendored
@ -135,6 +135,14 @@ jobs:
|
||||
WORKSPACE_ID=00000000-0000-0000-0000-000000000000 \
|
||||
PLATFORM_URL=http://localhost:8080 \
|
||||
/tmp/smoke/bin/python -c "
|
||||
# Importing main is the strongest smoke test we can do here:
|
||||
# main.py is the entry point and pulls every other module
|
||||
# transitively. If the build script missed an import rewrite
|
||||
# (e.g. left a bare \`from transcript_auth import ...\` instead
|
||||
# of \`from molecule_runtime.transcript_auth import ...\` — the
|
||||
# 0.1.16 incident), this fails with ModuleNotFoundError instead
|
||||
# of shipping to PyPI and breaking every workspace startup.
|
||||
import molecule_runtime.main # noqa: F401
|
||||
from molecule_runtime import a2a_client, a2a_tools
|
||||
from molecule_runtime.builtin_tools import memory
|
||||
from molecule_runtime.adapters import get_adapter, BaseAdapter, AdapterConfig
|
||||
|
||||
@ -42,8 +42,13 @@ from pathlib import Path
|
||||
# matches one of these) gets rewritten to use the package prefix.
|
||||
#
|
||||
# Closed list (not "every .py we copy") because a typo in workspace/ would
|
||||
# otherwise leak into a wrong rewrite. Update this when adding a new
|
||||
# top-level module to workspace/.
|
||||
# otherwise leak into a wrong rewrite. The set is asserted against
|
||||
# `workspace/*.py` at build time — if the disk contents drift from this
|
||||
# list (new module added, old one removed), the build fails loud instead
|
||||
# of silently shipping unrewritten imports. That gap caused 0.1.16 to
|
||||
# ship `from transcript_auth import ...` (unrewritten — module added
|
||||
# without updating this set), which broke every workspace startup with
|
||||
# `ModuleNotFoundError: No module named 'transcript_auth'`.
|
||||
TOP_LEVEL_MODULES = {
|
||||
"a2a_cli",
|
||||
"a2a_client",
|
||||
@ -53,15 +58,12 @@ TOP_LEVEL_MODULES = {
|
||||
"adapter_base",
|
||||
"agent",
|
||||
"agents_md",
|
||||
"claude_sdk_executor",
|
||||
"cli_executor",
|
||||
"config",
|
||||
"consolidation",
|
||||
"coordinator",
|
||||
"events",
|
||||
"executor_helpers",
|
||||
"heartbeat",
|
||||
"hermes_executor",
|
||||
"initial_prompt",
|
||||
"main",
|
||||
"molecule_ai_status",
|
||||
@ -69,7 +71,10 @@ TOP_LEVEL_MODULES = {
|
||||
"plugins",
|
||||
"preflight",
|
||||
"prompt",
|
||||
"runtime_wedge",
|
||||
"shared_runtime",
|
||||
"transcript_auth",
|
||||
"watcher",
|
||||
}
|
||||
|
||||
# Subdirectory packages — these are already real packages (they have or will
|
||||
@ -250,6 +255,26 @@ def main() -> int:
|
||||
print(f"error: source not a directory: {src}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
# Drift gate: assert TOP_LEVEL_MODULES matches workspace/*.py.
|
||||
# Without this, a new top-level module added to workspace/ ships
|
||||
# with unrewritten `from <name> import` statements that explode at
|
||||
# runtime with ModuleNotFoundError. (See 0.1.16 transcript_auth
|
||||
# incident — closed list silently went stale.)
|
||||
on_disk_modules = {
|
||||
f.stem for f in src.glob("*.py")
|
||||
if f.stem not in {"__init__", "conftest"}
|
||||
}
|
||||
missing = on_disk_modules - TOP_LEVEL_MODULES
|
||||
stale = TOP_LEVEL_MODULES - on_disk_modules
|
||||
if missing or stale:
|
||||
print("error: TOP_LEVEL_MODULES drifted from workspace/*.py contents:", file=sys.stderr)
|
||||
if missing:
|
||||
print(f" in workspace/ but NOT in TOP_LEVEL_MODULES (will ship un-rewritten): {sorted(missing)}", file=sys.stderr)
|
||||
if stale:
|
||||
print(f" in TOP_LEVEL_MODULES but NOT in workspace/ (no-op, but misleading): {sorted(stale)}", file=sys.stderr)
|
||||
print(" Edit scripts/build_runtime_package.py:TOP_LEVEL_MODULES to match.", file=sys.stderr)
|
||||
return 3
|
||||
|
||||
pkg_dir = out / "molecule_runtime"
|
||||
print(f"[build] source: {src}")
|
||||
print(f"[build] output: {out}")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user