Introduction
rusta is a macOS-only CLI for creating and managing Ubuntu VMs on Apple
Silicon using Tart.
It is the spiritual successor to ubuntu-tart-vm.sh, exposing its features
through a subcommand-based UX rather than a single mega-script. It handles
cloning Ubuntu OCI images, provisioning guests over SSH, wiring up Docker,
and keeping track of a default VM so day-to-day commands stay short.
Requirements
- Apple Silicon Mac (
arm64). The CLI aborts at startup on any other architecture. - macOS with Homebrew available on
PATH. - Outbound HTTPS to
ghcr.iofor OCI image pulls andrusta versions.
tart, sshpass, and (only for docker-setup) docker are auto-installed
via Homebrew on demand.
Where next?
- Installation — install via Homebrew, crates.io, or from source.
- Quick Start — the shortest path from zero to an SSH session in a fresh Ubuntu VM.
- Commands — full CLI command reference.
Installation
Homebrew (recommended)
brew install pallewela/tap/rusta
This installs the latest prebuilt aarch64-apple-darwin binary from the
GitHub Releases page.
Upgrades land via brew upgrade rusta.
Manual download
Grab the tarball for the release you want from GitHub Releases, then:
tar -xzf rusta-vX.Y.Z-aarch64-apple-darwin.tar.gz
install -m 0755 rusta /usr/local/bin/rusta
The tarball also ships shell completions and man pages under share/.
Install them alongside the binary:
# man pages
sudo install -d /usr/local/share/man/man1
sudo install -m 0644 share/man/man1/*.1 /usr/local/share/man/man1/
# zsh completions (adjust to your $fpath)
sudo install -m 0644 share/zsh/site-functions/_rusta /usr/local/share/zsh/site-functions/
# bash / fish — install to your shell's standard completion path
Each release page lists the SHA256 of the tarball; verify with
shasum -a 256 before installing.
From crates.io
cargo install rusta-cli
The crate is named rusta-cli on crates.io because rusta was already
taken; the installed binary is still rusta.
From source
cargo install --path .
# or
cargo install --git https://github.com/pallewela/rusta
Shell completions
If you installed via cargo install or built from source, the prebuilt
completion scripts aren’t on disk — generate them yourself at any time:
rusta completions bash > /usr/local/etc/bash_completion.d/rusta
rusta completions zsh > "${fpath[1]}/_rusta"
rusta completions fish > ~/.config/fish/completions/rusta.fish
Homebrew installs and the Manual download tarball already include these
under share/, so no extra step is needed there.
Quick Start
# Create + provision a default Ubuntu 24.04 VM
rusta create
# Boot it (graphics window by default; pass --no-gui for headless)
rusta up
# SSH in
rusta ssh
# Find its IP
rusta ip
# Shut it down gracefully
rusta down
The first time you run an argument-less command with more than one VM
present, rusta interactively prompts you to pick a default and persists
the choice to ~/.local/share/rusta/state.toml.
At a glance
| Subcommand | Purpose |
|---|---|
rusta up [<vm>] | Start a VM (graphics window by default; --no-gui for headless). |
rusta down [<vm>] | Gracefully shut down a VM (--force to hard-stop). |
rusta create [<vm>] | Create and provision a new Ubuntu VM. |
rusta delete <vm> | Delete a VM (requires confirmation or --yes). |
rusta list | List Tart VMs and indicate the current default. |
rusta versions | List available Ubuntu OCI tags from ghcr.io/cirruslabs/ubuntu. |
rusta default [<vm>] | Print or set the default VM. |
rusta ip [<vm>] | Print the guest IP of the VM. |
rusta ssh [<vm>] [-- cmd...] | Open an SSH session or run a command on the VM. |
rusta docker-setup [<vm>] | Install Docker in the VM and wire host SSH/Docker context. |
rusta ssh-copy [<vm>] | Copy host ~/.ssh/id_* and *.pem into the VM. |
rusta set-gui <vm> <on|off> | Set whether rusta up <vm> defaults to graphical (on) or headless (off). |
Global flags accepted by every subcommand:
--verbose— verbose logging.--log <file>— tee all stdout/stderr to the given file.--help,-h— print help and exit.
For full behavior, flags, and exit codes, see the Commands reference.
State
rusta keeps a small amount of host-side state under
~/.local/share/rusta/:
state.toml— the default VM name.run/<vm>.pid— PID of the headlesstart runprocess.provision/<vm>.sh— the generated provisioning script (kept for debugging).
Commands
rusta is invoked as:
rusta <command> [args...] [flags...]
Global flags
Accepted by every subcommand:
| Flag | Default | Description |
|---|---|---|
--verbose | off | Verbose logging (equivalent to set -x + LogLevel=INFO on SSH). |
--log <file> | — | Tee all stdout/stderr to the given file. |
--help, -h | — | Print help for the current (sub)command and exit 0. |
rusta (no args) and rusta --help print top-level help and exit 0.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success (including --help, versions, no-op when target already in desired state). |
| 1 | Bad usage, unmet prerequisite, validation failure, or runtime error. |
| 2 | VM not found (when an explicit name was given or default resolution failed). |
rusta up
rusta up [<vm>] [--graphical|-G|--graphics|--gui] [--no-gui|--no-graphics]
Boot a VM.
- Resolves
<vm>from the default VM if omitted. - If the VM is already running, prints
[skip]and exits 0. - Boots with a graphics window by default. VMs explicitly created without
--guiboot headless (tart run <vm> --no-graphics); VMs created with--guior VMs with no recorded preference boot graphical. --graphical(aliases:-G,--graphics,--gui) forces a graphics window for this invocation.--no-gui(alias:--no-graphics) forces headless boot, even for GUI-enabled VMs.--graphicaland--no-guiare mutually exclusive.- Backgrounds the run and writes the PID to
~/.local/share/rusta/run/<vm>.pid. - Waits for the Tart guest agent and prints the guest IP once available (best-effort).
rusta down
rusta down [<vm>] [--force|-f] [--timeout <secs>]
Stop a VM.
- Resolves
<vm>if omitted; prints[skip]if already stopped. - Graceful (default): issues
sudo shutdown -h nowviatart execand waits up to--timeoutseconds (default 60s) for thetart runprocess to exit. If the timeout expires, exits 1 with a hint to retry with--force. --force: skip the graceful path; calltart stop <vm>(or kill the recorded PID).- Removes the stale
~/.local/share/rusta/run/<vm>.pidon success.
rusta create
rusta create [<vm>] [flags]
Clone + provision a new Ubuntu VM.
| Flag | Default | Description |
|---|---|---|
--version <ver> | 24.04 | Ubuntu release line (OCI tag), resolved across configured sources. |
--image <name> | first image | Image family (repo) to clone, e.g. ubuntu-desktop; defaults to the first configured image. Composes with --source. |
--gui [pkg] | off / ubuntu-desktop | Install a desktop (ubuntu-desktop, xubuntu-desktop, lubuntu-desktop, lightdm). |
--cpus <n> | 6 | CPU count. |
--memory <mb> | 8192 | Memory in MB. |
--disk <gb> | 80 | Disk size in GB. |
--user <username> | admin | Guest login username (image-dependent). |
--password <password> | admin | Guest login password used by sshpass. |
--ssh-copy-keys | off | After provisioning, copy host SSH keys into the guest. |
--debug-no-headless | off | Run with a graphics window during provisioning (debug only). |
--source <registry> | (all sources) | Pin resolution to one configured source (registry prefix or label). |
--image-ref <ref> | (unset) | Clone this exact image reference verbatim, bypassing sources and images. Conflicts with --source and --image. |
rusta create never assumes a name: if <vm> is omitted and stdin is a
TTY it prompts (suggesting <image>-<version>, e.g. ubuntu-2404 or
ubuntu-desktop-2404); a non-TTY stdin exits 1. The chosen name is not
written to state.default_vm.
If the VM name already exists, creation is skipped and a recreate hint is
printed. Otherwise, rusta resolves which image to clone (the selected
image across configured sources), clones it,
sets CPU/memory/disk,
generates a provisioning script under
~/.local/share/rusta/provision/<vm>.sh, boots, runs the script, and
shuts the VM down.
rusta delete
rusta delete <vm> [--yes|-y] [--force-running]
Delete a VM from Tart’s storage.
- Requires an explicit
<vm>(no default-VM fallback). - Refuses to run if the VM is currently running unless
--force-runningis given (stop + delete in one shot). - Prompts for confirmation unless
--yesis given. - Clears
state.default_vmif it pointed at this VM.
rusta list
rusta list
Print a table of all Tart VMs with their status and a * next to the
resolved default. Exits 0 even if there are no VMs.
NAME STATUS DEFAULT
ubuntu-2404 running *
lab-22 stopped
rusta versions
rusta versions [--source <registry>] [--image <name>]
List the available OCI tags across the configured sources × images matrix
(see rusta source and rusta image). For each
(source, image) cell, rusta fetches a pull token and lists tags from
<host>/v2/<namespace>/<image>/tags/list, keeps X.Y tags, then merges and
sorts them, highlighting 24.04 as (default).
- With a single source and a single image the list is unannotated (legacy format).
- With multiple sources but one image, each tag shows which source(s) provide it;
on a conflict, the one
createwould pick (first by priority) is noted ((create uses <label>)). - With multiple images, each tag line groups providers by image:
<image>: <src>, <src> (create uses <src>). --source <registry>limits output to one source;--image <name>limits it to one image. They compose.- A source whose host is unreachable is skipped with a warning; a source that
simply lacks an image is a silent empty cell.
versionsonly fails (exit 1) when no cell produces any result.
$ rusta versions
22.04 ubuntu: cirruslabs, pallewela (create uses cirruslabs)
24.04 (default) ubuntu: cirruslabs ubuntu-desktop: pallewela
25.04 ubuntu-desktop: pallewela
rusta source
rusta source # list (default action)
rusta source add <registry> # e.g. ghcr.io/pallewela
rusta source rm <registry>
rusta source move <registry> <position> # 1-based priority
Manage the image sources that create and versions consider. A source is a
registry host + namespace prefix (e.g. ghcr.io/cirruslabs); rusta appends the
selected image (ubuntu by default) to form the repository.
Sources are an ordered list — position is priority, and the first source
advertising a requested version wins.
- When no sources are configured, rusta uses a built-in default of
ghcr.io/cirruslabs, preserving the original behavior. Adding your first source materializes that default ahead of it, socreatestill finds the stock images. addvalidates the prefix (a trailing/ubuntuis stripped; a tag is rejected). Onlyghcr.iois supported for now; other registries are rejected (you can still clone any image once-off withrusta create --image-ref <ref>).rmof the last remaining source re-seeds the default — rusta is never sourceless.rm/moveof an unknown source exit 2.
$ rusta source add ghcr.io/pallewela
[ok] Added source: ghcr.io/pallewela
$ rusta source list
==> Image sources (priority order):
1. ghcr.io/cirruslabs (cirruslabs)
2. ghcr.io/pallewela (pallewela)
rusta image
rusta image # list (default action)
rusta image add <name> # e.g. ubuntu-desktop
rusta image rm <name>
rusta image move <name> <position> # 1-based priority
Manage the image names (repositories) that rusta clones under each source. An
image is a single repository segment (e.g. ubuntu, ubuntu-desktop); the
namespace comes from the source. Images are a global, ordered list — position is
priority, and the first image is the create default.
- When no images are configured, rusta uses built-in defaults of
ubuntuandubuntu-desktop(withubuntuas the create default). Adding your first image materializes those defaults ahead of it. - The list is global (applied to every source).
rusta create --image <name>picks one for a run;versionsenumerates the full sources × images matrix. addvalidates the name (a single segment: no/, no:tag, lowercase). A source that doesn’t host an image is just skipped — not an error.rmof the last remaining image re-seeds the defaults — rusta is never imageless.rm/moveof an unknown image exit 2.
$ rusta image list
==> Images (priority order; first is the create default):
1. ubuntu (default)
2. ubuntu-desktop
$ rusta image add ubuntu-kairos
[ok] Added image: ubuntu-kairos
rusta default
rusta default [<vm>]
- No arg: print the resolved default VM, or
no default setand exit 1 (never prompts). - With arg: set
state.default_vm = <vm>. Exits 2 if<vm>does not exist.
rusta ip
rusta ip [<vm>]
Print tart ip <vm> (waits up to 60s). Exits 1 if no IP is obtained.
rusta ssh
rusta ssh [<vm>] [--auto-up] [-- cmd args...]
- Resolves
<vm>if omitted. - If the VM is not running, exits 1 with a hint to
rusta up. Pass--auto-upto boot first. - Connects via
sshpass -p <password> ssh <user>@<ip>with the standard rusta SSH options. - Anything after
--is executed as a remote command; otherwise an interactive shell is opened.
rusta docker-setup
rusta docker-setup [<vm>]
Install Docker Engine inside an existing VM and wire host-side
docker context + ~/.ssh/config alias.
- Resolves
<vm>if omitted; boots the VM if not running and shuts it back down at the end (only whenrustastarted it itself). - Ensures host has
sshpassanddocker(auto-installed via Homebrew). - Generates
~/.ssh/id_ed25519(empty passphrase) if missing andssh-copy-ids the public key into the guest. - Inside the guest, installs Docker via
curl -fsSL https://get.docker.com | sudo shonly ifdockeris absent. Adds$USERto thedockergroup and runssystemctl enable --now docker. - On host: idempotently appends a
Host docker-<vm>block to~/.ssh/configpinned to the observed IP and idempotently creates a Docker contextdocker-<vm>pointing atssh://<user>@docker-<vm>.
rusta ssh-copy
rusta ssh-copy [<vm>]
Copy host ~/.ssh/id_* and ~/.ssh/*.pem files into the guest’s
~/.ssh/.
- Resolves
<vm>if omitted; boots the VM if not running and shuts it back down at the end (only whenrustastarted it itself). - Exits 1 if the host has no
~/.ssh. - If no matching files exist, prints
[skip]and exits 0. - In the guest:
mkdir -p ~/.ssh && chmod 700 ~/.ssh,scps the files, normalizes permissions (*.pub→ 644, others → 600).
rusta set-gui
rusta set-gui <vm> <on|off>
Update the per-VM gui preference stored in state.toml for an existing
VM, so that rusta up <vm> defaults to the chosen boot mode on future
invocations.
on—rusta up <vm>boots with a graphics window by default.off—rusta up <vm>boots headlessly by default.- Requires an explicit
<vm>; exits 2 if the VM is not known to Tart. - Equivalent to the value
rusta createwrites based on whether--guiwas passed. Useful for VMs created before per-VMguitracking existed, or to flip an existing VM’s default without recreating it. - Per-invocation
--gui/--no-guionrusta upstill override the stored preference.
Security Policy
Supported Versions
Only the latest release of rusta is supported with security updates.
Older versions remain available on the
releases page but will not
receive patches.
| Version | Supported |
|---|---|
| Latest | :white_check_mark: |
| Older | :x: |
Reporting a Vulnerability
Do not file a public GitHub issue for security vulnerabilities.
Please use GitHub’s private vulnerability reporting:
- Open https://github.com/pallewela/rusta/security/advisories/new
- Describe the issue with as much detail as possible:
- affected version(s) (
rusta --version) - reproduction steps
- impact and any proposed mitigation
- affected version(s) (
You should receive an acknowledgement within 7 days. We aim to ship a fix or a status update within 30 days of confirmation.
Scope
In scope:
- The
rustabinary and any code in this repository. - The release artifacts published to GitHub Releases and crates.io
(
rusta-cli). - The Homebrew formula at
pallewela/homebrew-tap.
Out of scope:
- Vulnerabilities in Tart itself — report those to the Tart project.
- Vulnerabilities in the guest Ubuntu images we clone — report those to Canonical.
- Vulnerabilities in third-party crates we depend on — report those to the crate maintainers and the RustSec Advisory Database.
Disclosure
Once a fix is available we will:
- Publish a patch release.
- File a GitHub Security Advisory.
- Credit the reporter (unless anonymity is requested).
Changelog
All notable changes to this project are documented here. The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
[1.3.1] - 2026-06-27
Dependencies
- Bump taiki-e/install-action from 2.82.0 to 2.82.5 (#57) (32f7566)
[1.3.0] - 2026-06-22
Documentation
- Release v1.3.0 [skip release] (9774e58)
Features
- Configurable image sources and image names [bump:minor] (#56) (edd1d38)
[1.2.7] - 2026-06-20
Dependencies
- Bump actions/checkout from 6.0.3 to 7.0.0 (#55) (4608d34)
- Bump softprops/action-gh-release from 3.0.0 to 3.0.1 (#54) (3e88c85)
- Bump taiki-e/install-action from 2.81.10 to 2.82.0 (#53) (78d142f)
Documentation
- Release v1.2.7 [skip release] (3a4a719)
[1.2.6] - 2026-06-13
Dependencies
- Bump taiki-e/install-action from 2.81.6 to 2.81.10 (#51) (d3836eb)
- Bump codecov/codecov-action from 6.0.1 to 7.0.0 (#52) (adfa96d)
Documentation
- Release v1.2.6 [skip release] (daaf386)
[1.2.5] - 2026-06-06
Dependencies
- Bump actions/checkout from 6.0.2 to 6.0.3 (#50) (9db512c)
- Bump github/codeql-action from 4.36.0 to 4.36.2 (#49) (65da8a8)
- Bump taiki-e/install-action from 2.79.14 to 2.81.6 (#48) (301e156)
Documentation
- Release v1.2.5 [skip release] (f4c4ab4)
[1.2.4] - 2026-05-30
Dependencies
- Bump taiki-e/install-action from 2.79.5 to 2.79.14 (#47) (13a4008)
Documentation
- Release v1.2.4 [skip release] (af0d7ce)
[1.2.3] - 2026-05-23
Dependencies
- Bump actions/download-artifact from 4.3.0 to 8.0.1 (#46) (7bfc693)
- Bump taiki-e/install-action from 2.78.2 to 2.79.5 (#43) (f49c615)
- Bump github/codeql-action from 3.35.5 to 4.36.0 (#45) (7dae6c6)
- Bump actions/upload-artifact from 4.6.2 to 7.0.1 (#44) (e4853b7)
- Bump clap_mangen from 0.2.33 to 0.3.0 (#42) (c16a07e)
- Bump serde_json in the cargo-minor-and-patch group (#41) (b026145)
- Bump codecov/codecov-action from 5.5.4 to 6.0.1 (#40) (57799dd)
Documentation
- Release v1.2.3 [skip release] (eeeaf8c)
[1.2.2] - 2026-05-22
Bug Fixes
- Drop CHECK_INTERVAL to 1h so same-day releases surface (#39) (b36a058)
Documentation
- Release v1.2.2 [skip release] (54a8c53)
[1.2.1] - 2026-05-22
Bug Fixes
- Raise JOIN_TIMEOUT to 1s so real fetches complete (#38) (35ef17c)
Documentation
- Release v1.2.1 [skip release] (9b49966)
[1.2.0] - 2026-05-22
Documentation
- Release v1.2.0 [skip release] (3139742)
Features
- Color the update-availability notice [bump:minor] (#35) (a8efb61)
[1.1.0] - 2026-05-22
Documentation
- Release v1.1.0 [skip release] (55e7b41)
Features
- Notify users of new releases [bump:minor] (#33) (20bb1d5)
[1.0.30] - 2026-05-22
Documentation
[1.0.29] - 2026-05-22
Documentation
- Release v1.0.29 [skip release] (47963ad)
Features
- Add
rusta set-gui <vm> <on|off>subcommand (#30) (cdddc74)
[1.0.28] - 2026-05-22
Bug Fixes
- Default to graphical boot for VMs without recorded preference (#28) (2bb6568)
Documentation
- Release v1.0.28 [skip release] (1f601e4)
[1.0.27] - 2026-05-17
Documentation
- Release v1.0.27 [skip release] (72716dd)
Features
- Add shell completions and man pages (#26) (94f85b2)
[1.0.26] - 2026-05-17
Documentation
[1.0.25] - 2026-05-16
Documentation
[1.0.24] - 2026-05-16
Documentation
[1.0.23] - 2026-05-16
CI
- Verify Homebrew tap formula after dispatch (#22) (60e89e9)
Documentation
- Release v1.0.23 [skip release] (d823060)
[1.0.22] - 2026-05-16
Bug Fixes
- Target x86_64-unknown-linux-gnu explicitly (#21) (2f2c6a7)
Documentation
- Release v1.0.22 [skip release] (ffa37ba)
[1.0.21] - 2026-05-16
Documentation
- Release v1.0.21 [skip release] (b27a7c5)
Features
- Add cargo-fuzz harness for parse_list_json (#20) (45c5db7)
[1.0.20] - 2026-05-16
Documentation
- Release v1.0.20 [skip release] (129b815)
- Document SLSA Pinned-Dependencies exception (#19) (13a9052)
[1.0.19] - 2026-05-16
Bug Fixes
- Use github.sha for auto-tag checkout (#18) (568a0d3)
Documentation
- Release v1.0.19 [skip release] (66d3383)
[1.0.18] - 2026-05-16
Documentation
[1.0.17] - 2026-05-16
Documentation
- Release v1.0.17 [skip release] (9fb967a)
- Add explicit Requirements for acceptable contributions (#16) (647e6ab)
[1.0.16] - 2026-05-16
Documentation
- Release v1.0.16 [skip release] (7d6cf54)
- Add CONTRIBUTING and SUPPORT, link from README and mdBook (#15) (cfab863)
[1.0.15] - 2026-05-16
CI
- Generate provenance before release to satisfy immutability (#14) (19128e2)
Documentation
- Release v1.0.15 [skip release] (ade0f1c)
[1.0.14] - 2026-05-16
CI
- Support manual workflow_dispatch for recovery (#13) (454fa72)
- Use RELEASE_PAT and add CI workflow_dispatch (#12) (f125621)
- Use RELEASE_PAT for CHANGELOG API write (#11) (a00b2b0)
- Grant contents: write so auto-merge can be enabled (#10) (8f03115)
- Enable auto-merge on trusted-author PRs (#9) (d2f95f1)
- Pass –repo to gh pr review (#8) (cf4234e)
- Add auto-approve workflow for solo-maintainer PR flow (#6) (e5c1290)
Documentation
- Release v1.0.14 [skip release] (8ef0d59)
- Add mdBook documentation site with Pages deploy (#7) (2f2d391)
[1.0.13] - 2026-05-16
CI
- Generate SLSA Level 3 provenance for releases (#5) (9ffe832)
Documentation
- Release v1.0.13 [skip release] (83e1cd0)
[1.0.12] - 2026-05-16
CI
- Harden workflows for OSSF Scorecard (c2c08e4)
Documentation
- Release v1.0.12 [skip release] (e1e6826)
[1.0.11] - 2026-05-16
CI
Documentation
- Release v1.0.11 [skip release] (b2157bb)
[1.0.10] - 2026-05-16
CI
- Add cargo-deny, CodeQL, and CycloneDX SBOM on release (2ac77a4)
Documentation
- Release v1.0.10 [skip release] (8a7eb97)
[1.0.9] - 2026-05-16
Documentation
- Release v1.0.9 [skip release] (6219773)
Features
- Publish to crates.io as rusta-cli on release (6f1996c)
[1.0.8] - 2026-05-16
CI
- Collect coverage with cargo-llvm-cov and upload to Codecov (263db91)
Documentation
- Release v1.0.8 [skip release] (9271799)
[1.0.7] - 2026-05-16
Documentation
- Release v1.0.7 [skip release] (d94f0f4)
- Add CI, release, license, platform, and brew tap badges (0000d3a)
[1.0.6] - 2026-05-16
CI
- Commit CHANGELOG via GitHub API for signed-commits rule (47c6452)
- Bump git-cliff-action to v4 (a1e6770)
Dependencies
- Bump actions/checkout from 4 to 6 (#4) (fdf7788)
- Bump peter-evans/repository-dispatch from 3 to 4 (#1) (9d9cd59)
Documentation
[1.0.5] - 2026-05-16
Tests
- Gate fake-tart exec on VM running state (d04eeaf)
[1.0.4] - 2026-05-16
Dependencies
- Bump toml from 0.8.23 to 1.1.2+spec-1.1.0 (#2) (a048a08)
- Bump softprops/action-gh-release from 2 to 3 (#3) (6bb8410)
[1.0.3] - 2026-05-16
Dependencies
- Add Dependabot config for cargo and github-actions (4711f2c)
[1.0.2] - 2026-05-16
Bug Fixes
- Auto-size NAME and STATUS columns (dfc1520)
[1.0.1] - 2026-05-13
CI
- Scope bump scan to head commit, auth tag push with RELEASE_PAT (f0f46f9)
[1.0.0] - 2026-05-13
CI
- Auto-tag green main commits with patch bump by default (0a6d046)
Features
- Default boot mode follows create-time –gui choice (742280e)
[0.1.1] - 2026-05-13
CI
- Add tagged-release workflow and brew install docs (beaeb17)
- Add GitHub Actions workflow for build and test (a6dcd5b)
Chores
- Initial commit (4f4f341)
Features
- Require explicit VM name, prompt interactively when missing (1b98fa0)
Contributing to rusta
Thanks for your interest in rusta. This document covers how to set up
a development environment, the conventions the project follows, and how
to get a change merged.
For bug reports, feature requests, and other support questions, see SUPPORT.md. For security vulnerabilities, see SECURITY.md — please do not file them as public issues.
Quick start
git clone https://github.com/pallewela/rusta.git
cd rusta
cargo build
cargo test
The integration tests under tests/ exercise the CLI surface
end-to-end against stubbed tart/brew/ssh binaries, so they run on
any platform that supports a Rust toolchain — you do not need an actual
Tart install to develop.
How to submit a change
- Fork the repository on GitHub.
- Create a feature branch:
git checkout -b feat/short-description. - Make your changes. Run
cargo build,cargo test, andcargo fmtbefore committing. - Commit using Conventional Commits
— the auto-generated CHANGELOG keys off the commit prefix:
feat: ...— new featurefix: ...— bug fixdocs: ...— documentation onlytest: ...— test-only changeci: ...— CI/release pipeline changerefactor: ...— code change that neither fixes a bug nor adds a featurechore: ...— anything else
- Push the branch and open a pull request against
main.
What happens after you open a PR
- CI runs on macOS arm64 (build, integration tests with coverage, cargo-deny advisories/licenses).
- CodeQL analyzes the Rust source and the workflow YAMLs.
- For PRs by the maintainer or by Dependabot, the
auto-approveworkflow approves the PR once CI passes and enables auto-merge. Squash-merge happens as soon as every required gate is green. - For PRs by external contributors, the workflow does not
auto-approve. The maintainer will review by hand; expect a response
within ~7 days. Mention
@pallewelain the PR description if you need an earlier look.
After merge, the existing automation handles tagging, release notes, CHANGELOG update, GitHub Release with SBOM + SLSA provenance, crates.io publish, and the Homebrew tap bump.
Versioning & releases
- Versioning scheme: Semantic Versioning 2.0.0
(semver.org) —
MAJOR.MINOR.PATCH. - Release tags: every release is identified in version control by
an annotated git tag named
vMAJOR.MINOR.PATCH(e.g.v1.0.16). The tag set onmainis the source of truth for the list of releases the project has ever cut — list them withgit tag --list 'v*' --sort=-v:refname. - Publication channels for each tag:
- A GitHub Release
with the
aarch64-apple-darwintarball, a CycloneDX SBOM, and a Sigstore-signed SLSA provenance attestation. - A crates.io version of
rusta-cli. - A Homebrew tap formula bump.
- A GitHub Release
with the
- Tagging is fully automated. Maintainers do not create tags by
hand. The
Auto-tagworkflow runs after CI succeeds onmainand:- Inspects the head commit message to pick the SemVer bump level
—
[bump:major]/BREAKING CHANGE→ major;[bump:minor]→ minor; default → patch;[skip release]→ no tag. - Regenerates
CHANGELOG.mdviagit-cliff. - Commits the updated CHANGELOG and pushes a new annotated tag pointing at that commit.
- Inspects the head commit message to pick the SemVer bump level
—
- Tag immutability policy. Once pushed, release tags are
considered immutable: they are not deleted, moved, or rewritten.
The repository enforces this with a
protect-tagsruleset onrefs/tags/v*(deletionandnon_fast_forwardblocked). If a release contains a defect, the fix ships as a new patch release with an incremented tag, never as a rewritten old tag.
Requirements for acceptable contributions
This section uses RFC 2119 language. MUST items are hard requirements — CI enforces them and a PR cannot merge without satisfying every one. SHOULD items are expectations during review; deviating from one is fine if you have a reason.
MUST (enforced by CI)
- The change builds.
cargo build --all-targets --lockedsucceeds onmacos-latest(Apple Silicon). - All tests pass.
cargo llvm-cov --locked --lcovsucceeds; CI uploads the lcov to Codecov. - No new advisories or banned crates.
cargo deny --locked check(advisories, licenses, bans, sources) exits clean. New dependencies must use a license ondeny.toml’s allow-list — typically MIT / Apache-2.0 / BSD / ISC / Unicode / CDLA-Permissive. Copyleft licenses (GPL, AGPL, LGPL, MPL beyond what is already allowed) are not accepted. - CodeQL passes. No new high-severity findings in the Rust source or workflow YAML.
- Conventional commit prefix. The PR’s commits use
Conventional Commits —
feat:,fix:,docs:,test:,ci:,refactor:,chore:.[skip release]is reserved for the auto-tag CHANGELOG commit; do not use it manually. - Verified signed commits. The
mainbranch protection rule requires all incoming commits to be signed. Sign your commits with either a GPG or SSH key configured on your GitHub account, or rebase through the GitHub web UI which signs automatically. - License agreement. Contributions are accepted under the MIT License only. By opening a PR you confirm that your contribution is yours to license under MIT.
SHOULD (review expectations)
- Tests covering the change. New features SHOULD ship with at
least one integration test under
tests/exercising the new behaviour end-to-end. Bug fixes SHOULD ship with a regression test that fails onmainand passes with the fix. - No coverage regression. Codecov reports a delta on each PR. A small regression (a couple of percent on a non-critical path) is acceptable if justified in the PR description; large drops will be pushed back.
cargo fmtapplied.cargo fmt --checkshould produce no diff. CI does not currently fail on this but it is the de-facto project style.cargo clippyclean. Runcargo clippy --all-targets -- -W clippy::alland address findings in code you touched. Pre-existing warnings are not your problem.- Edition 2021. Stick to the
Cargo.tomleditionsetting. Edition bumps are a separate, coordinated change. - Minimal dependencies. Prefer the standard library or an existing transitive dependency over pulling a new crate. Adding a new direct dependency SHOULD be justified in the PR description.
- Documentation in step with code. User-facing behaviour changes
SHOULD update the relevant docs:
README.mdfor top-level Quick Start or command summary.docs/src/commands.md(and adjacent pages) for the mdBook site. TheCHANGELOG.mdis regenerated automatically from conventional commits; do not edit it by hand.
- Comments explain why, not what. Default to no comments. Add one only when a non-obvious invariant, constraint, or workaround warrants it. Identifiers should carry the meaning.
Coding conventions
- Rust edition 2021. The
Cargo.tomleditionfield is the source of truth. - Tests. Integration tests live under
tests/. Unit tests live next to the code under#[cfg(test)] mod tests {}. - Comments. See the SHOULD item above.
- Dependencies. See the MUST and SHOULD items above.
Project structure
src/ # CLI entry point and command implementations
src/commands/ # One module per subcommand (up, down, list, …)
src/tart.rs # Wrapper around the `tart` binary
src/state.rs # Persisted CLI state (default VM, etc.)
tests/ # Integration tests using a stubbed environment
.github/workflows/ # CI, release, security, and Scorecard workflows
docs/ # mdBook documentation source (also published as a site)
License
By contributing, you agree that your contributions will be licensed under the MIT License, the same license as the project.
Getting Support for rusta
This page tells you where to file what. Picking the right channel keeps things moving and avoids cross-talk.
Bug reports
If something does not work as documented, please open a bug report on the issue tracker:
→ https://github.com/pallewela/rusta/issues/new?labels=bug
Useful information to include:
rusta --version- macOS version (
sw_vers -productVersion) and chip (uname -m). - The exact command you ran.
- The output you got vs. the output you expected.
- Re-run with
--verboseand include the trailing output if relevant.
Enhancement requests
For new features or behaviour changes, open a feature request:
→ https://github.com/pallewela/rusta/issues/new?labels=enhancement
Explain the use case first (“I want to X because Y”), then the proposed
shape of the feature. Concrete examples (“rusta foo --bar should
print Z”) are easier to act on than abstract descriptions.
Questions and discussion
For “how do I…?” questions, also use the
issue tracker — open an
issue with the question label. If GitHub Discussions are enabled on
the repo, they are equally welcome there.
Security vulnerabilities
Do not file security issues as public GitHub issues. Use GitHub’s private vulnerability reporting flow as described in SECURITY.md.
Response expectations
rusta is maintained on a best-effort basis by a single person.
Realistic expectations:
| Item | Target |
|---|---|
| Acknowledgement of a bug report | 7 days |
| Acknowledgement of a feature request | 14 days |
| Security advisory acknowledgement | 7 days |
| Patch for a confirmed regression | 30 days |
There are no paid support tiers and no SLAs. Pull requests are the fastest way to get something fixed; see CONTRIBUTING.md.
Development
cargo build
cargo test
The integration tests under tests/ exercise the CLI surface end-to-end
against stubbed tart/brew/ssh binaries, so they run without an
actual Tart install.
Releasing
Maintainer notes for cutting a new release:
- Bump
versioninCargo.toml(e.g.0.2.0), commit aschore: release v0.2.0. git tag v0.2.0 && git push --tags.- The
Releaseworkflow (.github/workflows/release.yml) builds onmacos-latest, attachesrusta-v0.2.0-aarch64-apple-darwin.tar.gzto a GitHub Release, and (when theTAP_REPO_TOKENsecret is configured) dispatches arusta-releaseevent topallewela/homebrew-tapso the Formula picks up the new version and sha256 automatically.