Interactive setup. Writes release.toml (the source of truth) and generates one
.github/workflows/release.yml from it.
otf-release init [--force]
| Flag | Effect |
|---|---|
--force |
Overwrite existing release.toml / release.yml without prompting. |
init takes no --adapter flag — it asks. Implemented in crates/core/src/init.rs.
- Choose adapters (spacebar multi-select):
npm,crates.io,generic. The enabled set is recorded inrelease.toml; a polyglot repo can enable several. - Auto-configure npm packages (the tool owns the build; no prompt). For each publishable npm
package,
initreads itspackage.json: if it declares ascripts.build, the package gets an inline-build publish entry (npm run buildruns in the package's own publish job — no separate build job or artifact staging), and npm's pack/publish lifecycle hooks (prepublish,prepublishOnly,prepack,prepare) are stripped frompackage.json(with a printed notice) so npm can't re-run a build behind the pipeline. See adapters/npm.md. npm workspace manifests that are not release packages (for example fixture or benchmark folders without aversion) are skipped and listed with the reason. - List publishable cargo packages, then multi-select: "Which packages need built
artifacts before publish?" (npm packages are handled in step 2, so they are not offered here.)
For each selected package, prompt for:
- mode —
publish(build, then push to the ecosystem's registry) orbuild-only(build, then attach the artifacts to a GitHub Release — no registry push); - build matrix? — if yes, pick targets from the built-in registry. Each selected target is
written with every reconciled field (triple, runner,
stage_as,ext,cross,vm) already filled in — the tool owns that mapping, so there is nothing to hand-tune. Niche targets (musl, FreeBSD, 32-bit,win32-arm64) are offered but off by default; - the build command and the artifacts glob to stage;
- for
build-onlypackages: the archive format (auto/tar.gz/zip— binaries always ship as archives), any extra files to bundle inside each archive, and whether to attach achecksums.txt.
- mode —
- Offer
skip_publish— asked only when the repo configured abuild-onlypackage and other discovered crates are still publishable. Those crates are listed (pre-selected) and your answer is recorded inrelease.toml. This matters for a binary-distribution Cargo workspace: its library crates carry nopublish = false, so without this they would be pushed to crates.io on the firstpublishrun. Skipped packages are still versioned in lockstep — only the push is suppressed. A repo that publishes everything is never asked. - For the generic adapter (if enabled):
initscans the repo for recognized manifests that carry a version and presents them in a multi-select to import — so you don't hand-type manifest paths (single project or monorepo). Generic is the custom-way path, so the scan spans all project types (Cargo.toml,package.json,deno.json,pyproject.toml, …), not just ones lacking a native adapter. Per imported package you supply only the optional build/artifacts and publish command; you can also add packages by hand. See adapters/generic.md. - Persist
release.tomland generaterelease.ymlfrom it. Both writes are guarded: re-running warns before overwrite (--forceto replace). - Choose a global git tag format from common options:
v{version},{version},{name}@{version}, or{name}@v{version}(plus custom input).initinspects existing local tags and marks the matching pattern as suggested when it can. With no tags, multi-package repos default to{name}@{version}to avoid tag collisions. If you edit a detected pattern to migrate schemes, the detected pattern is saved aslegacy_tag_formatsso release history still works. - Choose where release notes are maintained: one root
CHANGELOG.md, or per-packageCHANGELOG.mdfiles. - Choose what GitHub Release descriptions contain for
build-onlypackages: auto-generated GitHub notes, curated changelog notes, or a semantic-style commit list since the previous matching configured tag. In package-level changelog scope, curated GitHub Release notes combine the released sections from all configured packages.
The committed source of truth. Every other command (version, publish) reads it instead of
taking an --adapter flag. See configuration.md for the full schema.
adapters = ["crates.io"]
changelog_scope = "root"
# Library crates in the workspace that must never reach crates.io (asked in step 4).
skip_publish = ["opentf-release-core", "opentf-release-adapters"]
[[package]]
name = "opentf-release"
adapter = "crates.io"
mode = "build-only" # artifacts -> GitHub Release, no registry push
matrix = true
command = "cargo build --release --target {triple}" # {triple}/{ext}/{bin} expand per target
artifacts = "target/{triple}/release/otf-release{ext}"
bin_name = "otf-release"
archive = "auto" # the default: .zip on Windows, .tar.gz elsewhere
checksums = true
# One table per target, written in full from the built-in registry.
[[package.targets]]
name = "linux"
arch = "x86_64"
triple = "x86_64-unknown-linux-gnu"
runner = "ubuntu-latest"
stage_as = "linux-x64"
ext = ""
# "auto-generate" | "curated-changelog" | "semantic-commits"
github_release_notes = "auto-generate"From the config, init emits jobs:
- a
check-releasejob that decides whether downstream jobs should run. It is a one-liner —should_release=$(otf-release check)— delegating to the binary like every other job, so it can't drift from what actually ships.checkreturnstrueif any configured package has a real version whose tag doesn't exist yet (publish/github-releaseare per-package idempotent and skip the rest); it needsfetch-depth: 0so the tags are present to compare against; - a
build-<pkg>job per package with a build step (a matrix when build matrix is yes); - a single
publishjob when registry publishing is enabled — runsotf-release publishonce, and the CLI loops the enabled adapters internally; - a
github-releasejob when any package isbuild-only— attaches its staged artifacts to a GitHub Release tagged fromtag_format, idempotently. The defaultGITHUB_TOKEN+contents: write. Its release body follows the globalgithub_release_notessetting.
For npm repos, generated jobs detect the package manager from the root lockfile: bun.lockb /
bun.lock use Bun, pnpm-lock.yaml uses pnpm, yarn.lock uses Yarn, and otherwise npm is used.
- Repo-specific build steps are yours to refine.
initwires the DAG, secrets, and the target reconciliation; the build command and artifacts glob are the parts only you can supply. - VM targets need no runner, but do need patience. FreeBSD builds inside a guest on the Linux runner; the aarch64 leg is fully emulated and much slower than a native build. See configuration.md.
- npm workspace discovery only imports real packages. A workspace
package.jsonmust have a stringnameandversionto become a release package. Missing fields are reported as skipped; malformed JSON is still treated as a broken manifest and stops the scan.
init is the generator; ci-workflow.md is the shape of what it produces
and why. Read them together when setting up a repo.
- configuration.md — the
release.tomlschema. - ci-workflow.md — the generated workflow, explained.
- commands/publish.md — the command the
publishjob runs.