ADR-027: Published to PyPI via trusted publishing; the scaffold ships inside the package
Status Active · Filed 2026-08-04 · Issue #3 · Influenced by ADR-009 · ADR-021
Context
Distribution was the open half of #3:
the layout question resolved into the read/write boundary
(ADR-021), and pip install was named early as the path that
makes the vendored-deadweight worry moot — an installed Luria carries the
scaffold and the tool, not the project’s history. A trusted publisher is now
registered on PyPI, expecting the package from a publish.yml workflow in a
pypi environment.
Building the wheel surfaced that the existing packaging worked by
accident. The setuptools config declared the template as parent-relative
package data (luria = ["../template/**/*"]), which landed template/ at
the wheel root — installing a bare, unnamespaced template/ directory
into site-packages, where it would collide with any other package shipping
one. luria init then found it only because Path(__file__).parent.parent
from inside site-packages happens to be site-packages. Functional, and
wrong in the way that breaks on the first neighbour.
Decision
Trusted publishing, environment-gated. publish.yml builds on a GitHub
release (or manual dispatch); the publish job runs in the pypi environment
with id-token: write and nothing else — the OIDC identity is the whole
credential. No API token exists to leak, rotate, or forget. Build and
publish are separate jobs so the environment gate covers the smallest
surface and the artifact that ships is byte-identical to the one tested.
The scaffold ships inside the package. The wheel carries the template at
luria/template/ (hatchling force-include mapping the top-level directory
in), and luria init resolves the packaged location first, falling back to
the repository top level in a checkout. Both truths hold at once: an install
is self-contained and collision-free; a visitor to the repository still
meets template/ at the top level, which ADR-021 put there on
purpose.
The smoke test is the release gate. Before anything ships, the workflow
cold-installs the built wheel into a fresh venv and runs the full first-user
path — init → index → journal new → index → lint — in an empty directory.
This is the only configuration that exercises the packaged template path;
the dogfooding repo structurally cannot (ADR-009’s limit, met
before: a checkout always has the top-level fallback). Fired before
trusting, per DP-6 — and it
immediately caught the scaffolded CLAUDE.md’s illustrative wikilinks
leaking as day-one warnings in a fresh project.
The sdist is a build input, not a browsing surface. It carries the
package, the template, the tests and the metadata — not the record. The
record’s home is the repository; shipping ~300 KB of Luria’s own history in
every pip download would be the deadweight #3
worried about, relocated.
Alternatives considered
- API token in a repository secret. The thing trusted publishing exists to retire: a long-lived credential that can leak in a log, needs rotation nobody remembers, and grants more than one workflow needs.
- Publish on tag push instead of release. Nearly equivalent; the
release object is kept as the trigger because it carries notes and makes
“what shipped” a browsable artifact — and
workflow_dispatchcovers the escape hatch either way. - Symlink
luria/template → ../templateto satisfy setuptools without changing backends. Works on the platforms it works on; a checkout on Windows without symlink privileges quietly gets a broken package dir. A build-time mapping has no runtime footprint in the repo. - Keep setuptools and accept the wheel-root template. Functional today, and a name collision away from clobbering another package’s files — the kind of accident that is invisible until a user’s environment breaks.
Consequences
pip install luriais the installation path; the README says so.- Releasing is: merge, cut a GitHub release, watch
publish.yml. The version lives inpyproject.tomland must be bumped per release — PyPI will refuse a reused version, loudly, which is the right failure. - Project metadata (classifiers, URLs, keywords) now renders a real PyPI page; the author is listed by GitHub handle.
- The
template = "luria/template"mapping is one more place the scaffold’s location is spelled;init._template_dir()is the single reader.