repolith
Declarative orchestrator for Rust toolchains spread across multiple sibling git repositories.
โก Parallel ยท ๐ Cancellation-aware ยท ๐พ Cache-first
repolith reads one repolith.toml describing remote repositories and the
actions to run against them โ git clone, cargo install, docker build โ then
executes the plan in parallel layers, with content-addressed caching
so untouched work never re-runs and a shared CancellationToken so
Ctrl-C cleanly aborts every in-flight subprocess.
What it looks like
Given a manifest:
[]
= "0.1"
= "my-stack"
[[]]
= "shared-types"
= "https://github.com/example-org/shared-types"
= "../libs/shared-types"
[[]]
= "git-clone"
[[]]
= "migration-tool"
= "https://github.com/example-org/migration-tool"
= "../tools/migration-tool"
[[]]
= "git-clone"
[[]]
= "cargo-install"
= "migrate"
= "~/.local/bin"
You preview, then sync:
$ repolith status
+-------------------------------+--------+---------------+
| Action | Status | Reason |
+===============================+========+===============+
| shared-types::git-clone::0 | stale | NoCachedBuild |
| migration-tool::git-clone::0 | stale | NoCachedBuild |
| migration-tool::cargo-install | stale | NoCachedBuild |
+-------------------------------+--------+---------------+
$ repolith sync --dry-run --explain
โข shared-types::git-clone::0: NoCachedBuild
โข migration-tool::git-clone::0: NoCachedBuild
โข migration-tool::cargo-install::1: NoCachedBuild
dry-run: 3 action(s) would run
$ repolith sync
OK shared-types::git-clone::0 (55 ms)
OK migration-tool::git-clone::0 (188 ms)
OK migration-tool::cargo-install::1 (4.2 s)
$ repolith sync
up to date โ 0 stale actions
The second sync is a no-op โ the SQLite cache holds last-run input
hashes per action, so nothing re-runs unless an upstream HEAD or a cargo
feature actually changed.
Why use it
- One declarative file. Everything your stack needs to bootstrap, in
repolith.toml. No bash glue, no per-machine README dance. - Parallel by default.
tokio::FuturesUnordered+Semaphorecap concurrency at--jobs N(default =num_cpus). Layer N+1 starts only when layer N settles โ typed dependencies, no race conditions. - Cancels cleanly. A shared
CancellationTokenplumbed through every action'sCtx. First failure in--fail-fast(default) cancels in-flight peers;--keep-goinglets the layer settle then halts. On Unix the subprocess process group is signalled (SIGTERM โ grace โ SIGKILL) so cargo'srustc/ linker grandchildren get reaped too. - Cache-first. Every successful build writes a
BuildEventto a SQLite store (WAL) keyed by content-addressed input hashes. Re-runs are near-instant when nothing changed. - Hardened argv. URLs validated against a scheme allowlist with
nested-userinfo / host / path-segment leading-dash checks;
--argv separator before every user URL as defense in depth; crate names + feature flags rejected if they could break cargo's--featureslist.
What it is NOT
The negative scope is fixed and will not evolve:
- Not a CI runner โ no distributed execution, no remote workers.
- Not a toolchain manager โ
rustupis fine. - Not a package manager โ
cargois fine. - Not a process supervisor โ
systemd/launchd/docker composeare fine; repolith reads heartbeats, doesn't write them. - Not a monorepo tool โ
cargo workspacesalready covers single-repo workspace publishing. - Not a hermetic build system โ hermeticity is opt-in per action, not the default.
Quick start
# 1. Install the binary from crates.io
# 2. Write a manifest in your stack root (see the reference below,
# or start from repolith.toml.example)
&&
# 3. Preview what would happen
# 4. Go
Building from source works too: git clone https://github.com/anatta-rs/repolith && cargo build --release.
repolith status prints a cache hit/miss table without running anything.
repolith sync -k keeps a layer running after a failure (useful for
surfacing every failure of a layer in one pass).
See repolith.toml.example for a full annotated
manifest.
Manifest reference
A manifest is one [orchestrator] block plus any number of [[node]]
blocks; each node carries the actions to run against it, in order.
[orchestrator]
| Field | Required | Description |
|---|---|---|
schema_version |
yes | Manifest schema. Current requirement: ~0.1 (e.g. "0.1"). |
name |
yes | Human-readable stack name, shown in logs. |
[[node]]
| Field | Required | Description |
|---|---|---|
id |
yes | Unique node id. Also the default crate name for cargo-install, and the prefix of every action id ({id}::{kind}::{index}). |
git |
per action | Source URL (https://, ssh://, or git@host:path). Source for git-clone. |
path |
per action | Local checkout directory, relative to the manifest. Destination for git-clone, source tree for cargo-install. |
Action kinds
kind = "git-clone" โ fetch the node's source into path. No fields of
its own:
[[]]
= "git-clone"
kind = "cargo-install" โ cargo install from the node's source tree.
All three fields are optional:
[[]]
= "cargo-install"
= "migrate" # default: the node's `id`
= ["postgres", "tls"] # default: none
= "~/.local/bin" # default: ~/.repolith/bin (`~` expands at run time)
kind = "docker" โ docker build an image from the node's checkout
(build-only: running containers stays out of scope, see
What it is NOT). Requires path on the node; tag is
the only required field:
[[]]
= "docker"
= "my-org/app:latest" # required โ docker reference charset only
= "build/Dockerfile" # default: Dockerfile (relative to context)
= "build" # default: the node's `path`
dockerfile and context must stay inside the node's checkout: relative
paths only, no .., validated at parse time and re-checked after symlink
resolution at build time.
kind = "repolith" โ federation: the node's checkout contains its own
repolith.toml, executed as a nested plan (orchestrator-of-orchestrators).
Requires path on the node; the one field is optional:
[[]]
= "repolith"
= "repolith.toml" # default โ relative to the node's `path`
The child stack keeps its own local cache
(<stack>/.repolith/cache.db), exactly as if you had run repolith sync
in that directory. Guard rails: manifest cycles (A -> B -> A) are
rejected with the offending chain, federation depth is capped at 8,
--jobs N bounds the whole tree (one global pool, never N per level),
and Ctrl-C cancels every level down to the subprocess groups. The
manifest path obeys the same two-stage containment as docker's paths.
Actions on the same node run in declaration order; independent nodes run in parallel.
Architecture
5 crates, layered execution with FuturesUnordered + CancellationToken +
Semaphore. Full diagram + the FailFast / KeepGoing sequence diagrams
- design decisions live in
ARCHITECTURE.md.
| Crate | Purpose |
|---|---|
repolith-core |
Types, traits (Action, Cache), manifest parser, layered Plan. |
repolith-cache |
SqliteCache (rusqlite, bundled, WAL). |
repolith-engine |
Async Orchestrator with cancellation + semaphore. |
repolith-actions |
GitClone (feature git), CargoInstall (feature cargo), DockerBuild (feature docker). |
repolith-cli |
repolith sync / status โ the binary you run. |
Status
v0.0.4 shipped. 5 crates, 2 builtin actions, parallel layered execution
with cancellation, SQLite cache (WAL), URL-injection hardening,
process-group cancel propagation, node-id path-traversal validator. Full
test suite under cargo test --workspace --all-features (80+ tests at last
count). See CHANGELOG.md for the per-release breakdown
(three pre-public audit cycles between v0.0.1 and v0.0.4 closed every
must-fix item surfaced). All 5 crates are published on
crates.io; releases are automated
with release-plz โ merging the release PR
publishes, tags, and cuts the GitHub Release.
Roadmap
- M2 โ Neo4j cache backend (shared, graph-queryable build events).
(
dockeraction: โ shipped. Federationkind = "repolith": โ shipped.) - M3 โ watch mode (re-plan on file change),
template_applyaction drivingAttachedEntry::Outbound.
Security
See SECURITY.md for the threat model, the env-allowlist
policy, and the GitHub Security Advisories private-reporting form for
vulnerability disclosure.
Contributing
PRs welcome. See CONTRIBUTING.md for the dev setup,
local CI gates, and worked recipes for adding a new action or a new cache
backend.
License
Dual-licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.