Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


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

# One run per commit: PR events carry the CI; pushes only build main
# (Heart's ws_ci gate reads main-HEAD conclusions). Superseded runs are
Expand All @@ -30,13 +41,32 @@ jobs:
matrix:
python-version: ["3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Checkout PyAutoHeart
uses: actions/checkout@v4
with:
path: PyAutoHeart
# The body map + firewall checker. Pinned to main: this gate tests
# Heart against the CURRENT organism identity, the same pairing a local
# workspace has.
- name: Checkout PyAutoMind (the body map — the firewall gate reads it)
uses: actions/checkout@v4
with:
repository: PyAutoLabs/PyAutoMind
path: PyAutoMind
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: PyAutoHeart/pyproject.toml
- name: Install (PyYAML core + pytest dev extra)
working-directory: PyAutoHeart
run: pip install -e ".[dev]"
- name: Run tests
working-directory: PyAutoHeart
run: pytest tests/ -q
- name: Tenant firewall (instance facts stay in declared config surfaces)
run: >
python3 PyAutoMind/scripts/repos_sync.py --check
--only "tenant firewall (organ code)"
--root "$GITHUB_WORKSPACE"
8 changes: 4 additions & 4 deletions bin/pyauto-heart
Original file line number Diff line number Diff line change
Expand Up @@ -376,9 +376,9 @@ pyauto-heart smoke [WORKSPACE ...] [--prepare-only] [--rebuild]
[--python PATH] [--root DIR] [--state-dir DIR]

Prepare a separate, cached Python environment for each selected workspace,
then run its workspace-owned smoke runner. The default is all six workspaces;
valid filters are: autofit, autogalaxy, autolens, autolens_test, euclid,
howtolens.
then run its workspace-owned smoke runner. The workspace table (keys,
directories, install chains) is the smoke: block of config/repos.yaml; the
default is every workspace configured there.

Environment dependencies come from each workspace's CI smoke_install.sh (or a
metadata-derived fallback for a legacy workspace), not from the invoking shell.
Expand All @@ -389,7 +389,7 @@ source imports, and Jupyter kernel ownership before science scripts begin.
--prepare-only Build/reuse and preflight; do not run workspace scripts
--rebuild Force selected cached environments to rebuild
--python PATH Base Python used to create environments (default: current)
--root DIR PyAutoLabs root (default: \$PYAUTO_ROOT)
--root DIR organism root (default: \$PYAUTO_ROOT)
--state-dir DIR Environment/cache root (default: \$HEART_STATE_DIR)
EOF
}
Expand Down
33 changes: 33 additions & 0 deletions config/repos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,39 @@ version_skew:
euclid_strong_lens_modeling_pipeline: {library: PyAutoLens, package: autolens}
autolens_assistant: {library: PyAutoLens, package: autolens}

# Smoke-suite table: the workspaces the local smoke runner (heart/smoke.py)
# can prepare and run — each key names its workspace checkout directory and the
# library install chain — plus the repo -> import-package map the preflight
# proves. Read strictly by heart/smoke.py (a missing block fails loudly rather
# than silently preparing nothing); an adopter replaces this with their own
# workspaces. Repo IDENTITY here must match the body map, same as above.
smoke:
import_names:
PyAutoNerves: autonerves
PyAutoFit: autofit
PyAutoArray: autoarray
PyAutoGalaxy: autogalaxy
PyAutoLens: autolens
workspaces:
autofit:
directory: autofit_workspace
chain: [PyAutoNerves, PyAutoFit]
autogalaxy:
directory: autogalaxy_workspace
chain: [PyAutoNerves, PyAutoFit, PyAutoArray, PyAutoGalaxy]
autolens:
directory: autolens_workspace
chain: [PyAutoNerves, PyAutoFit, PyAutoArray, PyAutoGalaxy, PyAutoLens]
autolens_test:
directory: autolens_workspace_test
chain: [PyAutoNerves, PyAutoFit, PyAutoArray, PyAutoGalaxy, PyAutoLens]
euclid:
directory: euclid_strong_lens_modeling_pipeline
chain: [PyAutoNerves, PyAutoFit, PyAutoArray, PyAutoGalaxy, PyAutoLens]
howtolens:
directory: HowToLens
chain: [PyAutoNerves, PyAutoFit, PyAutoArray, PyAutoGalaxy, PyAutoLens]

# Excluded — not polled. Listed here for documentation only.
excluded:
- admin_jammy # personal tooling
Expand Down
85 changes: 23 additions & 62 deletions heart/smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,14 @@
from pathlib import Path
from typing import Iterable, Mapping, Sequence

