Skip to content

Commit 25e8572

Browse files
committed
refactor: extract smoke workspace table to config/repos.yaml + firewall gate
Clears the three PyAutoHeart tenant-firewall findings by the recorded decision rule (PyAutoMind#198): - heart/smoke.py: WORKSPACES + IMPORT_NAMES move to a smoke: block in config/repos.yaml — the declared config surface an adopting fork replaces — behind a strict loader (the version_skew idiom: a missing block fails loudly). Which workspaces exist is instance policy, not runner mechanics. Residual --root help reworded to 'organism root'. - tests/test_smoke.py: synthetic LibraryA/LibraryB chains under an 'organism' root — nothing matches a spec against a real repo list. - tests/test_release_run.py: per_project key -> demo_workspace — merged generically, never matched. heart-tests.yml restructures to a path-based checkout with a PyAutoMind sibling and gains the repos_sync.py firewall leg via its new --only selector, so future instance-fact drift fails in the PR that authors it. Note: the gate step needs the --only selector on PyAutoMind main — merge after the PyAutoMind PR.
1 parent 3abf4cb commit 25e8572

6 files changed

Lines changed: 128 additions & 94 deletions

File tree

.github/workflows/heart-tests.yml

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,21 @@ name: Heart Tests
55
# correct?", which nothing else runs automatically: the health agent runs
66
# Heart's checks *against other repos*, never Heart's own tests.
77
#
8-
# Deliberately ONLY pytest. It must not run `pyauto-heart tick`/`readiness` or
9-
# any health check — those need live repo checkouts + network and are the
10-
# scheduled health agent's job, not a PR gate. The suite is stdlib + PyYAML
11-
# only, so it stays fast and flake-free.
8+
# pytest plus ONE drift leg. The suite must not run `pyauto-heart tick`/
9+
# `readiness` or any health check — those need live repo checkouts + network
10+
# and are the scheduled health agent's job, not a PR gate. The suite is stdlib
11+
# + PyYAML only, so it stays fast and flake-free. The one non-pytest step is
12+
# the tenant-firewall gate (PyAutoMind#198): every firewall finding to date
13+
# merged through a green PR because no PR CI ran the check, so the leg an
14+
# organ PR can cause is verified here, in the PR that authors it.
15+
#
16+
# TWO repos are checked out on purpose: the firewall gate is
17+
# `PyAutoMind/scripts/repos_sync.py --only "tenant firewall (organ code)"`,
18+
# which reads the body map from a PyAutoMind sibling — the same workspace
19+
# layout a local checkout has. `--only` keeps Mind-side legs (map blocks,
20+
# registries) from reddening Heart PRs on drift Heart cannot cause; organs not
21+
# checked out (Brain, Hands) are skipped by the check itself, so this gates
22+
# exactly Heart. Both repos are public — the default GITHUB_TOKEN suffices.
1223

1324
# One run per commit: PR events carry the CI; pushes only build main
1425
# (Heart's ws_ci gate reads main-HEAD conclusions). Superseded runs are
@@ -30,13 +41,32 @@ jobs:
3041
matrix:
3142
python-version: ["3.12", "3.13"]
3243
steps:
33-
- uses: actions/checkout@v4
44+
- name: Checkout PyAutoHeart
45+
uses: actions/checkout@v4
46+
with:
47+
path: PyAutoHeart
48+
# The body map + firewall checker. Pinned to main: this gate tests
49+
# Heart against the CURRENT organism identity, the same pairing a local
50+
# workspace has.
51+
- name: Checkout PyAutoMind (the body map — the firewall gate reads it)
52+
uses: actions/checkout@v4
53+
with:
54+
repository: PyAutoLabs/PyAutoMind
55+
path: PyAutoMind
3456
- name: Set up Python ${{ matrix.python-version }}
3557
uses: actions/setup-python@v5
3658
with:
3759
python-version: ${{ matrix.python-version }}
3860
cache: pip
61+
cache-dependency-path: PyAutoHeart/pyproject.toml
3962
- name: Install (PyYAML core + pytest dev extra)
63+
working-directory: PyAutoHeart
4064
run: pip install -e ".[dev]"
4165
- name: Run tests
66+
working-directory: PyAutoHeart
4267
run: pytest tests/ -q
68+
- name: Tenant firewall (instance facts stay in declared config surfaces)
69+
run: >
70+
python3 PyAutoMind/scripts/repos_sync.py --check
71+
--only "tenant firewall (organ code)"
72+
--root "$GITHUB_WORKSPACE"

bin/pyauto-heart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -376,9 +376,9 @@ pyauto-heart smoke [WORKSPACE ...] [--prepare-only] [--rebuild]
376376
[--python PATH] [--root DIR] [--state-dir DIR]
377377
378378
Prepare a separate, cached Python environment for each selected workspace,
379-
then run its workspace-owned smoke runner. The default is all six workspaces;
380-
valid filters are: autofit, autogalaxy, autolens, autolens_test, euclid,
381-
howtolens.
379+
then run its workspace-owned smoke runner. The workspace table (keys,
380+
directories, install chains) is the smoke: block of config/repos.yaml; the
381+
default is every workspace configured there.
382382
383383
Environment dependencies come from each workspace's CI smoke_install.sh (or a
384384
metadata-derived fallback for a legacy workspace), not from the invoking shell.
@@ -389,7 +389,7 @@ source imports, and Jupyter kernel ownership before science scripts begin.
389389
--prepare-only Build/reuse and preflight; do not run workspace scripts
390390
--rebuild Force selected cached environments to rebuild
391391
--python PATH Base Python used to create environments (default: current)
392-
--root DIR PyAutoLabs root (default: \$PYAUTO_ROOT)
392+
--root DIR organism root (default: \$PYAUTO_ROOT)
393393
--state-dir DIR Environment/cache root (default: \$HEART_STATE_DIR)
394394
EOF
395395
}

