Package distribution¶
Homebrew and Scoop are opt-in dispatches; the tap or bucket owns the manifest logic¶
Id: 462ac46c-c9db-4a4a-b8ae-bbe5c14af09f
Type: decision
Status: active
Evidence: confirmed
Source: ADR-017 (docs/adr/017-opt-in-homebrew-scoop-distribution.md); the live reference is the keycast tap and bucket repositories
Revisit when: the release build matrix becomes per-architecture, or an ecosystem's own bump tooling changes shape
See: external-integrations.md#a-freshly-generated-project-is-green-on-first-push-with-zero-external-accounts — 392b6dcf-6e9d-441d-bf7b-64ba362578d7 — as of 2026-10-11
See: package-distribution.md#optional-pypi-publishing — 20be7463-ca95-4d01-82c9-71f632d1b4be — as of 2026-10-11
See: package-distribution.md#the-tap-and-bucket-are-scaffolded-by-their-own-templates-not-by-a-docspackaging-bundle — 9be17e7b-110f-4de7-8b6d-a0d42350e9cb — as of 2026-10-19
include_homebrew and include_scoop are independent toggles, off by default
and asked only when the project can distribute natively. When enabled,
release.yml gains bump-homebrew/bump-scoop jobs that fire a
repository_dispatch at the owner's homebrew-tap/scoop-bucket repository
with only the package name and version. A listener workflow living in the tap
or bucket does the manifest work with each ecosystem's own tooling; the
generated project no longer ships a listener bundle: the tap and bucket are
scaffolded by their own templates (see the entry below). The jobs
are gated on a fine-grained PAT with Contents-write on the target repository
only, skip with a visible warning when the secret is unset, run with no
workflow permissions of their own, and never join the check gate.
Reason: generating a formula, cask or manifest in every producing project
duplicates sha256 computation, PyPI resource resolution, per-OS asset matching
and each ecosystem's audit rules — and their bugs — once per generated project.
Dispatching keeps that logic in one downstream place, written with the tools
the ecosystems maintain for exactly this job. The jobs live in release.yml
because a release event fired by the workflow token cannot trigger another
workflow. The listener opens its own PR with its own token, so the PAT needs no
pull-request scope.
Rejected alternative: render and push the formula/manifest from the generated project — the original design of this record, replaced for the duplication above.
Rejected alternative: one shared enum or parent toggle — an owner may want one ecosystem only.
Consequence: binaries are single-architecture per OS; the Scoop PyPI fallback is best-effort because Scoop's tooling assumes binary downloads; the listener is not validated by this template's tests; payload validation is the listener's job.
The tap and bucket are scaffolded by their own templates, not by a docs/packaging bundle¶
Id: 9be17e7b-110f-4de7-8b6d-a0d42350e9cb
Type: decision
Status: active
Evidence: confirmed
Source: maintainer instruction, 2026-10-19
Revisit when: the dispatch event types or payload keys of copier-homebrew-tap / copier-scoop-bucket change, or those repositories are retired
See: package-distribution.md#homebrew-and-scoop-are-opt-in-dispatches-the-tap-or-bucket-owns-the-manifest-logic — 462ac46c-c9db-4a4a-b8ae-bbe5c14af09f — as of 2026-10-19
include_homebrew and include_scoop no longer render a docs/packaging/
folder (reference listener workflows plus a README per ecosystem). The
generated REPOSITORY_SETUP.rst instead promotes the maintainer's
dedicated repositories: copier-homebrew-tap and copier-scoop-bucket scaffold
the tap or bucket (their templates already carry the update-formula,
update-cask and update-manifest dispatch listeners this project's
release.yml fires at), and homebrew-tap-skills and scoop-bucket-skills
are the agent skills that add, fix, triage, retire and verify packages
afterwards. The dispatch jobs, the event types, the payload keys and the
*_TOKEN secrets are unchanged, so existing adopters keep working.
Reason: scaffolding and maintaining a tap or bucket now has dedicated homes; a copy-into-your-tap bundle in every generated project is a second, drifting copy of what those templates own.
Rejected alternative: keep the bundle as a reference copy — superseded by the dedicated templates, which render the same listeners as a maintained tap or bucket. Further alternatives considered: unknown.
Consequence: the generated project no longer tests listener workflow shape
(permissions, failure reporting); that lives in the tap and bucket templates.
The setup guide hard-codes the hasansezertasan/ repositories, matching how the
template already names itself.
primary_executable is the single source for the distribution payload¶
Id: 40888b40-730c-4575-b68c-401b4eeccc06
Type: decision
Status: active
Evidence: confirmed
Source: ADR-017 (docs/adr/017-opt-in-homebrew-scoop-distribution.md)
Revisit when: a fourth executable architecture is added, or the launcher stops needing network access on first run
See: standalone-executables.md#three-architecture-named-executable-toggles-instead-of-one-brand-toggle — d5aed1a0-0b7c-4ac3-9d75-fb9568d9e34d — as of 2026-10-11
A hidden computed variable picks freezer over compiler over launcher, or
is empty when no executable toggle is on. Non-empty means a Homebrew cask and a
Scoop binary manifest fed from the GitHub release; empty means a Homebrew
formula built from the PyPI sdist and a Scoop pipx manifest.
Reason: PyInstaller and Nuitka produce fully self-contained binaries; the
PyCrucible launcher still needs the network on first run, so it is the fallback
of last resort. One computed variable mirrors the role primary_component
plays for console-script precedence: no template re-spells the ordering
inline.
Rejected alternative: re-derive the ordering per template — the drift
primary_component already exists to prevent.
Optional PyPI publishing¶
Id: 20be7463-ca95-4d01-82c9-71f632d1b4be
Type: decision
Status: active
Evidence: confirmed
Source: issue #382; maintainer decisions while implementing it, 2026-10-11; ADR-039 (docs/adr/039-optional-pypi-publishing.md)
Revisit when: the PyPI project name becomes its own question, or Homebrew / Scoop gain a non-PyPI source for a pure-Python package
See: release-automation.md#the-workflow_dispatch-trigger-on-releaseyml-stays-the-publish-environment-is-the-containment — 0d93a35d-46a3-4ac6-b072-fc07da789842 — as of 2026-10-11
include_pypi_publish is a boolean toggle, seeded on by every preset, so
existing projects and copier update keep publishing. When it is off:
release.ymldoes not renderpypi-publish, and drops it from theneedsofattach-github-releaseandfinalize-release.buildstill produces and attests the wheel and sdist, andattach-github-releasestill uploads them, so every release keeps its distributions.buildkeeps thepublishenvironment. It mints build-provenance attestations there, and the environment's main-only deployment-branch policy is what stops aworkflow_dispatchrun from another branch. So the setup guide drops only the PyPI publisher registration. The "Restrict thepublishenvironment tomain" step stays.- The README and
installation.rstinstall fromgit+https://github.com/<user>/<repo>(uv tool install,uvx --from,uv add,pip install) and say the release assets carry the wheel and sdist. The README PyPI badges and its PyPI-based Analysis links are dropped. The PyPI publication-attestation text ininstallation.rstis dropped too, along with PyPI mentions inCONTRIBUTING.md, therepo-setupskill and the after-copy message.
Homebrew and Scoop. A Homebrew formula is built from the PyPI sdist, and
the Scoop pipx manifest polls PyPI. Only a prebuilt executable (a cask or binary
manifest) is fetched from the GitHub Release. A hidden can_distribute_natively
(is_app and either PyPI publishing or a primary_executable) therefore gates
include_homebrew and include_scoop. Without it they are not asked, and their
default repeats the guard, so a preset cannot turn on a tap or bucket that
could never update.
Reason: every generated project used to publish to PyPI unconditionally:
release.yml always rendered pypi-publish, and attach-github-release and
finalize-release both listed it in needs. A project that must not go to PyPI
had no supported path to a release, and hand-editing the generated workflow
conflicted on every copier update. Publishing is wrong or impossible for
private or internal projects distributed from Git or a private index, for a
project whose PyPI name (github_repo_name) already belongs to someone else
(trusted publishing fails on the first release), and for projects that ship only
other artifacts or only want GitHub Releases.
Rejected alternative: leave Homebrew/Scoop ungated and document the caveat. Rejected: it renders a tap/bucket dispatch whose formula or manifest can never resolve, which is a failure on first release that the template can prevent at question time.
Rejected alternative: point the install docs at a GitHub Release wheel
download. Rejected in favor of Git URL installs, which work with every installer
the docs already show, can be pinned with @<tag>, and work over SSH for
private repositories.
Rejected alternative: make the PyPI project name its own question in this change. Deferred as a separate concern: it decouples the distribution name from the package name, which touches far more than the publish job.
Consequence: a project can release without PyPI, and that choice survives
copier update. An explicit --data include_homebrew=true still renders the
tap even when the question is gated off — Copier applies passed data to an
unasked question, the existing contract of the is_app gate too. The
template-ci litestar-kafka scenario renders with PyPI off so actionlint checks
the web variant of finalize-release's needs without pypi-publish.
The build stays hatchling plus hatch-vcs; the other build and versioning tools are out¶
Id: 1dbc52d3-7ba7-4bbf-b12a-299169c86160 Type: decision Status: active Evidence: confirmed Source: issue #392; maintainer scoping answers, 2026-10-12 Revisit when: a generated project gains an i18n workflow, or hatch-vcs stops fitting the tag-based release flow
[build-system] requires stays hatchling plus hatch-vcs (with the
c-extension additions — cython>=3.0.0 and setuptools>=70 — when
include_c_extensions; that setuptools is a build-time compiler dep, not a
version source). setuptools-scm stays out as a direct requirement: hatch-vcs
already builds on setuptools-scm's version-inference logic (its options surface
through [tool.hatch.version.raw-options]), so there is nothing to gain by
declaring it separately; adopting setuptools-scm as the version source itself
would mean swapping the whole backend hatchling → setuptools.
hatch-babel stays out: there is no i18n story yet,
so it waits for a real translation workflow instead of shipping preemptively.
hatch-autorun stays out: it injects a .pth that auto-executes code at
interpreter startup (before first import), which the supply-chain posture
rejects.
Reason: each was a commented-out pin in the pre-adoption keycast
pyproject.toml quoted in the issue. None earned a copier-gated option:
setuptools-scm would fork the one versioning story the tag-based release flow
assumes, and the other two buy nothing until a project actually needs their
capability.
Rejected alternative: copier-gated options for hatch-babel, setuptools-scm or hatch-autorun — rejected: a toggle for a rejected tool is surface without a reader, and in setuptools-scm's case a fork in the versioning story.