import yaml

FINGERPRINT_SCHEMA = 1
MARKER_NAME = ".pyauto-smoke-environment.json"

HEART_HOME = Path(__file__).resolve().parents[1]
CONFIG_PATH = HEART_HOME / "config" / "repos.yaml"


@dataclass(frozen=True)
class WorkspaceSpec:
Expand All @@ -40,69 +45,25 @@ class WorkspaceSpec:
chain: tuple[str, ...]


WORKSPACES: dict[str, WorkspaceSpec] = {
"autofit": WorkspaceSpec(
"autofit", "autofit_workspace", ("PyAutoNerves", "PyAutoFit")
),
"autogalaxy": WorkspaceSpec(
"autogalaxy",
"autogalaxy_workspace",
("PyAutoNerves", "PyAutoFit", "PyAutoArray", "PyAutoGalaxy"),
),
"autolens": WorkspaceSpec(
"autolens",
"autolens_workspace",
(
"PyAutoNerves",
"PyAutoFit",
"PyAutoArray",
"PyAutoGalaxy",
"PyAutoLens",
),
),
"autolens_test": WorkspaceSpec(
"autolens_test",
"autolens_workspace_test",
(
"PyAutoNerves",
"PyAutoFit",
"PyAutoArray",
"PyAutoGalaxy",
"PyAutoLens",
),
),
"euclid": WorkspaceSpec(
"euclid",
"euclid_strong_lens_modeling_pipeline",
(
"PyAutoNerves",
"PyAutoFit",
"PyAutoArray",
"PyAutoGalaxy",
"PyAutoLens",
),
),
"howtolens": WorkspaceSpec(
"howtolens",
"HowToLens",
(
"PyAutoNerves",
"PyAutoFit",
"PyAutoArray",
"PyAutoGalaxy",
"PyAutoLens",
),
),
}
def load_smoke_config(
config_path: Path | str = CONFIG_PATH,
) -> tuple[dict[str, WorkspaceSpec], dict[str, str]]:
"""Workspace specs + repo -> import-name map, from the policy file's
``smoke`` block. Strict: a missing block is a config bug and fails
loudly rather than silently preparing nothing (the ``version_skew``
idiom). Which workspaces exist is instance policy, so it lives in
``config/repos.yaml`` — the declared surface an adopting fork replaces —
not in this module."""
cfg = yaml.safe_load(Path(config_path).read_text()) or {}
block = cfg["smoke"]
workspaces = {
key: WorkspaceSpec(key, spec["directory"], tuple(spec["chain"]))
for key, spec in block["workspaces"].items()
}
return workspaces, dict(block["import_names"])


IMPORT_NAMES = {
"PyAutoNerves": "autonerves",
"PyAutoFit": "autofit",
"PyAutoArray": "autoarray",
"PyAutoGalaxy": "autogalaxy",
"PyAutoLens": "autolens",
}
WORKSPACES, IMPORT_NAMES = load_smoke_config()


class SmokeEnvironmentError(RuntimeError):
Expand Down Expand Up @@ -588,7 +549,7 @@ def _parser() -> argparse.ArgumentParser:
default=Path(
os.environ.get("PYAUTO_ROOT", Path(__file__).resolve().parents[2])
),
help="PyAutoLabs organism root",
help="organism root",
)
parser.add_argument(
"--state-dir",
Expand Down
9 changes: 7 additions & 2 deletions tests/test_release_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,12 +255,17 @@ def test_migration_blocked_by_timeouts_alone():
def test_migration_blocked_by_per_project_counts_alone():
"""`per_project` is merged independently of `totals`, so it can be the only
adverse signal — and the guard must use the same definition of "adverse"
that `validate._has_adverse_evidence` does."""
that `validate._has_adverse_evidence` does.

The project key is deliberately synthetic (`demo_workspace`): it is merged
generically, never matched against a repo list, so a real name here would
be an instance fact in organ code (the tenant firewall's concern) for no
test value."""
d = rr.decide(
{"ts": "2026-08-14T20:00:00+00:00",
"stages": {"integrate": {"status": "pass"}},
"totals": {"passed": 9, "failed": 0, "skipped": 0, "timeout": 0},
"per_project": {"autolens_workspace":
"per_project": {"demo_workspace":
{"passed": 6, "failed": 1, "skipped": 0, "timeout": 0}}},
{"last_ingested_run_id": 7}, _run(run_id=7),
)
Expand Down
Loading
Loading