Operational detail for working inside this repo: the autohands CLI, the
pre-build steps, workspace folder structure, config files, and release.yml.
What PyAutoHands is and the Brain/Heart/Build boundary live in
AGENTS.md — read that first; read this only when changing the
build pipeline itself.
PyAutoHands runs no release-readiness checks of its own (that is PyAutoHeart's job). It automates:
- Building and releasing packages to TestPyPI, then PyPI
- Running workspace Python scripts (integration tests)
- Converting Python scripts to Jupyter notebooks and executing them
- Committing generated notebooks to workspace
mainbranches and tagging each workspace with a version matching the released library
The pipeline is triggered via GitHub Actions (release.yml) and is manually dispatched with configurable options. Release-readiness gating happens upstream: the PyAutoBrain release agent calls pyauto-heart readiness and only dispatches release.yml on a green verdict.
Every operation in this repo is invokable from the shell via the autohands dispatcher at bin/autohands. List subcommands with autohands help; print the docstring for one with autohands help <subcommand> (or autohands <subcommand> --help).
Recommended alias for ~/.bashrc:
alias autohands-help='$HOME/Code/PyAutoLabs/PyAutoHands/bin/autohands help'The dispatcher routes to the underlying bash script directly, or to the Python tool with PYTHONPATH already set so the internal build_util / result_collector / env_config imports resolve. The same operations remain callable as Claude skills (/pre_build, /verify_install, /review_release); use the skill when you want the validation + summary wrapper, the CLI when you just want to fire the underlying tool.
Before triggering a build, run:
bash $HOME/Code/PyAutoLabs/PyAutoHands/bin/autohands pre_build [minor_version]
# minor_version defaults to 1
# (equivalent to: bash $HOME/Code/PyAutoLabs/PyAutoHands/pre_build.sh [minor_version])This script does the following for each repo:
| Repo | black | generate.py | commit & push |
|---|---|---|---|
autofit_workspace |
yes | yes (autofit) |
yes |
autogalaxy_workspace |
yes | yes (autogalaxy) |
yes |
autolens_workspace |
yes | yes (autolens) |
yes |
autofit_workspace_test |
yes | no | yes |
autogalaxy_workspace_test |
yes | no | yes |
autolens_workspace_test |
yes | no | yes |
euclid_strong_lens_modeling_pipeline |
yes | no | yes |
HowToGalaxy |
yes | yes (howtogalaxy) |
yes |
HowToLens |
yes | yes (howtolens) |
yes |
HowToFit |
yes | yes (howtofit) |
yes |
Before the per-repo loop, pre_build.sh invokes PyAutoBrain/bin/ensure_workspace_labels.sh to assert the canonical pending-release label across every release-window repo (idempotent — a no-op when nothing has drifted).
Release-readiness checking is not Build's job — PyAutoHands is a pure executor. The version-skew check that used to live here (verify_workspace_versions.sh, a fail-fast guard against a workspace pinned ahead of its installed library, or a config/general.yaml ↔ version.txt disagreement) now lives in PyAutoHeart as the version_skew check feeding pyauto-heart readiness. The PyAutoBrain release agent gates on pyauto-heart readiness before invoking pre_build; a human running pre_build directly is trusted to have checked readiness first. See PyAutoHeart for the resolution precedence (config/general.yaml:version.workspace_version, then version.txt) — mirroring autoconf.workspace.check_version. Since PyAutoBuild#120, releases no longer write workspace version pins or commit __init__.py stamps back to library mains (wheels are stamped at build time; tags are the release anchor): the runtime check enforces a compatibility floor (version.minimum_library_version, bumped deliberately — PyAutoNerves#118), and Heart's version_skew check needs a follow-up rework to compare floors against release tags rather than stamp-vs-pin.
generate.py is run from the workspace root with PYTHONPATH pointing at PyAutoHands/autohands/. Only specific safe directories are committed — never output/, output_model/, or run-generated artefacts. After all workspaces are done, PyAutoHands itself is committed and pushed, then gh workflow run release.yml dispatches the GitHub Actions release.
Each workspace repo (autofit_workspace, autogalaxy_workspace, autolens_workspace, their _test variants, and the lecture repos HowToGalaxy/HowToLens) has the following expected structure. Only these paths should ever be committed.
| Folder / file | autofit | autogalaxy | autolens | Notes |
|---|---|---|---|---|
config/ |
yes | yes | yes | PyAutoNerves config files |
dataset/ |
yes | yes | yes | Allowlisted real observational data only; simulated datasets are never committed (#126/#150) |
notebooks/ |
yes | yes | yes | Generated from scripts/ by generate.py |
scripts/ |
yes | yes | yes | Source Python scripts |
slam_pipeline/ |
no | no | yes | autolens only |
output/ |
— | — | — | Always empty — kept under git with a .gitignore only |
| Root-level files | yes | yes | yes | README.md, llms-full.txt, workspace_index.json, requirements.txt, LICENSE* — committed by release.yml on the runner, not by pre_build.sh (#156) |
output/contents — run results; the folder itself exists only via.gitignoreoutput_model/— model JSON/pickle artefacts written during script executionpath/to/model/or any nested model JSON files written at runtime.fitsfiles outsidedataset/(e.g.image.fits,dataset.fitsgenerated by simulators intoscripts/or other subdirectories)
# Run all tests
pytest
# Run a single test
pytest tests/test_files_to_run.py::test_script_orderWhen running Python from Codex or any restricted environment, set writable cache directories so numba and matplotlib do not fail on unwritable home or source-tree paths:
NUMBA_CACHE_DIR=/tmp/numba_cache MPLCONFIGDIR=/tmp/matplotlib pytestThis workspace is often imported from /mnt/c/... and Codex may not be able to write to module __pycache__ directories or /home/jammy/.cache, which can cause import-time numba caching failures without this override.
All scripts in autohands/ are run from within a checked-out workspace directory (not from this repo root). They rely on PYTHONPATH including the PyAutoHands directory.
run_python.py <project> <directory>— Executes Python scripts in a workspace folder, skipping files listed inconfig/no_run.yamlrun.py <project> <directory> [--visualise]— Executes Jupyter notebooks in a workspace folder, skipping files inconfig/no_run.yamlgenerate.py <project>— Converts Python scripts inscripts/to.ipynbnotebooks innotebooks/, run from within the workspace rootgenerate_markdown.py <project> [--only <substring>]— Renders the curated scripts listed in the workspace'sconfig/build/markdown_examples.yamlto executed markdown pages with output images undermarkdown/, plus an index, committed so examples are readable on GitHub. Manual / at-release only, never per-commit; refusesPYAUTO_TEST_MODE(truncated searches make wrong images — model-fit reruns instead resume from the completedoutput/cache); never rendersfeatures/scripts; restores tracked files a script modifies (e.g. simulators rewritingdataset/). Rules and rationale in the module docstring.script_matrix.py <project1> [project2 ...]— Outputs a JSON matrix of{name, directory}pairs for GitHub Actions matrix strategytag_and_merge.sh --version <version>— Commits pending changes and tags library repos (PyAutoNerves, PyAutoFit, PyAutoArray, PyAutoGalaxy, PyAutoLens) for releaseurl_check— URL hygiene moved to PyAutoHeart (Heart owns all health checking).autohands url_checkis now a thin shim topyauto-heart url_check; the ecosystem-wide sweep runs from PyAutoHeart's centralurl-check.ymlworkflow (replacing the old per-repourl_check.ymlworkflows). The runnable scripts live atPyAutoHeart/heart/checks/url_check*.{sh,py}.bump_colab_urls.sh <new-tag>— Rewrites everycolab.research.google.com/github/PyAutoLabs/<repo>/blob/<old-tag>/...URL in cwd to use<new-tag>, where<repo>is one ofautofit_workspace,autogalaxy_workspace,autolens_workspace,HowToFit,HowToGalaxy,HowToLens. Called by therelease_workspacesandbump_library_colab_urlsjobs inrelease.ymlso README/docs Colab links always pin to the just-released tag. Idempotent; skips URLs not in canonical PyAutoLabs/date-tagged form.
generate.py → generate_autofit.py + build_util.py:
add_notebook_quotes.pytransforms triple-quoted docstrings into# %%cell markers in a temp.pyfileipynb-py-convertconverts the temp file to.ipynbbuild_util.uncomment_jupyter_magic()restores commented-out Jupyter magic commands (e.g.# %matplotlib→%matplotlib)build_util.inject_colab_setup()prepends the standard Google Colab setup cell pair (see "Google Colab architecture" below)- Generated notebooks are
git add -fed directly
The workspaces guard optional-dependency examples with the script idiom
if importlib.util.find_spec("<optional-dep>") is None:
print("Skipping ...")
sys.exit(0)As a .py script that is a clean exit 0. In a Jupyter kernel the same call
raises SystemExit, nbclient marks the cell as errored and
jupyter nbconvert --execute exits non-zero — so an intended skip is
reported as a notebook failure. CI never sees it (its matrices install the
optional extras); users and local runs without them do.
The fix is classification at the point of execution, not a source rewrite:
build_util.is_clean_skip_exit(output) inspects the combined
stdout/stderr of an nbconvert run and returns True only when the run's single
CellExecutionError terminates in SystemExit: 0 (ANSI escapes stripped
first — IPython colours the traceback). SystemExit: 1 and every other
exception stay failures. build_util.execute_notebook consults it in its
CalledProcessError branch and records a PASS, alongside the existing
InversionException exemption.
Rewriting the guard at generation time was rejected: it would need a source-level transform of arbitrary "skip the rest of the notebook" control flow, would diverge notebook semantics from the script the user reads, and would require regenerating and committing notebooks across every workspace.
Propagation. Each workspace repo carries its own
.github/scripts/run_smoke.py (no template sync — PyAutoHeart's reusable
smoke-tests.yml deliberately leaves the runner in the workspace), and that
copy has its own execute_notebook used by the PR smoke gate. It already
imports from PyAutoHands (env_config, build_util.py_to_notebook, with
PyAutoHands/autohands on PYTHONPATH), so adoption is a two-line change per
workspace:
from build_util import is_clean_skip_exit
...
rc, output = execute_notebook(nb_path, env)
if rc != 0 and is_clean_skip_exit(output):
rc = 0Workspaces carrying a run_smoke.py copy: autofit_workspace,
autogalaxy_workspace, autolens_workspace, autofit_workspace_test,
autogalaxy_workspace_test, autolens_workspace_test,
autocti_workspace_test, HowToGalaxy, HowToLens (nine copies, five distinct
revisions — they have drifted). Only the three user-facing workspaces
(autofit_workspace, autogalaxy_workspace, autolens_workspace) have a
notebook execution leg, so only those three needed the edit — applied
2026-07-25. The HowTo and *_test runners are scripts-only (no
smoke_notebooks.txt, no nbconvert path) and require nothing.
Every published notebook must be runnable on Google Colab with zero local installation. Four pieces, spread over three organs plus PyAutoNerves:
- Runtime bootstrap —
PyAutoNerves/autoconf/setup_colab.py. A_PROJECTSregistry (autofit,autogalaxy,autolens,howtofit,howtogalaxy,howtolens) maps each notebook repo to its package stack, workspace repo and Colab directory.setup_colab.setup("<project>")is a no-op outside Colab; on Colab it pip-installs the stack (--no-deps— Colab ships the scientific base), shallow-clones the workspace at the tag matching the installed release (default branch as fallback) and points autoconf's config/output paths at it. - Generation-time injection —
build_util.inject_colab_setup(notebook, project), called bygenerate.py/generate_autofit.pyafter every py→ipynb conversion. It prepends a markdown explainer + code cell callingsetup_colab.setup("<project>"), immediately after the notebook's title cell. Notebooks whose script already hand-writes asetup_colabcall are left untouched.build_util.COLAB_PROJECTSmust stay in sync with the PyAutoNerves registry; an unknown project fails generation loudly. Coverage is therefore guaranteed by construction — every generated notebook is Colab-ready, with no per-script maintenance. - Release maintenance —
bump_colab_urls.sh(above) re-pins every canonical Colab URL in READMEs/docs/notebooks to the just-released tag, from therelease_workspacesandbump_library_colab_urlsjobs. Only date-taggedPyAutoLabs/<repo>URLs are bumped — unpinned or wrong-owner URLs are invisible to it, which is why Heart forbids them (next item). - Monitoring — PyAutoHeart's central
url-check.yml(weekly): the offline guard (heart/checks/url_check.sh) forbids Colab URL forms that rot (Binder,Jammy2211owner,/blob/release/, unpinned/blob/main/, chapter paths pointing at workspace repos instead of the HowTo repos); the live audit (url_check_live.py) converts Colab URLs to raw-GitHub form and 404-checks that each linked notebook actually exists at its pinned tag.
build_util.find_scripts_in_folder() enforces a specific ordering:
- Scripts with "simulator" in the path (data must be generated first)
- Scripts named
start_here.py - All other scripts
Each workspace owns its own build config under <workspace>/config/build/:
no_run.yaml— flat list of script/notebook patterns to skip during execution. Required: every build target must own one (an empty file is valid and skips nothing).run.pyraisesFileNotFoundErrorif it is missing.profile_smoke.yaml/profile_release.yaml— defaults + per-pattern overrides for environment variablesvisualise_notebooks.yaml— flat list of notebook stems to run when the--visualiseflag is used. Optional: a workspace without one simply has nothing marked for visualisation.
config/build/ is the single source of truth — autohands/config/ holds no
per-project config fallbacks. The keyed-dict fallbacks (no_run.yaml,
copy_files.yaml, visualise_notebooks.yaml) were removed once every build
target owned its own files; the only file left in autohands/config/ is
workspaces.yaml, which is build policy (the run matrix), not workspace
config.
The copy_files.yaml mechanism — which copied listed scripts verbatim into
notebooks/ instead of converting them — was removed entirely. It resolved to
an empty list in every workspace and had produced no output in any build.
BUILD_PYTHON_INTERPRETER— Python interpreter to use for script execution (defaults topython3)PYAUTO_TEST_MODE— Set to1for workspace runs,0for*_testworkspace runsPYAUTO_SMALL_DATASETS— Set to1for workspace runs (caps grids to 15x15), not set for*_testrunsPYAUTO_FAST_PLOTS— Set to1for workspace runs (skipstight_layout()in subplots and critical curve/caustic overlays in plots), not set for*_testrunsJAX_ENABLE_X64— Set toTrueduring CI runs
The workflow (release.yml) is manually dispatched with inputs:
minor_version— appended to date-based version (format:YYYY.M.D.minor)rehearsal— the one mode switch (defaultfalse= full real release). Whentrue, it is TestPyPI-only rehearsal mode: build every package from source, publish to TestPyPI, emit the resolved version as thetestpypi-rehearsal-versionartifact, then STOP (no PyPI upload, no git tag, no notebook/version commits, no Colab bumps). This is the mode the Heart/Brain release-validation gate dispatches so it can install and validate the built wheels.
(The legacy skip_scripts / skip_notebooks / skip_release force-through knobs and the
update_notebook_visualisations path were removed with the Heart/Build split — Build is a pure
executor with no ad-hoc skip levers or inline notebook-visualisation job. "Build without
releasing" is now exactly what rehearsal mode is for.)
release.yml is a pure executor: it builds, tests-the-install, publishes to PyPI, tags every library and workspace, and commits generated notebooks + Colab URL bumps to the workspaces (version stamps are build-tree-only since #120 — no __init__.py commit-backs to library mains, no workspace version pins). Workspace-integration validation (the old find_scripts / generate_notebooks / run_scripts / run_notebooks / analyze_results jobs) moved to PyAutoHeart's workspace-validation.yml; release readiness is gated upstream by the PyAutoBrain release agent via pyauto-heart readiness before this workflow is dispatched. The script_matrix.py / run_python.py / run.py / aggregate_results.py primitives remain here and are checked out + reused by the Heart workflow.
The never-rewrite-history rules live in AGENTS.md and apply
here as everywhere.