Repository navigation
Description
Overview
heart/smoke.py is the local mirror of what CI does — "one isolated environment per workspace, prepared from the workspace-owned installer". Which workspaces it can prepare comes from the smoke: block in config/repos.yaml, and the CTI repos are absent from it entirely: import_names has no PyAutoCTI, and workspaces has no autocti or autocti_test.
So pyauto-heart smoke autocti cannot work, and neither can a local run of autocti_workspace_test. As of 2026-08-24 there are two CTI smoke surfaces — autocti_workspace_test (3 scripts, ~20 s) and autocti_workspace (new in autocti_workspace#28, 3 curated scripts, ~132 s cold) — and the only way to exercise either is to push and wait for CI. That is the slowest possible loop for the repo group that just acquired new coverage.
Plan
- Add
PyAutoCTI: autoctitosmoke.import_namesso the preflight can prove a CTI environment at all. - Add
autoctiandautocti_testworkspace entries with chain[PyAutoNerves, PyAutoFit, PyAutoArray, PyAutoCTI]. - Give the local runner arcticpy without creating a second copy of the recipe — the thing refactor: one canonical Heart-owned arcticpy CI install for every CTI repo #170 exists to prevent.
- Verify by actually preparing a CTI environment locally and running a suite through it, not by "the config parses".
The arcticpy decision
import autocti hard-requires arcticpy, which is not a pip dependency. In CI this is solved: .github/actions/install-arcticpy owns the recipe and the single arcticpy==2.6 pin, and Heart's smoke-tests.yml runs it behind the arcticpy: true input before the workspace epilogue. A composite action cannot be invoked from heart/smoke.py, so the local runner needs an equivalent.
The prompt offered three options. Two of them turn out to be answers to different questions, so both are taken:
| question | answer |
|---|---|
| where does the recipe live? | Extract the action's steps into .github/actions/install-arcticpy/install_arcticpy.sh. The action calls it via ${{ github.action_path }}; smoke.py calls the same file from the Heart checkout. One file, one pin, both consumers execute identical bytes. |
| what triggers it locally? | A per-workspace arcticpy: true key in the smoke: block, mirroring the arcticpy: true input the CI callers already pass. Explicit rather than inferred from PyAutoCTI in chain. |
The rejected option is the prompt's middle one — a Python leg in smoke.py mirroring the recipe. That is precisely the divergence #170 was created to end (the recipe had drifted into four copies), and it would put a second copy of the 2.6 pin in the tree.
Extraction is safe for the action's reach: a composite action is downloaded with its whole directory, so ${{ github.action_path }}/install_arcticpy.sh resolves for a consumer that has no checkout of PyAutoHeart — the property that made a composite action the right shape in the first place. Its version / sudo / install-gsl inputs keep working, passed through as environment variables.
How far the local runner goes (decided)
It builds arcticpy, but never touches system packages. smoke.py runs the shared script with the GSL leg disabled: the script probes for gsl/gsl_version.h and, if absent, fails with the exact apt-get line to run. Where GSL is present it builds and installs arcticpy into the isolated venv unattended, so pyauto-heart smoke autocti works in one step.
Rejected: running apt-get (a local dev command should not mutate system packages, and it is a hard no-op on macOS); and preflight-only "arcticpy must already be importable" (leaves the manual step this task exists to remove).
Detailed implementation plan
Affected Repositories
- PyAutoHeart (primary, and the only repo changed)
Branch Survey
| Repository | Current Branch | Dirty? |
|---|---|---|
| ./PyAutoHeart | main | clean |
Suggested branch: feature/heart-smoke-table-autocti
Implementation Steps
.github/actions/install-arcticpy/install_arcticpy.sh— new. Carries the recipe verbatim (GSL, build deps, runtime deps, the pinned build, theimportlib.metadataverify) plus the long WHY comment. Env contract:ARCTICPY_VERSION(default2.6— the pin),ARCTICPY_INSTALL_GSL,ARCTICPY_SUDO,PYTHON(defaultpython). Writesversion=to$GITHUB_OUTPUTonly when that variable is set, so it is equally usable outside Actions..github/actions/install-arcticpy/action.yml— its five run-steps collapse to onebash "${{ github.action_path }}/install_arcticpy.sh"with the inputs mapped to those variables. Inputs, defaults and theversionoutput are unchanged, so every consumer is byte-identical at the call site.config/repos.yaml—PyAutoCTI: autoctiinimport_names;autoctiandautocti_testentries carryingarcticpy: true.heart/smoke.py—WorkspaceSpecgainsarcticpy: bool(defaultFalse);load_smoke_configreads it._install_environmentruns the shared script before the workspace epilogue when the flag is set — the same order as CI.environment_fingerprinthashes the shared script under a fixed key when the flag is set, so editing the recipe or bumping the pin invalidates the cached environment instead of silently reusing a stale one. (It cannot go through the existingrelative_to(organism_root)path — the script lives in the Heart checkout, not the organism root.)- GSL probe with an actionable error naming the apt line.
- Tests —
tests/test_smoke.py/tests/test_repo_config.pyfor the new key, the fingerprint sensitivity, and the ordering.
Verification (the acceptance criterion)
Prepare a CTI environment through the local runner and run a suite through it. autocti_workspace_test is the cheap one (~20 s); autocti_workspace is the new 132 s suite.
Key Files
.github/actions/install-arcticpy/{action.yml,install_arcticpy.sh}config/repos.yaml— thesmoke:blockheart/smoke.py—WorkspaceSpec,load_smoke_config,_install_environment,environment_fingerprint
Two traps not to rediscover
--no-depssuppresses arcticpy's runtime imports too:arcticpy/read_noise.pyimportsscipyandmatplotlib, and__init__.pyimports it.- arcticpy exposes no
__version__.arcticpy.__version__raisesAttributeErroron a healthy install; useimportlib.metadata.version("arcticpy").
Original Prompt
Click to expand starting prompt
Heart's local smoke runner cannot run any CTI workspace — no autocti entry
Type: maintenance
Target: pyautoheart
Repos:
- @PyAutoHeart
Difficulty: medium
Autonomy: supervised
Priority: normal
Status: formalised
Filed: 2026-08-24
PyAutoHeart/heart/smoke.py is the local smoke runner — "one isolated
environment per workspace, prepared from the workspace-owned installer", the
local mirror of what CI does. Which workspaces it can prepare is declared in
config/repos.yaml under the smoke: block, and the CTI repos are absent
from it entirely.
So pyauto-heart smoke autocti cannot work, and neither can a local smoke run of
autocti_workspace_test — the only way to exercise CTI smoke is to push and let
CI do it. That is a slow loop for the repo group that has just acquired two smoke
suites.
Work
-
Add
PyAutoCTI: autoctitosmoke.import_names. -
Add the two workspace entries with the correct chain
([PyAutoNerves, PyAutoFit, PyAutoArray, PyAutoCTI]). -
Handle arcticpy. This is the real design question. A composite action
cannot be invoked fromheart/smoke.py, so the local runner needs an
equivalent. Decide deliberately between: factoring the recipe into a shell
script that both the action andsmoke.pycall; a small Python leg in
smoke.pythat mirrors it; or a per-workspacearcticpy: trueflag in the
smoke:block thatsmoke.pyhonours.Whatever is chosen, there must remain exactly one place the recipe and the
2.6pin live. Re-creating a second copy would undo PyAutoHeart#170. -
Verify by actually running it — prepare a CTI environment through the
local runner and run both suites, not just "the config parses".
Context
PyAutoMind/complete/2026/08/arcticpy-install-standardisation.md — why the
recipe has one owner and what breaks when it does not.
Activity
smoke: keep satellite repo names out of organ code (tenant firewall)
Jammy2211 commentedon Aug 24, 2026
Shipped
PR #173 merged (
2ccefe3). All four checks green:pytest (3.12),pytest (3.13), and thearcticpy-actionself-testbuild (3.12)/build (3.13)— the last of which exercises the extracted script end-to-end, since it triggers on.github/actions/install-arcticpy/**.The acceptance criterion — both suites actually run locally:
pyauto-heart smoke autocti_testpyauto-heart smoke autoctiBoth cold, environments built from scratch by the local runner with arcticpy 2.6 from the shared recipe.
Design: two of the three options answered different questions, so both were taken — the recipe extracted to
install_arcticpy.sh(one file, called by the action via${{ github.action_path }}and bysmoke.pyfrom the Heart checkout), triggered by a per-workspacearcticpy: truekey. The Python-leg option was rejected as the divergence #170 exists to prevent. All three action consumers pass nowith:block, so no call site changed.Three things this turned up that inspection would not have:
action.yml's input default andarcticpy-action.yml's|| '2.6'. The second meant the self-test would have gone on proving the old version built after a bump.ls a b cexits non-zero when any operand is missing, and GSL lives in exactly one prefix — so it reported the headers absent on every machine that has them.pip checkcan never pass in a CTI environment. arcticpy declaresnumpy~=1.21and is installed--no-depson purpose, so the preflight built a perfect environment and immediately destroyed it. Without handling this,arcticpy: truewould have been inert. The tolerance is one package, one requirement, and only for workspaces that declared the flag.And one caught in review: CI's tenant-firewall check failed after 602 tests passed — three satellite repo names had leaked into organ code, which
tests/test_smoke.py's own fixture comment warns against. Fixed by rewording two comments and routing the chain assertion throughimport_namesrather than growing the allowlist. Note for anyone verifying this locally: the checker silently skips an organ directory it cannot find, so a lowercase checkout gives a vacuous OK — it was re-run with the expected casing, and confirmed to still catch a deliberately planted fact.Generated by Claude Code