Skip to content

smoke: add the CTI workspaces to the local runner, with one arcticpy recipe #172

Description

@Jammy2211

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: autocti to smoke.import_names so the preflight can prove a CTI environment at all.
  • Add autocti and autocti_test workspace 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

  1. .github/actions/install-arcticpy/install_arcticpy.sh — new. Carries the recipe verbatim (GSL, build deps, runtime deps, the pinned build, the importlib.metadata verify) plus the long WHY comment. Env contract: ARCTICPY_VERSION (default 2.6the pin), ARCTICPY_INSTALL_GSL, ARCTICPY_SUDO, PYTHON (default python). Writes version= to $GITHUB_OUTPUT only when that variable is set, so it is equally usable outside Actions.
  2. .github/actions/install-arcticpy/action.yml — its five run-steps collapse to one bash "${{ github.action_path }}/install_arcticpy.sh" with the inputs mapped to those variables. Inputs, defaults and the version output are unchanged, so every consumer is byte-identical at the call site.
  3. config/repos.yamlPyAutoCTI: autocti in import_names; autocti and autocti_test entries carrying arcticpy: true.
  4. heart/smoke.py
    • WorkspaceSpec gains arcticpy: bool (default False); load_smoke_config reads it.
    • _install_environment runs the shared script before the workspace epilogue when the flag is set — the same order as CI.
    • environment_fingerprint hashes 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 existing relative_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.
  5. Teststests/test_smoke.py / tests/test_repo_config.py for 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 — the smoke: block
  • heart/smoke.pyWorkspaceSpec, load_smoke_config, _install_environment, environment_fingerprint

Two traps not to rediscover

  • --no-deps suppresses arcticpy's runtime imports too: arcticpy/read_noise.py imports scipy and matplotlib, and __init__.py imports it.
  • arcticpy exposes no __version__. arcticpy.__version__ raises AttributeError on a healthy install; use importlib.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

  1. Add PyAutoCTI: autocti to smoke.import_names.

  2. Add the two workspace entries with the correct chain
    ([PyAutoNerves, PyAutoFit, PyAutoArray, PyAutoCTI]).

  3. Handle arcticpy. This is the real design question. A composite action
    cannot be invoked from heart/smoke.py, so the local runner needs an
    equivalent. Decide deliberately between: factoring the recipe into a shell
    script that both the action and smoke.py call; a small Python leg in
    smoke.py that mirrors it; or a per-workspace arcticpy: true flag in the
    smoke: block that smoke.py honours.

    Whatever is chosen, there must remain exactly one place the recipe and the
    2.6 pin live.
    Re-creating a second copy would undo PyAutoHeart#170.

  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions