Skip to content

Template 0.1.25: publish the app's Python code on PyPI at each release (opt-in, trusted publishing) - #46

Merged
IvanHCenalmor merged 1 commit into
mainfrom
claude/template-0.1.25-pypi
Nov 12, 2026
Merged

IvanHCenalmor merged 1 commit into
mainfrom
claude/template-0.1.25-pypi

Conversation

@IvanHCenalmor

@IvanHCenalmor IvanHCenalmor commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

Template 0.1.25, based on main after #45 (0.1.24). It makes the preprint's claim true: GitHub Actions publish an app's own Python code (src/<package>) as a package on PyPI. Publishing is off until the developer turns it on.

Design (chosen 2026-11-12)

  • Workflow. New .github/workflows/publish_pypi.yml in every app. It runs at a published release, the same moment as the installers, with the same version (the tag without the v).
  • Off by default. Both jobs have if: vars.PYPI_PUBLISH == 'true' || vars.PYPI_PUBLISH == 'testpypi'. An app that does not use it sees skipped jobs, never failures.
  • Authentication. PyPI trusted publishing: environment pypi, id-token: write on the upload job only, and pypa/gh-action-pypi-publish v1.14.3 pinned by SHA. There is no secret and no token fallback.
  • TestPyPI. PYPI_PUBLISH=testpypi uploads to TestPyPI, as a trial.
  • Two jobs, as PyPA recommends. build has no special permission and stores dist/. publish downloads it and uploads it.
  • Developer setup. On pypi.org, add a pending publisher (owner, repository, publish_pypi.yml, pypi). On GitHub, add the repository variable PYPI_PUBLISH=true. To turn it off, delete the variable.

.tools/python/pypi_package.py (new)

Stops before building, with an ::error annotation, when:

  • there is no package in src/;
  • the version is not X.Y.Z;
  • the version is already on the index (/pypi/<name>/<version>/json);
  • another workflow in the repository uploads to PyPI;
  • distribution.pip names a different package than setup.py;
  • setup.py still has a placeholder;
  • pyproject.toml gives another version than the release.

Then:

  • It writes the release version into setup.py of the checkout, which is not committed. The tag points to the commit before the "Update version" commit, so the committed version can't be trusted.
  • It runs python -m build. The wheel is built from the sdist.
  • It checks the name and version of both files.
  • It refuses URL dependencies, which PyPI rejects.
  • It checks that the wheel has the same dependencies as the sdist.

Package fixes

  • setup.py
    • The dependencies are read from requirements.txt, one per line, as written. Options, paths and URLs are left out, with a warning in the build log.
    • The installer runs pip install --no-deps --no-build-isolation with requirements.txt next to setup.py. Any line setuptools would refuse is dropped, so the new setup.py cannot break an installation.
    • README.md is the long description. A Source URL is added from GITHUB_REPOSITORY.
    • The placeholders PYTHON_PROJ_NAME, VERSION_NUMBER and PYTHON_VERSION keep their exact lines, for the form, bump_constructor.py, merge_requirements.py and check_placeholders.py. The form's template test (LC_TEMPLATE_DIR) passes against this branch.
  • MANIFEST.in (new) adds requirements.txt to the sdist.
  • bump_version.py now writes the release version into setup.py.

Name: distribution.pip is a check, not a rename

The package is published under setup.py's name=. When distribution.pip is present, it must be the same name (PEP 503 normalised); otherwise the build stops. I chose this over silently renaming, because a rename could publish the wrong code under the App Centre's name. For example, VLab4Mic's src/ only holds vlab4mic_lc_tools.

Migration 0.1.24 → 0.1.25

Surgical and idempotent. It never deletes, never raises for a customised file, and keeps line endings.

  • Creates publish_pypi.yml and pypi_package.py. A file of the repository's own with the same name is kept (SKIPPED).
  • NucleiSky (its own publish_to_pypi.yml) does not get publish_pypi.yml. The log says that nothing is needed to keep publishing its way, and how to switch.
  • setup.py is replaced only when it is exactly the earlier template file, with any name, version and Python series. In that case MANIFEST.in is added too. A hand-edited setup.py is left alone.
  • bump_version.py: three known blocks are changed.

