harmont 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- harmont-0.0.1/LICENSE +21 -0
- harmont-0.0.1/PKG-INFO +186 -0
- harmont-0.0.1/README.md +168 -0
- harmont-0.0.1/harmont/__init__.py +168 -0
- harmont-0.0.1/harmont/_decorator.py +68 -0
- harmont-0.0.1/harmont/_deps.py +188 -0
- harmont-0.0.1/harmont/_envelope.py +100 -0
- harmont-0.0.1/harmont/_keys.py +121 -0
- harmont-0.0.1/harmont/_registry.py +44 -0
- harmont-0.0.1/harmont/_step.py +99 -0
- harmont-0.0.1/harmont/_target.py +104 -0
- harmont-0.0.1/harmont/_toolchain.py +79 -0
- harmont-0.0.1/harmont/_typing.py +97 -0
- harmont-0.0.1/harmont/_unwrap.py +56 -0
- harmont-0.0.1/harmont/_validation.py +11 -0
- harmont-0.0.1/harmont/cache.py +80 -0
- harmont-0.0.1/harmont/cmake.py +127 -0
- harmont-0.0.1/harmont/composer.py +109 -0
- harmont-0.0.1/harmont/dotnet.py +116 -0
- harmont-0.0.1/harmont/elm.py +143 -0
- harmont-0.0.1/harmont/go.py +117 -0
- harmont-0.0.1/harmont/gradle.py +137 -0
- harmont-0.0.1/harmont/haskell.py +257 -0
- harmont-0.0.1/harmont/json_emit.py +69 -0
- harmont-0.0.1/harmont/keygen.py +156 -0
- harmont-0.0.1/harmont/npm.py +118 -0
- harmont-0.0.1/harmont/ocaml.py +145 -0
- harmont-0.0.1/harmont/perl.py +86 -0
- harmont-0.0.1/harmont/pipeline.py +172 -0
- harmont-0.0.1/harmont/python.py +141 -0
- harmont-0.0.1/harmont/ruby.py +108 -0
- harmont-0.0.1/harmont/rust.py +139 -0
- harmont-0.0.1/harmont/triggers.py +135 -0
- harmont-0.0.1/harmont/types.py +12 -0
- harmont-0.0.1/harmont/zig.py +172 -0
- harmont-0.0.1/harmont.egg-info/PKG-INFO +186 -0
- harmont-0.0.1/harmont.egg-info/SOURCES.txt +82 -0
- harmont-0.0.1/harmont.egg-info/dependency_links.txt +1 -0
- harmont-0.0.1/harmont.egg-info/requires.txt +7 -0
- harmont-0.0.1/harmont.egg-info/top_level.txt +1 -0
- harmont-0.0.1/pyproject.toml +88 -0
- harmont-0.0.1/setup.cfg +4 -0
- harmont-0.0.1/tests/test_cache.py +73 -0
- harmont-0.0.1/tests/test_cmake.py +68 -0
- harmont-0.0.1/tests/test_composer.py +76 -0
- harmont-0.0.1/tests/test_decorator.py +103 -0
- harmont-0.0.1/tests/test_deps.py +80 -0
- harmont-0.0.1/tests/test_dotnet.py +78 -0
- harmont-0.0.1/tests/test_elm.py +133 -0
- harmont-0.0.1/tests/test_envelope.py +186 -0
- harmont-0.0.1/tests/test_examples_render.py +69 -0
- harmont-0.0.1/tests/test_go.py +91 -0
- harmont-0.0.1/tests/test_gradle.py +78 -0
- harmont-0.0.1/tests/test_har_28_example.py +84 -0
- harmont-0.0.1/tests/test_haskell.py +183 -0
- harmont-0.0.1/tests/test_haskell_cabal_alias.py +35 -0
- harmont-0.0.1/tests/test_json_emit.py +192 -0
- harmont-0.0.1/tests/test_keygen.py +318 -0
- harmont-0.0.1/tests/test_keys.py +97 -0
- harmont-0.0.1/tests/test_npm.py +117 -0
- harmont-0.0.1/tests/test_ocaml.py +69 -0
- harmont-0.0.1/tests/test_perl.py +62 -0
- harmont-0.0.1/tests/test_pipeline.py +36 -0
- harmont-0.0.1/tests/test_pipeline_fixtures.py +83 -0
- harmont-0.0.1/tests/test_pipeline_lowering.py +124 -0
- harmont-0.0.1/tests/test_python.py +133 -0
- harmont-0.0.1/tests/test_registry.py +81 -0
- harmont-0.0.1/tests/test_ruby.py +74 -0
- harmont-0.0.1/tests/test_rust.py +168 -0
- harmont-0.0.1/tests/test_sh_shorthand.py +44 -0
- harmont-0.0.1/tests/test_step_chain.py +90 -0
- harmont-0.0.1/tests/test_step_sh.py +86 -0
- harmont-0.0.1/tests/test_strict_signature.py +129 -0
- harmont-0.0.1/tests/test_target.py +109 -0
- harmont-0.0.1/tests/test_target_cross_module.py +73 -0
- harmont-0.0.1/tests/test_target_fixtures.py +154 -0
- harmont-0.0.1/tests/test_target_unwrap.py +79 -0
- harmont-0.0.1/tests/test_toolchain.py +108 -0
- harmont-0.0.1/tests/test_toolchain_compose.py +82 -0
- harmont-0.0.1/tests/test_triggers.py +76 -0
- harmont-0.0.1/tests/test_typing_markers.py +74 -0
- harmont-0.0.1/tests/test_validation.py +30 -0
- harmont-0.0.1/tests/test_zig.py +67 -0
- harmont-0.0.1/tests/test_zig_toolchain.py +82 -0
harmont-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marko Vejnovic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
harmont-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: harmont
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python DSL for Harmont CI pipelines — emits v0 IR JSON
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/harmont-dev/harmont-py
|
|
7
|
+
Project-URL: Homepage, https://harmont.dev
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: croniter<3,>=1.4
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest-cov>=4.1; extra == "dev"
|
|
15
|
+
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
16
|
+
Requires-Dist: ruff>=0.2; extra == "dev"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# harmont-py
|
|
20
|
+
|
|
21
|
+
[](LICENSE)
|
|
22
|
+
|
|
23
|
+
Python DSL for defining [Harmont](https://harmont.dev) CI pipelines.
|
|
24
|
+
|
|
25
|
+
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`](https://github.com/harmont-dev/harmont-cli) consumes that IR and runs the pipeline locally in Docker or on the hosted Harmont cloud.
|
|
26
|
+
|
|
27
|
+
The package installs as `harmont` and you import it as `harmont`:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import harmont as hm
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
### 1. Write a pipeline
|
|
36
|
+
|
|
37
|
+
A pipeline file lives at `.harmont/<slug>.py` in your repo:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import harmont as hm
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@hm.pipeline("hello")
|
|
44
|
+
def hello() -> hm.Step:
|
|
45
|
+
return (
|
|
46
|
+
hm.sh("echo 'hello from harmont'", label="hello")
|
|
47
|
+
.sh("uname -a", label="env")
|
|
48
|
+
)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2. Install
|
|
52
|
+
|
|
53
|
+
Not yet on PyPI. Install from source (Python 3.11+):
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
git clone https://github.com/harmont-dev/harmont-py
|
|
57
|
+
cd harmont-py
|
|
58
|
+
pip install -e .
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
If you arrived here from the [`harmont-cli`](https://github.com/harmont-dev/harmont-cli) Quick start, you already did this — skip to Step 3.
|
|
62
|
+
|
|
63
|
+
Development extras (pytest, mypy, ruff):
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
pip install -e '.[dev]'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 3. Run
|
|
70
|
+
|
|
71
|
+
Use the [Harmont CLI](https://github.com/harmont-dev/harmont-cli):
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
hm run hello
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`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`).
|
|
78
|
+
|
|
79
|
+
## DSL surface
|
|
80
|
+
|
|
81
|
+
| Primitive | Returns | What it does |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| `hm.sh(cmd, cwd=..., label=...)` | `Step` | Start a chain in one call (= `hm.scratch().sh(cmd, ...)`) |
|
|
84
|
+
| `hm.scratch()` | `Step` | Empty root; chain with `.sh(...)` for an explicit start |
|
|
85
|
+
| `Step.sh(cmd, cwd=..., ...)` | `Step` | Run a shell command; chained `.sh` shares container state |
|
|
86
|
+
| `Step.fork(label=...)` | `Step` | Branch a shared base into parallel work |
|
|
87
|
+
| `hm.wait()` | `Step` | Explicit synchronization barrier |
|
|
88
|
+
| `@hm.target()` | decorator | Reusable, memoized building block |
|
|
89
|
+
| `@hm.pipeline("slug")` | decorator | Register a pipeline (multiple per file are fine) |
|
|
90
|
+
| `hm.pipeline(*leaves, env=..., default_image=...)` | `dict` | Factory form — build the v0 IR dict directly (used in tests) |
|
|
91
|
+
|
|
92
|
+
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`.
|
|
93
|
+
|
|
94
|
+
## Language toolchains
|
|
95
|
+
|
|
96
|
+
`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):
|
|
97
|
+
|
|
98
|
+
| Call | Project type |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `hm.rust(path=..., version="stable")` | cargo + clippy + rustfmt |
|
|
101
|
+
| `hm.haskell(ghc="9.6.7", cabal="latest")` | cabal (call `.cabal(path)` to build a package) |
|
|
102
|
+
| `hm.python(path=..., uv_version="latest")` | uv-based Python project |
|
|
103
|
+
| `hm.go(path=..., version="1.23.2")` | go build/test/vet/fmt |
|
|
104
|
+
| `hm.npm(path=..., version="20")` | npm + arbitrary scripts |
|
|
105
|
+
| `hm.gradle(path=..., jdk="21", kotlin=False)` | Java or Kotlin via Gradle |
|
|
106
|
+
| `hm.cmake(path=..., lang="c"\|"cpp")` | C/C++ via CMake + CTest |
|
|
107
|
+
| `hm.dotnet(path=..., channel="8.0")` | .NET via dotnet CLI |
|
|
108
|
+
| `hm.ruby(path=..., version="default")` | Bundler + Rake |
|
|
109
|
+
| `hm.ocaml(path=..., compiler="5.1.1")` | opam + Dune |
|
|
110
|
+
| `hm.zig(path=..., version="0.13.0")` | zig build/test/fmt |
|
|
111
|
+
| `hm.perl(path=...)` | cpanm + prove |
|
|
112
|
+
| `hm.composer(path=..., laravel=False)` | PHP / Laravel via Composer |
|
|
113
|
+
| `hm.elm(path=..., elm_version="0.19.1")` | Elm |
|
|
114
|
+
|
|
115
|
+
Working examples for each toolchain live in [`harmont-cli/examples/`](https://github.com/harmont-dev/harmont-cli/tree/main/examples).
|
|
116
|
+
|
|
117
|
+
## Composing with targets
|
|
118
|
+
|
|
119
|
+
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.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from typing import Annotated
|
|
123
|
+
|
|
124
|
+
import harmont as hm
|
|
125
|
+
from harmont.haskell import HaskellPackage, HaskellToolchain
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@hm.target()
|
|
129
|
+
def apt_base(base: Annotated[hm.Step, hm.BaseImage("ubuntu-24.04")]) -> hm.Step:
|
|
130
|
+
return base.sh("apt-get update").sh("apt-get install -y python3")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@hm.target()
|
|
134
|
+
def ghc() -> HaskellToolchain:
|
|
135
|
+
return hm.haskell(ghc="9.6.7")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@hm.target()
|
|
139
|
+
def api(ghc: hm.Target[HaskellToolchain]) -> HaskellPackage:
|
|
140
|
+
return ghc.cabal(path="api")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@hm.pipeline("ci")
|
|
144
|
+
def ci(
|
|
145
|
+
apt_base: hm.Target[hm.Step],
|
|
146
|
+
api: hm.Target[HaskellPackage],
|
|
147
|
+
) -> tuple[hm.Step, ...]:
|
|
148
|
+
return (apt_base.sh("./run-smoke"), api)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
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.
|
|
152
|
+
|
|
153
|
+
<details>
|
|
154
|
+
<summary>How rendering works</summary>
|
|
155
|
+
|
|
156
|
+
`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).
|
|
157
|
+
|
|
158
|
+
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.
|
|
159
|
+
|
|
160
|
+
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).
|
|
161
|
+
|
|
162
|
+
The JSON wire format and cache-key algorithm are stable; see module docstrings under `harmont/` for the contract.
|
|
163
|
+
|
|
164
|
+
</details>
|
|
165
|
+
|
|
166
|
+
## Build & test
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
170
|
+
pip install -e '.[dev]'
|
|
171
|
+
|
|
172
|
+
pytest # all tests
|
|
173
|
+
pytest -v --tb=short
|
|
174
|
+
mypy --strict harmont
|
|
175
|
+
ruff check .
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`pytest` is configured to treat warnings as errors (`filterwarnings = ["error"]`).
|
|
179
|
+
|
|
180
|
+
## See also
|
|
181
|
+
|
|
182
|
+
- [`harmont-cli`](https://github.com/harmont-dev/harmont-cli) — the CLI that runs pipelines defined with this package (`hm run`).
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
MIT. See [`LICENSE`](LICENSE).
|
harmont-0.0.1/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# harmont-py
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
|
|
5
|
+
Python DSL for defining [Harmont](https://harmont.dev) CI pipelines.
|
|
6
|
+
|
|
7
|
+
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`](https://github.com/harmont-dev/harmont-cli) consumes that IR and runs the pipeline locally in Docker or on the hosted Harmont cloud.
|
|
8
|
+
|
|
9
|
+
The package installs as `harmont` and you import it as `harmont`:
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
import harmont as hm
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
### 1. Write a pipeline
|
|
18
|
+
|
|
19
|
+
A pipeline file lives at `.harmont/<slug>.py` in your repo:
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import harmont as hm
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@hm.pipeline("hello")
|
|
26
|
+
def hello() -> hm.Step:
|
|
27
|
+
return (
|
|
28
|
+
hm.sh("echo 'hello from harmont'", label="hello")
|
|
29
|
+
.sh("uname -a", label="env")
|
|
30
|
+
)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 2. Install
|
|
34
|
+
|
|
35
|
+
Not yet on PyPI. Install from source (Python 3.11+):
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
git clone https://github.com/harmont-dev/harmont-py
|
|
39
|
+
cd harmont-py
|
|
40
|
+
pip install -e .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If you arrived here from the [`harmont-cli`](https://github.com/harmont-dev/harmont-cli) Quick start, you already did this — skip to Step 3.
|
|
44
|
+
|
|
45
|
+
Development extras (pytest, mypy, ruff):
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
pip install -e '.[dev]'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 3. Run
|
|
52
|
+
|
|
53
|
+
Use the [Harmont CLI](https://github.com/harmont-dev/harmont-cli):
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
hm run hello
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`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`).
|
|
60
|
+
|
|
61
|
+
## DSL surface
|
|
62
|
+
|
|
63
|
+
| Primitive | Returns | What it does |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `hm.sh(cmd, cwd=..., label=...)` | `Step` | Start a chain in one call (= `hm.scratch().sh(cmd, ...)`) |
|
|
66
|
+
| `hm.scratch()` | `Step` | Empty root; chain with `.sh(...)` for an explicit start |
|
|
67
|
+
| `Step.sh(cmd, cwd=..., ...)` | `Step` | Run a shell command; chained `.sh` shares container state |
|
|
68
|
+
| `Step.fork(label=...)` | `Step` | Branch a shared base into parallel work |
|
|
69
|
+
| `hm.wait()` | `Step` | Explicit synchronization barrier |
|
|
70
|
+
| `@hm.target()` | decorator | Reusable, memoized building block |
|
|
71
|
+
| `@hm.pipeline("slug")` | decorator | Register a pipeline (multiple per file are fine) |
|
|
72
|
+
| `hm.pipeline(*leaves, env=..., default_image=...)` | `dict` | Factory form — build the v0 IR dict directly (used in tests) |
|
|
73
|
+
|
|
74
|
+
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`.
|
|
75
|
+
|
|
76
|
+
## Language toolchains
|
|
77
|
+
|
|
78
|
+
`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):
|
|
79
|
+
|
|
80
|
+
| Call | Project type |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `hm.rust(path=..., version="stable")` | cargo + clippy + rustfmt |
|
|
83
|
+
| `hm.haskell(ghc="9.6.7", cabal="latest")` | cabal (call `.cabal(path)` to build a package) |
|
|
84
|
+
| `hm.python(path=..., uv_version="latest")` | uv-based Python project |
|
|
85
|
+
| `hm.go(path=..., version="1.23.2")` | go build/test/vet/fmt |
|
|
86
|
+
| `hm.npm(path=..., version="20")` | npm + arbitrary scripts |
|
|
87
|
+
| `hm.gradle(path=..., jdk="21", kotlin=False)` | Java or Kotlin via Gradle |
|
|
88
|
+
| `hm.cmake(path=..., lang="c"\|"cpp")` | C/C++ via CMake + CTest |
|
|
89
|
+
| `hm.dotnet(path=..., channel="8.0")` | .NET via dotnet CLI |
|
|
90
|
+
| `hm.ruby(path=..., version="default")` | Bundler + Rake |
|
|
91
|
+
| `hm.ocaml(path=..., compiler="5.1.1")` | opam + Dune |
|
|
92
|
+
| `hm.zig(path=..., version="0.13.0")` | zig build/test/fmt |
|
|
93
|
+
| `hm.perl(path=...)` | cpanm + prove |
|
|
94
|
+
| `hm.composer(path=..., laravel=False)` | PHP / Laravel via Composer |
|
|
95
|
+
| `hm.elm(path=..., elm_version="0.19.1")` | Elm |
|
|
96
|
+
|
|
97
|
+
Working examples for each toolchain live in [`harmont-cli/examples/`](https://github.com/harmont-dev/harmont-cli/tree/main/examples).
|
|
98
|
+
|
|
99
|
+
## Composing with targets
|
|
100
|
+
|
|
101
|
+
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.
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from typing import Annotated
|
|
105
|
+
|
|
106
|
+
import harmont as hm
|
|
107
|
+
from harmont.haskell import HaskellPackage, HaskellToolchain
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@hm.target()
|
|
111
|
+
def apt_base(base: Annotated[hm.Step, hm.BaseImage("ubuntu-24.04")]) -> hm.Step:
|
|
112
|
+
return base.sh("apt-get update").sh("apt-get install -y python3")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@hm.target()
|
|
116
|
+
def ghc() -> HaskellToolchain:
|
|
117
|
+
return hm.haskell(ghc="9.6.7")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@hm.target()
|
|
121
|
+
def api(ghc: hm.Target[HaskellToolchain]) -> HaskellPackage:
|
|
122
|
+
return ghc.cabal(path="api")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@hm.pipeline("ci")
|
|
126
|
+
def ci(
|
|
127
|
+
apt_base: hm.Target[hm.Step],
|
|
128
|
+
api: hm.Target[HaskellPackage],
|
|
129
|
+
) -> tuple[hm.Step, ...]:
|
|
130
|
+
return (apt_base.sh("./run-smoke"), api)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
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.
|
|
134
|
+
|
|
135
|
+
<details>
|
|
136
|
+
<summary>How rendering works</summary>
|
|
137
|
+
|
|
138
|
+
`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).
|
|
139
|
+
|
|
140
|
+
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.
|
|
141
|
+
|
|
142
|
+
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).
|
|
143
|
+
|
|
144
|
+
The JSON wire format and cache-key algorithm are stable; see module docstrings under `harmont/` for the contract.
|
|
145
|
+
|
|
146
|
+
</details>
|
|
147
|
+
|
|
148
|
+
## Build & test
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
152
|
+
pip install -e '.[dev]'
|
|
153
|
+
|
|
154
|
+
pytest # all tests
|
|
155
|
+
pytest -v --tb=short
|
|
156
|
+
mypy --strict harmont
|
|
157
|
+
ruff check .
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`pytest` is configured to treat warnings as errors (`filterwarnings = ["error"]`).
|
|
161
|
+
|
|
162
|
+
## See also
|
|
163
|
+
|
|
164
|
+
- [`harmont-cli`](https://github.com/harmont-dev/harmont-cli) — the CLI that runs pipelines defined with this package (`hm run`).
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT. See [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""harmont — chain-style Python DSL for Harmont CI pipelines.
|
|
2
|
+
|
|
3
|
+
The whole public surface:
|
|
4
|
+
|
|
5
|
+
scratch() -> Step (root)
|
|
6
|
+
sh(cmd, **kw) -> Step (== scratch().sh(cmd, **kw))
|
|
7
|
+
Step.sh(cmd, **kw) -> Step
|
|
8
|
+
Step.fork(label=None) -> Step
|
|
9
|
+
wait(*, continue_on_failure=False) -> Step
|
|
10
|
+
|
|
11
|
+
pipeline(*leaves, env=None, default_image=None) -> dict (v0 IR)
|
|
12
|
+
pipeline_to_json(p, **kw) -> str
|
|
13
|
+
|
|
14
|
+
@pipeline(slug, ..., triggers=[...], allow_manual=True) -> decorator
|
|
15
|
+
push(branch=..., tag=...) -> PushTrigger
|
|
16
|
+
pull_request(branches=..., types=...) -> PullRequestTrigger
|
|
17
|
+
schedule(cron=...) -> ScheduleTrigger
|
|
18
|
+
dump_registry_json() -> str (HAR-9 envelope)
|
|
19
|
+
|
|
20
|
+
Cache helpers: ttl, on_change, forever, compose.
|
|
21
|
+
|
|
22
|
+
``hm.pipeline`` is polymorphic. When called with positional ``Step``
|
|
23
|
+
arguments it builds a v0 IR dict (the factory). When called with no
|
|
24
|
+
positionals or a string slug it returns a decorator that registers a
|
|
25
|
+
function as a CI pipeline (HAR-9).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from typing import TYPE_CHECKING, Any
|
|
31
|
+
|
|
32
|
+
from . import _decorator
|
|
33
|
+
from ._envelope import dump_registry_json
|
|
34
|
+
from ._step import Step, scratch, wait
|
|
35
|
+
from ._target import clear_target_cache, target # noqa: F401 clear_target_cache used by tests
|
|
36
|
+
from ._typing import BaseImage, Target
|
|
37
|
+
from .cache import (
|
|
38
|
+
CacheCompose,
|
|
39
|
+
CacheForever,
|
|
40
|
+
CacheNone,
|
|
41
|
+
CacheOnChange,
|
|
42
|
+
CachePolicy,
|
|
43
|
+
CacheTTL,
|
|
44
|
+
)
|
|
45
|
+
from .cmake import cmake
|
|
46
|
+
from .composer import composer
|
|
47
|
+
from .dotnet import dotnet
|
|
48
|
+
from .elm import elm
|
|
49
|
+
from .go import go
|
|
50
|
+
from .gradle import gradle
|
|
51
|
+
from .haskell import haskell
|
|
52
|
+
from .npm import npm
|
|
53
|
+
from .ocaml import ocaml
|
|
54
|
+
from .perl import perl
|
|
55
|
+
from .pipeline import pipeline as _pipeline_factory
|
|
56
|
+
from .pipeline import pipeline_to_json
|
|
57
|
+
from .python import python
|
|
58
|
+
from .ruby import ruby
|
|
59
|
+
from .rust import rust
|
|
60
|
+
from .triggers import pull_request, push, schedule
|
|
61
|
+
from .types import Pipeline
|
|
62
|
+
from .zig import zig
|
|
63
|
+
|
|
64
|
+
if TYPE_CHECKING:
|
|
65
|
+
from datetime import timedelta
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def pipeline(*args: Any, **kwargs: Any) -> Any:
|
|
69
|
+
"""Polymorphic entry point.
|
|
70
|
+
|
|
71
|
+
- ``pipeline(*leaves, env=..., default_image=...)`` — every
|
|
72
|
+
positional arg is a :class:`Step`; returns the v0 IR dict (the
|
|
73
|
+
factory).
|
|
74
|
+
- ``pipeline(slug=None, *, name=..., triggers=..., allow_manual=...,
|
|
75
|
+
env=..., default_image=...)`` — no positionals or a string slug;
|
|
76
|
+
returns a decorator that registers the wrapped function in the
|
|
77
|
+
module-level :data:`~harmont._registry.REGISTRATIONS` table
|
|
78
|
+
(HAR-9).
|
|
79
|
+
|
|
80
|
+
The discriminant is the *type* of the positional arguments: any
|
|
81
|
+
non-Step positional (including a string slug, or no positional at
|
|
82
|
+
all) routes to the decorator path.
|
|
83
|
+
"""
|
|
84
|
+
if args and all(isinstance(a, Step) for a in args):
|
|
85
|
+
return _pipeline_factory(*args, **kwargs)
|
|
86
|
+
return _decorator.pipeline(*args, **kwargs)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def ttl(duration: timedelta) -> CacheTTL:
|
|
90
|
+
return CacheTTL(duration=duration)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def on_change(*paths: str) -> CacheOnChange:
|
|
94
|
+
return CacheOnChange(paths=tuple(paths))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def forever(env_keys: tuple[str, ...] = ()) -> CacheForever:
|
|
98
|
+
return CacheForever(env_keys=env_keys)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def compose(*policies: CachePolicy) -> CacheCompose:
|
|
102
|
+
return CacheCompose(policies=tuple(policies))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def sh(
|
|
106
|
+
cmd: str,
|
|
107
|
+
*,
|
|
108
|
+
cwd: str | None = None,
|
|
109
|
+
label: str | None = None,
|
|
110
|
+
cache: CachePolicy | None = None,
|
|
111
|
+
env: dict[str, str] | None = None,
|
|
112
|
+
timeout_seconds: int | None = None,
|
|
113
|
+
image: str | None = None,
|
|
114
|
+
key: str | None = None,
|
|
115
|
+
) -> Step:
|
|
116
|
+
"""Shorthand for ``scratch().sh(cmd, ...)`` — start a chain in one call."""
|
|
117
|
+
return scratch().sh(
|
|
118
|
+
cmd,
|
|
119
|
+
cwd=cwd,
|
|
120
|
+
label=label,
|
|
121
|
+
cache=cache,
|
|
122
|
+
env=env,
|
|
123
|
+
timeout_seconds=timeout_seconds,
|
|
124
|
+
image=image,
|
|
125
|
+
key=key,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
__all__ = [
|
|
130
|
+
"BaseImage",
|
|
131
|
+
"CacheCompose",
|
|
132
|
+
"CacheForever",
|
|
133
|
+
"CacheNone",
|
|
134
|
+
"CacheOnChange",
|
|
135
|
+
"CachePolicy",
|
|
136
|
+
"CacheTTL",
|
|
137
|
+
"Pipeline",
|
|
138
|
+
"Step",
|
|
139
|
+
"Target",
|
|
140
|
+
"cmake",
|
|
141
|
+
"compose",
|
|
142
|
+
"composer",
|
|
143
|
+
"dotnet",
|
|
144
|
+
"dump_registry_json",
|
|
145
|
+
"elm",
|
|
146
|
+
"forever",
|
|
147
|
+
"go",
|
|
148
|
+
"gradle",
|
|
149
|
+
"haskell",
|
|
150
|
+
"npm",
|
|
151
|
+
"ocaml",
|
|
152
|
+
"on_change",
|
|
153
|
+
"perl",
|
|
154
|
+
"pipeline",
|
|
155
|
+
"pipeline_to_json",
|
|
156
|
+
"pull_request",
|
|
157
|
+
"push",
|
|
158
|
+
"python",
|
|
159
|
+
"ruby",
|
|
160
|
+
"rust",
|
|
161
|
+
"schedule",
|
|
162
|
+
"scratch",
|
|
163
|
+
"sh",
|
|
164
|
+
"target",
|
|
165
|
+
"ttl",
|
|
166
|
+
"wait",
|
|
167
|
+
"zig",
|
|
168
|
+
]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""@hm.pipeline decorator — see docs/superpowers/specs/2026-05-10-har-9-imperfect-dsl-design.md."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from ._deps import call_with_deps, validate_target_signature
|
|
9
|
+
from ._registry import PipelineRegistration, register
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
|
|
14
|
+
from .triggers import Trigger
|
|
15
|
+
|
|
16
|
+
_SLUG_RE = re.compile(r"^[a-z][a-z0-9-]{0,63}$")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _validate_slug(slug: str) -> None:
|
|
20
|
+
if not _SLUG_RE.match(slug):
|
|
21
|
+
msg = (
|
|
22
|
+
f"invalid pipeline slug {slug!r}\n"
|
|
23
|
+
f" → use lowercase letters, digits, and '-', "
|
|
24
|
+
f"start with a letter, max 64 chars"
|
|
25
|
+
)
|
|
26
|
+
raise ValueError(msg)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def pipeline(
|
|
30
|
+
slug: str | None = None,
|
|
31
|
+
*,
|
|
32
|
+
name: str | None = None,
|
|
33
|
+
triggers: tuple[Trigger, ...] | list[Trigger] = (),
|
|
34
|
+
allow_manual: bool = True,
|
|
35
|
+
env: dict[str, str] | None = None,
|
|
36
|
+
default_image: str | None = None,
|
|
37
|
+
) -> Callable[[Callable[..., Any]], Callable[[], Any]]:
|
|
38
|
+
"""Register a function as a CI pipeline.
|
|
39
|
+
|
|
40
|
+
The wrapped function returns a :class:`Step`, a tuple of leaves
|
|
41
|
+
(:data:`Pipeline`), or any toolchain wrapper that
|
|
42
|
+
:func:`harmont._unwrap.as_leaves` can coerce. The function may
|
|
43
|
+
declare dependencies as parameters (pytest-style); each parameter
|
|
44
|
+
name is resolved against the global target registry.
|
|
45
|
+
"""
|
|
46
|
+
def decorator(fn: Callable[..., Any]) -> Callable[[], Any]:
|
|
47
|
+
validate_target_signature(fn)
|
|
48
|
+
resolved = slug if slug is not None else fn.__name__
|
|
49
|
+
_validate_slug(resolved)
|
|
50
|
+
|
|
51
|
+
@wraps(fn)
|
|
52
|
+
def wrapper() -> Any:
|
|
53
|
+
return call_with_deps(fn)
|
|
54
|
+
|
|
55
|
+
register(
|
|
56
|
+
PipelineRegistration(
|
|
57
|
+
slug=resolved,
|
|
58
|
+
name=name if name is not None else resolved,
|
|
59
|
+
triggers=tuple(triggers),
|
|
60
|
+
allow_manual=allow_manual,
|
|
61
|
+
env=env,
|
|
62
|
+
default_image=default_image,
|
|
63
|
+
fn=wrapper,
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
return wrapper
|
|
67
|
+
|
|
68
|
+
return decorator
|