A small constellation build system written in Rust. A constellation is a group of related repositories that are built, cleaned and versioned together.
basis can:
- Build constellations of repositories driven by a single YAML manifest,
with per-action command sets (
build,clean, or any custom action). - Track versions per repository — Rust via
Cargo.toml, C++ via a.versionfile (and/orCMakeLists.txt). - Synchronise versions across every repository to one common value.
- Bump one component and propagate the new version into every repository that depends on it.
- Verify identity — check that each repo's
git config user.emailand the e-mail on its GPG signing key are on an allowed domain. - Report status — git state plus version-sync state of the whole
constellation via
basis status.
cargo install --path .
# or run from the workspace:
cargo run -- <args>basis install bootstraps a whole constellation from its manifest repository:
basis install acme/platform # -> https://github.com/acme/platform
basis install git@github.com:acme/platform.git --into platform --branch mainIt clones the manifest repo into a directory (named after the repo by default,
or --into DIR), reads its basis.yaml, and then clones every member repo with
a url: into its path, next to the manifest:
platform/
basis.yaml # from acme/platform
core/ # cloned from its url:
engine/ # cloned from its url:
org/repo is shorthand for a GitHub HTTPS URL; a full git URL (https://…,
git@…, file://…) works too. Members already present are skipped, members
without a url: are reported. After installing, run basis status /
basis build from inside the constellation directory.
Run basis install without an argument from inside an existing
constellation (the manifest is found by walking up, like every other command) to
clone just the members that are still missing — handy after adding a repo to the
manifest, or to finish a partial checkout. Present members are left untouched.
basis install clones the manifest repo, but a constellation often needs loose
files too — helper scripts a _postclone hook calls, shared configs. Declare
them so they are fetched explicitly instead of silently depending on a full
clone of the manifest repo:
files:
- path: gen-dbg.sh
url: https://raw.githubusercontent.com/acme/constellation/main/gen-dbg.sh
executable: trueThey are downloaded (via curl) before members are cloned, so a hook can
use them. Files already present are left untouched.
A repo may define a special _postclone action — basis runs it (in the repo's
directory) automatically right after that repo is cloned by basis install. Use
it to patch generated or local-dev files. Actions whose name starts with _ are
hooks: they run automatically and are hidden from the basis action listing.
- name: release-generator
path: release-generator
url: https://github.com/acme/release-generator
lang: other
actions:
_postclone: ["../gen-dbg.sh"] # regenerate local-dev overrides on cloneconstellation: my-product
version: 1.2.0 # optional canonical version of the constellation
email_domain: corp.com # optional identity policy (see `basis verify`)
repos:
- name: core # unique name, used with --repo
path: core # path relative to the manifest
lang: rust # rust | cpp
url: https://github.com/acme/core # optional canonical git URL
actions:
build: [cargo build --release]
clean: [cargo clean]
- name: engine
path: engine
lang: cpp
url: git@github.com:acme/engine.git
provides: core # optional package name exposed to dependents
version_file: .version # optional, default: .version
cmake_file: CMakeLists.txt # optional, default: CMakeLists.txt
actions:
build:
- cmake -B build -S .
- cmake --build build
clean: [rm -rf build]- Each repo defines a map of action → ordered shell commands. Commands run
in the repo's directory via
sh -c. actionskeys are arbitrary;buildandcleanget dedicated subcommands, anything else is reachable throughbasis run <action>.
basis # list all actions in the manifest
basis [--repo NAME]... [-k] [-n] [--tmux|--no-tmux]
basis build # run the build action everywhere
basis run # run the run action (e.g. services)
basis test --repo core --tmux # run test, forced into a tmux display
basis install [org/repo] [--into DIR] [--branch B] # clone a constellation # (no arg: clone missing # members of current manifest) basis update [--repo NAME]... # git pull --ff-only the cloned repos basis status # git + version status of all repos basis verify # check git/GPG e-mail domains basis display [NAME] [--detached|--kill] # launch a tmux dev dashboard
basis version # alias of version show
basis version show # list every repo's version
basis version set <X.Y.Z> # set an explicit version everywhere
basis version sync [--to <X.Y.Z>] # converge all repos onto one version
basis version bump [--major|--minor|--patch|--to X.Y.Z]
`build`, `clean`, `run`, `test`, … are not special — they are just action names
looked up in the manifest. The reserved names `install`, `update`, `status`, `verify`,
`display`, `version` are the only ones that cannot double as actions.
Common flags:
* `-f, --file <PATH>` — manifest path (default `basis.yaml`). A bare filename is
searched for **upward** from the current directory (like git finds `.git`), so
you can run `basis` from any subfolder of the constellation. A path with a
directory component (e.g. `../basis.yaml`) is used as-is. Repo paths resolve
against the directory the manifest was found in.
* `-r, --repo <NAME>` — restrict to specific repos (repeatable).
* `-k, --keep-going` — continue across repos even if one command fails.
* `-n, --dry-run` — print commands without executing them.
* `-t, --tmux` — run the action in a per-task tmux display (one pane per repo,
in parallel); pairs with `--detached` and `--layout <L>`.
### Version sync target
`basis version sync` chooses its target in this order:
1. `--to <X.Y.Z>` if given,
2. otherwise the manifest's top-level `version:`,
3. otherwise the highest semver found among the repositories.
For Rust repos it rewrites `[package].version` in `Cargo.toml` (preserving
formatting). For C++ repos it writes the `.version` file and patches the
`project(... VERSION x.y.z ...)` call in the CMake file when present.
### Bumping a component and its dependents
`basis version bump <repo>` raises one component's version (default `--patch`,
or `--major` / `--minor` / `--to X.Y.Z`) and then rewrites every repository that
depends on it:
* **Rust dependents** — the matching entry in `[dependencies]`,
`[dev-dependencies]` or `[build-dependencies]` gets its `version` updated,
keeping `path`, features and `package =` renames intact.
* **C++ dependents** — `find_package(<name> <ver> ...)` in the CMake file is
re-pinned.
Matching is by the bumped repo's **provided name**: its `provides:` field if
set, otherwise the Rust crate name (`[package].name`), otherwise the repo name.
```sh
$ basis version bump core --minor
bumping core 1.0.0 -> 1.1.0 (provides 'core')
✓ core version set to 1.1.0
↳ app now requires core 1.1.0
↳ engine now requires core 1.1.0
A task names a display in the manifest — the tmux session it runs in. When that task is executed, basis spawns the display — one pane per repository, in parallel — lazily, at execution time (nothing is created beforehand).
Displays are meant for long-running tasks whose output you want to watch
(running services, watchers). One-shot tasks like build or clean should stay
inline — just don't give them a display:
tasks:
run:
display: services # run this task in the "services" display
layout: even-vertical
# build / clean: no display — they run inline in the current terminalWith that, basis run creates a session named services, gives every selected
repo that defines the action its own pane (in the repo's directory, running the
action's commands), applies the layout and attaches. Repos without that action
are skipped. This is the "display под задачу" — declared once in the config,
born only when the task runs.
The natural use is a long-running run task — start every service and watch
its output live, each in its own pane:
tasks:
run:
display: services
layout: even-vertical # stacked logs, one per repo
repos:
- name: api
actions: { run: ["cargo run --bin api"] }
- name: worker
actions: { run: ["cargo run --bin worker"] }
- name: web
actions: { run: ["npm run dev"] }basis run # api / worker / web each get a pane in "services", logs liveCommands are sent to a live shell, so a pane stays open after you Ctrl-C and you
can restart the process in place. Re-running basis run re-attaches to the same
session.
Closing a display: basis binds Ctrl-q (no prefix) to kill-session on
every display it creates, so pressing Ctrl+Q while attached closes the
display and stops the processes running in it. A normal Ctrl-b d detach still
just detaches, leaving the display running in the background.
Restarting a display: Ctrl+R (no prefix) restarts every pane — it sends
Ctrl-C to interrupt the running process and re-runs the command basis started
the pane with (stored in the @basis_restart pane option). Handy after a code
change: one key rebuilds/reruns the whole task. (Both bindings are tmux
server-global, so inside tmux they take over Ctrl+Q / Ctrl+R — the latter
replaces the shell's reverse-i-search in panes.)
Per-invocation overrides:
basis run # uses the task's display: setting
basis run --tmux # force a display (named <constellation>-run)
basis run --no-tmux # force the current terminal for this run
basis run --detached # with tmux: create but don't attach
basis run --layout tiled # with tmux: override the layoutA display can also be a named tmux session described in the manifest — a standing dev dashboard (servers, watchers, logs, a scratch shell):
displays:
dev:
session: myproj-dev # optional, default <constellation>-<display>
layout: tiled # tiled | even-horizontal | even-vertical | main-vertical | ...
panes:
- { repo: core, command: "cargo watch -x run" } # cmd in the repo dir
- { repo: engine, action: build } # reuse a repo action
- { name: logs, cwd: ., command: "tail -f log/dev.log" }
- { name: shell } # just a shell in base dirbasis display # list configured displays
basis display dev # create the session (if needed) and attach
basis display dev --detached # create but don't attach (prints attach hint)
basis display dev --kill # tear the session downEach pane starts in cwd if given, else the repo directory, else the
manifest directory. Its command is command if given, else the named action
of repo (its commands joined with &&), else a plain shell. Commands are sent
to a live shell, so a pane stays open after its task exits and you can re-run it.
Re-running basis display NAME is idempotent — it attaches to the existing
session instead of recreating it.
basis status lists every configured display and whether its tmux session is
currently up:
displays:
dev ● running 3 pane(s), tiled [demo-dev]
tests ○ stopped 2 pane(s), even-horizontal [demo-tests]
basis verify enforces that contributors use a company identity. For every repo
that has an e-mail-domain policy it checks:
git config user.emailresolves to an allowed domain, and- the OpenPGP signing key (
user.signingkey, or the key matching the git e-mail) has a user ID whose e-mail is on an allowed domain.
Domains come from email_domain (single) and/or email_domains (list). A repo
may override the constellation-wide policy with its own field. SSH-format signing
(gpg.format=ssh) carries no e-mail and is reported as unverifiable (not a
failure). The command exits non-zero if any checked repo fails.
$ basis verify
==> core
allowed domains: corp.com
✓ git email: dev@corp.com
✓ gpg key: ABCD1234 [dev@corp.com]
✓ okbasis status runs the same checks and shows a compact id ✓ / id ✗ / id !
column per repo (— when no policy applies), plus a summary line. Unlike
basis verify, status is informational and always exits 0; use verify as
the enforcing gate (e.g. in CI or a pre-push hook).
$ basis status
core rust 1.0.0 id ✓ main clean origin✓
app rust 1.0.0 id ✗ main dirty origin✗
versions: all versions at 1.0.0
identity: 1 repo(s) fail (run `basis verify` for details)Each repo may declare a canonical git url:. basis status compares it against
the local origin remote and reports one of:
origin✓—originmatches the canonical URL,origin✗—originpoints somewhere else (the expected/actual pair is listed below the table),no-origin— the repo has nooriginremote,missing— the repo directory has not been cloned yet.
URLs are compared after normalisation, so git@github.com:acme/core.git and
https://github.com/acme/core are treated as the same repository (scheme,
git@ userinfo, and a trailing .git are ignored).
A runnable example lives in examples/. From there:
basis -f examples/basis.yaml status
basis -f examples/basis.yaml version sync
basis -f examples/basis.yaml build -n