Release automation¶
release-please is the sole release orchestrator¶
Id: 7ec8a937-fd4d-41f2-96b7-824fe3be913b
Type: decision
Status: active
Evidence: confirmed
Source: ADR-002 (docs/adr/002-release-please-for-release-automation.md); validated against the keycast release pipeline (keycast#11)
Revisit when: the open re-evaluation below is decided, or the generated project's version stops being sourced from git tags
See: release-automation.md#the-release-automation-choice-is-under-re-evaluation — d9131d49-3c27-48f8-a903-e97caf4acff6 — as of 2026-10-11
See: package-distribution.md#optional-pypi-publishing — 20be7463-ca95-4d01-82c9-71f632d1b4be — as of 2026-10-12
Every generated project ships an unconditional release-please pipeline in one
unified release.yml: Conventional Commits on main open a release PR, its
merge cuts the tag and release, and every downstream job (build, PyPI — optional
since ADR-039 —, executables, Docker, docs, SBOM, notifications) gates on its
outputs. The
former release_automation choice (none, release-please, release-it,
release-drafter) and the separate cd.yml are gone. Versioning is sourced from
git tags through hatch-vcs, so release-please never edits a version literal and
uv.lock cannot desync; bump-minor-pre-major keeps pre-1.0 projects pre-1.0.
Reason: offering three tools multiplied the integration surface for a feature each project configures once. release-please is Python-native, derives the version from commits the template already enforces, and produces a reviewable release PR before any tag exists. Two of the three hard-won keycast fixes were already in the template, so it was the option with the most evidence.
Rejected alternative: release-it — JS-oriented, needs a Node runtime, and a
human picks the bump every time. It is a manual workflow_dispatch with an
explicit bump, keeps no changelog file, and its audit trail is a commit after
the fact rather than a reviewable PR before the tag.
Rejected alternative: release-drafter — label-driven, writes release notes
but no CHANGELOG.md. Its model is push/PR to a draft, then a manual publish;
it needs PR-labelling hygiene and is generic rather than Python-native.
release-please, by contrast, is a pure GitHub Action with first-class
release-type: python, a generated and committed CHANGELOG.md, and a
reviewable release PR as its audit trail.
Rejected alternative: keeping a none escape hatch — projects that want no
automation delete the files after generation; accepted for a template whose
audience publishes releases.
Consequence: the PyPI trusted publisher must register release.yml as the
workflow. Rendering plus actionlint validate the workflow across option
combinations, but draft-tag timing, OIDC publishing and reconciliation can only
be exercised by a live release. Conventional Commits become a soft
requirement for the version bump to work, which matches the commit tooling
the template already ships. The template has no CHANGELOG.md seed file:
release-please owns the changelog, and a scaffold-time seed would wipe an
adopting project's release history on update.
The release is created as a draft, finalized after artifacts attach¶
Id: 398b3cd9-e57b-4856-b2c7-76752c0ec37f
Type: decision
Status: active
Evidence: confirmed
Source: ADR-002 (docs/adr/002-release-please-for-release-automation.md); the wrong-version bug caught in cobo#49
Revisit when: release-please creates releases after artifact upload natively, or computes its commit range from drafts
See: release-automation.md#release-please-is-the-sole-release-orchestrator — 7ec8a937-fd4d-41f2-96b7-824fe3be913b — as of 2026-10-11
release-please creates the GitHub release with draft: true and
force-tag-creation: true. Build jobs attach wheels, executables and the SBOM
to the draft; finalize-release un-drafts it and then closes the phantom
next-release PR and re-dispatches the workflow, bounded.
Reason: a public release must never exist without its artifacts, so the release stays a draft until they are attached. GitHub withholds a draft's git tag until publication, so without forced tag creation the build job would run before the tag exists and hatch-vcs would stamp the previous version. The phantom PR exists because release-please computes its commit range from the latest published release and ignores drafts.
Rejected alternative: draft: false — a window with a public release and no
artifacts.
Consequence: PyPI publishing uses Trusted Publishing (id-token: write),
and the publish checkout uses fetch-depth: 0 so hatch-vcs can see the release
tag.
Consequence: docs deployment lives in the same workflow (deploy-docs,
after finalization) because an event fired by finalize-release's token cannot
trigger a separate workflow.
The workflow_dispatch trigger on release.yml stays; the publish environment is the containment¶
Id: 0d93a35d-46a3-4ac6-b072-fc07da789842
Type: decision
Type: constraint
Status: active
Evidence: confirmed
Source: ADR-002 (docs/adr/002-release-please-for-release-automation.md)
Revisit when: the GitHub API for re-running a workflow stops requiring the dispatch trigger, or environment deployment-branch policies change
See: release-automation.md#the-release-is-created-as-a-draft-finalized-after-artifacts-attach — 398b3cd9-e57b-4856-b2c7-76752c0ec37f — as of 2026-10-11
See: supply-chain-security.md#provenance-minting-shares-the-protected-publication-boundary — a0396603-cb34-4151-9d5b-1fc77eb31f6e — as of 2026-10-11
release.yml keeps workflow_dispatch. The publish jobs run in a publish
environment whose deployment-branch policy is limited to main; the generated
setup guide ships that policy as a scriptable step with a check, because GitHub
auto-creates the environment unprotected on first use.
Reason: the trigger looks like a publish-escalation surface — a manual run
executes the workflow file from the selected branch, so the release_created
guard cannot stop someone with write access from dispatching a guard-free copy
and minting a trusted-publishing token. But finalize-release's reconciliation
re-dispatches the workflow through the API, which requires the trigger; that
re-dispatch is the documented exception to the workflow token's loop
prevention, which is why the step needs no PAT. GitHub evaluates an environment's branch policy outside the workflow file, so
the environment contains what the trigger exposes.
Rejected alternative: remove the trigger — breaks release-PR reconciliation.
Release workflows use GITHUB_TOKEN, with two documented manual workarounds¶
Id: bc96b93e-9b9e-4496-8fbb-b215f1d9a04c
Type: decision
Type: workaround
Status: active
Evidence: confirmed
Source: ADR-002 (docs/adr/002-release-please-for-release-automation.md); issue #332
Revisit when: GitHub lets the workflow token start workflow runs or grants it workflow write access
See: release-automation.md#release-please-is-the-sole-release-orchestrator — 7ec8a937-fd4d-41f2-96b7-824fe3be913b — as of 2026-10-11
Release-please runs with the implicit GITHUB_TOKEN. Two consequences are
handled procedurally in the generated setup guide rather than with a stronger
credential: the release PR reports no required status contexts until a human
closes and reopens it, and a merge that changes workflow files between the
release PR merging and the release run finishing must wait, because the
release API
then refuses tag creation with a 403.
Reason: events created with the workflow token do not start workflow runs,
and the token cannot be granted workflow write access. A PAT or App token would
fix both but is a standing credential with write access to main; closing and
reopening the PR (every required context listens for reopened) and pausing
workflow merges are the cheaper side of that trade. Editing the PR body is not
equivalent — two of the checks do not listen for edited.
Rejected alternative: a PAT or GitHub App token for release-please.
The stale draft-release comment link is an accepted cosmetic defect¶
Id: 2c6a6034-1936-4026-b10a-93445ace87b1
Type: decision
Status: active
Evidence: confirmed
Source: ADR-002 (docs/adr/002-release-please-for-release-automation.md); issue #143, PR #152
Revisit when: release-please rewrites its release comment after un-drafting, or the draft flow is replaced
See: release-automation.md#the-release-is-created-as-a-draft-finalized-after-artifacts-attach — 398b3cd9-e57b-4856-b2c7-76752c0ec37f — as of 2026-10-11
release-please posts its "created releases" comment once, at draft-creation
time, with the transient untagged-<hash> slug; after finalization that link
404s. It is left as is.
Reason: the release, tag, artifacts and SBOM are all correct; only a link on
an already-merged PR is dead. draft: false would repair the link but
reintroduce the no-artifacts window, and having finalize-release patch the
comment adds API surface for no functional gain.
Rejected alternative: patch the comment in finalize-release — still
available without switching tools, listed as an option in the re-evaluation.
Commitizen is a commit helper, never a release tool¶
Id: ee1d2fad-67ab-4f4a-a531-73458da14eb1
Type: decision
Status: active
Evidence: confirmed
Source: ADR-004 (docs/adr/004-commitizen-as-commit-helper-not-release-tool.md)
Revisit when: the re-evaluation below selects a Commitizen-based orchestrator
See: release-automation.md#release-please-is-the-sole-release-orchestrator — 7ec8a937-fd4d-41f2-96b7-824fe3be913b — as of 2026-10-11
The generated [tool.commitizen] block contains only the convention name. The
release-coupled keys (tag format, version provider and scheme,
major_version_zero, changelog-on-bump) were removed, so cz bump has no
configured tagging or changelog behaviour. The commit-message hooks stay;
Commitizen is always included (the include_commitizen toggle was dropped
because its hooks were unconditional anyway).
Reason: as shipped, Commitizen was configured as a second complete release
pipeline that tags the same releases and rewrites the same CHANGELOG.md as
release-please — a direct conflict the first release decision never compared.
Versioning, tagging and changelog get exactly one owner; Commitizen keeps the
orthogonal half, helping to write and lint conforming commits. A cz bump
pipeline also has neither release-please's reviewable release PR nor
tag-sourced versions by default, and a pipeline where Commitizen writes the
version does not guarantee uv.lock cannot drift on release; with versions
from git tags via hatch-vcs and no tool editing a literal, it cannot.
Rejected alternative: keep both configured as "aligned" — competing tags and two changelog writers on one file.
Consequence: message linting overlaps the PR-title check under the required squash-by-title merge strategy; accepted because the hook gives feedback while authoring, before a PR exists.
The release-automation choice is under re-evaluation¶
Id: d9131d49-3c27-48f8-a903-e97caf4acff6
Type: decision
Status: open
Evidence: confirmed
Source: ADR-037 (docs/adr/037-release-automation-reevaluation.md), a research record last refreshed 2026-10-05; upstream release-please issues #2515 and #2605; release automation research note (candidate tables and source links)
Revisit when: git-cliff ships prerelease version calculation (#1380), RC series become frequent, or a maintainer picks one of the four options
See: release-automation.md#release-please-is-the-sole-release-orchestrator — 7ec8a937-fd4d-41f2-96b7-824fe3be913b — as of 2026-10-11
See: release-automation.md#commitizen-is-a-commit-helper-never-a-release-tool — ee1d2fad-67ab-4f4a-a531-73458da14eb1 — as of 2026-10-11
Two friction points reopened the tool choice: a stable release cut after
release candidates lists only changes since the last RC (release-please has no
prerelease/stable distinction and no config remedy; the npm CLI team works
around the same behaviour by hand), and the draft-then-finalize dance plus
phantom-PR reconciliation are workarounds layered on release-please's model.
The research frames tools as commit helpers, changelog engines and release
orchestrators, and compares Commitizen (cz bump, fixes the RC range with one
setting, medium effort, reverses the commit-helper decision, loses the
reviewable release PR) against git-cliff (a changelog engine only; high effort,
RC math stays hand-written). Four options stand: stay and document, migrate to
Commitizen, git-cliff inside a bespoke orchestrator, or defer. Until decided,
nothing changes.
Why this needs an answer: both replacements need a standing privileged
credential or a maintainer-local bump under the full preset's ruleset, and
release.yml fans out to every generated project while being validated only
by rendering until a live release runs. Whether the RC gap bites often enough
to justify that blast radius is a maintainer judgement, not a research result.
Optional Docker Hub configuration gates the publish fan-out¶
Id: f6f9eea4-dd38-4a74-98ed-f67e436c393d
Type: decision
Status: active
Evidence: confirmed
Source: PR #51, shared-gate review discussion; commit f48250e
Verification: corroborated — the web release workflow makes both PyPI and Docker publication depend on docker-publish-preflight
Revisit when: a new irreversible publication channel is added or Docker Hub opt-in semantics change
Docker Hub publication is optional and its credential pair is all-or-nothing: neither credential means GHCR-only publication, both enable Docker Hub, and a partial pair fails a shared preflight before either PyPI or Docker publication.
Reason: PyPI uploads are irreversible. A credential check inside only the Docker job would allow PyPI publication to finish concurrently before Docker failed, stranding a partially published release. The shared prerequisite rejects that known configuration error before the publish jobs fan out; it does not make all later external uploads transactional.
Rejected alternative: validate the credentials only inside Docker publication. Review moved the check into a shared prerequisite specifically to prevent the PyPI race. Requiring Docker Hub for every web release is not the selected contract; GHCR-only publication remains supported.
The narrowly scoped ghalint exceptions exist because conditional steps need
job-scoped credential-presence information and GitHub does not expose the
secrets context directly in conditions. The token itself is reduced to a
presence flag, not exposed job-wide. The exceptions are recorded in the
generated .github/ghalint.yaml rather than disabling that policy globally.
Released-issue notifications are one inline job in release.yml¶
Id: c8cd015d-12ff-47a7-9447-40b69a2b1a82
Type: decision
Status: active
Evidence: confirmed
Source: ADR-010 §2 (docs/adr/010-pr-docs-previews-and-released-issue-notifications.md)
Revisit when: the release lifecycle is split across workflows
See: release-automation.md#the-release-is-created-as-a-draft-finalized-after-artifacts-attach — 398b3cd9-e57b-4856-b2c7-76752c0ec37f — as of 2026-10-11
A notify-released-issues job, gated on a created release and running after
finalization, uses a single inline github-script step to comment the release
tag and URL on every issue the release closed. It is always on, with no toggle.
Reason: the upstream implementation shipped a committed Python script and a
JS helper behind a reusable workflow — more moving parts than a single-maintainer
project needs. Inline in release.yml matches keeping the whole release
lifecycle in one workflow, and it is zero-config: no closed issues means no
notification, so it cannot fail an unused feature.
Rejected alternative: a separate workflow_call file with committed scripts.