Skip to content

Commit 8f67126

Browse files
feat(dsl)!: ubuntu:24.04 default for imageless roots; drop default_image (#144)
1 parent 12204b1 commit 8f67126

85 files changed

Lines changed: 249 additions & 275 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills/convert-gha/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Convert existing GitHub Actions workflows (`.github/workflows/*.yml` / `*.yaml`)
5454
| `actions/cache` | **Not needed — caching is implicit in Harmont** | Harmont automatically caches build artifacts, dependency installs, and toolchain outputs between runs. Remove all cache steps. |
5555
| `actions/setup-*` (setup-node, setup-python, etc.) | Harmont toolchains (`hm.js`, `hm.python`, etc.) | Toolchains handle installation. Specify version via toolchain config. |
5656
| `actions/checkout` | **Not needed — source is always available** | Harmont automatically provides the source code to every step. |
57-
| `runs-on: ubuntu-latest` | `default_image: "ubuntu:24.04"` | Harmont runs steps in Docker containers |
57+
| `runs-on: ubuntu-latest` | (default base is `ubuntu:24.04`; set a per-step `image="..."` to override) | Harmont runs steps in Docker containers |
5858
| `services:` (e.g., postgres) | Service containers in step config | Check docs for service container syntax |
5959
| `matrix:` | Multiple pipelines or parameterized steps | No direct matrix — may need separate pipeline definitions or `.fork()` |
6060
| `env:` / `secrets.*` | `env: {}` on pipeline or step | Secrets must be passed as environment variables |

.claude/skills/write-pipeline/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Write, modify, or extend Harmont CI pipelines defined in `.hm/pipeline.py` (Pyth
5858
- Prefer toolchains over raw `sh()` calls when a toolchain exists for the language.
5959
- Use `.fork()` for steps that can run in parallel.
6060
- Set triggers (`push`, `pull_request`) appropriate to the project.
61-
- Use `default_image: "ubuntu:24.04"` unless the project needs something specific.
61+
- Steps run on `ubuntu:24.04` by default; set a per-step `image="..."` only when a specific step needs a different base.
6262
- Set `env: {"CI": "true"}` on the pipeline.
6363

6464
5. **Validate the pipeline renders correctly:**

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ def project() -> PythonToolchain:
114114

115115
@hm.pipeline(
116116
"ci",
117-
default_image="ubuntu:24.04",
118117
triggers=[hm.push(branch="main")],
119118
)
120119
def ci(project: hm.Target[PythonToolchain]) -> tuple[hm.Step, ...]:
@@ -148,7 +147,6 @@ const pipelines: PipelineDefinition[] = [
148147
project.fmt(),
149148
project.typecheck(),
150149
],
151-
{ defaultImage: "ubuntu:24.04" },
152150
),
153151
},
154152
];
@@ -192,7 +190,7 @@ maps it over for you:
192190
- `actions/setup-*` → replaced by a typed toolchain
193191
- `actions/cache` → not needed (Harmont caches Docker layers automatically)
194192
- `jobs.*.needs` → the DAG `hm` derives from your code
195-
- `runs-on``default_image`
193+
- `runs-on`per-step `image=` (the default base is `ubuntu:24.04`)
196194

197195
The result is a pipeline you can run **locally** before it ever hits CI.
198196

crates/hm-dsl-engine/harmont-py/harmont/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
Step.fork(label=None) -> Step
99
wait(*, continue_on_failure=False) -> Step
1010
11-
pipeline(leaves, *, env=None, default_image=None) -> dict (v0 IR)
11+
pipeline(leaves, *, env=None) -> dict (v0 IR)
1212
pipeline_to_json(p, **kw) -> str
1313
1414
@pipeline(slug, ..., triggers=[...], allow_manual=True) -> decorator
@@ -68,12 +68,12 @@ def pipeline(*args: Any, **kwargs: Any) -> Any:
6868
6969
Factory form — first positional is a list/tuple of ``Step``s:
7070
71-
pipeline([step1, step2, ...], env=None, default_image=None) -> dict
71+
pipeline([step1, step2, ...], env=None) -> dict
7272
7373
Decorator form — no positionals or a string slug:
7474
7575
@pipeline(slug=None, *, name=None, triggers=(), allow_manual=True,
76-
env=None, default_image=None)
76+
env=None)
7777
def my_pipeline() -> Step: ...
7878
7979
The discriminant is the type of the first positional argument:

