Skip to content

smoke: add the CTI workspaces to the local runner, sharing one arcticpy recipe - #173

Merged
Jammy2211 merged 2 commits into
mainfrom
feature/heart-smoke-table-autocti
Aug 24, 2026
Merged

smoke: add the CTI workspaces to the local runner, sharing one arcticpy recipe#173
Jammy2211 merged 2 commits into
mainfrom
feature/heart-smoke-table-autocti

Conversation

@Jammy2211

Copy link
Copy Markdown
Contributor

Summary

Closes #172.

heart/smoke.py is the local mirror of CI, but the smoke: block in config/repos.yaml had no autocti entries and import_names had no PyAutoCTI. So neither CTI smoke suite could be run locally — the only way to exercise them was to push and wait, which is the slowest possible loop for the repo group that just acquired new coverage (autocti_workspace#28 gave that repo its first CI).

The arcticpy design decision

import autocti hard-requires arcticpy, which is not a pip dependency, and a composite action cannot be invoked from Python. Two of the three options in the task turn out to answer different questions, so both are taken:

question answer
where does the recipe live? The action's five run-steps are extracted 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 running identical bytes.
what triggers it locally? A per-workspace arcticpy: true key in the smoke: block, mirroring the input the CTI CI callers already pass. Declared rather than inferred from PyAutoCTI in chain.

Rejected: a Python leg in smoke.py mirroring the recipe. That is exactly the divergence #170 was created to end.

Extraction is safe for the action's cross-repo reach — a composite action is downloaded with its whole directory, so ${{ github.action_path }}/install_arcticpy.sh resolves for a consumer with no PyAutoHeart checkout. All three consumers (lib-tests.yml ×2, smoke-tests.yml) call the action with no with: block at all, so nothing at any call site changes.

The pin had quietly re-acquired two copies

While extracting, found that arcticpy==2.6 was written out in three places, not one:

  • action.yml's version input default: "2.6"
  • .github/workflows/arcticpy-action.yml's version: ${{ inputs.version || '2.6' }}

The second one matters. After a pin bump, the self-test would have gone on building the old version — the one workflow whose entire job is to prove a recipe change works would have been testing the wrong thing. Both now defer to the script's single default.

Two things only running it could have found

1. The GSL probe was broken in the way that matters. My first cut used ls a b c, which exits non-zero when any operand is missing — so on every machine that actually has GSL (in exactly one prefix) it reported the headers absent and refused to build. It now tests each prefix independently, and the list is overridable via ARCTICPY_GSL_PREFIXES, which also serves the no-root workaround in PyAutoCTI/AGENTS.md.

2. pip check can never pass in a CTI environment. The preflight built a perfect environment and then destroyed it:

arcticpy 2.6 has requirement numpy~=1.21, but you have numpy 2.5.2.

arcticpy declares numpy~=1.21 and is installed with --no-deps on purpose — honouring it downgrades numpy below 2.0 and breaks the rest of the stack. So that metadata stays permanently unsatisfied and pip check reports it every single time. Without handling this, arcticpy: true would have been useless.

The preflight now tolerates exactly that one line, and only for a workspace that declared arcticpy: true. Any other broken requirement — in arcticpy or anything else — still fails.

Local behaviour: builds arcticpy, never touches the system

smoke.py runs the shared script with the GSL leg disabled. A local dev command must not mutate system packages, and apt-get does not exist on macOS — so instead of reaching for sudo it proves the headers are present and fails with the exact install line for the platform. Where GSL is present it builds unattended, so pyauto-heart smoke autocti works in one step.

The fingerprint hashes the shared script under a fixed key, so editing the recipe or bumping the pin invalidates the cached environment rather than silently reusing one built from the old recipe. (It cannot go through the existing relative_to(organism_root) path — the script lives in the Heart checkout, which need not sit under the organism root.)

Test Plan

The acceptance criterion — actually running the suites, not "the config parses". Python 3.12, environments prepared by the local runner from scratch:

suite result
pyauto-heart smoke autocti_test env built (arcticpy 2.6 via the shared recipe), preflight passed, 3/3 PASSdataset_1d/model_fit.py 12.3s, imaging_ci/model_fit.py 8.0s, plot/subplots.py 11.1s
pyauto-heart smoke autocti env built, preflight passed, 3/3 PASSdataset_1d/modeling/start_here.py 30.1s, features/species_x3.py 27.6s, imaging_ci/modeling/start_here.py 159.1s

Both are cold numbers (datasets simulated from scratch). The autocti suite's own AGENTS.md records 132 s for CI; 217 s here reflects a slower container, not a regression.

Unit tests: 602 pass (587 pre-existing + 15 new), zero failures.

New coverage:

  • arcticpy defaults off and is read from config; only the CTI workspaces request it; every chain repo has an import name (the omission that hid PyAutoCTI weakens the preflight silently rather than failing it).

  • The fingerprint tracks the shared recipe, and a non-arcticpy workspace is unaffected by it.

  • The recipe runs before the workspace epilogue (CI's order), with an explicit PYTHON and ARCTICPY_INSTALL_GSL=false.

  • A missing installer raises rather than skipping silently.

  • Three tests exercising the real script: the GSL probe accepts headers in any one prefix (the regression above), refuses with an actionable message without them, and never reaches for sudo when the GSL leg is disabled.

  • Four tests on the pip check tolerance: the arcticpy line passes, any other conflict still fails, the arcticpy line is not tolerated for a workspace that never asked for it, and a clean check passes through.

  • Both CTI suites run and pass through the local runner

  • 602/602 unit tests

  • All three action consumers verified to pass no version override

  • arcticpy-action.yml self-test green on this PR — it triggers on .github/actions/install-arcticpy/**, so it exercises the extracted script end-to-end on 3.12 and 3.13


Generated by Claude Code

claude added 2 commits August 24, 2026 21:28
…py recipe

heart/smoke.py is the local mirror of CI, but the `smoke:` block in
config/repos.yaml had no autocti entries and import_names had no PyAutoCTI — so
neither CTI smoke suite could be run locally, and the only way to exercise them
was to push and wait. That is the slowest possible loop for the repo group that
just acquired new coverage (autocti_workspace#28 gave that repo its first CI).

The design question was arcticpy: `import autocti` hard-requires it, it is not a
pip dependency, and a composite action cannot be invoked from Python. Two of the
three options in the task turn out to answer different questions, so both are
taken:

  * WHERE THE RECIPE LIVES — the action's five run-steps are extracted into
    .github/actions/install-arcticpy/install_arcticpy.sh. The action calls it via
    ${{ github.action_path }} (a composite action is downloaded with its whole
    directory, so cross-repo consumers with no PyAutoHeart checkout still work);
    smoke.py calls the same file out of the Heart checkout. One file, one pin,
    both consumers running identical bytes.

  * WHAT TRIGGERS IT — a per-workspace `arcticpy: true` key in the `smoke:`
    block, mirroring the input the CTI CI callers already pass. Declared rather
    than inferred from `PyAutoCTI in chain`, so CI and the local runner are
    configured by the same explicit statement.

Rejected: a Python leg in smoke.py mirroring the recipe. That is the divergence
#170 was created to end.

While extracting, found the pin had quietly acquired two more copies:
action.yml's `version` input defaulted to "2.6", and arcticpy-action.yml passed
`${{ inputs.version || '2.6' }}`. The latter matters — the self-test would have
gone on proving the OLD version built after a bump. Both now defer to the
script's single default; no consumer passes `version` at all.

Two things found by running it rather than reading it:

  * The GSL probe used `ls a b c`, which exits non-zero when ANY operand is
    missing — so on every machine that actually has GSL (in exactly one prefix)
    it reported the headers absent and refused to build. Now tests each prefix
    independently, and the list is overridable via ARCTICPY_GSL_PREFIXES, which
    also serves the no-root workaround in PyAutoCTI/AGENTS.md.

  * `pip check` can NEVER pass in a CTI environment. arcticpy declares
    numpy~=1.21 and is installed with --no-deps on purpose, since honouring it
    downgrades numpy below 2.0 and breaks the stack, so the preflight reported
    "arcticpy 2.6 has requirement numpy~=1.21, but you have numpy 2.5.2" and
    destroyed every environment immediately after building it. The preflight now
    tolerates exactly that one line, and only for a workspace that declared
    `arcticpy: true`; any other broken requirement still fails.

The local leg never runs apt: a dev command must not mutate system packages, and
apt-get does not exist on macOS. It proves the headers are present and fails with
the install line instead.

The fingerprint hashes the shared script, so editing the recipe or bumping the
pin invalidates the cached environment rather than silently reusing a stale one.

Verified end to end, not just "the config parses" — `pyauto-heart smoke
autocti_test` on Python 3.12 built its environment (arcticpy 2.6 from the shared
recipe), passed preflight, and ran 3/3 scripts PASS.

Refs #172

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014wiN7R1yaeGj6k1Pa1FEq4
CI's `repos_sync.py --check --only "tenant firewall (organ code)"` failed the
pytest job — after all 602 tests passed — on three instance facts the previous
commit introduced. The firewall keeps satellite repo names out of PyAutoBrain /
PyAutoHeart / PyAutoHands so an adopting fork has a small, declared set of files
to rewrite. tests/test_smoke.py's own fixture comment says exactly this, and I
added the names anyway.

  * heart/smoke.py and install_arcticpy.sh are UNLISTED, where any instance fact
    is drift. Both mentions were prose in comments and carried no weight:
    "inferred from ``PyAutoCTI in chain``" -> "inferred from the chain's
    contents", and a pointer to the CTI library's AGENTS.md by name -> by role.

  * tests/test_repo_config.py is allowlisted, but for {PyAutoCTI,
    autocti_workspace, autocti_workspace_test} only; the new chain assertion
    added PyAutoArray and PyAutoFit. Rather than grow the entry — the list's own
    comment says never to do that casually — the assertion now goes through
    `import_names`, comparing package names (autonerves/autofit/autoarray/
    autocti) instead of repo-name literals. It says the same thing and
    additionally proves every chain repo resolves in the map, so it is a
    slightly better test than the one it replaces.

Verified by running the real check, not by inspection: with PyAutoHeart symlinked
under a root using the casing the checker expects (it silently SKIPS an organ
directory it cannot find, so a lowercase checkout gives a vacuous OK), it reports
OK — and still reports the mismatch when a fact is deliberately planted.

602 tests still pass.

Refs #172

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014wiN7R1yaeGj6k1Pa1FEq4
@Jammy2211
Jammy2211 merged commit 2ccefe3 into main Aug 24, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants