Skip to main content

Python DSL for Harmont CI pipelines — emits v0 IR JSON

Project description

harmont-py

license

Python DSL for defining Harmont CI pipelines.

Pipelines are chains of shell commands, branched with .fork(), synchronized with hm.wait(), registered with a decorator, and rendered to a JSON IR. The companion harmont-cli consumes that IR and runs the pipeline locally in Docker or on the hosted Harmont cloud.

The package installs as harmont and you import it as harmont:

import harmont as hm

Quick start

1. Write a pipeline

A pipeline file lives at .harmont/<slug>.py in your repo:

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")
    )

2. Install

Not yet on PyPI. Install from source (Python 3.11+):

git clone https://github.com/harmont-dev/harmont-py
cd harmont-py
pip install -e .

If you arrived here from the harmont-cli Quick start, you already did this — skip to Step 3.

Development extras (pytest, mypy, ruff):

pip install -e '.[dev]'

3. Run

Use the Harmont CLI:

hm run hello

hm run walks .harmont/*.py, imports each file (triggering the decorators), renders the registered pipeline to JSON, and executes it (locally in Docker by default, or against the cloud via hm cloud run).

DSL surface

Primitive Returns What it does
hm.sh(cmd, cwd=..., label=...) Step Start a chain in one call (= hm.scratch().sh(cmd, ...))
hm.scratch() Step Empty root; chain with .sh(...) for an explicit start
Step.sh(cmd, cwd=..., ...) Step Run a shell command; chained .sh shares container state
Step.fork(label=...) Step Branch a shared base into parallel work
hm.wait() Step Explicit synchronization barrier
@hm.target() decorator Reusable, memoized building block
@hm.pipeline("slug") decorator Register a pipeline (multiple per file are fine)
hm.pipeline(*leaves, env=..., default_image=...) dict Factory form — build the v0 IR dict directly (used in tests)

Cache policies (hm.ttl, hm.on_change, hm.forever, hm.compose), triggers (hm.push, hm.pull_request, hm.schedule), and matrix axes are documented in the module docstrings; start at harmont/__init__.py.

Language toolchains

harmont ships first-class wrappers for the common toolchains. Each exposes the actions that make sense for that ecosystem (e.g. .build(), .test(), .clippy(), .fmt() for Rust; .test(), .lint(), .fmt(), .typecheck() for Python):

Call Project type
hm.rust(path=..., version="stable") cargo + clippy + rustfmt
hm.haskell(ghc="9.6.7", cabal="latest") cabal (call .cabal(path) to build a package)
hm.python(path=..., uv_version="latest") uv-based Python project
hm.go(path=..., version="1.23.2") go build/test/vet/fmt
hm.npm(path=..., version="20") npm + arbitrary scripts
hm.gradle(path=..., jdk="21", kotlin=False) Java or Kotlin via Gradle
hm.cmake(path=..., lang="c"|"cpp") C/C++ via CMake + CTest
hm.dotnet(path=..., channel="8.0") .NET via dotnet CLI
hm.ruby(path=..., version="default") Bundler + Rake
hm.ocaml(path=..., compiler="5.1.1") opam + Dune
hm.zig(path=..., version="0.13.0") zig build/test/fmt
hm.perl(path=...) cpanm + prove
hm.composer(path=..., laravel=False) PHP / Laravel via Composer
hm.elm(path=..., elm_version="0.19.1") Elm

Working examples for each toolchain live in harmont-cli/examples/.

Composing with targets

For larger pipelines, factor toolchain setup into @hm.target() and let pipelines depend on them by parameter name. Target[T] and Annotated[Step, BaseImage("...")] are typed markers that unwrap cleanly under mypy and pyright.

from typing import Annotated

import harmont as hm
from harmont.haskell import HaskellPackage, HaskellToolchain


@hm.target()
def apt_base(base: Annotated[hm.Step, hm.BaseImage("ubuntu-24.04")]) -> hm.Step:
    return base.sh("apt-get update").sh("apt-get install -y python3")


@hm.target()
def ghc() -> HaskellToolchain:
    return hm.haskell(ghc="9.6.7")


@hm.target()
def api(ghc: hm.Target[HaskellToolchain]) -> HaskellPackage:
    return ghc.cabal(path="api")


@hm.pipeline("ci")
def ci(
    apt_base: hm.Target[hm.Step],
    api: hm.Target[HaskellPackage],
) -> tuple[hm.Step, ...]:
    return (apt_base.sh("./run-smoke"), api)

Every fixture parameter must carry a marker or default value; unmarked parameters raise at decoration time. Memoization scope is one dump_registry_json render, so two targets that depend on the same apt_base share a single step.

How rendering works

hm.sh(...).sh(...) builds a chain of frozen Step dataclasses. Each .sh() returns a new Step carrying the parent reference. The hm.pipeline() factory walks back from each leaf, topo-sorts, and emits a version: "0" IR dict matching the schema in harmont-pipeline (Haskell side).

When used as a decorator, @hm.pipeline("slug") registers the wrapped function with a module-level registry. hm.dump_registry_json() walks every .harmont/*.py, imports each (which triggers the decorators), and returns the full envelope.

A chain edge — parent.sh(cmd, ...) — emits builds_in: "<parent key>" in the v0 IR JSON. The edge encodes synchronisation and state inheritance: the local executor reuses the parent's container; the cloud planner boots from its snapshot. A step rooted at scratch() has builds_in: null and boots from image="..." (or the pipeline's default_image) locally; the cloud planner ignores image (it always boots from the Freestyle base).

The JSON wire format and cache-key algorithm are stable; see module docstrings under harmont/ for the contract.

Build & test

python3 -m venv .venv && source .venv/bin/activate
pip install -e '.[dev]'

pytest                                  # all tests
pytest -v --tb=short
mypy --strict harmont
ruff check .

pytest is configured to treat warnings as errors (filterwarnings = ["error"]).

See also

  • harmont-cli — the CLI that runs pipelines defined with this package (hm run).

License

MIT. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

harmont-0.0.1.tar.gz (63.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

harmont-0.0.1-py3-none-any.whl (52.3 kB view details)

Uploaded Python 3

File details

Details for the file harmont-0.0.1.tar.gz.

File metadata

  • Download URL: harmont-0.0.1.tar.gz
  • Upload date:
  • Size: 63.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for harmont-0.0.1.tar.gz
Algorithm Hash digest
SHA256 0dab3e1b1548e639839d850f64423df11db809f9a0f557f9cf55e75f2939cf73
MD5 0dac306b39cbc092cae632027def5d8f
BLAKE2b-256 e8dce0f2e7902b5ca0c0a487205454518064585176bfba0d9c46583095ea4d1e

See more details on using hashes here.

Provenance

The following attestation bundles were made for harmont-0.0.1.tar.gz:

Publisher: release.yml on harmont-dev/harmont-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file harmont-0.0.1-py3-none-any.whl.

File metadata

  • Download URL: harmont-0.0.1-py3-none-any.whl
  • Upload date:
  • Size: 52.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for harmont-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 6a2dc72e8d4955051d02d8b81606775ddb7eae3383587b902d4a4ac2d60c4b97
MD5 ebb53d08347047f2571580b436f16d38
BLAKE2b-256 9932d5b3a43025c503f5c01ba6fe266160fe6feb899325a4e5d749cfc9d90229

See more details on using hashes here.

Provenance

The following attestation bundles were made for harmont-0.0.1-py3-none-any.whl:

Publisher: release.yml on harmont-dev/harmont-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page