Release automation tools: changelog engines vs release orchestrators¶
Status¶
Research snapshot kept from the former ADR-037 (last refreshed 2026-10-05). It is evidence, not the decision: the decision and its status live in the release-automation re-evaluation entry of context/release-automation.md. ADR-NNN mentions below name the former records, readable at commit c06222e.
Context¶
What we run today¶
The template standardized on release-please (ADR-002) and reduced
Commitizen to a commit-authoring/linting helper only (ADR-004). In the
generated template/.github/workflows/release.yml.jinja, release-please is not
merely "the release tool" β it performs five distinct responsibilities, and
up to fourteen downstream jobs (some render only with their toggle) are bolted
directly to its outputs:
- Parses Conventional Commits and computes the next version.
- Opens and maintains the reviewable Release PR (accumulate-then-merge).
- On merge, creates the git tag (
force-tag-creation: true) and a draft GitHub Release with generated notes. - Writes
CHANGELOG.mdusing the project'schangelog-sectionsemoji taxonomy. - Emits
release_created/tag_name/versionoutputs that gatebuild,pypi-publish,sbom,docker-publish(-preflight),build-launcher/build-freezer/build-compiler,attach-github-release,finalize-release,bump-homebrew/bump-scoop,deploy-docs, andnotify-released-issues.
Two consequences of release-please's model are load-bearing complexity in the
workflow today: the draft β attach SBOM/artifacts/Sigstore provenance
(#318) β
un-draft sequence (because release-please creates the GitHub Release
atomically at PR-merge time, before the build artifacts exist), and the phantom-PR reconciliation in
finalize-release (because release-please derives its commit range from the
latest published release and ignores drafts). Versioning is git-tag-sourced
via hatch-vcs (dynamic = ["version"]), so release-please never edits a static
version literal and uv.lock cannot desync.
A third constraint landed after this research began and is not specific to
release-please: a merge that changes .github/workflows/ after the release PR
merges but before the Release run finishes can make GitHub's release API refuse
tag/release creation with a 403, because GITHUB_TOKEN cannot be granted
workflow write access
(#332). ADR-002
now documents the merge pause and recovery
(#339). Any
candidate that creates the tag or release from a workflow with GITHUB_TOKEN
inherits this; only a model where a maintainer pushes the tag from their own
clone would plausibly sidestep it (not verified).
Why we are re-opening the decision¶
Three friction points motivated this review:
- Release-candidate to stable changelog range. After cutting several
prereleases (
v1.2.0-rc.1,v1.2.0-rc.2) and then a final stablev1.2.0, the stable release's notes contain only the changes since the last RC, not all changes since the last stable (v1.1.0). The observed cause is that release-please computes the commit range from the last release marker with no prerelease/stable distinction. - Stale draft-release comment link. release-please posts its
π€ Created releases:comment on the merged Release PR once, at draft-creation time, capturing the transient/releases/tag/untagged-<hash>slug. Whenfinalize-releaseun-drafts the release, GitHub moves it to/releases/tag/vX.Y.Zand theuntagged-β¦link 404s. This is tracked and accepted as a cosmetic limitation in #143 (closed) and documented by #152 (merged), which added the inline comment at thefinalize-release"Publish the draft release" step and the corresponding ADR-002 Consequences bullet. - General "unconventional usage" friction of the release-PR model β the draft dance and phantom-PR reconciliation are workarounds layered on top of release-please's opinions rather than features of it.
The prior art in ADR-002 compared release-please against release-it and
release-drafter; ADR-004 compared it against a Commitizen cz bump pipeline.
This ADR widens the field to the current (2025β2026) landscape and re-frames the
question around a distinction ADR-002/004 never drew explicitly: the difference
between a changelog engine, a release orchestrator, and a commit
helper.
A conceptual frame: three roles in release automation¶
The candidate tools are frequently compared as if interchangeable, but they occupy three different layers. Naming the layers is the point of this section β it is the frame for the discussion this ADR is meant to open.
| Role | Responsibility | Examples |
|---|---|---|
| Commit helper | Author and lint Conventional Commit messages | Commitizen cz commit, commit hooks |
| Changelog engine | Turn a commit range into release notes / CHANGELOG.md |
git-cliff, the changelog half of any tool |
| Release orchestrator | Decide when to release, compute the version, tag, create the GitHub Release, gate downstream jobs | release-please, semantic-release, cz bump, cocogitto, knope |
The critical insight for this decision: release-please is a release
orchestrator, git-cliff is a changelog engine, and Commitizen can act as
either a commit helper (its role today, per ADR-004) or a release
orchestrator (cz bump). A changelog engine cannot replace an orchestrator
on its own β it fills exactly one of the five responsibilities above. That
single fact dominates the effort comparison later.
A second, non-obvious insight: both named pain points are artifacts of release-please's specific orchestration model ("maintain a Release PR; create the GitHub Release atomically at merge; range from the last published release"). A tool that instead lets the pipeline sequence tag β build β release can create a non-draft release with artifacts already attached (eliminating pain point 2's draft dance) and can be pointed at the last stable tag for changelog range (addressing pain point 1) β provided the tool supports it. Whether each candidate actually supports it is the empirical question the research answered.
Research method¶
Two background "deep-research" passes were run (fan-out web search β source fetch β per-claim adversarial verification with a 3-vote majority β synthesis). Both preferred primary sources: tool source code, config schemas, official docs, and specific GitHub issues/PRs.
- Pass 1 (landscape). 6 angles, 23 sources fetched, 96 claims extracted, 25 verified β 23 confirmed, 2 refuted. Established the candidate set and each tool's fit against the hatch-vcs / uv / GitHub Actions / Trusted-Publishing / no-Node constraints.
- Pass 2 (decisive questions). 6 angles, 21 sources fetched, 75 claims extracted, 25 verified β 23 confirmed, 0 refuted, 2 unverified. Narrowed to the two questions that actually decide the outcome: (1) is release-please's RCβstable changelog range a bug or configurable, and (2) do the leading alternatives accumulate changelog from the last stable across RCs.
Verification caveat, recorded honestly. Pass 2's final synthesis step
failed on a session-usage limit, but all 23 claims were already
adversarially verified before it did (0 refutations in pass 2), and the
load-bearing claims were re-checked by hand against the per-agent transcripts:
the Commitizen changelog_merge_prerelease fix, the Python Semantic Release
source-code trace, and the npm-team manual-workaround quote each appear in
multiple independent verifier transcripts quoting primary sources. cocogitto's
RC-range behavior did not survive into the verified top-25. The raw
extraction leaned toward "cocogitto shares the bug" on the strength of
cocogitto issue #362, but on re-check that issue concerns version computation from a
prerelease tag (not the changelog range) and was closed as completed on
2025-10-21, so it is no evidence either way.
Findings: the candidate landscape¶
Fit of each researched tool against the template's fixed constraints (hatch-vcs git-tag versioning with no static literal edits; uv/tox/prek; GitHub Actions + PyPI Trusted Publishing; no Node runtime; squash-by-PR-title Conventional Commits).
| Tool | Layer | Runtime | hatch-vcs fit | GitHub Release | Fixes RC changelog | Verdict |
|---|---|---|---|---|---|---|
| release-please (incumbent) | Orchestrator | Pure GHA | Yes (git-tag sourced) | Yes (draft) | No | Baseline |
Commitizen (cz bump) |
Helper β Orchestrator | Python (installed) | Yes (scm provider; no version literal) |
No (add gh release) |
Yes (changelog_merge_prerelease) |
Strongest replacement |
| Python Semantic Release | Orchestrator | Python (Docker action, no Node) | Yes if importlib + no version_toml |
Immediate | No (shares the bug) | Python-native but no gain |
cocogitto (cog bump) |
Orchestrator | Rust (libgit2) | Yes (tag-based) | Via extra step | Unverified | Node-free, unproven on RC |
| knope | Orchestrator | Rust | Yes (tag-derived with empty versioned_files) |
Yes (Release step) |
No (starts from latest tag) | Rust orchestrator; supports CC and PR bot, but shares RC truncation |
| git-cliff | Changelog engine | Rust | Yes (tag-sourced) | No (pair with gh release) |
Range yes (ignore_tags); no prerelease version calc (#1380) |
Not an orchestrator |
| semantic-release (JS) | Orchestrator | Node | β | Immediate | No | Excluded by the no-Node constraint (as release-it was in ADR-002) |
| changesets | Orchestrator | Node | β | β | β | JS-ecosystem; extra changeset files |
| hatch-regex-commit | (version source) | Hatch plugin | No (static regex source, mutually exclusive with hatch-vcs) | No | No | Disqualified |
Pain point 1: RC to stable changelog range¶
Verdict: a genuine release-please limitation with no built-in config remedy. Commitizen fixes it with one setting; git-cliff can produce the right range with configuration but leaves prerelease version calculation to the orchestrator.
Evidence for release-please (all primary):
- It finds "the last merged release PR and use[s] the associated commit sha as the previous marker from which to gather commits", with no documented distinction between prerelease and stable β so an intervening RC PR becomes the previous marker (manifest-releaser.md, design.md).
- The
versioningoption (default/always-bump-patch/always-bump-minor) controls only the bump size, andprerelease/prerelease-typecontrol only the prerelease label/number β none re-anchor the changelog range to the last stable tag (customizing.md). - Mishandling a prerelease as "latest" is maintainer-labeled
type: bug(#2267); clean prereleaseβstable conversion is an open, unresolved feature request (#2515, open since Mar 2025) and there is no answer to "ignore-rctags" (#2605). - Independent confirmation that this bites real projects: the npm CLI team works around the identical behavior by hand β "release-please only generates changelogs based on the diff from the previous prerelease. So when coming out of prerelease mode, we combine all the prerelease changelogs and manually update the notes of the GitHub Release after it has been created" (npm/cli wiki).
Per-alternative behavior:
- Commitizen β fixes it.
--merge-prerelease/ configchangelog_merge_prerelease"Collects changes from prereleases into the next non-prerelease version" (changelog docs). A bug where it was ignored duringcz bump --changelog(#1694) was fixed in v4.11.3, 2026-01-13 ("fix the issue thatchangelog_merge_prereleasenot working oncz bump", CHANGELOG). Reliability on the bump path is therefore recent β a real caveat. - Python Semantic Release β shares the bug. Verified against PSR's source:
it builds its tag lookup from all tags (RCs included), walks commits from
HEAD until the first matching tag, and sets the released version's changelog
elementsto exactly thatunreleasedset β i.e. only commits since the last RC (release_history.html). - cocogitto β unverified. Did not reach the verified top-25; the issue first read as evidence (#362, closed) is about bump computation from prerelease tags, not the changelog range.
- knope β shares the truncation behavior without changesets.
PrepareReleasenatively parses Conventional Commits and supports tag-derived versioning without versioned files (fitting hatch-vcs), while Knope Bot provides release PRs. However,PrepareReleasedocuments the most recent SemVer tag as its unconditional starting point for commit gathering, with no mechanism analogous tochangelog_merge_prereleaseorignore_tagsto fold RC commits into a stable release β so an intervening RC tag truncates stable notes just like release-please unless maintainers manually author separate change files. - git-cliff β can produce the range, but does not version prereleases.
ignore_tags(or--ignore-tags) folds the matched tags' commits into the next tag, so ignoring-rctags when cutting a stable yields notes from the last stable; an explicit commit range (git cliff v1.1.0..v1.2.0) does the same (git config docs). What it lacks is prerelease version calculation for--bumped-version(#1380, open; milestoned 2.15.0), so the orchestrator must compute RC numbers and choose the tag filter per release (ignore RCs for a stable cut, not for an RC cut).
Pain point 2: stale draft-release comment link¶
Verdict: incidental to release-please's draft flow; a tag-push model sidesteps it, but no researched tool "fixes the comment" because no other tool posts that comment.
The untagged-<hash> link exists only because release-please creates the
GitHub Release as a draft at merge time and comments once with the then-current
(draft) URL (see #143 / #152). Any pipeline that instead creates the git tag
first, builds artifacts, and then creates a non-draft GitHub Release with
assets already attached never has an untagged-<hash> phase β so both
Commitizen and git-cliff variants remove this class of problem as a side effect,
at the cost of the maintainer owning the tagβbuildβrelease sequencing. This is
not a reason to switch on its own; it is a bonus of any move away from the
draft-then-publish dance, and #143's option (2) ("have finalize-release
rewrite the comment") remains available without switching tools.
Deep comparison: git-cliff vs Commitizen as replacements¶
Because both pain points and the hatch-vcs constraint point at these two tools, they are compared here in the concrete shape a migration would take. The yardstick is the five responsibilities and the gating contract above.
git-cliff as a replacement¶
git-cliff is a changelog engine (plus a version calculator via
--bumped-version). It does not tag, does not create a GitHub Release, does not
open a PR, and does not decide whether to release. It therefore replaces exactly
one of release-please's five responsibilities (the changelog, and part of
the version math); everything else becomes bespoke workflow code.
A git-cliff pipeline is necessarily a tag-push / manual-dispatch model:
- A maintainer (or a small script) creates and pushes the tag β which is ideal for hatch-vcs, since the tag is the version source of truth.
- A workflow on
push: tagsrunsgit-cliff --latestfor the release notes andgit-cliff(full) to regenerateCHANGELOG.md, committed back. - Build / SBOM / executables run, then
gh release create <tag> β¦ dist/*publishes a non-draft release with assets already attached.
Effort is high: a new cliff.toml must reproduce the 11-section emoji
changelog-sections taxonomy; the "is there anything to release" gate, tag
creation, changelog commit-back, and release creation are all hand-written; and
pain point 1 is fixable only by hand β the orchestrator must compute RC version
numbers itself (#1380) and pass --ignore-tags for stable cuts. The net result
would be more custom workflow code than today, with pain point 1 solved by
that custom code rather than by the tool. git-cliff's realistic role is as the changelog
component inside a hand-rolled orchestrator, not as a drop-in replacement.
Commitizen as a replacement¶
cz bump is a release orchestrator: in one command it computes the version
from commits, creates the git tag, and writes CHANGELOG.md. With
version_provider = "scm" it never edits a version literal (the version
lives only in the tag), matching hatch-vcs exactly β though with
update_changelog_on_bump it still writes CHANGELOG.md and makes a bump
commit. It does not open a Release PR and does not create a GitHub
Release, so a gh release step is added.
cz bump also signals "nothing to release", but it does so by raising
NoneIncrementExit with exit status 21 rather than a silent exit 0 (see
Commitizen exit codes).
In CI, the replacement workflow cannot treat status 21 as a failure, nor can it
simply swallow it: it must explicitly catch status 21 as a clean no-op and
synthesize a boolean job output (mirroring release-please's release_created) to
gate the fourteen downstream jobs (build, PyPI, Docker, Sigstore, etc.). Without
that custom status trap and output bridge, a no-bump push either fails CI on
exit 21 or runs the downstream release jobs after a blindly ignored error.
The [tool.commitizen] block grows from today's single name line back to a
release configuration (reversing ADR-004's deliberate reduction):
[tool.commitizen]
name = "cz_conventional_commits"
version_provider = "scm" # no version literal; hatch-vcs compatible
tag_format = "v$version"
version_scheme = "semver2" # v1.2.0-rc.0, not PEP 440 v1.2.0rc0
update_changelog_on_bump = true
changelog_merge_prerelease = true # fixes pain point 1 (cz >= 4.11.3; template pins 4.17.0)
major_version_zero = true # mirrors bump-minor-pre-major; 0.x only
major_version_zero is not a drop-in for release-please's
bump-minor-pre-major: release-please's option silently stops applying after
1.0, whereas Commitizen
documents
that cz bump fails on a 1.x+ version while it is set. The template would have
to render it only for pre-1.0 projects (or the migration would have to remove
it before the first 1.0 bump).
version_scheme = "semver2" matters: Commitizen defaults to pep440, which
tags prereleases as v1.2.0rc0, but the docker-publish job parses the tag
with docker/metadata-action's type=semver, which requires a valid SemVer
string. release-please emits SemVer prerelease tags today, so the sketch keeps
them.
The sketch is not changelog-complete as shown. cz_conventional_commits'
built-in parser (4.17.0) only extracts feat, fix, refactor, perf, and
breaking changes into changelog context, while the template's
release-please-config.json publishes emoji sections for docs, test,
build, ci, and deps too. Because a changelog template can only format
commits that the parser retained, a custom Jinja template cannot restore categories
filtered out by the built-in parser. Keeping the template's taxonomy requires
switching from name = "cz_conventional_commits" to name = "cz_customize" with
a full [tool.commitizen.customize] table (defining schema_pattern,
bump_pattern, bump_map, commit_parser, changelog_pattern, change_type_map,
and change_type_order β without commit_parser, changelog generation exits with
NoPatternMapError) or authoring a custom Commitizen plugin. Moreover, because
cz_customize does not inherit the built-in authoring prompts, retaining
interactive cz commit support requires recreating questions and
message_template in the config or maintaining a dedicated plugin. This adds
significant custom configuration or plugin maintenance that narrows the effort
gap with git-cliff.
Effort is medium and lower than git-cliff: no bespoke version math, the tool
is already installed, and the version calculation relies on Commitizen rather
than custom git log parsing; however, the workflow still needs custom shell
handling to trap exit 21 (NoneIncrementExit) into a release_created output
for the fourteen downstream jobs, and the changelog needs the cz_customize
parser table above (in pyproject.toml rather than a separate cliff.toml).
The real costs are architectural, not lines of code: it reverses ADR-004
(Commitizen becomes a release tool again β the tag/changelog conflict that
ADR-004 avoided is moot once release-please is gone, but the narrative must be
rewritten), and it loses the reviewable Release PR that ADR-002 valued.
Effort and fit, side by side¶
| Dimension | git-cliff | Commitizen | release-please (today) |
|---|---|---|---|
| Replaces version calc | Partial (--bumped-version) |
Yes (native) | Yes |
| Creates git tag | No (scripted) | Yes (cz bump) |
Yes |
| Writes CHANGELOG | Yes (new cliff.toml) |
Yes (needs cz_customize or plugin for docs/test/build/ci/deps) |
Yes |
| "Should release?" gate | No (fully bespoke) | Partial (exits 21; needs custom trap + output bridge for 14 jobs) | Yes (release_created) |
| Creates GitHub Release | No (gh release) |
No (gh release) |
Yes (draft) |
| Reviewable Release PR | Lost | Lost | Yes (its model) |
| Fixes pain point 1 (RC) | With custom code (ignore_tags + own RC math) |
Yes (one setting) | No |
| Fixes pain point 2 (link) | Incidental | Incidental | No (accepted) |
| Already in the stack | No | Yes | Yes |
| Net effort | High | Medium | Baseline |
| Reverses an ADR | No | Yes (ADR-004) | β |
What both alternatives cost regardless: the reviewable Release PR disappears (release-please's signature feature per ADR-002), replaced by a tag-push / dispatch trigger. What both alternatives remove: the draft β un-draft dance and the phantom-PR reconciliation, so the workflow gets simpler in those two places.
What both alternatives must newly solve β credentials and triggers. release-please
lands its changelog through a PR and creates the tag from the merge, so it needs only
GITHUB_TOKEN. A CI-driven replacement instead faces credential constraints depending
on the architecture:
- Bypassing branch protection: pushing a bump/changelog commit straight to
mainis rejected by protected branch rulesets (such as thefullpreset, ADR-021) unless the pushing identity is granted a bypass β requiring a PAT or GitHub App token with standing bypass permissions, which ADR-002 deliberately avoided; - Workflow trigger model: if the design splits release steps across workflows
(e.g. git-cliff's canonical
push: tagsworkflow), a tag pushed usingGITHUB_TOKENdoes not trigger subsequent workflow runs due to GitHub's loop prevention, requiring a PAT or App token. Conversely, if Commitizen preserves the template's single-workflow fan-out where downstream jobs run in the same workflow and check out the new tag ref, no secondary workflow trigger is needed; and - Changelog ordering (git-cliff): a changelog committed after the tag leaves the tagged commit without its own changelog entry.
Thus, a standing privileged credential (PAT or GitHub App token) is required if branch
protection must be bypassed or if the design relies on a separate tag-triggered workflow.
Running the bump locally is not a clean escape hatch under the full preset either:
its ruleset has bypass_actors: [], requires a PR, and allows only squash merges, so a
maintainer cannot push the bump commit to main directly, and squash-merging it through
a PR rewrites its SHA, leaving the locally created tag off main. A local flow would need a
two-step "changelog PR, then tag the merged SHA" sequence (e.g. calculating the next tag
and generating the changelog via cz changelog --unreleased-version="<tag>" in a PR,
then tagging the merged commit on main) or a ruleset bypass.
Options under consideration¶
No option is selected. They are recorded for discussion.
- Stay on release-please; document and optionally work around. Treat the RCβstable range as a rare edge case with a known manual remedy (concatenate prerelease sections, as the npm team does), mirroring how pain point 2 is already handled (#143 / #152). Optionally implement #143's option (2) to rewrite the stale comment. Lowest risk; keeps the Release PR; changes nothing structural. Refines ADR-002 with a documented limitation.
- Migrate to Commitizen (
cz bump,scmprovider). Genuinely fixes pain point 1, stays Python-native, reuses an installed tool, and removes the draft dance and phantom-PR reconciliation. Costs: rebuild GitHub Release creation + SBOM/artifact attach aroundgh release, lose the Release PR, and reverse ADR-004. Medium effort on the most safety-critical workflow the template ships. Would supersede ADR-002's tool choice and reverse ADR-004. - Adopt git-cliff inside a hand-rolled orchestrator. Highest effort; fixes
pain point 1 only through that orchestrator's own RC versioning and
per-release
--ignore-tagschoice. Only worth considering if the changelog formatting (git-cliff's templating) becomes a first-class requirement independent of the release model. Not recommended on current evidence. - Record research only; defer. Keep this ADR at Proposed, change nothing, and revisit when git-cliff ships prerelease version calculation (#1380, 2.15.0), or when the RC-release workflow becomes frequent enough that the manual workaround is painful in practice.
Decision drivers and open questions¶
- How often do we actually cut RC series? Pain point 1 only bites when prereleases precede a stable. If RCs are rare, option 1 is likely sufficient.
- How much is the reviewable Release PR worth? It was a primary reason ADR-002 chose release-please. Options 2 and 3 both discard it.
- Is reversing ADR-004 acceptable? Option 2 re-promotes Commitizen to a release tool; the ADR-004 reasoning must be re-examined, not just overwritten.
- Template blast radius.
release.ymlfans out to every generated project; any change is validated only by rendering +actionlintuntil a live release exercises it (the ADR-002 validation limit and cobo#49 precedent). - Is a release credential acceptable? Options 2 and 3 need a PAT or App
token to push to protected
main(and to trigger a tag workflow if split across workflows; see "What both alternatives must newly solve"), or a maintainer-local bump. - Unresolved by research: cocogitto's RC-range changelog behavior; and whether a Commitizen migration can faithfully reproduce the tag β build β attach SBOM/provenance β publish ordering β an implementation spike would settle it.
Consequences¶
- If option 1 is chosen: this ADR is Accepted as "no change; documented
limitation"; ADR-002 gains a Consequences bullet for pain point 1 (parallel
to the pain-point-2 bullet added by #152), and optionally a
finalize-releasecomment-rewrite step lands for pain point 2. No workflow restructuring. - If option 2 is chosen: this ADR supersedes ADR-002's tool selection and
reverses ADR-004;
release.yml,pyproject.toml[tool.commitizen], the removal ofrelease-please-config.json/-manifest.json, README, CLAUDE.md, and CONTRIBUTING (PyPI Trusted Publishing still targetsrelease.yml) all change; a release credential (or a maintainer-local bump) is chosen; a live release must validate the new tag β build β attach β publish ordering. - If option 3 is chosen: as option 2 plus a bespoke orchestrator and a
cliff.toml, and RC version calculation stays hand-written until git-cliff ships #1380. - If option 4 is chosen: status stays Proposed; nothing changes; the ADR is the durable record of why the alternatives were not adopted yet.
Until a decision is made, the pipeline is unchanged: release-please remains the orchestrator, Commitizen remains a commit helper (ADR-004), and pain point 2 remains an accepted, documented cosmetic limitation (#143 / #152).
Sources¶
Primary sources cited above, grouped by tool.
- release-please: design.md, customizing.md, manifest-releaser.md, config schema, issues #2267, #2447, #2515, #2605, #2769.
- npm CLI (independent confirmation of pain point 1): Release-Process wiki.
- Commitizen: version_provider, bump, changelog, exit codes, cz_customize, issue #1694, CHANGELOG, commitizen-action.
- Python Semantic Release: repo, docs, release_history source.
- cocogitto: repo, bump docs, changelog docs, issue #362.
- knope: repo, PrepareRelease, package versioning, bot workflow.
- git-cliff:
git configuration (
ignore_tags/count_tags), issue #1380, issue #692, issue #588. - hatch-regex-commit: PyPI, repo.
- Internal: #143, #152, #318, #332, #339, ADR-002, ADR-004.