repolith-cli 0.0.8

Declarative orchestrator for Rust toolchains spread across multiple sibling git repositories.
repolith-cli-0.0.8 is not a library.

repolith

Declarative orchestrator for Rust toolchains spread across multiple sibling git repositories.

โšก Parallel ยท ๐Ÿ›‘ Cancellation-aware ยท ๐Ÿ’พ Cache-first

CI License Rust crates.io PRs welcome

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:

[orchestrator]
schema_version = "0.1"
name = "my-stack"

[[node]]
id = "shared-types"
git = "https://github.com/example-org/shared-types"
path = "../libs/shared-types"
  [[node.action]]
  kind = "git-clone"

[[node]]
id = "migration-tool"
git = "https://github.com/example-org/migration-tool"
path = "../tools/migration-tool"
  [[node.action]]
  kind = "git-clone"
  [[node.action]]
  kind = "cargo-install"
  crate = "migrate"
  install_to = "~/.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 + Semaphore cap 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 CancellationToken plumbed through every action's Ctx. First failure in --fail-fast (default) cancels in-flight peers; --keep-going lets the layer settle then halts. On Unix the subprocess process group is signalled (SIGTERM โ†’ grace โ†’ SIGKILL) so cargo's rustc / linker grandchildren get reaped too.
  • Cache-first. Every successful build writes a BuildEvent to 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 --features list.

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 โ€” rustup is fine.
  • Not a package manager โ€” cargo is fine.
  • Not a process supervisor โ€” systemd / launchd / docker compose are fine; repolith reads heartbeats, doesn't write them.
  • Not a monorepo tool โ€” cargo workspaces already 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
cargo install repolith-cli

# 2. Write a manifest in your stack root (see the reference below,
#    or start from repolith.toml.example)
mkdir -p ~/my-stack && cd ~/my-stack
$EDITOR repolith.toml

# 3. Preview what would happen
repolith sync --dry-run --explain

# 4. Go
repolith sync

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:

[[node.action]]
kind = "git-clone"

kind = "cargo-install" โ€” cargo install from the node's source tree. All three fields are optional:

[[node.action]]
kind = "cargo-install"
crate = "migrate"              # default: the node's `id`
features = ["postgres", "tls"] # default: none
install_to = "~/.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:

[[node.action]]
kind = "docker"
tag = "my-org/app:latest"      # required โ€” docker reference charset only
dockerfile = "build/Dockerfile" # default: Dockerfile (relative to context)
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:

[[node.action]]
kind = "repolith"
manifest = "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.

Cache backends

The build cache is pluggable (Cache trait in repolith-core). Two backends ship today; --cache (or REPOLITH_CACHE) selects one:

Backend Select with Storage When
sqlite (default) โ€” local file, ~/.repolith/cache.db (--cache-path) single machine โ€” zero config
neo4j --cache neo4j shared server, build events as graph data multi-machine / federated stacks

The Neo4j backend reads REPOLITH_NEO4J_URI, REPOLITH_NEO4J_USER, and REPOLITH_NEO4J_PASS from the environment โ€” credentials never live in repolith.toml, and these variables are never forwarded to spawned subprocesses. Schema: one (:Action {id}) node per action with a LAST relationship to its most recent (:BuildEvent); layer writes are one transaction. NamespacedCache (library-level) lets multiple stacks share one server without id collisions.

Architecture

5 crates, layered execution with FuturesUnordered + CancellationToken + Semaphore. Full diagram + the FailFast / KeepGoing sequence diagrams

Crate Purpose
repolith-core Types, traits (Action, Cache), manifest parser, layered Plan.
repolith-cache SqliteCache (rusqlite, bundled, WAL), Neo4jCache (feature neo4j), NamespacedCache.
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

M2 complete. 5 crates, 4 action kinds (git-clone, cargo-install, docker, repolith federation), 2 cache backends (SQLite default, Neo4j opt-in with a live-server contract suite in CI), parallel layered execution with tree-wide cancellation, URL-injection + path-traversal hardening. Full test suite under cargo test --workspace --all-features (110+ tests at last count). The crates.io badge above always shows the current published version; see CHANGELOG.md for the per-release breakdown. 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 โ€” โœ… complete: docker action, federation kind = "repolith", Neo4j cache backend.
  • M3 โ€” watch mode (re-plan on file change), template_apply action driving AttachedEntry::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:

at your option.