config/repos.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,39 @@ version_skew:
113113
euclid_strong_lens_modeling_pipeline: {library: PyAutoLens, package: autolens}
114114
autolens_assistant: {library: PyAutoLens, package: autolens}
115115

116+
# Smoke-suite table: the workspaces the local smoke runner (heart/smoke.py)
117+
# can prepare and run — each key names its workspace checkout directory and the
118+
# library install chain — plus the repo -> import-package map the preflight
119+
# proves. Read strictly by heart/smoke.py (a missing block fails loudly rather
120+
# than silently preparing nothing); an adopter replaces this with their own
121+
# workspaces. Repo IDENTITY here must match the body map, same as above.
122+
smoke:
123+
import_names:
124+
PyAutoNerves: autonerves
125+
PyAutoFit: autofit
126+
PyAutoArray: autoarray
127+
PyAutoGalaxy: autogalaxy
128+
PyAutoLens: autolens
129+
workspaces:
130+
autofit:
131+
directory: autofit_workspace
132+
chain: [PyAutoNerves, PyAutoFit]
133+
autogalaxy:
134+
directory: autogalaxy_workspace
135+
chain: [PyAutoNerves, PyAutoFit, PyAutoArray, PyAutoGalaxy]
136+
autolens:
137+
directory: autolens_workspace
138+
chain: [PyAutoNerves, PyAutoFit, PyAutoArray, PyAutoGalaxy, PyAutoLens]
139+
autolens_test:
140+
directory: autolens_workspace_test
141+
chain: [PyAutoNerves, PyAutoFit, PyAutoArray, PyAutoGalaxy, PyAutoLens]
142+
euclid:
143+
directory: euclid_strong_lens_modeling_pipeline
144+
chain: [PyAutoNerves, PyAutoFit, PyAutoArray, PyAutoGalaxy, PyAutoLens]
145+
howtolens:
146+
directory: HowToLens
147+
chain: [PyAutoNerves, PyAutoFit, PyAutoArray, PyAutoGalaxy, PyAutoLens]
148+
116149
# Excluded — not polled. Listed here for documentation only.
117150
excluded:
118151
- admin_jammy # personal tooling

heart/smoke.py

Lines changed: 23 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,14 @@
2929
from pathlib import Path
3030
from typing import Iterable, Mapping, Sequence
3131

32+
import yaml
33+
3234
FINGERPRINT_SCHEMA = 1
3335
MARKER_NAME = ".pyauto-smoke-environment.json"
3436

37+
HEART_HOME = Path(__file__).resolve().parents[1]
38+
CONFIG_PATH = HEART_HOME / "config" / "repos.yaml"
39+
3540

3641
@dataclass(frozen=True)
3742
class WorkspaceSpec:
@@ -40,69 +45,25 @@ class WorkspaceSpec:
4045
chain: tuple[str, ...]
4146

4247

