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.
Files changed (84) hide show
  1. harmont-0.0.1/LICENSE +21 -0
  2. harmont-0.0.1/PKG-INFO +186 -0
  3. harmont-0.0.1/README.md +168 -0
  4. harmont-0.0.1/harmont/__init__.py +168 -0
  5. harmont-0.0.1/harmont/_decorator.py +68 -0
  6. harmont-0.0.1/harmont/_deps.py +188 -0
  7. harmont-0.0.1/harmont/_envelope.py +100 -0
  8. harmont-0.0.1/harmont/_keys.py +121 -0
  9. harmont-0.0.1/harmont/_registry.py +44 -0
  10. harmont-0.0.1/harmont/_step.py +99 -0
  11. harmont-0.0.1/harmont/_target.py +104 -0
  12. harmont-0.0.1/harmont/_toolchain.py +79 -0
  13. harmont-0.0.1/harmont/_typing.py +97 -0
  14. harmont-0.0.1/harmont/_unwrap.py +56 -0
  15. harmont-0.0.1/harmont/_validation.py +11 -0
  16. harmont-0.0.1/harmont/cache.py +80 -0
  17. harmont-0.0.1/harmont/cmake.py +127 -0
  18. harmont-0.0.1/harmont/composer.py +109 -0
  19. harmont-0.0.1/harmont/dotnet.py +116 -0
  20. harmont-0.0.1/harmont/elm.py +143 -0
  21. harmont-0.0.1/harmont/go.py +117 -0
  22. harmont-0.0.1/harmont/gradle.py +137 -0
  23. harmont-0.0.1/harmont/haskell.py +257 -0
  24. harmont-0.0.1/harmont/json_emit.py +69 -0
  25. harmont-0.0.1/harmont/keygen.py +156 -0
  26. harmont-0.0.1/harmont/npm.py +118 -0
  27. harmont-0.0.1/harmont/ocaml.py +145 -0
  28. harmont-0.0.1/harmont/perl.py +86 -0
  29. harmont-0.0.1/harmont/pipeline.py +172 -0
  30. harmont-0.0.1/harmont/python.py +141 -0
  31. harmont-0.0.1/harmont/ruby.py +108 -0
  32. harmont-0.0.1/harmont/rust.py +139 -0
  33. harmont-0.0.1/harmont/triggers.py +135 -0
  34. harmont-0.0.1/harmont/types.py +12 -0
  35. harmont-0.0.1/harmont/zig.py +172 -0
  36. harmont-0.0.1/harmont.egg-info/PKG-INFO +186 -0
  37. harmont-0.0.1/harmont.egg-info/SOURCES.txt +82 -0
  38. harmont-0.0.1/harmont.egg-info/dependency_links.txt +1 -0
  39. harmont-0.0.1/harmont.egg-info/requires.txt +7 -0
  40. harmont-0.0.1/harmont.egg-info/top_level.txt +1 -0
  41. harmont-0.0.1/pyproject.toml +88 -0
  42. harmont-0.0.1/setup.cfg +4 -0
  43. harmont-0.0.1/tests/test_cache.py +73 -0
  44. harmont-0.0.1/tests/test_cmake.py +68 -0
  45. harmont-0.0.1/tests/test_composer.py +76 -0
  46. harmont-0.0.1/tests/test_decorator.py +103 -0
  47. harmont-0.0.1/tests/test_deps.py +80 -0
  48. harmont-0.0.1/tests/test_dotnet.py +78 -0
  49. harmont-0.0.1/tests/test_elm.py +133 -0
  50. harmont-0.0.1/tests/test_envelope.py +186 -0
  51. harmont-0.0.1/tests/test_examples_render.py +69 -0
  52. harmont-0.0.1/tests/test_go.py +91 -0
  53. harmont-0.0.1/tests/test_gradle.py +78 -0
  54. harmont-0.0.1/tests/test_har_28_example.py +84 -0
  55. harmont-0.0.1/tests/test_haskell.py +183 -0
  56. harmont-0.0.1/tests/test_haskell_cabal_alias.py +35 -0
  57. harmont-0.0.1/tests/test_json_emit.py +192 -0
  58. harmont-0.0.1/tests/test_keygen.py +318 -0
  59. harmont-0.0.1/tests/test_keys.py +97 -0
  60. harmont-0.0.1/tests/test_npm.py +117 -0
  61. harmont-0.0.1/tests/test_ocaml.py +69 -0
  62. harmont-0.0.1/tests/test_perl.py +62 -0
  63. harmont-0.0.1/tests/test_pipeline.py +36 -0
  64. harmont-0.0.1/tests/test_pipeline_fixtures.py +83 -0
  65. harmont-0.0.1/tests/test_pipeline_lowering.py +124 -0
  66. harmont-0.0.1/tests/test_python.py +133 -0
  67. harmont-0.0.1/tests/test_registry.py +81 -0
  68. harmont-0.0.1/tests/test_ruby.py +74 -0
  69. harmont-0.0.1/tests/test_rust.py +168 -0
  70. harmont-0.0.1/tests/test_sh_shorthand.py +44 -0
  71. harmont-0.0.1/tests/test_step_chain.py +90 -0
  72. harmont-0.0.1/tests/test_step_sh.py +86 -0
  73. harmont-0.0.1/tests/test_strict_signature.py +129 -0
  74. harmont-0.0.1/tests/test_target.py +109 -0
  75. harmont-0.0.1/tests/test_target_cross_module.py +73 -0
  76. harmont-0.0.1/tests/test_target_fixtures.py +154 -0
  77. harmont-0.0.1/tests/test_target_unwrap.py +79 -0
  78. harmont-0.0.1/tests/test_toolchain.py +108 -0
  79. harmont-0.0.1/tests/test_toolchain_compose.py +82 -0
  80. harmont-0.0.1/tests/test_triggers.py +76 -0
  81. harmont-0.0.1/tests/test_typing_markers.py +74 -0
  82. harmont-0.0.1/tests/test_validation.py +30 -0
  83. harmont-0.0.1/tests/test_zig.py +67 -0
  84. 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](https://img.shields.io/badge/license-MIT-blue.svg)](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).
@@ -0,0 +1,168 @@
1
+ # harmont-py
2
+
3
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](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