The polyglot monorepo release tool | dispat
Skip to main content<br>Why one more monorepo tool?<br>Every major monorepo tool can topologically sort a dependency graph: build everything in order, then publish everything, or publish only what changed, one package at a time. Turborepo, Nx, Bazel, Pants, Buck2 and moon schedule work across such a graph; Lerna, changesets, Rush, Melos, cargo-release, semantic-release, the npm, pnpm and yarn workspace commands and the Maven and Gradle reactors turn one into a release. As of dispat 1.0.0 in August 2026 the split is always the same: the language-agnostic ones stop before the release, and the ones that publish are built around a single ecosystem. Only nx release and release-please reach further, neither all the way. Two situations break that model in practice:<br>An error in the middle of a run. Half the packages are published and half are not. Most tools either abort the whole run or carry on and leave you to reconstruct what shipped. Where recovery exists it is a registry query, as in lerna publish from-package, and a registry answers only whether a version is already there, never what the run still owes. It is one ecosystem's answer too: there is no equivalent for a Docker tag, a GitHub release, or a Maven deploy that simply rejects the version it has. Recovery ends up being a script you write.<br>A consumer that can only be built once its provider is published. A Node package can be built before its consumers publish, but a Docker image is often buildable only by pulling its base image from a registry, which means the provider has to be published first. Build everything then publish everything assumes every ecosystem behaves like npm, and mixed graphs break it.<br>Modern projects are exactly that mix: many packages on different infrastructure, npm next to Docker next to Go, wired into one dependency graph. dispat is built for that case. Concepts works both situations through end to end.<br>Releases the graph, not a list<br>Consumer and provider ordering, parallel builds and publishes with separate concurrency budgets, and isBuildWaitingPublish for the ecosystems, Docker among them, where a consumer can only build once its provider is published.
Blast radius written in the commit<br>feat(core): releases core alone, ^ reaches its direct consumers, ^^ the transitive closure, and +N exactly N edges. Nothing is released on a guess.
Self-healing runs, because a release is a distributed transaction<br>Publishing a graph means irreversible writes across independent services with no rollback to fall back on, so each package's leg commits by durably recording its own completion: the annotated git tag, written only once the publish succeeded. A broken package skips only its true dependants while everything else keeps releasing, and re-running is the recovery: the plan is a pure function of history, graph and configuration, so the next run recomputes the same transaction and executes only the legs whose record is missing. No state files, no double releases and no repair scripts. Details.
Release control from commits<br>%beta starts a prerelease train and %beta>stable graduates it, Release-As: none holds a package and Release-As: auto resumes it, Release-As: 2.0.0 pins an exact version and cancel(pkg) discards pending work. It is written in commits, so release decisions are reviewed and versioned like code. Details.
Polyglot by construction: any language, any registry, any tooling<br>Stages are shell commands fed a rich DISPAT_* environment, release state lives in git tags, and dispat reads and rewrites thirty-five manifest formats across twenty ecosystems, npm to go.mod to Podfile to the project files Unity, Godot, Unreal, Defold and O3DE keep a game's version in, so dispat compute can derive the dependency graph and each package's starting version from the repository you already have. And because an unchanged package is simply not in the plan, there is no task cache to manage, clear or distrust: BuildKit layers, an Nx, Turborepo or Bazel cache and the Gradle build cache all keep working inside the stage, and none of them can change what is versioned, ordered or tagged.
Every release step is also a command, with the records built in<br>Per-package changelogs, annotated tags, GitHub releases and an optional release commit, each also runnable alone (dispat changelog, commit, github) with the release stage finding the work done and skipping it. dispat status dry-runs the whole plan, a release lock refuses two releases of one repository at once, and dispat release -p core (or -s libs, -g platform) ships a subset at exactly the versions a full release would have given it, with dispat if, exec, autowriter and autoreplacer as the glue a custom pipeline is assembled from. Details.
Lightweight libraries, usable on their own<br>Parsing commit messages and reading and rewriting dependency manifests are problems far older than releases, so dispat keeps all three as standalone Go modules with no dependency on the...