43-
WORKSPACES: dict[str, WorkspaceSpec] = {
44-
"autofit": WorkspaceSpec(
45-
"autofit", "autofit_workspace", ("PyAutoNerves", "PyAutoFit")
46-
),
47-
"autogalaxy": WorkspaceSpec(
48-
"autogalaxy",
49-
"autogalaxy_workspace",
50-
("PyAutoNerves", "PyAutoFit", "PyAutoArray", "PyAutoGalaxy"),
51-
),
52-
"autolens": WorkspaceSpec(
53-
"autolens",
54-
"autolens_workspace",
55-
(
56-
"PyAutoNerves",
57-
"PyAutoFit",
58-
"PyAutoArray",
59-
"PyAutoGalaxy",
60-
"PyAutoLens",
61-
),
62-
),
63-
"autolens_test": WorkspaceSpec(
64-
"autolens_test",
65-
"autolens_workspace_test",
66-
(
67-
"PyAutoNerves",
68-
"PyAutoFit",
69-
"PyAutoArray",
70-
"PyAutoGalaxy",
71-
"PyAutoLens",
72-
),
73-
),
74-
"euclid": WorkspaceSpec(
75-
"euclid",
76-
"euclid_strong_lens_modeling_pipeline",
77-
(
78-
"PyAutoNerves",
79-
"PyAutoFit",
80-
"PyAutoArray",
81-
"PyAutoGalaxy",
82-
"PyAutoLens",
83-
),
84-
),
85-
"howtolens": WorkspaceSpec(
86-
"howtolens",
87-
"HowToLens",
88-
(
89-
"PyAutoNerves",
90-
"PyAutoFit",
91-
"PyAutoArray",
92-
"PyAutoGalaxy",
93-
"PyAutoLens",
94-
),
95-
),
96-
}
48+
def load_smoke_config(
49+
config_path: Path | str = CONFIG_PATH,
50+
) -> tuple[dict[str, WorkspaceSpec], dict[str, str]]:
51+
"""Workspace specs + repo -> import-name map, from the policy file's
52+
``smoke`` block. Strict: a missing block is a config bug and fails
53+
loudly rather than silently preparing nothing (the ``version_skew``
54+
idiom). Which workspaces exist is instance policy, so it lives in
55+
``config/repos.yaml`` — the declared surface an adopting fork replaces —
56+
not in this module."""
57+
cfg = yaml.safe_load(Path(config_path).read_text()) or {}
58+
block = cfg["smoke"]
59+
workspaces = {
60+
key: WorkspaceSpec(key, spec["directory"], tuple(spec["chain"]))
61+
for key, spec in block["workspaces"].items()
62+
}
63+
return workspaces, dict(block["import_names"])
9764

9865

99-
IMPORT_NAMES = {
100-
"PyAutoNerves": "autonerves",
101-
"PyAutoFit": "autofit",
102-
"PyAutoArray": "autoarray",
103-
"PyAutoGalaxy": "autogalaxy",
104-
"PyAutoLens": "autolens",
105-
}
66+
WORKSPACES, IMPORT_NAMES = load_smoke_config()
10667

10768

10869
class SmokeEnvironmentError(RuntimeError):
@@ -588,7 +549,7 @@ def _parser() -> argparse.ArgumentParser:
588549
default=Path(
589550
os.environ.get("PYAUTO_ROOT", Path(__file__).resolve().parents[2])
590551
),
591-
help="PyAutoLabs organism root",
552+
help="organism root",
592553
)
593554
parser.add_argument(
594555
"--state-dir",

tests/test_release_run.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,12 +255,17 @@ def test_migration_blocked_by_timeouts_alone():
255255
def test_migration_blocked_by_per_project_counts_alone():
256256
"""`per_project` is merged independently of `totals`, so it can be the only
257257
adverse signal — and the guard must use the same definition of "adverse"
258-
that `validate._has_adverse_evidence` does."""
258+
that `validate._has_adverse_evidence` does.
259+
260+
The project key is deliberately synthetic (`demo_workspace`): it is merged
261+
generically, never matched against a repo list, so a real name here would
262+
be an instance fact in organ code (the tenant firewall's concern) for no
263+
test value."""
259264
d = rr.decide(
260265
{"ts": "2026-08-14T20:00:00+00:00",
261266
"stages": {"integrate": {"status": "pass"}},
262267
"totals": {"passed": 9, "failed": 0, "skipped": 0, "timeout": 0},
263-
"per_project": {"autolens_workspace":
268+
"per_project": {"demo_workspace":
264269
{"passed": 6, "failed": 1, "skipped": 0, "timeout": 0}}},
265270
{"last_ingested_run_id": 7}, _run(run_id=7),
266271
)

0 commit comments

Comments
 (0)