diff --git a/.claude/skills/convert-gha/SKILL.md b/.claude/skills/convert-gha/SKILL.md index e50796c2..cb1e9eb1 100644 --- a/.claude/skills/convert-gha/SKILL.md +++ b/.claude/skills/convert-gha/SKILL.md @@ -54,7 +54,7 @@ Convert existing GitHub Actions workflows (`.github/workflows/*.yml` / `*.yaml`) | `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. | | `actions/setup-*` (setup-node, setup-python, etc.) | Harmont toolchains (`hm.js`, `hm.python`, etc.) | Toolchains handle installation. Specify version via toolchain config. | | `actions/checkout` | **Not needed — source is always available** | Harmont automatically provides the source code to every step. | - | `runs-on: ubuntu-latest` | `default_image: "ubuntu:24.04"` | Harmont runs steps in Docker containers | + | `runs-on: ubuntu-latest` | (default base is `ubuntu:24.04`; set a per-step `image="..."` to override) | Harmont runs steps in Docker containers | | `services:` (e.g., postgres) | Service containers in step config | Check docs for service container syntax | | `matrix:` | Multiple pipelines or parameterized steps | No direct matrix — may need separate pipeline definitions or `.fork()` | | `env:` / `secrets.*` | `env: {}` on pipeline or step | Secrets must be passed as environment variables | diff --git a/.claude/skills/write-pipeline/SKILL.md b/.claude/skills/write-pipeline/SKILL.md index ac349116..fdc62ffa 100644 --- a/.claude/skills/write-pipeline/SKILL.md +++ b/.claude/skills/write-pipeline/SKILL.md @@ -58,7 +58,7 @@ Write, modify, or extend Harmont CI pipelines defined in `.hm/pipeline.py` (Pyth - Prefer toolchains over raw `sh()` calls when a toolchain exists for the language. - Use `.fork()` for steps that can run in parallel. - Set triggers (`push`, `pull_request`) appropriate to the project. - - Use `default_image: "ubuntu:24.04"` unless the project needs something specific. + - Steps run on `ubuntu:24.04` by default; set a per-step `image="..."` only when a specific step needs a different base. - Set `env: {"CI": "true"}` on the pipeline. 5. **Validate the pipeline renders correctly:** diff --git a/README.md b/README.md index bcc98501..ebc4862e 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,6 @@ def project() -> PythonToolchain: @hm.pipeline( "ci", - default_image="ubuntu:24.04", triggers=[hm.push(branch="main")], ) def ci(project: hm.Target[PythonToolchain]) -> tuple[hm.Step, ...]: @@ -148,7 +147,6 @@ const pipelines: PipelineDefinition[] = [ project.fmt(), project.typecheck(), ], - { defaultImage: "ubuntu:24.04" }, ), }, ]; @@ -192,7 +190,7 @@ maps it over for you: - `actions/setup-*` → replaced by a typed toolchain - `actions/cache` → not needed (Harmont caches Docker layers automatically) - `jobs.*.needs` → the DAG `hm` derives from your code -- `runs-on` → `default_image` +- `runs-on` → per-step `image=` (the default base is `ubuntu:24.04`) The result is a pipeline you can run **locally** before it ever hits CI. diff --git a/crates/hm-dsl-engine/harmont-py/harmont/__init__.py b/crates/hm-dsl-engine/harmont-py/harmont/__init__.py index 9cfc586c..9448277f 100644 --- a/crates/hm-dsl-engine/harmont-py/harmont/__init__.py +++ b/crates/hm-dsl-engine/harmont-py/harmont/__init__.py @@ -8,7 +8,7 @@ Step.fork(label=None) -> Step wait(*, continue_on_failure=False) -> Step - pipeline(leaves, *, env=None, default_image=None) -> dict (v0 IR) + pipeline(leaves, *, env=None) -> dict (v0 IR) pipeline_to_json(p, **kw) -> str @pipeline(slug, ..., triggers=[...], allow_manual=True) -> decorator @@ -68,12 +68,12 @@ def pipeline(*args: Any, **kwargs: Any) -> Any: Factory form — first positional is a list/tuple of ``Step``s: - pipeline([step1, step2, ...], env=None, default_image=None) -> dict + pipeline([step1, step2, ...], env=None) -> dict Decorator form — no positionals or a string slug: @pipeline(slug=None, *, name=None, triggers=(), allow_manual=True, - env=None, default_image=None) + env=None) def my_pipeline() -> Step: ... The discriminant is the type of the first positional argument: diff --git a/crates/hm-dsl-engine/harmont-py/harmont/_decorator.py b/crates/hm-dsl-engine/harmont-py/harmont/_decorator.py index 67c841b0..e9d77872 100644 --- a/crates/hm-dsl-engine/harmont-py/harmont/_decorator.py +++ b/crates/hm-dsl-engine/harmont-py/harmont/_decorator.py @@ -34,7 +34,6 @@ def pipeline( triggers: tuple[Trigger, ...] | list[Trigger] = (), allow_manual: bool = True, env: dict[str, str] | None = None, - default_image: str | None = None, timeout: str | int | None = None, ) -> Callable[[Callable[..., Any]], Callable[[], Any]]: """Register a function as a CI pipeline (decorator form). @@ -56,8 +55,6 @@ def pipeline( allow_manual: When ``True``, the pipeline can be triggered manually via the UI or API in addition to its configured triggers. env: Pipeline-level environment variables applied to every step. - default_image: Local-mode Docker base image applied to root steps - that lack an explicit ``image`` or ``builds_in`` parent. timeout: Whole-build wall-clock budget ("30m", "1h", or int seconds). The build is killed and fails as timed out once it elapses. @@ -86,7 +83,6 @@ def wrapper() -> Any: triggers=tuple(triggers), allow_manual=allow_manual, env=env, - default_image=default_image, fn=wrapper, timeout=timeout, ) diff --git a/crates/hm-dsl-engine/harmont-py/harmont/_envelope.py b/crates/hm-dsl-engine/harmont-py/harmont/_envelope.py index 972ae3dd..936cb379 100644 --- a/crates/hm-dsl-engine/harmont-py/harmont/_envelope.py +++ b/crates/hm-dsl-engine/harmont-py/harmont/_envelope.py @@ -40,7 +40,7 @@ def _render_one( except TypeError as e: msg = f"pipeline {reg.slug!r}: invalid return value\n → {e}" raise TypeError(msg) from e - ir = _assemble(leaves, env=reg.env, default_image=reg.default_image, timeout=reg.timeout) + ir = _assemble(leaves, env=reg.env, timeout=reg.timeout) resolve_pipeline_keys( ir.get("graph", {}), pipeline_org=pipeline_org, diff --git a/crates/hm-dsl-engine/harmont-py/harmont/_pipeline.py b/crates/hm-dsl-engine/harmont-py/harmont/_pipeline.py index cf601d86..38b25ea1 100644 --- a/crates/hm-dsl-engine/harmont-py/harmont/_pipeline.py +++ b/crates/hm-dsl-engine/harmont-py/harmont/_pipeline.py @@ -27,19 +27,24 @@ if TYPE_CHECKING: from ._step import Step +# Across-the-board default image for imageless root steps. The SDK's +# toolchains assume an apt-capable base (apt-get), so ubuntu:24.04 is the +# universal default; child steps boot from their parent's snapshot and +# stay imageless. +DEFAULT_IMAGE = "ubuntu:24.04" + def pipeline( leaves: list[Step] | tuple[Step, ...], *, env: dict[str, str] | None = None, - default_image: str | None = None, timeout: str | int | None = None, ) -> dict[str, Any]: """Top-level factory. Returns a JSON-shaped dict (version "0"). - ``default_image`` is the local-mode fallback Docker image: it - applies to every command step that lacks both a ``builds_in`` - parent edge and a per-step ``image`` override. + Every imageless root command step (one with no ``builds_in`` parent + and no per-step ``image``) is stamped with ``DEFAULT_IMAGE`` + (``ubuntu:24.04``). Set a per-step ``image=`` on a step to override. ``timeout`` is a whole-build wall-clock budget (``"30m"``, ``"1h"``, or an int number of seconds). When it elapses the build is killed and @@ -52,15 +57,9 @@ def pipeline( ) raise ValueError(msg) out: dict[str, Any] = {"version": "0"} - if default_image is not None: - out["default_image"] = default_image if timeout is not None: out["timeout_seconds"] = parse_duration(timeout) - out["graph"] = _lower_to_graph( - list(leaves), - env=env, - default_image=default_image, - ) + out["graph"] = _lower_to_graph(list(leaves), env=env) return out @@ -68,7 +67,6 @@ def _lower_to_graph( leaves: list[Step], *, env: dict[str, str] | None = None, - default_image: str | None = None, ) -> dict[str, Any]: """Walk back via `parent`, topo-sort, emit petgraph-serde graph dict. @@ -86,7 +84,7 @@ def _lower_to_graph( for i, s in enumerate(command_steps): idx_by_id[id(s)] = i - # Track which node indices have a builds_in parent (for default_image). + # Track which node indices have a builds_in parent (child steps stay image-less). has_builds_in_parent: set[int] = set() nodes: list[dict[str, Any]] = [] @@ -156,11 +154,13 @@ def _lower_to_graph( pre_wait_indices.append(node_idx) - # Apply default_image to root nodes (those without a builds_in parent). - if default_image is not None: - for i, node in enumerate(nodes): - if i not in has_builds_in_parent and "image" not in node["step"]: - node["step"]["image"] = default_image + # Stamp the default image on every root command step that lacks an + # explicit one. Root steps boot from an image tag (not a parent + # snapshot); child steps inherit the parent's committed snapshot and + # must stay image-less. + for i, node in enumerate(nodes): + if i not in has_builds_in_parent and "image" not in node["step"]: + node["step"]["image"] = DEFAULT_IMAGE return { "nodes": nodes, diff --git a/crates/hm-dsl-engine/harmont-py/harmont/_registry.py b/crates/hm-dsl-engine/harmont-py/harmont/_registry.py index 43b6ad4c..0eb5c7b8 100644 --- a/crates/hm-dsl-engine/harmont-py/harmont/_registry.py +++ b/crates/hm-dsl-engine/harmont-py/harmont/_registry.py @@ -22,7 +22,6 @@ class PipelineRegistration: triggers: tuple[Trigger, ...] allow_manual: bool env: dict[str, str] | None - default_image: str | None fn: Callable[[], object] timeout: str | int | None = None diff --git a/crates/hm-dsl-engine/harmont-py/harmont/_step.py b/crates/hm-dsl-engine/harmont-py/harmont/_step.py index b3e1bea2..2fb8e090 100644 --- a/crates/hm-dsl-engine/harmont-py/harmont/_step.py +++ b/crates/hm-dsl-engine/harmont-py/harmont/_step.py @@ -37,8 +37,9 @@ class Step: timeout_seconds: int | None = None image: str | None = None """Local-mode Docker base image override for this step. Ignored when - the step has a ``builds_in`` parent (the parent's snapshot wins); - falls back to the pipeline's ``default_image`` when unset.""" + the step has a ``builds_in`` parent (the parent's snapshot wins). + When unset, root steps fall back to ``ubuntu:24.04``; child steps + inherit the parent's snapshot.""" runner: str | None = None """Step-executor plugin runner name. ``None`` = default (Docker).""" diff --git a/crates/hm-dsl-engine/harmont-py/tests/test_cmake.py b/crates/hm-dsl-engine/harmont-py/tests/test_cmake.py index a17e1cc5..6615498c 100644 --- a/crates/hm-dsl-engine/harmont-py/tests/test_cmake.py +++ b/crates/hm-dsl-engine/harmont-py/tests/test_cmake.py @@ -19,7 +19,7 @@ def _cmds(p: dict) -> list[str]: class TestCMakeToolchain: def test_default_toolchain_installs_cmake_ninja_ccache(self): tc = hm.cmake() - p = hm.pipeline([tc.installed], default_image="ubuntu:24.04") + p = hm.pipeline([tc.installed]) cmds = _cmds(p) apt_cmd = next(c for c in cmds if "apt-get install" in c) assert "cmake" in apt_cmd @@ -28,14 +28,14 @@ def test_default_toolchain_installs_cmake_ninja_ccache(self): def test_clang_18_compiler_installs_clang_18(self): tc = hm.cmake(compiler="clang-18") - p = hm.pipeline([tc.installed], default_image="ubuntu:24.04") + p = hm.pipeline([tc.installed]) cmds = _cmds(p) apt_cmd = next(c for c in cmds if "apt-get install" in c) assert "clang-18" in apt_cmd def test_gcc_14_compiler_installs_gcc_14_and_gpp_14(self): tc = hm.cmake(compiler="gcc-14") - p = hm.pipeline([tc.installed], default_image="ubuntu:24.04") + p = hm.pipeline([tc.installed]) cmds = _cmds(p) apt_cmd = next(c for c in cmds if "apt-get install" in c) assert "gcc-14" in apt_cmd @@ -48,7 +48,7 @@ def test_invalid_compiler_raises_valueerror(self): def test_ccache_false_omits_ccache_from_apt_and_flags(self): tc = hm.cmake(ccache=False) proj = tc.project(path=".") - p = hm.pipeline([proj.built], default_image="ubuntu:24.04") + p = hm.pipeline([proj.built]) cmds = _cmds(p) apt_cmd = next(c for c in cmds if "apt-get install" in c) assert "ccache" not in apt_cmd @@ -60,7 +60,7 @@ def test_toolchain_shared_across_projects_single_apt_install(self): tc = hm.cmake() proj1 = tc.project(path="svc1") proj2 = tc.project(path="svc2") - p = hm.pipeline([proj1.built, proj2.built], default_image="ubuntu:24.04") + p = hm.pipeline([proj1.built, proj2.built]) cmds = _cmds(p) apt_installs = [c for c in cmds if "apt-get install" in c] assert len(apt_installs) == 1 @@ -74,14 +74,14 @@ def test_toolchain_shared_across_projects_single_apt_install(self): class TestCMakeProject: def test_build_produces_configure_and_build_commands(self): proj = hm.cmake(path="svc") - p = hm.pipeline([proj.built], default_image="ubuntu:24.04") + p = hm.pipeline([proj.built]) cmds = _cmds(p) assert any("cmake -S . -B build" in c for c in cmds) assert any("cmake --build" in c for c in cmds) def test_warmup_uses_relative_build_dir_after_cd(self): proj = hm.cmake(path="infra/agent") - p = hm.pipeline([proj.built], default_image="ubuntu:24.04") + p = hm.pipeline([proj.built]) cmds = _cmds(p) warmup = next(c for c in cmds if "cmake -S . -B build" in c) assert "cd infra/agent" in warmup @@ -90,49 +90,49 @@ def test_warmup_uses_relative_build_dir_after_cd(self): def test_uses_ninja_generator_by_default(self): proj = hm.cmake(path="svc") - p = hm.pipeline([proj.built], default_image="ubuntu:24.04") + p = hm.pipeline([proj.built]) cmds = _cmds(p) configure_cmd = next(c for c in cmds if "cmake -S" in c) assert "-G Ninja" in configure_cmd def test_no_build_type_by_default(self): proj = hm.cmake(path="svc") - p = hm.pipeline([proj.built], default_image="ubuntu:24.04") + p = hm.pipeline([proj.built]) cmds = _cmds(p) configure_cmd = next(c for c in cmds if "cmake -S" in c) assert "CMAKE_BUILD_TYPE" not in configure_cmd def test_defines_cmake_build_type(self): proj = hm.cmake(path="svc", defines={"CMAKE_BUILD_TYPE": "Debug"}) - p = hm.pipeline([proj.built], default_image="ubuntu:24.04") + p = hm.pipeline([proj.built]) cmds = _cmds(p) configure_cmd = next(c for c in cmds if "cmake -S" in c) assert "CMAKE_BUILD_TYPE=Debug" in configure_cmd def test_defines_produces_d_flags(self): proj = hm.cmake(path="svc", defines={"BUILD_TESTING": "ON"}) - p = hm.pipeline([proj.built], default_image="ubuntu:24.04") + p = hm.pipeline([proj.built]) cmds = _cmds(p) configure_cmd = next(c for c in cmds if "cmake -S" in c) assert "-DBUILD_TESTING=ON" in configure_cmd def test_defines_build_shared_libs(self): proj = hm.cmake(path="svc", defines={"BUILD_SHARED_LIBS": "ON"}) - p = hm.pipeline([proj.built], default_image="ubuntu:24.04") + p = hm.pipeline([proj.built]) cmds = _cmds(p) configure_cmd = next(c for c in cmds if "cmake -S" in c) assert "-DBUILD_SHARED_LIBS=ON" in configure_cmd def test_defines_cmake_cxx_standard(self): proj = hm.cmake(path="svc", defines={"CMAKE_CXX_STANDARD": "20"}) - p = hm.pipeline([proj.built], default_image="ubuntu:24.04") + p = hm.pipeline([proj.built]) cmds = _cmds(p) configure_cmd = next(c for c in cmds if "cmake -S" in c) assert "-DCMAKE_CXX_STANDARD=20" in configure_cmd def test_preset_produces_preset_flag_and_no_build_type(self): proj = hm.cmake(path="svc", preset="ci-linux") - p = hm.pipeline([proj.built], default_image="ubuntu:24.04") + p = hm.pipeline([proj.built]) cmds = _cmds(p) configure_cmd = next(c for c in cmds if "--preset" in c) assert "--preset ci-linux" in configure_cmd @@ -140,7 +140,7 @@ def test_preset_produces_preset_flag_and_no_build_type(self): def test_ccache_true_adds_compiler_launcher_flags(self): proj = hm.cmake(path="svc", ccache=True) - p = hm.pipeline([proj.built], default_image="ubuntu:24.04") + p = hm.pipeline([proj.built]) cmds = _cmds(p) configure_cmd = next(c for c in cmds if "cmake -S" in c) assert "-DCMAKE_C_COMPILER_LAUNCHER=ccache" in configure_cmd @@ -148,7 +148,7 @@ def test_ccache_true_adds_compiler_launcher_flags(self): def test_test_produces_ctest_with_output_on_failure_and_parallel(self): proj = hm.cmake(path="svc") - p = hm.pipeline([proj.test()], default_image="ubuntu:24.04") + p = hm.pipeline([proj.test()]) cmds = _cmds(p) test_cmd = next(c for c in cmds if "ctest" in c) assert "--output-on-failure" in test_cmd @@ -156,28 +156,28 @@ def test_test_produces_ctest_with_output_on_failure_and_parallel(self): def test_test_includes_incremental_build(self): proj = hm.cmake(path="svc") - p = hm.pipeline([proj.test()], default_image="ubuntu:24.04") + p = hm.pipeline([proj.test()]) cmds = _cmds(p) test_cmd = next(c for c in cmds if "ctest" in c) assert "cmake --build" in test_cmd def test_test_uses_absolute_path_for_standalone_step(self): proj = hm.cmake(path="infra/agent") - p = hm.pipeline([proj.test()], default_image="ubuntu:24.04") + p = hm.pipeline([proj.test()]) cmds = _cmds(p) test_cmd = next(c for c in cmds if "ctest" in c) assert "cmake --build infra/agent/build" in test_cmd def test_install_with_prefix(self): proj = hm.cmake(path="svc") - p = hm.pipeline([proj.install(prefix="/usr/local")], default_image="ubuntu:24.04") + p = hm.pipeline([proj.install(prefix="/usr/local")]) cmds = _cmds(p) install_cmd = next(c for c in cmds if "cmake --install" in c) assert "--prefix /usr/local" in install_cmd def test_fmt_runs_clang_format_dry_run(self): proj = hm.cmake(path="svc") - p = hm.pipeline([proj.fmt()], default_image="ubuntu:24.04") + p = hm.pipeline([proj.fmt()]) cmds = _cmds(p) fmt_cmd = next(c for c in cmds if "xargs clang-format" in c) assert "--dry-run --Werror" in fmt_cmd @@ -190,7 +190,7 @@ def test_fmt_parent_is_toolchain_installed(self): def test_lint_runs_run_clang_tidy(self): proj = hm.cmake(path="svc") - p = hm.pipeline([proj.lint()], default_image="ubuntu:24.04") + p = hm.pipeline([proj.lint()]) cmds = _cmds(p) assert any("run-clang-tidy" in c for c in cmds) @@ -201,7 +201,7 @@ def test_lint_parent_is_built(self): def test_package_runs_cpack(self): proj = hm.cmake(path="svc") - p = hm.pipeline([proj.package()], default_image="ubuntu:24.04") + p = hm.pipeline([proj.package()]) cmds = _cmds(p) assert any("cpack" in c for c in cmds) @@ -214,7 +214,7 @@ def test_package_runs_cpack(self): class TestCMakeVcpkg: def test_deps_vcpkg_produces_bootstrap_command(self): proj = hm.cmake(path="svc", deps="vcpkg") - p = hm.pipeline([proj.built], default_image="ubuntu:24.04") + p = hm.pipeline([proj.built]) cmds = _cmds(p) assert any("bootstrap-vcpkg" in c for c in cmds) @@ -224,7 +224,7 @@ def test_invalid_deps_raises_valueerror(self): def test_vcpkg_step_has_on_change_cache_policy(self): proj = hm.cmake(path="svc", deps="vcpkg") - p = hm.pipeline([proj.built], default_image="ubuntu:24.04") + p = hm.pipeline([proj.built]) nodes = p["graph"]["nodes"] vcpkg_node = next(n for n in nodes if "bootstrap-vcpkg" in n["step"]["cmd"]) assert vcpkg_node["step"]["cache"]["policy"] == "on_change" @@ -237,17 +237,17 @@ def test_vcpkg_step_has_on_change_cache_policy(self): class TestCMakeBareForm: def test_bare_build_produces_cmake_build(self): - p = hm.pipeline([hm.cmake.build()], default_image="ubuntu:24.04") + p = hm.pipeline([hm.cmake.build()]) cmds = _cmds(p) assert any("cmake --build" in c for c in cmds) def test_bare_test_produces_ctest(self): - p = hm.pipeline([hm.cmake.test()], default_image="ubuntu:24.04") + p = hm.pipeline([hm.cmake.test()]) cmds = _cmds(p) assert any("ctest" in c for c in cmds) def test_bare_fmt_produces_clang_format(self): - p = hm.pipeline([hm.cmake.fmt()], default_image="ubuntu:24.04") + p = hm.pipeline([hm.cmake.fmt()]) cmds = _cmds(p) assert any("clang-format" in c for c in cmds) @@ -284,6 +284,6 @@ class TestCMakeWithBase: def test_providing_base_skips_apt_install(self): base = hm.scratch().sh("custom base", label="base") proj = hm.cmake(path="svc", base=base) - p = hm.pipeline([proj.built], default_image="ubuntu:24.04") + p = hm.pipeline([proj.built]) cmds = _cmds(p) assert not any("apt-get install" in c for c in cmds) diff --git a/crates/hm-dsl-engine/harmont-py/tests/test_decorator.py b/crates/hm-dsl-engine/harmont-py/tests/test_decorator.py index 7d5aa6c0..abb6f35b 100644 --- a/crates/hm-dsl-engine/harmont-py/tests/test_decorator.py +++ b/crates/hm-dsl-engine/harmont-py/tests/test_decorator.py @@ -25,7 +25,6 @@ def whatever() -> hm.Step: assert reg.triggers == () assert reg.allow_manual is True assert reg.env is None - assert reg.default_image is None def test_default_slug_from_function_name(): @@ -44,14 +43,13 @@ def ci() -> hm.Step: assert REGISTRATIONS[0].name == "Continuous Integration" -def test_forwards_env_and_default_image(): - @hm.pipeline("ci", env={"FOO": "bar"}, default_image="alpine:3.20") +def test_forwards_env(): + @hm.pipeline("ci", env={"FOO": "bar"}) def ci() -> hm.Step: return hm.scratch().sh("echo") reg = REGISTRATIONS[0] assert reg.env == {"FOO": "bar"} - assert reg.default_image == "alpine:3.20" def test_allow_manual_false(): diff --git a/crates/hm-dsl-engine/harmont-py/tests/test_e2e_fixtures.py b/crates/hm-dsl-engine/harmont-py/tests/test_e2e_fixtures.py index 417046ec..2d106491 100644 --- a/crates/hm-dsl-engine/harmont-py/tests/test_e2e_fixtures.py +++ b/crates/hm-dsl-engine/harmont-py/tests/test_e2e_fixtures.py @@ -65,7 +65,6 @@ def _build_monorepo_ci() -> dict: web_project.run("lint"), ], env={"CI": "true"}, - default_image="ubuntu:24.04", ) @@ -75,7 +74,6 @@ def _build_rust_release() -> dict: return hm.pipeline( [project.build(), project.test(), project.clippy(), project.fmt(), project.doc()], env={"CI": "true"}, - default_image="ubuntu:24.04", ) @@ -103,7 +101,6 @@ def _build_zig_node_polyglot() -> dict: web.run("lint"), ], env={"CI": "true"}, - default_image="ubuntu:24.04", ) @@ -120,7 +117,6 @@ def _build_kitchen_sink() -> dict: py_web.lint(), ], env={"CI": "true"}, - default_image="ubuntu:24.04", ) @@ -136,7 +132,6 @@ def _build_cmake_advanced() -> dict: return hm.pipeline( [project.test(), project.lint(), project.fmt()], env={"CI": "true"}, - default_image="ubuntu:24.04", ) @@ -154,8 +149,13 @@ def test_e2e_fixture(name: str) -> None: ir = SCENARIOS[name]() assert ir["version"] == "0" - assert ir["default_image"] == "ubuntu:24.04" assert len(ir["graph"]["nodes"]) > 0 + # Root steps (no builds_in parent) should carry the ubuntu:24.04 default image. + nodes = ir["graph"]["nodes"] + edges = ir["graph"]["edges"] + child_idxs = {e[1] for e in edges if e[2] == "builds_in"} + roots = [n for i, n in enumerate(nodes) if i not in child_idxs] + assert all(n["step"].get("image") == "ubuntu:24.04" for n in roots) assert ir["graph"]["edge_property"] == "directed" for node in ir["graph"]["nodes"]: diff --git a/crates/hm-dsl-engine/harmont-py/tests/test_elixir.py b/crates/hm-dsl-engine/harmont-py/tests/test_elixir.py index 5589999e..ea042abd 100644 --- a/crates/hm-dsl-engine/harmont-py/tests/test_elixir.py +++ b/crates/hm-dsl-engine/harmont-py/tests/test_elixir.py @@ -21,7 +21,7 @@ def _step_by_substring(p: dict, needle: str) -> dict: def test_elixir_object_form_full_chain(): ex = hm.elixir(path="apps/api") - p = hm.pipeline([ex.compile()], default_image="ubuntu:24.04") + p = hm.pipeline([ex.compile()]) cmds = _cmds(p) assert any("apt-get install" in c for c in cmds) assert any("erlang" in c.lower() for c in cmds) @@ -33,7 +33,6 @@ def test_elixir_actions_share_install_step(): ex = hm.elixir(path=".") p = hm.pipeline( [ex.compile(), ex.test(), ex.format(), ex.credo()], - default_image="ubuntu:24.04", ) cmds = _cmds(p) assert len([c for c in cmds if "mix deps.get" in c]) == 1 @@ -115,7 +114,7 @@ def test_elixir_dialyzer_chains_through_plt(): def test_elixir_with_base_skips_apt(): base = hm.scratch().sh("custom base", label="base") ex = hm.elixir(path=".", base=base) - p = hm.pipeline([ex.compile()], default_image="ubuntu:24.04") + p = hm.pipeline([ex.compile()]) cmds = _cmds(p) assert not any("apt-get update && apt-get install -y" in c for c in cmds) assert any("custom base" in c for c in cmds) diff --git a/crates/hm-dsl-engine/harmont-py/tests/test_envelope.py b/crates/hm-dsl-engine/harmont-py/tests/test_envelope.py index 430f93be..48fe783b 100644 --- a/crates/hm-dsl-engine/harmont-py/tests/test_envelope.py +++ b/crates/hm-dsl-engine/harmont-py/tests/test_envelope.py @@ -111,14 +111,13 @@ def ci() -> hm.Pipeline: assert cmds == ["a", "b"] -def test_pipeline_forwards_env_and_default_image_to_assemble(): - @hm.pipeline("ci", env={"CI": "true"}, default_image="alpine:3.20") +def test_pipeline_forwards_env_to_assemble(): + @hm.pipeline("ci", env={"CI": "true"}) def ci() -> hm.Step: return hm.scratch().sh("echo") out = json.loads(hm.dump_registry_json()) definition = out["pipelines"][0]["definition"] - assert definition["default_image"] == "alpine:3.20" # Pipeline-level env is merged into node env dicts. for node in _graph_nodes(definition): assert node["env"].get("CI") == "true" diff --git a/crates/hm-dsl-engine/harmont-py/tests/test_examples_render.py b/crates/hm-dsl-engine/harmont-py/tests/test_examples_render.py index 3b52a8a8..5442a9ae 100644 --- a/crates/hm-dsl-engine/harmont-py/tests/test_examples_render.py +++ b/crates/hm-dsl-engine/harmont-py/tests/test_examples_render.py @@ -64,7 +64,12 @@ def test_example_renders_to_v0_ir( assert definition.get("graph", {}).get("nodes"), ( f"{example_dir.name}: ci pipeline has no nodes" ) - assert definition.get("default_image"), ( - f"{example_dir.name}: ci pipeline missing default_image — local " - "executor falls back to alpine and apt-get-based examples die" + nodes = definition.get("graph", {}).get("nodes", []) + edges = definition.get("graph", {}).get("edges", []) + child_idxs = {e[1] for e in edges if e[2] == "builds_in"} + roots = [n for i, n in enumerate(nodes) if i not in child_idxs] + assert roots, f"{example_dir.name}: ci pipeline has no root steps" + assert all("image" in n["step"] for n in roots), ( + f"{example_dir.name}: a root step is missing an image — the lowering " + f"should stamp the ubuntu:24.04 default on every imageless root" ) diff --git a/crates/hm-dsl-engine/harmont-py/tests/test_go.py b/crates/hm-dsl-engine/harmont-py/tests/test_go.py index 4b0b6fe4..5b9cdf14 100644 --- a/crates/hm-dsl-engine/harmont-py/tests/test_go.py +++ b/crates/hm-dsl-engine/harmont-py/tests/test_go.py @@ -21,7 +21,7 @@ def _step_by_substring(p: dict, needle: str) -> dict: def test_go_object_form_full_chain(): go = hm.go(path="svc") - p = hm.pipeline([go.build()], default_image="ubuntu:24.04") + p = hm.pipeline([go.build()]) cmds = _cmds(p) assert any("apt-get install" in c for c in cmds) assert any("go.dev/dl/" in c for c in cmds) @@ -30,7 +30,7 @@ def test_go_object_form_full_chain(): def test_go_actions_share_install_step(): go = hm.go(path="svc") - p = hm.pipeline([go.build(), go.test(), go.vet(), go.fmt()], default_image="ubuntu:24.04") + p = hm.pipeline([go.build(), go.test(), go.vet(), go.fmt()]) cmds = _cmds(p) assert len([c for c in cmds if "go.dev/dl/" in c]) == 1 assert any("go build ./..." in c for c in cmds) @@ -78,7 +78,7 @@ def test_go_action_labels_auto_generated(): def test_go_with_base_skips_apt(): base = hm.scratch().sh("custom base", label="base") go = hm.go(path="svc", base=base) - p = hm.pipeline([go.build()], default_image="ubuntu:24.04") + p = hm.pipeline([go.build()]) cmds = _cmds(p) assert not any("apt-get install" in c for c in cmds) assert any("custom base" in c for c in cmds) diff --git a/crates/hm-dsl-engine/harmont-py/tests/test_js.py b/crates/hm-dsl-engine/harmont-py/tests/test_js.py index d41aee79..e1126d9a 100644 --- a/crates/hm-dsl-engine/harmont-py/tests/test_js.py +++ b/crates/hm-dsl-engine/harmont-py/tests/test_js.py @@ -273,7 +273,7 @@ def test_default_label(runtime: str, expected: str) -> None: ) def test_pipeline_ir(opts: dict) -> None: p = js.project(**opts) - ir = hm.pipeline([p.run("test"), p.run("lint")], default_image="ubuntu:24.04") + ir = hm.pipeline([p.run("test"), p.run("lint")]) assert ir["version"] == "0" assert len(ir["graph"]["nodes"]) >= 4 diff --git a/crates/hm-dsl-engine/harmont-py/tests/test_json_emit.py b/crates/hm-dsl-engine/harmont-py/tests/test_json_emit.py index 59b27705..08167f99 100644 --- a/crates/hm-dsl-engine/harmont-py/tests/test_json_emit.py +++ b/crates/hm-dsl-engine/harmont-py/tests/test_json_emit.py @@ -110,9 +110,10 @@ def test_pipeline_env_merged_into_node_env(): assert _nodes(out)[0]["env"]["DEBIAN_FRONTEND"] == "noninteractive" -def test_default_image_emitted_when_set(): - out = _emit(pipeline([scratch().sh("a", label="a")], default_image="alpine:3")) - assert out["default_image"] == "alpine:3" +def test_imageless_root_emitted_with_ubuntu_default(): + out = _emit(pipeline([scratch().sh("a", label="a")])) + assert _nodes(out)[0]["step"]["image"] == "ubuntu:24.04" + assert "default_image" not in out def test_cache_ttl_resolves_key(): @@ -158,7 +159,8 @@ def test_cache_on_change_paths_round_trip(tmp_path): def test_no_optional_fields_when_not_set(): out = _emit(pipeline([scratch().sh("x", label="x")])) s = _nodes(out)[0]["step"] - assert "image" not in s + # Root imageless steps now receive ubuntu:24.04 automatically. + assert s.get("image") == "ubuntu:24.04" assert "timeout_seconds" not in s assert "cache" not in s diff --git a/crates/hm-dsl-engine/harmont-py/tests/test_pipeline.py b/crates/hm-dsl-engine/harmont-py/tests/test_pipeline.py index 9cbeb7c9..d4259b8d 100644 --- a/crates/hm-dsl-engine/harmont-py/tests/test_pipeline.py +++ b/crates/hm-dsl-engine/harmont-py/tests/test_pipeline.py @@ -42,12 +42,23 @@ def test_pipeline_rejects_legacy_variadic_step_form(): assert "hm.pipeline([a, b])" in str(exc.value) -def test_pipeline_default_image_lowers_to_dict(): - p = pipeline( - [scratch().sh("echo", label="a", image="ubuntu:24.04")], - default_image="alpine:3.20", - ) - assert p["default_image"] == "alpine:3.20" - step = p["graph"]["nodes"][0]["step"] - assert step["image"] == "ubuntu:24.04" - assert step["label"] == "a" +def test_imageless_root_gets_ubuntu_default(): + p = pipeline([scratch().sh("echo hi", label="a")]) + nodes = p["graph"]["nodes"] + assert nodes[0]["step"]["image"] == "ubuntu:24.04" + # No top-level default_image key is emitted anymore. + assert "default_image" not in p + + +def test_explicit_root_image_is_preserved(): + p = pipeline([scratch().sh("echo hi", label="a", image="alpine:3.20")]) + assert p["graph"]["nodes"][0]["step"]["image"] == "alpine:3.20" + + +def test_child_step_stays_imageless(): + root = scratch().sh("echo p", label="p") + child = root.sh("echo c", label="c") + p = pipeline([child]) + nodes = {n["step"]["key"]: n["step"] for n in p["graph"]["nodes"]} + # parent (root) gets the default; child boots from parent snapshot. + assert "image" not in nodes["c"] diff --git a/crates/hm-dsl-engine/harmont-py/tests/test_py_uv.py b/crates/hm-dsl-engine/harmont-py/tests/test_py_uv.py index c6f4d300..919c99fd 100644 --- a/crates/hm-dsl-engine/harmont-py/tests/test_py_uv.py +++ b/crates/hm-dsl-engine/harmont-py/tests/test_py_uv.py @@ -26,7 +26,7 @@ def _step_by_substring(p: dict, needle: str) -> dict: class TestUvObjectForm: def test_full_chain(self): proj = hm.py.uv(path="svc") - p = hm.pipeline([proj.test()], default_image="ubuntu:24.04") + p = hm.pipeline([proj.test()]) cmds = _cmds(p) assert any("apt-get install" in c for c in cmds) assert any("astral.sh/uv/install.sh" in c for c in cmds) @@ -37,7 +37,6 @@ def test_shared_install(self): proj = hm.py.uv(path="svc") p = hm.pipeline( [proj.test(), proj.lint(), proj.fmt(), proj.typecheck()], - default_image="ubuntu:24.04", ) cmds = _cmds(p) assert len([c for c in cmds if "astral.sh/uv/install.sh" in c]) == 1 @@ -142,7 +141,7 @@ def test_image_emitted_on_apt_step(self): def test_base_skips_apt(self): base = hm.scratch().sh("custom base", label="base") proj = hm.py.uv(path="svc", base=base) - p = hm.pipeline([proj.test()], default_image="ubuntu:24.04") + p = hm.pipeline([proj.test()]) cmds = _cmds(p) assert not any("apt-get install" in c for c in cmds) assert any("custom base" in c for c in cmds) diff --git a/crates/hm-dsl-engine/harmont-py/tests/test_python.py b/crates/hm-dsl-engine/harmont-py/tests/test_python.py index 55cf98f0..cace93a0 100644 --- a/crates/hm-dsl-engine/harmont-py/tests/test_python.py +++ b/crates/hm-dsl-engine/harmont-py/tests/test_python.py @@ -22,7 +22,7 @@ def _step_by_substring(p: dict, needle: str) -> dict: def test_python_object_form_full_chain(): py = hm.python(path="svc") - p = hm.pipeline([py.test()], default_image="ubuntu:24.04") + p = hm.pipeline([py.test()]) cmds = _cmds(p) assert any("apt-get install" in c for c in cmds) assert any("astral.sh/uv/install.sh" in c for c in cmds) @@ -32,7 +32,7 @@ def test_python_object_form_full_chain(): def test_python_actions_share_install_step(): py = hm.python(path="svc") - p = hm.pipeline([py.test(), py.lint(), py.fmt(), py.typecheck()], default_image="ubuntu:24.04") + p = hm.pipeline([py.test(), py.lint(), py.fmt(), py.typecheck()]) cmds = _cmds(p) assert len([c for c in cmds if "astral.sh/uv/install.sh" in c]) == 1 assert len([c for c in cmds if "apt-get install" in c]) == 1 @@ -120,7 +120,7 @@ def test_python_image_emitted_on_apt_step(): def test_python_with_base_skips_apt(): base = hm.scratch().sh("custom base", label="base") py = hm.python(path="svc", base=base) - p = hm.pipeline([py.test()], default_image="ubuntu:24.04") + p = hm.pipeline([py.test()]) cmds = _cmds(p) assert not any("apt-get install" in c for c in cmds) assert any("custom base" in c for c in cmds) diff --git a/crates/hm-dsl-engine/harmont-py/tests/test_registry.py b/crates/hm-dsl-engine/harmont-py/tests/test_registry.py index 54f45a44..fe084e9b 100644 --- a/crates/hm-dsl-engine/harmont-py/tests/test_registry.py +++ b/crates/hm-dsl-engine/harmont-py/tests/test_registry.py @@ -28,7 +28,6 @@ def test_register_appends(): triggers=(), allow_manual=True, env=None, - default_image=None, fn=lambda: None, ) register(reg) @@ -44,7 +43,6 @@ def test_register_duplicate_slug_raises(): triggers=(), allow_manual=True, env=None, - default_image=None, fn=fn, ) ) @@ -56,7 +54,6 @@ def test_register_duplicate_slug_raises(): triggers=(), allow_manual=True, env=None, - default_image=None, fn=fn, ) ) @@ -73,7 +70,6 @@ def test_clear_resets(): triggers=(), allow_manual=True, env=None, - default_image=None, fn=fn, ) ) diff --git a/crates/hm-dsl-engine/harmont-py/tests/test_rust.py b/crates/hm-dsl-engine/harmont-py/tests/test_rust.py index fe65c060..6f44b556 100644 --- a/crates/hm-dsl-engine/harmont-py/tests/test_rust.py +++ b/crates/hm-dsl-engine/harmont-py/tests/test_rust.py @@ -26,7 +26,7 @@ def _step_by_substring(p: dict, needle: str) -> dict: class TestRustToolchain: def test_full_chain(self): tc = hm.rust.toolchain(path="cli") - p = hm.pipeline([tc.build()], default_image="ubuntu:24.04") + p = hm.pipeline([tc.build()]) cmds = _cmds(p) assert any("apt-get install" in c for c in cmds) assert any("sh.rustup.rs" in c for c in cmds) @@ -36,7 +36,6 @@ def test_actions_share_install_step(self): tc = hm.rust.toolchain(path="cli") p = hm.pipeline( [tc.build(), tc.test(), tc.clippy(), tc.fmt(), tc.doc()], - default_image="ubuntu:24.04", ) cmds = _cmds(p) assert len([c for c in cmds if "sh.rustup.rs" in c]) == 1 @@ -118,7 +117,7 @@ def test_image_emitted_on_apt_step(self): def test_with_base_skips_apt(self): base = hm.scratch().sh("custom base", label="base") tc = hm.rust.toolchain(path="cli", base=base) - p = hm.pipeline([tc.build()], default_image="ubuntu:24.04") + p = hm.pipeline([tc.build()]) cmds = _cmds(p) assert not any("apt-get install" in c for c in cmds) assert any("custom base" in c for c in cmds) @@ -151,7 +150,7 @@ def test_warmup_in_pipeline(self): ". $HOME/.cargo/env && cd cli && cargo test --workspace --locked", label=":rust: test", ) - p = hm.pipeline([t, tc.fmt()], default_image="ubuntu:24.04") + p = hm.pipeline([t, tc.fmt()]) cmds = _cmds(p) assert any("cargo build --workspace --tests --locked" in c for c in cmds) assert any("cargo test --workspace --locked" in c for c in cmds) @@ -232,7 +231,7 @@ def test_toolchain_escape_hatch(self): def test_with_base_skips_apt(self): base = hm.scratch().sh("custom base", label="base") proj = hm.rust.project(path="cli", base=base) - p = hm.pipeline([proj.test(), proj.clippy(), proj.fmt()], default_image="ubuntu:24.04") + p = hm.pipeline([proj.test(), proj.clippy(), proj.fmt()]) cmds = _cmds(p) assert not any("apt-get install" in c for c in cmds) assert any("custom base" in c for c in cmds) @@ -246,7 +245,7 @@ def test_labels(self): def test_pipeline_ir(self): proj = hm.rust.project(path="cli") - p = hm.pipeline([proj.test(), proj.clippy(), proj.fmt()], default_image="ubuntu:24.04") + p = hm.pipeline([proj.test(), proj.clippy(), proj.fmt()]) cmds = _cmds(p) assert any("cargo build --workspace --tests --locked" in c for c in cmds) assert any("cargo test --workspace --locked" in c for c in cmds) diff --git a/crates/hm-dsl-engine/harmont-py/tests/test_toolchain_compose.py b/crates/hm-dsl-engine/harmont-py/tests/test_toolchain_compose.py index d64a7e36..7e5d6167 100644 --- a/crates/hm-dsl-engine/harmont-py/tests/test_toolchain_compose.py +++ b/crates/hm-dsl-engine/harmont-py/tests/test_toolchain_compose.py @@ -37,7 +37,7 @@ def test_deterministic_emission(): def build() -> dict: rust = hm.rust.toolchain(path="cli") - return hm.pipeline([rust.build(), rust.test()], default_image="ubuntu:24.04") + return hm.pipeline([rust.build(), rust.test()]) assert build() == build() @@ -49,7 +49,6 @@ def test_mixed_pipeline_compiles(): go = hm.go(path="services/api") p = hm.pipeline( [rust.test(), rust.clippy(), node.install(), go.build(), go.test()], - default_image="ubuntu:24.04", ) assert p["version"] == "0" assert len(p["graph"]["nodes"]) > 0 @@ -80,7 +79,6 @@ def test_apt_base_shared_across_toolchains(): py = hm.py.uv(path="dsls/harmont-py", base=base) p = hm.pipeline( [rust.build(), py.test()], - default_image="ubuntu:24.04", ) cmds = _cmds(p) assert len([c for c in cmds if "apt-get install" in c]) == 1 @@ -96,7 +94,7 @@ def test_apt_base_default_label(): def test_apt_base_custom_image(): base = hm.apt_base(packages=("curl",), image="debian:bookworm") rust = hm.rust.toolchain(path=".", base=base) - p = hm.pipeline([rust.build()], default_image="ubuntu:24.04") + p = hm.pipeline([rust.build()]) apt_step = _step_by_substring(p, "apt-get install") assert apt_step.get("image") == "debian:bookworm" diff --git a/crates/hm-dsl-engine/harmont-py/tests/test_zig.py b/crates/hm-dsl-engine/harmont-py/tests/test_zig.py index 4bb55296..015c347f 100644 --- a/crates/hm-dsl-engine/harmont-py/tests/test_zig.py +++ b/crates/hm-dsl-engine/harmont-py/tests/test_zig.py @@ -20,7 +20,7 @@ def _step_by_substring(p: dict, needle: str) -> dict: def test_zig_object_form_full_chain(): z = hm.zig(path="svc") - p = hm.pipeline([z.build()], default_image="ubuntu:24.04") + p = hm.pipeline([z.build()]) cmds = _cmds(p) assert any("ziglang.org" in c for c in cmds) assert any("cd svc && zig build" in c for c in cmds) @@ -28,7 +28,7 @@ def test_zig_object_form_full_chain(): def test_zig_actions_share_install(): z = hm.zig(path="svc") - p = hm.pipeline([z.build(), z.test(), z.fmt()], default_image="ubuntu:24.04") + p = hm.pipeline([z.build(), z.test(), z.fmt()]) cmds = _cmds(p) assert len([c for c in cmds if "ziglang.org" in c]) == 1 assert any("zig build test" in c for c in cmds) @@ -80,5 +80,5 @@ def test_zig_new_version_uses_new_url_format(): def test_zig_with_base_skips_apt(): base = hm.scratch().sh("custom base", label="base") z = hm.zig(path="svc", base=base) - p = hm.pipeline([z.build()], default_image="ubuntu:24.04") + p = hm.pipeline([z.build()]) assert not any("apt-get install" in c for c in _cmds(p)) diff --git a/crates/hm-dsl-engine/harmont-py/tests/test_zig_toolchain.py b/crates/hm-dsl-engine/harmont-py/tests/test_zig_toolchain.py index 7f4e744b..7ea94f36 100644 --- a/crates/hm-dsl-engine/harmont-py/tests/test_zig_toolchain.py +++ b/crates/hm-dsl-engine/harmont-py/tests/test_zig_toolchain.py @@ -58,7 +58,7 @@ def lib_a(zig: hm.Target[ZigToolchain]) -> ZigProject: def lib_b(zig: hm.Target[ZigToolchain]) -> ZigProject: return zig.project(path="lib-b") - @hm.pipeline("ci", default_image="ubuntu:24.04") + @hm.pipeline("ci") def ci( lib_a: hm.Target[ZigProject], lib_b: hm.Target[ZigProject], diff --git a/crates/hm-dsl-engine/harmont-ts/src/pipeline.ts b/crates/hm-dsl-engine/harmont-ts/src/pipeline.ts index a531fb92..bf745598 100644 --- a/crates/hm-dsl-engine/harmont-ts/src/pipeline.ts +++ b/crates/hm-dsl-engine/harmont-ts/src/pipeline.ts @@ -3,15 +3,18 @@ import { parseDuration } from "./duration.js"; import { resolveKeys } from "./keys.js"; import type { Step } from "./step.js"; +// Across-the-board default image for imageless root steps. The SDK's +// toolchains assume an apt-capable base (apt-get), so ubuntu:24.04 is the +// universal default; child steps boot from their parent's snapshot. +const DEFAULT_IMAGE = "ubuntu:24.04"; + export interface PipelineOptions { readonly env?: Readonly>; - readonly defaultImage?: string; readonly timeout?: string | number; } export interface PipelineIR { version: string; - default_image?: string; timeout_seconds?: number; graph: { nodes: GraphNode[]; @@ -41,9 +44,6 @@ export function pipeline( } const ir: PipelineIR = { version: "0", graph: lowerToGraph(leaves, opts) }; - if (opts?.defaultImage != null) { - ir.default_image = opts.defaultImage; - } if (opts?.timeout != null) { ir.timeout_seconds = parseDuration(opts.timeout); } @@ -116,11 +116,9 @@ function lowerToGraph( preWaitIndices.push(nodeIdx); } - if (opts?.defaultImage != null) { - for (let i = 0; i < nodes.length; i++) { - if (!hasBuildsInParent.has(i) && !("image" in nodes[i].step)) { - nodes[i].step.image = opts.defaultImage; - } + for (let i = 0; i < nodes.length; i++) { + if (!hasBuildsInParent.has(i) && !("image" in nodes[i].step)) { + nodes[i].step.image = DEFAULT_IMAGE; } } diff --git a/crates/hm-dsl-engine/harmont-ts/tests/e2e-fixtures.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/e2e-fixtures.test.ts index 546366ea..c5d872c6 100644 --- a/crates/hm-dsl-engine/harmont-ts/tests/e2e-fixtures.test.ts +++ b/crates/hm-dsl-engine/harmont-ts/tests/e2e-fixtures.test.ts @@ -70,11 +70,16 @@ describe("E2E pipeline fixtures", () => { webProject.run("test"), webProject.run("lint"), ], - { env: { CI: "true" }, defaultImage: "ubuntu:24.04" }, + { env: { CI: "true" } }, ); expect(ir.version).toBe("0"); - expect(ir.default_image).toBe("ubuntu:24.04"); + const monorepoChildIdxs = new Set( + ir.graph.edges.filter((e: any) => e[2] === "builds_in").map((e: any) => e[1]), + ); + const monorepoRoots = ir.graph.nodes.filter((_: any, i: number) => !monorepoChildIdxs.has(i)); + expect(monorepoRoots.length).toBeGreaterThan(0); + expect(monorepoRoots.every((n: any) => "image" in n.step)).toBe(true); expect(ir.graph.nodes.length).toBeGreaterThan(0); assertFixture("monorepo-ci", ir); }); @@ -84,7 +89,7 @@ describe("E2E pipeline fixtures", () => { const ir = pipeline( [project.build(), project.test(), project.clippy(), project.fmt(), project.doc()], - { env: { CI: "true" }, defaultImage: "ubuntu:24.04" }, + { env: { CI: "true" } }, ); expect(ir.version).toBe("0"); @@ -112,7 +117,7 @@ describe("E2E pipeline fixtures", () => { web.run("test"), web.run("lint"), ], - { env: { CI: "true" }, defaultImage: "ubuntu:24.04" }, + { env: { CI: "true" } }, ); expect(ir.version).toBe("0"); @@ -125,7 +130,7 @@ describe("E2E pipeline fixtures", () => { const ir = pipeline( [cProject.build(), cProject.test(), cProject.fmt(), pyWeb.test(), pyWeb.lint()], - { env: { CI: "true" }, defaultImage: "ubuntu:24.04" }, + { env: { CI: "true" } }, ); expect(ir.version).toBe("0"); @@ -141,7 +146,7 @@ describe("E2E pipeline fixtures", () => { const ir = pipeline( [project.test(), project.lint(), project.fmt()], - { env: { CI: "true" }, defaultImage: "ubuntu:24.04" }, + { env: { CI: "true" } }, ); expect(ir.version).toBe("0"); diff --git a/crates/hm-dsl-engine/harmont-ts/tests/examples.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/examples.test.ts index 71288277..dec87182 100644 --- a/crates/hm-dsl-engine/harmont-ts/tests/examples.test.ts +++ b/crates/hm-dsl-engine/harmont-ts/tests/examples.test.ts @@ -44,7 +44,15 @@ describe.skipIf(examples.length === 0)("examples render to v0 IR", () => { expect(ci.pipeline.version).toBe("0"); expect(ci.pipeline.graph.nodes.length).toBeGreaterThan(0); expect(ci.pipeline.graph.edge_property).toBe("directed"); - expect(ci.pipeline.default_image).toBeTruthy(); + + // Every root step (no builds_in parent) must have an image stamped on it + const nodes = ci.pipeline.graph.nodes; + const childIdxs = new Set( + ci.pipeline.graph.edges.filter((e: any) => e[2] === "builds_in").map((e: any) => e[1]), + ); + const roots = nodes.filter((_: any, i: number) => !childIdxs.has(i)); + expect(roots.length).toBeGreaterThan(0); + expect(roots.every((n: any) => "image" in n.step)).toBe(true); // Verify all nodes have required fields for (const node of ci.pipeline.graph.nodes) { diff --git a/crates/hm-dsl-engine/harmont-ts/tests/integration.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/integration.test.ts index 8cec87b6..6c022bbd 100644 --- a/crates/hm-dsl-engine/harmont-ts/tests/integration.test.ts +++ b/crates/hm-dsl-engine/harmont-ts/tests/integration.test.ts @@ -25,7 +25,7 @@ beforeEach(() => { }); describe("full pipeline build", () => { - it("creates install -> build -> test chain with cache, env, defaultImage", () => { + it("creates install -> build -> test chain with cache, env", () => { const install = scratch() .sh("npm ci", { label: "install", cache: forever() }); const build = install @@ -35,7 +35,6 @@ describe("full pipeline build", () => { const ir = pipeline([test], { env: { CI: "true" }, - defaultImage: "node:22-alpine", }); // version @@ -58,9 +57,13 @@ describe("full pipeline build", () => { expect(buildNode.env).toEqual({ ...base, CI: "true", NODE_ENV: "production" }); expect(testNode.env).toEqual({ ...base, CI: "true" }); - // default_image applies to root node only (install), not children - expect(ir.default_image).toBe("node:22-alpine"); - expect(installNode.step.image).toBe("node:22-alpine"); + // ubuntu:24.04 is automatically stamped on root steps (no builds_in parent) + const childIdxs = new Set( + ir.graph.edges.filter((e) => e[2] === "builds_in").map((e) => e[1]), + ); + const roots = ir.graph.nodes.filter((_n, i) => !childIdxs.has(i)); + expect(roots).toHaveLength(1); + expect(roots[0].step.image).toBe("ubuntu:24.04"); expect("image" in buildNode.step).toBe(false); expect("image" in testNode.step).toBe(false); @@ -171,11 +174,15 @@ describe("JSON snake_case output", () => { label: "build", cache: onChange("src/", "lib/"), })); - const ir = pipeline([s], { defaultImage: "ubuntu:24.04" }); + const ir = pipeline([s]); const json = JSON.stringify(ir); + // Root imageless step gets ubuntu:24.04 stamped on it + expect(json).toContain('"image":"ubuntu:24.04"'); + // Must NOT contain the removed top-level default_image field + expect(json).not.toContain('"default_image"'); + // Must contain snake_case keys - expect(json).toContain('"default_image"'); expect(json).toContain('"timeout_seconds"'); expect(json).toContain('"edge_property"'); expect(json).toContain('"node_holes"'); diff --git a/crates/hm-dsl-engine/harmont-ts/tests/pipeline.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/pipeline.test.ts index 10764699..81a604ff 100644 --- a/crates/hm-dsl-engine/harmont-ts/tests/pipeline.test.ts +++ b/crates/hm-dsl-engine/harmont-ts/tests/pipeline.test.ts @@ -7,12 +7,6 @@ function stepKeys(ir: any): string[] { return ir.graph.nodes.map((n: any) => n.step.key); } -function buildsInEdges(ir: any): [number, number][] { - return ir.graph.edges - .filter((e: any) => e[2] === "builds_in") - .map((e: any) => [e[0], e[1]]); -} - function dependsOnEdges(ir: any): [number, number][] { return ir.graph.edges .filter((e: any) => e[2] === "depends_on") @@ -48,13 +42,6 @@ describe("pipeline", () => { expect(() => pipeline([])).toThrow("at least one leaf"); }); - it("sets default_image on IR when provided", () => { - const p = pipeline([sh("echo", { label: "a", image: "ubuntu:24.04" })], { - defaultImage: "alpine:3.20", - }); - expect(p.default_image).toBe("alpine:3.20"); - expect(p.graph.nodes[0].step.image).toBe("ubuntu:24.04"); - }); }); describe("lowering: single chain", () => { @@ -185,25 +172,32 @@ describe("lowering: dedup", () => { }); }); -describe("lowering: default_image", () => { - it("applies default_image to root nodes without explicit image", () => { - const s = scratch().sh("echo"); - const ir = pipeline([s], { defaultImage: "ubuntu:24.04" }); +describe("lowering: default image", () => { + it("stamps ubuntu:24.04 on an imageless root", () => { + const s = scratch().sh("echo hi", { label: "a" }); + const ir = pipeline([s]); expect(ir.graph.nodes[0].step.image).toBe("ubuntu:24.04"); }); - it("does not override explicit image", () => { - const s = scratch().sh("echo", { image: "alpine:3.20" }); - const ir = pipeline([s], { defaultImage: "ubuntu:24.04" }); + it("preserves an explicit root image", () => { + const s = scratch({ image: "alpine:3.20" }).sh("echo hi", { label: "a" }); + const ir = pipeline([s]); expect(ir.graph.nodes[0].step.image).toBe("alpine:3.20"); }); - it("does not apply to child nodes with builds_in parent", () => { - const parent = scratch().sh("a", { label: "a" }); - const child = parent.sh("b", { label: "b" }); - const ir = pipeline([child], { defaultImage: "ubuntu:24.04" }); - expect(ir.graph.nodes[0].step.image).toBe("ubuntu:24.04"); - expect("image" in ir.graph.nodes[1].step).toBe(false); + it("leaves child steps imageless", () => { + const root = scratch().sh("echo p", { label: "p" }); + const child = root.sh("echo c", { label: "c" }); + const ir = pipeline([child]); + const byKey = Object.fromEntries( + ir.graph.nodes.map((n) => [n.step.key as string, n.step]), + ); + expect("image" in byKey["c"]).toBe(false); + }); + + it("emits no top-level default_image key", () => { + const ir = pipeline([scratch().sh("echo hi", { label: "a" })]); + expect("default_image" in ir).toBe(false); }); }); diff --git a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/cmake.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/cmake.test.ts index efdace7c..1143fde1 100644 --- a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/cmake.test.ts +++ b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/cmake.test.ts @@ -117,9 +117,7 @@ describe("cmake with vcpkg", () => { describe("cmake in pipeline", () => { it("produces valid IR", () => { const proj = cmake({ path: "." }); - const ir = pipeline([proj.build(), proj.test()], { - defaultImage: "ubuntu:24.04", - }); + const ir = pipeline([proj.build(), proj.test()]); expect(ir.graph.nodes.length).toBeGreaterThanOrEqual(3); }); }); diff --git a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/elixir.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/elixir.test.ts index 4e3423be..b5f9c7a0 100644 --- a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/elixir.test.ts +++ b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/elixir.test.ts @@ -150,9 +150,7 @@ describe("elixir install chain", () => { describe("elixir in pipeline", () => { it("produces valid IR", () => { const ex = elixir(); - const ir = pipeline([ex.compile(), ex.test(), ex.format()], { - defaultImage: "ubuntu:24.04", - }); + const ir = pipeline([ex.compile(), ex.test(), ex.format()]); expect(ir.graph.nodes.length).toBeGreaterThanOrEqual(5); expect(ir.version).toBe("0"); }); diff --git a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/go.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/go.test.ts index 83442b63..7b0e1f09 100644 --- a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/go.test.ts +++ b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/go.test.ts @@ -98,7 +98,7 @@ describe("go install chain", () => { describe("go in pipeline", () => { it("produces valid IR", () => { const g = go(); - const ir = pipeline([g.build(), g.test()], { defaultImage: "ubuntu:24.04" }); + const ir = pipeline([g.build(), g.test()]); expect(ir.graph.nodes.length).toBeGreaterThanOrEqual(3); expect(ir.version).toBe("0"); }); diff --git a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/js.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/js.test.ts index a5f23f52..d80772b5 100644 --- a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/js.test.ts +++ b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/js.test.ts @@ -372,45 +372,35 @@ describe("js.project actions", () => { describe("js.project pipeline IR", () => { it("produces valid IR for node+npm", () => { const p = js.project(); - const ir = pipeline([p.run("test"), p.run("lint")], { - defaultImage: "ubuntu:24.04", - }); + const ir = pipeline([p.run("test"), p.run("lint")]); expect(ir.version).toBe("0"); expect(ir.graph.nodes.length).toBeGreaterThanOrEqual(4); }); it("produces valid IR for node+pnpm", () => { const p = js.project({ pm: "pnpm" }); - const ir = pipeline([p.run("test"), p.run("lint")], { - defaultImage: "ubuntu:24.04", - }); + const ir = pipeline([p.run("test"), p.run("lint")]); expect(ir.version).toBe("0"); expect(ir.graph.nodes.length).toBeGreaterThanOrEqual(5); }); it("produces valid IR for node+yarn-berry", () => { const p = js.project({ pm: "yarn-berry" }); - const ir = pipeline([p.run("test"), p.run("lint")], { - defaultImage: "ubuntu:24.04", - }); + const ir = pipeline([p.run("test"), p.run("lint")]); expect(ir.version).toBe("0"); expect(ir.graph.nodes.length).toBeGreaterThanOrEqual(5); }); it("produces valid IR for bun+bun", () => { const p = js.project({ runtime: "bun" }); - const ir = pipeline([p.run("test"), p.run("lint")], { - defaultImage: "ubuntu:24.04", - }); + const ir = pipeline([p.run("test"), p.run("lint")]); expect(ir.version).toBe("0"); expect(ir.graph.nodes.length).toBeGreaterThanOrEqual(4); }); it("produces valid IR for deno", () => { const p = js.project({ runtime: "deno" }); - const ir = pipeline([p.run("test"), p.run("lint")], { - defaultImage: "ubuntu:24.04", - }); + const ir = pipeline([p.run("test"), p.run("lint")]); expect(ir.version).toBe("0"); expect(ir.graph.nodes.length).toBeGreaterThanOrEqual(4); }); diff --git a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/py/uv.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/py/uv.test.ts index c25463b4..fa6afb0d 100644 --- a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/py/uv.test.ts +++ b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/py/uv.test.ts @@ -131,7 +131,7 @@ describe("py.uv install chain", () => { describe("py.uv in pipeline", () => { it("produces valid IR", () => { const p = py.uv(); - const ir = pipeline([p.test(), p.lint()], { defaultImage: "ubuntu:24.04" }); + const ir = pipeline([p.test(), p.lint()]); expect(ir.graph.nodes.length).toBeGreaterThanOrEqual(4); expect(ir.version).toBe("0"); }); diff --git a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/python.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/python.test.ts index 9b6db68d..a1914ea4 100644 --- a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/python.test.ts +++ b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/python.test.ts @@ -98,7 +98,7 @@ describe("python install chain", () => { describe("python in pipeline", () => { it("produces valid IR", () => { const p = python(); - const ir = pipeline([p.test(), p.lint()], { defaultImage: "ubuntu:24.04" }); + const ir = pipeline([p.test(), p.lint()]); expect(ir.graph.nodes.length).toBeGreaterThanOrEqual(4); expect(ir.version).toBe("0"); }); diff --git a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/rust.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/rust.test.ts index f12a3ef9..028b9c37 100644 --- a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/rust.test.ts +++ b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/rust.test.ts @@ -142,9 +142,7 @@ describe("rust.toolchain", () => { it("produces valid pipeline IR", () => { const r = rust.toolchain(); - const ir = pipeline([r.build(), r.test(), r.clippy(), r.fmt()], { - defaultImage: "ubuntu:24.04", - }); + const ir = pipeline([r.build(), r.test(), r.clippy(), r.fmt()]); expect(ir.graph.nodes.length).toBeGreaterThanOrEqual(4); expect(ir.version).toBe("0"); }); @@ -237,9 +235,7 @@ describe("rust.project", () => { it("with base skips apt", () => { const base = sh("custom base"); const proj = rust.project({ path: "cli", base }); - const ir = pipeline([proj.test(), proj.clippy(), proj.fmt()], { - defaultImage: "ubuntu:24.04", - }); + const ir = pipeline([proj.test(), proj.clippy(), proj.fmt()]); const c = cmds(ir); expect( c.filter((cmd: string) => cmd.includes("apt-get install")), @@ -249,9 +245,7 @@ describe("rust.project", () => { it("produces valid pipeline IR", () => { const proj = rust.project({ path: "cli" }); - const ir = pipeline([proj.test(), proj.clippy(), proj.fmt()], { - defaultImage: "ubuntu:24.04", - }); + const ir = pipeline([proj.test(), proj.clippy(), proj.fmt()]); expect(ir.version).toBe("0"); expect(ir.graph.nodes.length).toBeGreaterThanOrEqual(4); }); diff --git a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/shared.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/shared.test.ts index 4bacbf62..187e4d0c 100644 --- a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/shared.test.ts +++ b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/shared.test.ts @@ -50,7 +50,7 @@ describe("aptBase", () => { }); const r = rust.toolchain({ base }); const p = uv({ path: "dsls/harmont-py", base }); - const ir = pipeline([r.build(), p.test()], { defaultImage: "ubuntu:24.04" }); + const ir = pipeline([r.build(), p.test()]); const cmds = ir.graph.nodes.map( (n: { step: { cmd: string } }) => n.step.cmd, ); diff --git a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/zig.test.ts b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/zig.test.ts index f1494255..1347f12f 100644 --- a/crates/hm-dsl-engine/harmont-ts/tests/toolchains/zig.test.ts +++ b/crates/hm-dsl-engine/harmont-ts/tests/toolchains/zig.test.ts @@ -88,9 +88,7 @@ describe("zig multi-project pipeline", () => { const tc = zig(); const a = tc.project("lib-a"); const b = tc.project("lib-b"); - const ir = pipeline([a.build(), a.test(), b.build(), b.test()], { - defaultImage: "ubuntu:24.04", - }); + const ir = pipeline([a.build(), a.test(), b.build(), b.test()]); expect(ir.graph.nodes.length).toBeGreaterThanOrEqual(5); expect(ir.version).toBe("0"); }); diff --git a/crates/hm-exec/src/local/runner/vm.rs b/crates/hm-exec/src/local/runner/vm.rs index 9c2d2b70..325634fc 100644 --- a/crates/hm-exec/src/local/runner/vm.rs +++ b/crates/hm-exec/src/local/runner/vm.rs @@ -78,12 +78,15 @@ async fn run_step_vm(vm: &HmVm, ctx: &StepContext, input: ExecutorInput) -> Resu let source = if let Some(ref snap) = input.parent_snapshot { ImageSource::Snapshot(SnapshotId::new(snap.0.clone())) } else { + // Imageless root step: default to an apt-capable base. The SDK's + // toolchains all assume `apt-get`, so `ubuntu:24.04` is the + // across-the-board default (alpine has no apt-get). ImageSource::Image( input .step .image .clone() - .unwrap_or_else(|| "alpine:latest".to_string()), + .unwrap_or_else(|| "ubuntu:24.04".to_string()), ) }; diff --git a/crates/hm-pipeline-ir/tests/e2e_fixtures.rs b/crates/hm-pipeline-ir/tests/e2e_fixtures.rs index e8c93112..7a6fd951 100644 --- a/crates/hm-pipeline-ir/tests/e2e_fixtures.rs +++ b/crates/hm-pipeline-ir/tests/e2e_fixtures.rs @@ -53,7 +53,7 @@ fn edge_kinds(g: &PipelineGraph) -> (usize, usize) { #[test] fn python_monorepo_ci() { let g = load_fixture("python", "monorepo-ci"); - assert_eq!(g.default_image(), Some("ubuntu:24.04")); + assert_eq!(g.default_image(), None); assert!(g.node_count() >= 15, "nodes: {}", g.node_count()); let labels = step_labels(&g); assert!(labels.iter().any(|l| l.contains("go"))); @@ -72,16 +72,28 @@ fn python_monorepo_ci() { #[test] fn python_rust_release() { let g = load_fixture("python", "rust-release"); - assert_eq!(g.default_image(), Some("ubuntu:24.04")); + assert_eq!(g.default_image(), None); assert!(g.node_count() >= 5, "nodes: {}", g.node_count()); let labels = step_labels(&g); assert!(labels.iter().any(|l| l.contains("rust"))); + // The DSL now injects image on each imageless root step directly. + let apt_base = g + .dag() + .graph() + .node_references() + .find(|(_, t)| t.step.key == "apt-base") + .map(|(_, t)| t.step.image.as_deref()); + assert_eq!( + apt_base, + Some(Some("ubuntu:24.04")), + "root step apt-base must carry explicit image" + ); } #[test] fn python_zig_node_polyglot() { let g = load_fixture("python", "zig-node-polyglot"); - assert_eq!(g.default_image(), Some("ubuntu:24.04")); + assert_eq!(g.default_image(), None); assert!(g.node_count() >= 10, "nodes: {}", g.node_count()); let labels = step_labels(&g); assert!(labels.iter().any(|l| l.contains("zig"))); @@ -95,7 +107,7 @@ fn python_zig_node_polyglot() { #[test] fn python_kitchen_sink() { let g = load_fixture("python", "kitchen-sink"); - assert_eq!(g.default_image(), Some("ubuntu:24.04")); + assert_eq!(g.default_image(), None); assert!(g.node_count() >= 10, "nodes: {}", g.node_count()); let labels = step_labels(&g); assert!(labels.iter().any(|l| l.contains("python"))); @@ -116,28 +128,28 @@ fn python_kitchen_sink() { #[test] fn ts_monorepo_ci() { let g = load_fixture("ts", "monorepo-ci"); - assert_eq!(g.default_image(), Some("ubuntu:24.04")); + assert_eq!(g.default_image(), None); assert!(g.node_count() >= 15); } #[test] fn ts_rust_release() { let g = load_fixture("ts", "rust-release"); - assert_eq!(g.default_image(), Some("ubuntu:24.04")); + assert_eq!(g.default_image(), None); assert!(g.node_count() >= 5); } #[test] fn ts_zig_node_polyglot() { let g = load_fixture("ts", "zig-node-polyglot"); - assert_eq!(g.default_image(), Some("ubuntu:24.04")); + assert_eq!(g.default_image(), None); assert!(g.node_count() >= 10); } #[test] fn ts_kitchen_sink() { let g = load_fixture("ts", "kitchen-sink"); - assert_eq!(g.default_image(), Some("ubuntu:24.04")); + assert_eq!(g.default_image(), None); assert!(g.node_count() >= 10); } diff --git a/crates/hm/README.md b/crates/hm/README.md index a66993a2..1f0821f8 100644 --- a/crates/hm/README.md +++ b/crates/hm/README.md @@ -79,7 +79,7 @@ def ci() -> hm.Step: "echo independent work", label="other", ) - return hm.pipeline(fetch, work, default_image="ubuntu:24.04") + return hm.pipeline(fetch, work) ``` For larger pipelines, compose with `@hm.target` and typed fixture params: diff --git a/crates/hm/src/commands/init_templates/cmake.py b/crates/hm/src/commands/init_templates/cmake.py index b37f0668..a0c4efdc 100644 --- a/crates/hm/src/commands/init_templates/cmake.py +++ b/crates/hm/src/commands/init_templates/cmake.py @@ -7,7 +7,6 @@ @hm.pipeline( "ci", env={"CI": "true"}, - default_image="ubuntu:24.04", triggers=[hm.push(branch="main")], ) def ci() -> tuple[hm.Step, ...]: diff --git a/crates/hm/src/commands/init_templates/elixir.py b/crates/hm/src/commands/init_templates/elixir.py index 46eefb28..085ea0d7 100644 --- a/crates/hm/src/commands/init_templates/elixir.py +++ b/crates/hm/src/commands/init_templates/elixir.py @@ -7,7 +7,6 @@ @hm.pipeline( "ci", env={"CI": "true", "MIX_ENV": "test"}, - default_image="ubuntu:24.04", triggers=[hm.push(branch="main")], ) def ci() -> tuple[hm.Step, ...]: diff --git a/crates/hm/src/commands/init_templates/js.ts b/crates/hm/src/commands/init_templates/js.ts index aeca70eb..5cfc833e 100644 --- a/crates/hm/src/commands/init_templates/js.ts +++ b/crates/hm/src/commands/init_templates/js.ts @@ -9,7 +9,6 @@ const pipelines: PipelineDefinition[] = [ triggers: [push({ branch: "main" })], pipeline: pipeline([project.run("build"), project.run("test"), project.run("lint")], { env: { CI: "true" }, - defaultImage: "ubuntu:24.04", }), }, ]; diff --git a/crates/hm/src/commands/init_templates/nextjs.ts b/crates/hm/src/commands/init_templates/nextjs.ts index 7fdd4544..2e4cbccb 100644 --- a/crates/hm/src/commands/init_templates/nextjs.ts +++ b/crates/hm/src/commands/init_templates/nextjs.ts @@ -9,7 +9,6 @@ const pipelines: PipelineDefinition[] = [ triggers: [push({ branch: "main" })], pipeline: pipeline([project.run("build"), project.run("lint")], { env: { CI: "true" }, - defaultImage: "ubuntu:24.04", }), }, ]; diff --git a/crates/hm/src/commands/init_templates/python.py b/crates/hm/src/commands/init_templates/python.py index 0d34c3b1..6b122046 100644 --- a/crates/hm/src/commands/init_templates/python.py +++ b/crates/hm/src/commands/init_templates/python.py @@ -13,7 +13,6 @@ def project() -> PythonToolchain: @hm.pipeline( "ci", env={"CI": "true"}, - default_image="ubuntu:24.04", triggers=[hm.push(branch="main")], ) def ci(project: hm.Target[PythonToolchain]) -> tuple[hm.Step, ...]: diff --git a/crates/hm/src/commands/init_templates/rust.py b/crates/hm/src/commands/init_templates/rust.py index 2a41052c..bcb64f56 100644 --- a/crates/hm/src/commands/init_templates/rust.py +++ b/crates/hm/src/commands/init_templates/rust.py @@ -13,7 +13,6 @@ def project() -> RustToolchain: @hm.pipeline( "ci", env={"CI": "true"}, - default_image="ubuntu:24.04", triggers=[hm.push(branch="main")], ) def ci(project: hm.Target[RustToolchain]) -> tuple[hm.Step, ...]: diff --git a/crates/hm/src/commands/init_templates/skill_convert_gha.md b/crates/hm/src/commands/init_templates/skill_convert_gha.md index e50796c2..cb1e9eb1 100644 --- a/crates/hm/src/commands/init_templates/skill_convert_gha.md +++ b/crates/hm/src/commands/init_templates/skill_convert_gha.md @@ -54,7 +54,7 @@ Convert existing GitHub Actions workflows (`.github/workflows/*.yml` / `*.yaml`) | `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. | | `actions/setup-*` (setup-node, setup-python, etc.) | Harmont toolchains (`hm.js`, `hm.python`, etc.) | Toolchains handle installation. Specify version via toolchain config. | | `actions/checkout` | **Not needed — source is always available** | Harmont automatically provides the source code to every step. | - | `runs-on: ubuntu-latest` | `default_image: "ubuntu:24.04"` | Harmont runs steps in Docker containers | + | `runs-on: ubuntu-latest` | (default base is `ubuntu:24.04`; set a per-step `image="..."` to override) | Harmont runs steps in Docker containers | | `services:` (e.g., postgres) | Service containers in step config | Check docs for service container syntax | | `matrix:` | Multiple pipelines or parameterized steps | No direct matrix — may need separate pipeline definitions or `.fork()` | | `env:` / `secrets.*` | `env: {}` on pipeline or step | Secrets must be passed as environment variables | diff --git a/crates/hm/src/commands/init_templates/skill_write_pipeline.md b/crates/hm/src/commands/init_templates/skill_write_pipeline.md index ac349116..fdc62ffa 100644 --- a/crates/hm/src/commands/init_templates/skill_write_pipeline.md +++ b/crates/hm/src/commands/init_templates/skill_write_pipeline.md @@ -58,7 +58,7 @@ Write, modify, or extend Harmont CI pipelines defined in `.hm/pipeline.py` (Pyth - Prefer toolchains over raw `sh()` calls when a toolchain exists for the language. - Use `.fork()` for steps that can run in parallel. - Set triggers (`push`, `pull_request`) appropriate to the project. - - Use `default_image: "ubuntu:24.04"` unless the project needs something specific. + - Steps run on `ubuntu:24.04` by default; set a per-step `image="..."` only when a specific step needs a different base. - Set `env: {"CI": "true"}` on the pipeline. 5. **Validate the pipeline renders correctly:** diff --git a/crates/hm/src/commands/init_templates/zig.ts b/crates/hm/src/commands/init_templates/zig.ts index aeb456cc..a755a0b8 100644 --- a/crates/hm/src/commands/init_templates/zig.ts +++ b/crates/hm/src/commands/init_templates/zig.ts @@ -9,7 +9,6 @@ const pipelines: PipelineDefinition[] = [ triggers: [push({ branch: "main" })], pipeline: pipeline([project.build(), project.test(), project.fmt()], { env: { CI: "true" }, - defaultImage: "ubuntu:24.04", }), }, ]; diff --git a/crates/hm/tests/chain_failure_render.rs b/crates/hm/tests/chain_failure_render.rs index ca08661e..4df4fe9b 100644 --- a/crates/hm/tests/chain_failure_render.rs +++ b/crates/hm/tests/chain_failure_render.rs @@ -10,7 +10,7 @@ const FAILING_PIPELINE_PY: &str = r#" import harmont as hm -@hm.pipeline("failing", default_image="alpine:3.20") +@hm.pipeline("failing") def failing() -> hm.Step: return hm.sh("exit 7", label="oops", image="alpine:3.20") "#; diff --git a/crates/hm/tests/cmd_run_local_autoselect.rs b/crates/hm/tests/cmd_run_local_autoselect.rs index 740d71bf..71ff0b61 100644 --- a/crates/hm/tests/cmd_run_local_autoselect.rs +++ b/crates/hm/tests/cmd_run_local_autoselect.rs @@ -10,7 +10,7 @@ const PIPELINE_PY: &str = r#" import harmont as hm -@hm.pipeline("only-one", default_image="alpine:3.20") +@hm.pipeline("only-one") def only_one() -> hm.Step: return hm.sh("echo autoselected", label="hi", image="alpine:3.20") "#; @@ -40,13 +40,13 @@ fn many_pipelines_still_requires_arg() { r#" import harmont as hm -@hm.pipeline("a", default_image="alpine:3.20") +@hm.pipeline("a") def a() -> hm.Step: - return hm.sh("echo a") + return hm.sh("echo a", image="alpine:3.20") -@hm.pipeline("b", default_image="alpine:3.20") +@hm.pipeline("b") def b() -> hm.Step: - return hm.sh("echo b") + return hm.sh("echo b", image="alpine:3.20") "#, ) .unwrap(); diff --git a/crates/hm/tests/cmd_run_local_format.rs b/crates/hm/tests/cmd_run_local_format.rs index dbb52ba4..78ea72bc 100644 --- a/crates/hm/tests/cmd_run_local_format.rs +++ b/crates/hm/tests/cmd_run_local_format.rs @@ -10,7 +10,7 @@ const PIPELINE_PY: &str = r#" import harmont as hm -@hm.pipeline("formatted", default_image="alpine:3.20") +@hm.pipeline("formatted") def formatted() -> hm.Step: return hm.sh("echo formatted-hello", label="hi", image="alpine:3.20") "#; diff --git a/crates/hm/tests/cmd_run_local_orchestrated.rs b/crates/hm/tests/cmd_run_local_orchestrated.rs index 7180d9d9..78869aaa 100644 --- a/crates/hm/tests/cmd_run_local_orchestrated.rs +++ b/crates/hm/tests/cmd_run_local_orchestrated.rs @@ -21,7 +21,7 @@ const PIPELINE_PY: &str = r#" import harmont as hm -@hm.pipeline("orchestrated", default_image="alpine:3.20") +@hm.pipeline("orchestrated") def orchestrated() -> hm.Step: return hm.sh("echo orchestrated hello", label="hi", image="alpine:3.20") "#; @@ -50,7 +50,7 @@ const CHAIN_PIPELINE_PY: &str = r#" import harmont as hm -@hm.pipeline("chain", default_image="alpine:3.20") +@hm.pipeline("chain") def chain() -> hm.Step: a = hm.sh("echo step-a > /tmp/a && cat /tmp/a", label="a", image="alpine:3.20") return a.sh("cat /tmp/a && echo step-b", label="b") diff --git a/crates/hm/tests/fixtures/pipelines/cow_chain.py b/crates/hm/tests/fixtures/pipelines/cow_chain.py index 70f39528..7ac9650c 100644 --- a/crates/hm/tests/fixtures/pipelines/cow_chain.py +++ b/crates/hm/tests/fixtures/pipelines/cow_chain.py @@ -2,9 +2,9 @@ import harmont as hm -@hm.pipeline("cow-chain", default_image="alpine:latest") +@hm.pipeline("cow-chain") def cow_chain(): - a = hm.sh("echo from-a > /workspace/a.txt", label="a") + a = hm.sh("echo from-a > /workspace/a.txt", label="a", image="alpine:latest") b = a.sh("cat /workspace/a.txt && echo from-b > /workspace/b.txt", label="b") c = b.sh("cat /workspace/a.txt && cat /workspace/b.txt && echo c-saw-both", label="c") return [c] diff --git a/crates/hm/tests/keep_going.rs b/crates/hm/tests/keep_going.rs index 7b9b572f..0027d055 100644 --- a/crates/hm/tests/keep_going.rs +++ b/crates/hm/tests/keep_going.rs @@ -9,7 +9,7 @@ const FORK_PIPELINE_PY: &str = r#" import harmont as hm -@hm.pipeline("two", default_image="alpine:3.20") +@hm.pipeline("two") def two(): root = hm.scratch().fork() a = root.sh("exit 1", label="fail-step", image="alpine:3.20") @@ -30,11 +30,11 @@ const CHAIN_PIPELINE_PY: &str = r#" import harmont as hm -@hm.pipeline("chain", default_image="alpine:3.20") +@hm.pipeline("chain") def chain(): a = hm.sh("exit 1", label="step-a", image="alpine:3.20") - b = a.sh("echo b", label="step-b", image="alpine:3.20") - c = b.sh("echo c", label="step-c", image="alpine:3.20") + b = a.sh("echo b", label="step-b") + c = b.sh("echo c", label="step-c") return c "#; diff --git a/crates/hm/tests/local_fork_cache.rs b/crates/hm/tests/local_fork_cache.rs index 411a770d..982eb0fd 100644 --- a/crates/hm/tests/local_fork_cache.rs +++ b/crates/hm/tests/local_fork_cache.rs @@ -25,16 +25,17 @@ fn write_pipeline(dir: &Path, marker_contents: &str) { import harmont as hm def build(): - base = hm.scratch().run( + base = hm.scratch().sh( "echo base-ran", label="base", cache=hm.forever(), + image="alpine:3.20", ) - child = base.fork(label="child").run( + child = base.fork(label="child").sh( "cat /workspace/marker.txt", label="child", ) - return hm.pipeline(child, default_image="alpine:3.20") + return hm.pipeline([child]) "#, ) .expect("pipeline.py"); diff --git a/crates/hm/tests/local_parallelism.rs b/crates/hm/tests/local_parallelism.rs index f18175a2..739d900d 100644 --- a/crates/hm/tests/local_parallelism.rs +++ b/crates/hm/tests/local_parallelism.rs @@ -23,9 +23,9 @@ fn write_pipeline(dir: &Path) { import harmont as hm def build(): - a = hm.scratch().run("sleep 3", label="sleep-a") - b = hm.scratch().run("sleep 3", label="sleep-b") - return hm.pipeline(a, b, default_image="alpine:3.20") + a = hm.scratch().sh("sleep 3", label="sleep-a", image="alpine:3.20") + b = hm.scratch().sh("sleep 3", label="sleep-b", image="alpine:3.20") + return hm.pipeline([a, b]) "#, ) .expect("pipeline.py"); diff --git a/examples/README.md b/examples/README.md index c638b8e4..f67c912c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -20,7 +20,7 @@ Minimal idiomatic starter projects, each wired up to a Harmont CI pipeline. Ever 1. Install the Harmont CLI (`cli/` in this repo, or `cargo install harmont-cli` once published). 2. `cd examples/` and run `hm run ci`. The CLI uses the project's `.hm/pipeline.py` and executes each step in a local Docker container (the default backend), sharing caches across runs. Use `--backend cloud` to submit the run to Harmont Cloud instead. -Every pipeline uses `default_image="ubuntu:24.04"` and the apt-base / language-install steps are cached forever — only the action leaves (`test`, `lint`, etc.) re-run after a code change. +Every pipeline runs on the default `ubuntu:24.04` base (set a per-step `image=` to override) and the apt-base / language-install steps are cached forever — only the action leaves (`test`, `lint`, etc.) re-run after a code change. ## What to copy diff --git a/examples/bun/.hm/pipeline.py b/examples/bun/.hm/pipeline.py index bec40093..4d95e853 100644 --- a/examples/bun/.hm/pipeline.py +++ b/examples/bun/.hm/pipeline.py @@ -7,7 +7,6 @@ @hm.pipeline( "ci", env={"CI": "true"}, - default_image="ubuntu:24.04", triggers=[hm.push(branch="main")], ) def ci() -> tuple[hm.Step, ...]: diff --git a/examples/c/.hm/pipeline.py b/examples/c/.hm/pipeline.py index f8ea110c..26a06ae5 100644 --- a/examples/c/.hm/pipeline.py +++ b/examples/c/.hm/pipeline.py @@ -7,7 +7,6 @@ @hm.pipeline( "ci", env={"CI": "true"}, - default_image="ubuntu:24.04", triggers=[hm.push(branch="main")], ) def ci() -> tuple[hm.Step, ...]: diff --git a/examples/cmake-advanced/.hm/pipeline.py b/examples/cmake-advanced/.hm/pipeline.py index c74440d3..218292e5 100644 --- a/examples/cmake-advanced/.hm/pipeline.py +++ b/examples/cmake-advanced/.hm/pipeline.py @@ -7,7 +7,6 @@ @hm.pipeline( "ci", env={"CI": "true"}, - default_image="ubuntu:24.04", triggers=[hm.push(branch="main"), hm.pr()], ) def ci() -> tuple[hm.Step, ...]: diff --git a/examples/cpp/.hm/pipeline.py b/examples/cpp/.hm/pipeline.py index 61cf6cf4..777f1e86 100644 --- a/examples/cpp/.hm/pipeline.py +++ b/examples/cpp/.hm/pipeline.py @@ -7,7 +7,6 @@ @hm.pipeline( "ci", env={"CI": "true"}, - default_image="ubuntu:24.04", triggers=[hm.push(branch="main")], ) def ci() -> tuple[hm.Step, ...]: diff --git a/examples/elixir-phoenix/.hm/pipeline.py b/examples/elixir-phoenix/.hm/pipeline.py index b6bd0206..cc089d42 100644 --- a/examples/elixir-phoenix/.hm/pipeline.py +++ b/examples/elixir-phoenix/.hm/pipeline.py @@ -12,7 +12,6 @@ def project(): @hm.pipeline( "ci", env={"CI": "true", "MIX_ENV": "test"}, - default_image="ubuntu:24.04", triggers=[hm.push(branch="main"), hm.pr()], ) def ci(project: hm.Target) -> tuple[hm.Step, ...]: @@ -31,7 +30,6 @@ def ci(project: hm.Target) -> tuple[hm.Step, ...]: @hm.pipeline( "deploy", env={"MIX_ENV": "prod"}, - default_image="ubuntu:24.04", triggers=[hm.push(branch="main")], ) def deploy(project: hm.Target) -> tuple[hm.Step, ...]: diff --git a/examples/elixir/.hm/pipeline.py b/examples/elixir/.hm/pipeline.py index 11ba30dc..d81d1671 100644 --- a/examples/elixir/.hm/pipeline.py +++ b/examples/elixir/.hm/pipeline.py @@ -7,7 +7,6 @@ @hm.pipeline( "ci", env={"CI": "true", "MIX_ENV": "test"}, - default_image="ubuntu:24.04", triggers=[hm.push(branch="main")], ) def ci() -> tuple[hm.Step, ...]: diff --git a/examples/go/.hm/pipeline.py b/examples/go/.hm/pipeline.py index 7c145a83..97eba899 100644 --- a/examples/go/.hm/pipeline.py +++ b/examples/go/.hm/pipeline.py @@ -13,7 +13,6 @@ def project() -> GoToolchain: @hm.pipeline( "ci", env={"CI": "true"}, - default_image="ubuntu:24.04", triggers=[hm.push(branch="main")], ) def ci(project: hm.Target[GoToolchain]) -> tuple[hm.Step, ...]: diff --git a/examples/nextjs/.hm/pipeline.py b/examples/nextjs/.hm/pipeline.py index 010cafba..c3bf8d4d 100644 --- a/examples/nextjs/.hm/pipeline.py +++ b/examples/nextjs/.hm/pipeline.py @@ -7,7 +7,6 @@ @hm.pipeline( "ci", env={"CI": "true"}, - default_image="ubuntu:24.04", triggers=[hm.push(branch="main")], ) def ci() -> tuple[hm.Step, ...]: diff --git a/examples/python-uv/.hm/pipeline.py b/examples/python-uv/.hm/pipeline.py index 3f3c0dcc..78baed7a 100644 --- a/examples/python-uv/.hm/pipeline.py +++ b/examples/python-uv/.hm/pipeline.py @@ -13,7 +13,6 @@ def project() -> PythonToolchain: @hm.pipeline( "ci", env={"CI": "true"}, - default_image="ubuntu:24.04", triggers=[hm.push(branch="main")], ) def ci(project: hm.Target[PythonToolchain]) -> tuple[hm.Step, ...]: diff --git a/examples/react/.hm/pipeline.py b/examples/react/.hm/pipeline.py index 0e4a8814..79e78cb1 100644 --- a/examples/react/.hm/pipeline.py +++ b/examples/react/.hm/pipeline.py @@ -7,7 +7,6 @@ @hm.pipeline( "ci", env={"CI": "true"}, - default_image="ubuntu:24.04", triggers=[hm.push(branch="main")], ) def ci() -> tuple[hm.Step, ...]: diff --git a/examples/rust/.hm/pipeline.py b/examples/rust/.hm/pipeline.py index e27a1490..b5d01135 100644 --- a/examples/rust/.hm/pipeline.py +++ b/examples/rust/.hm/pipeline.py @@ -13,7 +13,6 @@ def project() -> RustToolchain: @hm.pipeline( "ci", env={"CI": "true"}, - default_image="ubuntu:24.04", triggers=[hm.push(branch="main")], ) def ci(project: hm.Target[RustToolchain]) -> tuple[hm.Step, ...]: diff --git a/examples/typescript/.hm/pipeline.py b/examples/typescript/.hm/pipeline.py index 9da8404d..0bea8c87 100644 --- a/examples/typescript/.hm/pipeline.py +++ b/examples/typescript/.hm/pipeline.py @@ -7,7 +7,6 @@ @hm.pipeline( "ci", env={"CI": "true"}, - default_image="ubuntu:24.04", triggers=[hm.push(branch="main")], ) def ci() -> tuple[hm.Step, ...]: diff --git a/examples/zig-js/.hm/pipeline.py b/examples/zig-js/.hm/pipeline.py index 0d6fe5ca..f45f0850 100644 --- a/examples/zig-js/.hm/pipeline.py +++ b/examples/zig-js/.hm/pipeline.py @@ -53,7 +53,6 @@ def web_project(apt_base: hm.Target[hm.Step]) -> JsProject: @hm.pipeline( "ci", env={"CI": "true"}, - default_image="ubuntu:24.04", triggers=[hm.push(branch="main")], ) def ci( diff --git a/examples/zig/.hm/pipeline.py b/examples/zig/.hm/pipeline.py index e38289bf..f70ddb2c 100644 --- a/examples/zig/.hm/pipeline.py +++ b/examples/zig/.hm/pipeline.py @@ -7,7 +7,6 @@ @hm.pipeline( "ci", env={"CI": "true"}, - default_image="ubuntu:24.04", triggers=[hm.push(branch="main")], ) def ci() -> tuple[hm.Step, ...]: diff --git a/tests/e2e/fixtures/python/cmake-advanced.json b/tests/e2e/fixtures/python/cmake-advanced.json index 5a923649..b1ccf9b3 100644 --- a/tests/e2e/fixtures/python/cmake-advanced.json +++ b/tests/e2e/fixtures/python/cmake-advanced.json @@ -1,5 +1,4 @@ { - "default_image": "ubuntu:24.04", "graph": { "edge_property": "directed", "edges": [ diff --git a/tests/e2e/fixtures/python/kitchen-sink.json b/tests/e2e/fixtures/python/kitchen-sink.json index 028331bc..e1fcb4ea 100644 --- a/tests/e2e/fixtures/python/kitchen-sink.json +++ b/tests/e2e/fixtures/python/kitchen-sink.json @@ -1,5 +1,4 @@ { - "default_image": "ubuntu:24.04", "graph": { "edge_property": "directed", "edges": [ diff --git a/tests/e2e/fixtures/python/monorepo-ci.json b/tests/e2e/fixtures/python/monorepo-ci.json index e84a4f3b..ec0ef385 100644 --- a/tests/e2e/fixtures/python/monorepo-ci.json +++ b/tests/e2e/fixtures/python/monorepo-ci.json @@ -1,5 +1,4 @@ { - "default_image": "ubuntu:24.04", "graph": { "edge_property": "directed", "edges": [ diff --git a/tests/e2e/fixtures/python/rust-release.json b/tests/e2e/fixtures/python/rust-release.json index cd40ff9d..9d2af6e4 100644 --- a/tests/e2e/fixtures/python/rust-release.json +++ b/tests/e2e/fixtures/python/rust-release.json @@ -1,5 +1,4 @@ { - "default_image": "ubuntu:24.04", "graph": { "edge_property": "directed", "edges": [ diff --git a/tests/e2e/fixtures/python/zig-node-polyglot.json b/tests/e2e/fixtures/python/zig-node-polyglot.json index 76406024..0ddbebff 100644 --- a/tests/e2e/fixtures/python/zig-node-polyglot.json +++ b/tests/e2e/fixtures/python/zig-node-polyglot.json @@ -1,5 +1,4 @@ { - "default_image": "ubuntu:24.04", "graph": { "edge_property": "directed", "edges": [ diff --git a/tests/e2e/fixtures/ts/cmake-advanced.json b/tests/e2e/fixtures/ts/cmake-advanced.json index 5a923649..b1ccf9b3 100644 --- a/tests/e2e/fixtures/ts/cmake-advanced.json +++ b/tests/e2e/fixtures/ts/cmake-advanced.json @@ -1,5 +1,4 @@ { - "default_image": "ubuntu:24.04", "graph": { "edge_property": "directed", "edges": [ diff --git a/tests/e2e/fixtures/ts/kitchen-sink.json b/tests/e2e/fixtures/ts/kitchen-sink.json index 028331bc..e1fcb4ea 100644 --- a/tests/e2e/fixtures/ts/kitchen-sink.json +++ b/tests/e2e/fixtures/ts/kitchen-sink.json @@ -1,5 +1,4 @@ { - "default_image": "ubuntu:24.04", "graph": { "edge_property": "directed", "edges": [ diff --git a/tests/e2e/fixtures/ts/monorepo-ci.json b/tests/e2e/fixtures/ts/monorepo-ci.json index e84a4f3b..ec0ef385 100644 --- a/tests/e2e/fixtures/ts/monorepo-ci.json +++ b/tests/e2e/fixtures/ts/monorepo-ci.json @@ -1,5 +1,4 @@ { - "default_image": "ubuntu:24.04", "graph": { "edge_property": "directed", "edges": [ diff --git a/tests/e2e/fixtures/ts/rust-release.json b/tests/e2e/fixtures/ts/rust-release.json index cd40ff9d..9d2af6e4 100644 --- a/tests/e2e/fixtures/ts/rust-release.json +++ b/tests/e2e/fixtures/ts/rust-release.json @@ -1,5 +1,4 @@ { - "default_image": "ubuntu:24.04", "graph": { "edge_property": "directed", "edges": [ diff --git a/tests/e2e/fixtures/ts/zig-node-polyglot.json b/tests/e2e/fixtures/ts/zig-node-polyglot.json index 76406024..0ddbebff 100644 --- a/tests/e2e/fixtures/ts/zig-node-polyglot.json +++ b/tests/e2e/fixtures/ts/zig-node-polyglot.json @@ -1,5 +1,4 @@ { - "default_image": "ubuntu:24.04", "graph": { "edge_property": "directed", "edges": [