Checks

  • Tests: tests/test_template_sync_0_1_25.py (42 tests). They build the sdist and wheel of an initialised copy (--no-isolation --offline) and check the metadata: dependencies, description, version, Source URL. They also cover:
    • what setup.py passes to setuptools without requirements.txt or README.md (the installer case), and without packaging;
    • every stop of pypi_package.py;
    • bump_version.py with CRLF;
    • the migration: a 0.1.24 app, the uninitialised template, CRLF, customised files, NucleiSky's own workflow, missing folders, a second run;
    • the sync.py chain.
  • Older tests: tests/_v0_1_25.py keeps the frozen-resource tests comparing, and the pins table of test_template_sync_0_1_18.py has the two new actions.
  • Template CI: tests.yml now installs build==1.3.0 setuptools==80.9.0, so the build test runs in CI instead of skipping.
  • Results: in Ubuntu (WSL, Python 3.12), 332 passed, ruff 0.15.20 clean, mypy 1.20.2 clean. The new and changed test files also pass on Python 3.11.
  • Real apps: copies of NucleiSky, the Playground, Stardist2D, CellPatcher, VLab4Mic, CellTracksColab, LabChronicle, LabConstrictor_Demo and GuessTheCondition went through their whole chain (from 0.1.19 to 0.1.22 → 0.1.25), with exit 0. A second run changed nothing.
    • New setup.py: CellTracksColab, LabChronicle, Demo, Stardist2D.
    • Kept their own setup.py: Playground, GuessTheCondition, CellPatcher, VLab4Mic, NucleiSky.
    • Builds: each app that got the workflow built with pypi_package.py, except CellPatcher, which stops as intended: its setup.py needs morphobricks-* from GitHub URLs.

Not verified

  • Nothing was uploaded to PyPI or TestPyPI, and no release or tag was made. The upload job, the vars.* conditions and the trusted-publisher exchange were checked only by reading them. The first real check is an app release with PYPI_PUBLISH=testpypi.
  • The apps' own copies of .tools/docs/external_code_upload.md are not updated by the migration (they are rendered per app). The migration log links to the template's docs.

Decided

  • Exact pins on PyPI are kept (2026-11-12). The PyPI package declares the lines of requirements.txt exactly as written. For Stardist2D, that is tensorflow==2.20.0, numpy==2.1.3, jl-hidecode==0.0.1 and so on: the versions the app is tested with. The cost is that the package can be hard to install next to other packages. Loosening == to >= was considered and not chosen.

🤖 Generated with Claude Code

… release, off until turned on

- .github/workflows/publish_pypi.yml (new): at a published release, a build job and an upload job with PyPI
  trusted publishing (environment pypi, id-token: write, pypa/gh-action-pypi-publish pinned by SHA). Both
  jobs are skipped unless the repository variable PYPI_PUBLISH is "true" (PyPI) or "testpypi" (TestPyPI).
- .tools/python/pypi_package.py (new): stops early with the reason (no package in src/, version not X.Y.Z or
  already on the index, another workflow that publishes, distribution.pip naming another package, placeholder
  left), writes the release version into setup.py of the checkout, builds sdist + wheel and checks them.
- setup.py: dependencies from requirements.txt (lines setuptools would refuse are dropped, so the installer
  never breaks), README.md as description, the repository as Source link; MANIFEST.in ships requirements.txt.
- bump_version.py: the release also writes its version into setup.py.
- Migration 0.1.24 -> 0.1.25: surgical, idempotent, SKIPPED for customised files, keeps line endings; no
  publish_pypi.yml for a repository that already publishes to PyPI (NucleiSky).
- Docs: PyPI section in external_code_upload.md, migration_0_1_25.md, troubleshooting entry.
- Tests: test_template_sync_0_1_25.py (builds the sdist and wheel of an initialised copy, no upload);
  tests/_v0_1_25.py keeps the older frozen-resource tests comparing; tests.yml installs build + setuptools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IvanHCenalmor
IvanHCenalmor merged commit 513ce5d into main Nov 12, 2026
4 checks passed
@IvanHCenalmor
IvanHCenalmor deleted the claude/template-0.1.25-pypi branch November 12, 2026 23:17
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.

1 participant