diff --git a/README.md b/README.md index 44dd9ef..c15196b 100644 --- a/README.md +++ b/README.md @@ -1,258 +1,165 @@ -# harmont-cli - -[![license](https://img.shields.io/crates/l/harmont-cli.svg)](#license) - -Run CI pipelines on your own machine, in Docker, from a Python pipeline definition checked into your repo. - -Define the pipeline with the [`harmont-py`](https://github.com/harmont-dev/harmont-py) DSL, then `hm run` builds a fresh container per chain, runs the steps, and reuses snapshots across runs. The same definition runs unchanged on the hosted [Harmont](https://harmont.dev) cloud via `hm cloud run`. - -## Quick start - -### 1. Write a pipeline - -Pipelines live in `.harmont/.py`. Save this as `.harmont/hello.py`: +

+ harmont +

+ +

+ Pipelines as code. Run locally in Docker. Ship to cloud when ready. +

+ +

+ CI + crates.io + Discord + Slack + License +

+ +

+ Website · Docs · Discord · Slack +

+ +> [!WARNING] +> Harmont is in **early alpha**. Today it's a powerful local task runner — +> think `make` or `just`, but with Python-defined pipelines and automatic +> Docker isolation. The cloud CI/CD platform at +> [harmont.dev](https://harmont.dev) is under active development. APIs will +> change. We'd love your feedback — [join the +> Discord](https://discord.gg/hm-dev). + +## What is Harmont? + +Harmont lets you define CI/CD pipelines in Typescript/Python and run them +instantly on your machine in Docker containers. No YAML. No waiting for CI. No +`commit -m "run ci" --allow-empty` spam. Each pipeline step runs in an isolated +container with automatic caching, parallel execution, and reproducible builds. ```python import harmont as hm +from harmont.python import PythonToolchain - -@hm.pipeline("hello") -def hello() -> hm.Step: +@hm.target() +def project() -> PythonToolchain: + return hm.python(path=".") + +@hm.pipeline( + "ci", + default_image="ubuntu:24.04", + triggers=[hm.push(branch="main")], +) +def ci(project: hm.Target[PythonToolchain]) -> tuple[hm.Step, ...]: return ( - hm.sh("echo 'hello from harmont'", label="hello") - .sh("uname -a", label="env") + project.test(), + project.lint(), + project.fmt(), + project.typecheck(), ) ``` -### 2. Install - -Install the CLI from [crates.io](https://crates.io/crates/harmont-cli): - -```sh -cargo install harmont-cli -``` - -`hm run` also needs **Docker** and **Python 3.11+** with [`harmont`](https://pypi.org/project/harmont/): - -```sh -pip install harmont -``` - -
-Or build from source (contributors) - -```sh -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 dir on $PATH - -git clone https://github.com/harmont-dev/harmont-py -pip install -e ./harmont-py -``` - -
- -Verify: - -```sh -hm --version -``` - -### 3. Run - -From the repo root: - ```sh -hm run hello -``` - -The CLI walks `.harmont/*.py`, resolves the `hello` slug, renders the pipeline to JSON, and schedules chains across Docker containers. Forks run in parallel up to `--parallelism N` (default: host CPU count). - -If the repo declares only one pipeline, the slug is optional: - -```sh -hm run +curl -fsSL https://get.harmont.dev/install.sh | sh +hm run ci ``` -## DSL surface - -The DSL is small. See [`harmont-py`](https://github.com/harmont-dev/harmont-py) for the full reference. + -| Primitive | What it does | -|---|---| -| `hm.sh(cmd, label=...)` | Start a chain with one shell command | -| `.sh(cmd, label=..., cwd=...)` | Chain another command; shares container state with the parent | -| `.fork(label=...)` | Branch a shared base into parallel work | -| `hm.wait()` | Explicit synchronization barrier | -| `@hm.target()` | Reusable, memoized building block | -| `@hm.pipeline("slug")` | Register a pipeline (multiple per file are fine) | +## Quick Start -## Common flags +### 0. Download `hm` ```sh -hm run --parallelism 4 # cap concurrent chains -hm run --env FOO=bar # inject env vars -hm run --dir path/to/source # run against a different source root -hm run --format json # machine-readable event stream -hm run --no-watch # create the build and exit (don't stream events) -hm run --help # full flag reference +curl -fsSL https://get.harmont.dev/install.sh | sh ``` -## Cloud - -`hm cloud ` talks to `api.harmont.dev`. Credentials are stored file-backed at `~/.harmont/credentials.toml` (mode `0600`); the active org slug persists under `~/.config/harmont/state/harmont-cloud.kv`. - -| Command | What it does | -|---|---| -| `hm cloud login` | Browser-loopback OAuth (`--paste` to paste a token) | -| `hm cloud logout` | Forget stored credentials | -| `hm cloud whoami` | Show user + active org | -| `hm cloud org switch ` | Set the active organization | -| `hm cloud pipeline list` / `pipeline show ` | List or inspect pipelines | -| `hm cloud build list -p ` | List builds for a pipeline | -| `hm cloud build show -p ` / `watch -p ` / `cancel -p ` | Inspect or control a build | -| `hm cloud job list -p -b ` / `job show -p -b ` | Inspect jobs in a build | -| `hm cloud billing ` | Credit balance and usage (see below for verbs) | -| `hm cloud run [--plan-file PATH]` | Submit a pre-rendered plan JSON (defaults to `.harmont/plan.json`) | - -Source-archive upload for `cloud run` is in progress — pre-render to `.harmont/plan.json` for now. +### 1. Create a pipeline -
-Billing verbs - -| Command | What it does | -|---|---| -| `hm cloud billing balance` | Print the current credit balance | -| `hm cloud billing transactions [--limit N]` | List billing transactions (default 100) | -| `hm cloud billing usage` | Usage over a time window | -| `hm cloud billing topup` | Top up credits | -| `hm cloud billing redeem` | Redeem a promo code | - -
- -## Examples - -Eighteen idiomatic starter projects live under [`examples/`](./examples). Each has a `.harmont/pipeline.py` you can read, copy, and run: - -```sh -cd examples/rust -hm run ci -``` - -Toolchains covered: Rust, Haskell, Go, Python (uv), Java/Kotlin (Gradle), C and C++ (CMake), C# (dotnet), Ruby, Perl, PHP (Composer + Laravel), OCaml, Zig, Zig+JS monorepo, and npm-based stacks (React, Next.js, TypeScript). - -
-A two-branch pipeline using forks and a shared base image +Save this as `.harmont/ci.py`: ```python import harmont as hm - @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.sh("echo 'hello from harmont'", label="hello") + .sh("uname -a", label="env") ) - return hm.pipeline(fetch, work, default_image="ubuntu:24.04") ``` -
- -
-Composing larger pipelines with typed fixture-style targets - -```python -from typing import Annotated - -import harmont as hm - - -@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 smoke -``` - -Dependencies are resolved by parameter name from the `@hm.target` registry. `Target[T]` and `Annotated[Step, BaseImage("...")]` both unwrap cleanly under mypy and pyright. - -
- -## Build from source - -```sh -git clone https://github.com/harmont-dev/harmont-cli -cd harmont-cli -cargo build -cargo test # `local_*` tests need a running Docker daemon -cargo clippy --all-targets -- -D warnings -cargo fmt --check -``` - -The OpenAPI client is generated at build time from the vendored `openapi.json` via [progenitor](https://github.com/oxidecomputer/progenitor). - -## Repository layout - -Cargo workspace: - -- `crates/hm/` — the `hm` binary. -- `crates/hm-plugin-protocol/`, `crates/hm-plugin-sdk/` — public API for third-party plugins. -- `crates/hm-plugin-*` — bundled plugins (Docker executor, output formatters, cloud client). -- `examples/` — sample pipeline repos to `hm run` against. - -This repo mirrors the `cli/` and `examples/` directories of the private Harmont monorepo. Open issues and PRs here; maintainers land them upstream and a CI sync replays the result back. - -## Plugin authoring - -`hm` is plugin-driven via [Extism](https://extism.org). To write one, start a `cdylib` crate and depend on the SDK: +### 2. Run it ```sh -cargo new --lib my-plugin -cd my-plugin -cargo add --git https://github.com/harmont-dev/harmont-cli hm-plugin-sdk +hm run ci ``` -Implement one of `StepExecutor`, `SubcommandPlugin`, `LifecycleHook`, or `OutputFormatter`, declare a `PluginManifest`, and call `register_plugin!(...)`. Then build to WebAssembly: +If the repo declares only one pipeline, the slug is optional — just `hm run`. -```sh -cargo build --target wasm32-wasip1 --release -``` - -Install the resulting `.wasm`: +Browse the [16 example projects](./examples) for idiomatic pipelines in Rust, +Go, Python, Java, C++, React, Next.js, and more. -```sh -hm plugin install ./target/wasm32-wasip1/release/my_plugin.wasm -``` +## Community -Built-in output formatters: `human` (default), `json`. Select with `hm run --format `. Working examples live in `crates/hm-fixtures/src/bin/`. +- **Discord** — [discord.gg/hm-dev](https://discord.gg/hm-dev) +- **Slack** — [harmont-dev.slack.com](https://join.slack.com/t/harmont-dev/shared_invite/zt-3yt0tiv7r-qHm1O0p0nVh2GU~KKhUk9A) +- **Website** — [harmont.dev](https://harmont.dev) -## See also +## Documentation -- [`harmont-py`](https://github.com/harmont-dev/harmont-py) — the Python DSL this CLI consumes. +For the full pipeline reference, rich examples, and more — see the +[docs](https://harmont.dev/docs). ## License -Dual-licensed under either of +The CLI is dual-licensed under either of - Apache License, Version 2.0 ([`LICENSE-APACHE`](LICENSE-APACHE)) - MIT license ([`LICENSE-MIT`](LICENSE-MIT)) -at your option. +## Motivation + +> +> The reason [I](https://github.com/markovejnovic) started this project is +> because every other CI/CD tool I've used in my life has sucked. +> +> I've worked at [Tesla](https://tesla.com), [Bun](https://bun.com), +> [Mesa](https://mesa.dev) and never did I find a CI/CD system that was easy to +> use and was also fast. +> +> At Tesla, we used [Jenkins](https://www.jenkins.io/) -- executors are finite, +> so your builds are stuck in queues. +> +> At Bun, we used [Buildkite](https://buildkite.com/) -- large shell pipelines, +> and really pricy service, and a TS SDK that's only slightly better than +> YAMLs. +> +> At Mesa, I migrated everyone to use [BuildBuddy](https://www.buildbuddy.io/) +> and Buildkite. [Bazel](https://bazel.build/) is awesome, but the mental +> overhead required to use it is way too high. We, sadly, ended up reverting +> to plain Buildkite. +> +> I asked myself a couple questions: +> +> - **Why can't I run my CI/CD pipelines locally?** +> [act](https://github.com/nektos/act) is an awesome project, but it's +> surprisingly slow (not to the author's fault -- but rather GHA's model). +> - **Why is my CI/CD system not just a `Makefile`?** Why is there no `hm run` +> command that is shared between local dev and CI/CD? +> - **Why can't I get preview environments for Haskell, Rust, Zig or +> whatever?** Vercel does an awesome job with `next.js` preview environments, +> but there is no good way to do this for arbitrary environments. +> - **Why do we have to write YAMLs for our pipelines?** All my pipelines end +> up being [YAML documents from +> hell](https://ruuda.nl/2023/the-yaml-document-from-hell). I think we can do +> better. +> - **Why do I need `artifacts-upload` and `artifacts-download` everywhere?** +> I don't need it locally, so why do I need it in CI/CD? In other words, why +> aren't our CI/CD systems stateful? If my build scripts can write an +> `openapi.json` in the local directory, why do I need some magic to transfer +> it between individual steps? + +Harmont's goal is to make all these questions obsolete. CI/CD _can_ be better, +and that's what [Harmont](https://harmont.dev) is -- a CI/CD that sucks a lot +less. + +I quit my job at Mesa to build Harmont. I'm on limited time on my H1B grace +period, but I vehemently feel that CI/CD can be better -- and that's why I'm +taking this risk.