crates/hm-dsl-engine/harmont-py/harmont/_decorator.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ def pipeline(
3434
triggers: tuple[Trigger, ...] | list[Trigger] = (),
3535
allow_manual: bool = True,
3636
env: dict[str, str] | None = None,
37-
default_image: str | None = None,
3837
timeout: str | int | None = None,
3938
) -> Callable[[Callable[..., Any]], Callable[[], Any]]:
4039
"""Register a function as a CI pipeline (decorator form).
@@ -56,8 +55,6 @@ def pipeline(
5655
allow_manual: When ``True``, the pipeline can be triggered manually
5756
via the UI or API in addition to its configured triggers.
5857
env: Pipeline-level environment variables applied to every step.
59-
default_image: Local-mode Docker base image applied to root steps
60-
that lack an explicit ``image`` or ``builds_in`` parent.
6158
timeout: Whole-build wall-clock budget ("30m", "1h", or int
6259
seconds). The build is killed and fails as timed out once it
6360
elapses.
@@ -86,7 +83,6 @@ def wrapper() -> Any:
8683
triggers=tuple(triggers),
8784
allow_manual=allow_manual,
8885
env=env,
89-
default_image=default_image,
9086
fn=wrapper,
9187
timeout=timeout,
9288
)

crates/hm-dsl-engine/harmont-py/harmont/_envelope.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def _render_one(
4040
except TypeError as e:
4141
msg = f"pipeline {reg.slug!r}: invalid return value\n{e}"
4242
raise TypeError(msg) from e
43-
ir = _assemble(leaves, env=reg.env, default_image=reg.default_image, timeout=reg.timeout)
43+
ir = _assemble(leaves, env=reg.env, timeout=reg.timeout)
4444
resolve_pipeline_keys(
4545
ir.get("graph", {}),
4646
pipeline_org=pipeline_org,

crates/hm-dsl-engine/harmont-py/harmont/_pipeline.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,24 @@
2727
if TYPE_CHECKING:
2828
from ._step import Step
2929

30+
# Across-the-board default image for imageless root steps. The SDK's
31+
# toolchains assume an apt-capable base (apt-get), so ubuntu:24.04 is the
32+
# universal default; child steps boot from their parent's snapshot and
33+
# stay imageless.
34+
DEFAULT_IMAGE = "ubuntu:24.04"
35+
3036

3137
def pipeline(
3238
leaves: list[Step] | tuple[Step, ...],
3339
*,
3440
env: dict[str, str] | None = None,
35-
default_image: str | None = None,
3641
timeout: str | int | None = None,
3742
) -> dict[str, Any]:
3843
"""Top-level factory. Returns a JSON-shaped dict (version "0").
3944
40-
``default_image`` is the local-mode fallback Docker image: it
41-
applies to every command step that lacks both a ``builds_in``
42-
parent edge and a per-step ``image`` override.
45+
Every imageless root command step (one with no ``builds_in`` parent
46+
and no per-step ``image``) is stamped with ``DEFAULT_IMAGE``
47+
(``ubuntu:24.04``). Set a per-step ``image=`` on a step to override.
4348
4449
``timeout`` is a whole-build wall-clock budget (``"30m"``, ``"1h"``,
4550
or an int number of seconds). When it elapses the build is killed and
@@ -52,23 +57,16 @@ def pipeline(
5257
)
5358
raise ValueError(msg)
5459
out: dict[str, Any] = {"version": "0"}
55-
if default_image is not None:
56-
out["default_image"] = default_image
5760
if timeout is not None:
5861
out["timeout_seconds"] = parse_duration(timeout)
59-
out["graph"] = _lower_to_graph(
60-
list(leaves),
61-
env=env,
62-
default_image=default_image,
63-
)
62+
out["graph"] = _lower_to_graph(list(leaves), env=env)
6463
return out
6564

6665

6766
def _lower_to_graph(
6867
leaves: list[Step],
6968
*,
7069
env: dict[str, str] | None = None,
71-
default_image: str | None = None,
7270
) -> dict[str, Any]:
7371
"""Walk back via `parent`, topo-sort, emit petgraph-serde graph dict.
7472
@@ -86,7 +84,7 @@ def _lower_to_graph(
8684
for i, s in enumerate(command_steps):
8785
idx_by_id[id(s)] = i
8886

89-
# Track which node indices have a builds_in parent (for default_image).
87+
# Track which node indices have a builds_in parent (child steps stay image-less).
9088
has_builds_in_parent: set[int] = set()
9189

9290
nodes: list[dict[str, Any]] = []
@@ -156,11 +154,13 @@ def _lower_to_graph(
156154

157155
pre_wait_indices.append(node_idx)
158156

159-
# Apply default_image to root nodes (those without a builds_in parent).
160-
if default_image is not None:
161-
for i, node in enumerate(nodes):
162-
if i not in has_builds_in_parent and "image" not in node["step"]:
163-
node["step"]["image"] = default_image
157+
# Stamp the default image on every root command step that lacks an
158+
# explicit one. Root steps boot from an image tag (not a parent
159+
# snapshot); child steps inherit the parent's committed snapshot and
160+
# must stay image-less.
161+
for i, node in enumerate(nodes):
162+
if i not in has_builds_in_parent and "image" not in node["step"]:
163+
node["step"]["image"] = DEFAULT_IMAGE
164164

165165
return {
166166
"nodes": nodes,

crates/hm-dsl-engine/harmont-py/harmont/_registry.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ class PipelineRegistration:
2222
triggers: tuple[Trigger, ...]
2323
allow_manual: bool
2424
env: dict[str, str] | None
25-
default_image: str | None
2625
fn: Callable[[], object]
2726
timeout: str | int | None = None
2827

crates/hm-dsl-engine/harmont-py/harmont/_step.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ class Step:
3737
timeout_seconds: int | None = None
3838
image: str | None = None
3939
"""Local-mode Docker base image override for this step. Ignored when
40-
the step has a ``builds_in`` parent (the parent's snapshot wins);
41-
falls back to the pipeline's ``default_image`` when unset."""
40+
the step has a ``builds_in`` parent (the parent's snapshot wins).
41+
When unset, root steps fall back to ``ubuntu:24.04``; child steps
42+
inherit the parent's snapshot."""
4243

4344
runner: str | None = None
4445
"""Step-executor plugin runner name. ``None`` = default (Docker)."""

0 commit comments

Comments
 (0)