Repository layout. This repo is a Cargo workspace.
crates/hmis the CLI binary.crates/hm-plugin-protocolandcrates/hm-plugin-sdkare the public API for writing third-party plugins.crates/hm-plugin-*are the bundled plugins (Docker executor, output formatters, cloud client).examples/contains sample pipeline repos you canhm run --localagainst.This repo is a mirror of the
cli/andexamples/directories of the private Harmont monorepo. Open issues and PRs against this repo; maintainers will land them upstream and a CI mirror sync replays the result back here.
Command-line client for the Harmont CI platform. Run CI pipelines on your own machine, in Docker, from a Python pipeline definition checked into your repo.
Pipelines are written with the companion harmont-py DSL.
harmont-cli is not yet published to crates.io. Install from source:
git clone https://github.com/harmont-dev/harmont-cli
cd harmont-cli
cargo build --release
install -m 0755 target/release/hm /usr/local/bin/hm # or any directory on $PATHVerify:
hm --versionhm run --local shells out to Docker and to Python:
- Docker — the local executor boots a fresh container per chain.
- Python 3.11+ — used to render the pipeline definition to JSON.
harmont-py— the Python package that defines the pipeline DSL. Not yet on PyPI; install from git:
git clone https://github.com/harmont-dev/harmont-py
pip install -e ./harmont-pyPipelines live in .harmont/<slug>.py inside your repo. Each file uses the @hm.pipeline("slug") decorator to register one or more named pipelines. Save the following as .harmont/hello.py:
import harmont as hm
@hm.pipeline("hello")
def hello() -> hm.Step:
return (
hm.sh("echo 'hello from harmont'", label="hello")
.sh("uname -a", label="env")
)The DSL is small:
hm.sh(cmd, label=...)— start a chain with one shell command (shorthand forhm.scratch().sh(...))..sh(cmd, label=..., cwd=...)— chain another command. Chained.shcalls share filesystem state inside the same container.cwd="path"prependscd <path> &&to the command..fork(label=...)— branch into parallel work from a shared base.hm.wait()— explicit synchronization barrier.@hm.target()— reusable, memoized building block; compose into pipelines via fixture-style typed params (Target[T],Annotated[Step, BaseImage("...")]).
A two-branch variant:
@hm.pipeline("ci")
def ci() -> hm.Step:
setup = hm.sh(
"apt-get update && apt-get install -y curl",
label="apt",
)
fetch = setup.fork(label="branch-a").sh(
"curl -fsSL https://example.com",
label="fetch",
)
work = setup.fork(label="branch-b").sh(
"echo independent work",
label="other",
)
return hm.pipeline(fetch, work, default_image="ubuntu:24.04")For larger pipelines, compose with @hm.target and typed fixture params:
from typing import Annotated
@hm.target()
def apt_base(base: Annotated[hm.Step, hm.BaseImage("ubuntu:24.04")]) -> hm.Step:
return base.sh("apt-get update && apt-get install -y curl", label="apt")
@hm.target()
def smoke(apt_base: hm.Target[hm.Step]) -> hm.Step:
return apt_base.sh("curl -fsSL https://example.com", label="smoke")
@hm.pipeline("ci")
def ci(smoke: hm.Target[hm.Step]) -> hm.Step:
return smokeFor the full DSL surface (cache policies, matrix axes, soft-fail, timeouts), see the upstream harmont-py repo.
From the repo root:
hm run hello --localThe CLI walks .harmont/*.py, resolves the hello slug, renders the pipeline to JSON, and schedules the chains across Docker containers. Each chain inherits state from its parent; forks run in parallel up to --parallelism N (defaults to the host's available parallelism).
If the repo declares only one pipeline, the slug is optional:
hm run --localhm run --local --parallelism 4 # cap concurrent chains
hm run --local --env FOO=bar # inject env vars
hm run --local --dir path/to/source # run against a different source root
hm run --help # full flag referencehm cloud <verb> talks to the hosted Harmont API at api.harmont.dev.
Every cloud verb is delivered by the embedded hm-plugin-cloud WASM
plugin (no separate install step):
hm cloud login # browser-loopback OAuth (or --paste to
# paste a token directly)
hm cloud logout
hm cloud whoami # who am I + active org
hm cloud org list # orgs you belong to
hm cloud org use <slug> # set the active org (persisted)
hm cloud pipeline list
hm cloud build list # builds for the active org
hm cloud build show <id>
hm cloud build watch <id> # poll until terminal
hm cloud job show <id>
hm cloud billing show
hm cloud run [--plan-file PATH] # submit a pre-rendered plan JSON
# (defaults to .harmont/plan.json)Tokens are stored in the OS keyring. The active org slug is persisted
per-user under ~/.config/harmont/state/cloud.kv. Source-archive
upload for cloud run is plan-5 work — pre-render your pipeline to
.harmont/plan.json first.
git clone https://github.com/harmont-dev/harmont-cli
cd harmont-cli
cargo build
cargo test # Docker-dependent tests in `local_*` need a running daemon
cargo clippy --all-targets -- -D warnings
cargo fmt --checkThe OpenAPI client is generated at build time from the vendored openapi.json via progenitor. The snapshot ships with the crate.
harmont-py— the Python DSL used to define pipelines that this CLI runs.
Dual-licensed under either of
- Apache License, Version 2.0 (
LICENSE-APACHE) - MIT license (
LICENSE-MIT)
at your option.
hm is plugin-driven via Extism. To write a plugin:
cargo new --lib my-plugin
cd my-plugin
cargo add --git https://github.com/harmont-dev/harmont-cli hm-plugin-sdkImplement one of StepExecutor, SubcommandPlugin, LifecycleHook, or
OutputFormatter, declare a PluginManifest, and call
register_plugin!(...). Build with:
cargo build --target wasm32-wasip1 --releaseThe output .wasm can be installed with:
hm plugin install ./target/wasm32-wasip1/release/my_plugin.wasmSee crates/hm-fixtures/src/bin/ for minimal working examples.
Implement OutputFormatter::on_event to render each BuildEvent.
Plugins emit bytes via host::write_stdout or host::write_stderr.
Built-in formatters: human (default), json. Select with
hm run --format <name>.