66appropriate environment. Continues through failures and exits non-zero
77if any script failed.
88
9+ The env resolution itself is NOT implemented here: it is PyAutoHands's
10+ `autohands/env_config.py`, imported below. This file used to carry a copy, and
11+ the copy had already drifted (its `load_env_config` hardcoded
12+ `config/build/profile_smoke.yaml`, so the PR gate was structurally unable to read
13+ the release profile — the seed incident's failure mode 4/7). One resolver
14+ means the PR gate and the release runner cannot disagree about what a script's
15+ environment is. See PyAutoHands docs/env_profile_redesign.md §5 (#161 step 2).
16+
917Mirrors the logic of the `/smoke-test` skill so CI and local runs stay
1018in sync.
1119"""
1220
1321from __future__ import annotations
1422
15- import os
1623import subprocess
1724import sys
1825import time
1926from pathlib import Path
2027
21- import yaml
22-
2328
2429WORKSPACE = Path (__file__ ).resolve ().parents [2 ]
2530SMOKE_FILE = WORKSPACE / "smoke_tests.txt"
2631ENV_VARS_FILE = WORKSPACE / "config" / "build" / "profile_smoke.yaml"
2732SCRIPTS_DIR = WORKSPACE / "scripts"
2833
34+ # CI puts PyAutoHands/autohands on PYTHONPATH (PyAutoHeart's reusable
35+ # smoke-tests.yml clones it alongside the dependency chain); for local runs,
36+ # fall back to the sibling checkout.
37+ try :
38+ from env_config import build_env_for_script , load_env_config
39+ except ImportError : # pragma: no cover - local-run fallback
40+ sys .path .insert (0 , str (WORKSPACE .parent / "PyAutoHands" / "autohands" ))
41+ from env_config import build_env_for_script , load_env_config
42+
2943
3044def load_smoke_scripts () -> list [str ]:
3145 scripts : list [str ] = []
@@ -37,33 +51,20 @@ def load_smoke_scripts() -> list[str]:
3751 return scripts
3852
3953
40- def load_env_config () -> dict :
41- if not ENV_VARS_FILE .exists ():
42- return {"defaults" : {}, "overrides" : []}
43- return yaml .safe_load (ENV_VARS_FILE .read_text ()) or {}
44-
45-
46- def pattern_matches (pattern : str , script_path : str ) -> bool :
47- if "/" in pattern :
48- return pattern in script_path
49- return Path (script_path ).stem == pattern
54+ def load_cfg () -> dict | None :
55+ """Parsed env profile, or None when the workspace has none.
5056
51-
52- def build_env (script_rel : str , cfg : dict ) -> dict :
53- env = os .environ .copy ()
54- defaults = cfg .get ("defaults" ) or {}
55- env .update ({k : str (v ) for k , v in defaults .items ()})
56- for override in cfg .get ("overrides" ) or []:
57- if pattern_matches (override ["pattern" ], script_rel ):
58- for key in override .get ("unset" , []):
59- env .pop (key , None )
60- for key , val in (override .get ("set" ) or {}).items ():
61- env [key ] = str (val )
62- return env
57+ None flows through build_env_for_script -> None -> subprocess inherits the
58+ parent environment, which is what the old local copy's empty-config path
59+ did by hand.
60+ """
61+ if not ENV_VARS_FILE .exists ():
62+ return None
63+ return load_env_config (ENV_VARS_FILE )
6364
6465
65- def run_one (script_rel : str , cfg : dict ) -> tuple [str , int , float , str ]:
66- env = build_env ( script_rel , cfg )
66+ def run_one (script_rel : str , cfg : dict | None ) -> tuple [str , int , float , str ]:
67+ env = build_env_for_script ( Path ( script_rel ) , cfg )
6768 script_path = SCRIPTS_DIR / script_rel
6869 t0 = time .time ()
6970 result = subprocess .run (
@@ -86,7 +87,7 @@ def main() -> int:
8687 if not scripts :
8788 print ("No smoke test scripts listed." )
8889 return 0
89- cfg = load_env_config ()
90+ cfg = load_cfg ()
9091
9192 print (f"Running { len (scripts )} smoke test script(s) from { SMOKE_FILE .name } \n " )
9293 failures : list [tuple [str , int , str ]] = []
0 commit comments