otf-release is a single static binary (Rust) split into a registry-agnostic core and
one or more adapters. The core orchestrates a release; an adapter knows how one ecosystem
reads manifests, formats version ranges, talks to a registry, and publishes.
- Core never reads a manifest. No
package.json, noCargo.toml. The core only ever talks to anAdapter. This is what keeps it polyglot. - The adapter owns ecosystem policy, including the cascade rule (
dependent_bump) and range syntax (format_range) — not a shared config file. - One committed config.
release.toml(written byinit) records which adapters are enabled and the per-package build steps — it is the source of truth thatversion,publish, and the generatedrelease.ymlall derive from. Everything else is read from disk (manifests, changelogs,.artifacts/) and the registry/git (tags); no other state is persisted between runs. - Bumps are chosen by a human. Release notes can be curated from
[Unreleased]sections or generated from commit history, depending onrelease.toml.
Cargo.toml # workspace
crates/
core/ opentf-release-core # ecosystem-agnostic orchestration (lib)
src/
lib.rs
adapter.rs # Adapter trait + domain types (Pkg, Bump, DepKind, InternalDep)
graph.rs # dependency graph: topo sort + bump cascade engine
changelog.rs # Keep a Changelog parse/rewrite
preflight.rs # strict compliance gate
summary.rs # confirmation / dry-run rendering
version.rs # `version` command orchestration
publish.rs # `publish` command orchestration
init.rs # `release.yml` generator
adapters/ opentf-release-adapters # registry adapters (lib)
src/
lib.rs
npm/ # npm workspace adapter
cargo.rs # Cargo workspace adapter
generic.rs # manifest + user-command adapter
cli/ opentf-release # binary `otf-release` (clap)
src/
main.rs
cli ──▶ core ◀── adapters
└───────────────▶ adapters
coredefines theAdaptertrait and all domain types. It depends on nothing internal.adaptersdepends oncore(it implements the trait).clidepends on both: it constructs the concrete adapters and hands them to the core command functions as&dyn Adapter.
This direction means a new adapter can usually be added without touching core.
Defined in crates/core/src/adapter.rs:
Pkg— a discovered package normalized to ecosystem-agnostic terms (name, version, manifest/changelog paths,publishableflag, internal deps).Bump—Patch < Minor < Major, ordered somax()picks the strongest bump when a package is hit by several cascade paths.DepKind—Dep | PeerDep | DevDep(adapter-specific set; npm-flavored in v1).InternalDep— an edge to another package in the same monorepo, with its declared range.
discover ─▶ preflight ─▶ prompt ─▶ cascade ─▶ summary/confirm
─▶ branch ─▶ apply (versions, ranges, changelogs) ─▶ lockfile
─▶ commit ─▶ push ─▶ open PR
discover ─▶ filter (publishable & !is_published) ─▶ topo sort
─▶ for each: resolve_workspace_links ─▶ publish ─▶ tag + GH Release
(halt on first failure; re-run resumes forward)
See commands/version.md and
commands/publish.md for the step-by-step contracts.
Splitting core from adapters enforces rule #1 at the compiler level: core literally
cannot depend on the npm adapter, so it cannot reach into a package.json by accident. The
cli crate is the only place that names a concrete adapter.