feat(dev): hm dev local-deployment CLI + docker-gated CI#1
Merged
Conversation
16 tasks: clap scaffolding, naming/registry/topo modules, extend DockerClient with network + service + commit + inspect primitives, log mux, service spec converter, build_image_from_pipeline, `hm dev up` orchestrator, then down/ls/port-of/logs/exec, then docker-gated integration test, then PR-readiness. Cross-references the spec on harmont-py's feat/hm-dev-deploy branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
clap definitions for up | down | ls | logs | port-of | exec. Module placeholders for every component file referenced by later tasks. Every subcommand currently errors as "not yet implemented" so the rest of the plan can drop in implementations independently. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
10-hex-char worktree-hash from canonical path; 6-hex-char session-id per `hm dev up`. Constants for the four `harmont.*` Docker labels. resolve_worktree_root prefers `git rev-parse --show-toplevel` and falls back to cwd so we never refuse to run for lack of a git repo. Fix pre-existing deny(unsafe_code) lint in creds_store tests: the unsafe set_var calls were already there but now block test compilation. Added #[allow(unsafe_code)] scoped to the test module. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Serde types match the spec's v0 schema (§1). Unknown drivers fall back to RegEntry::Unhandled via #[serde(other)] so hm dev ls can display them. dump() shells out to `python -m harmont.dev --dump-registry`; HARMONT_PYTHON overrides the python binary for test environments. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Kahn's algorithm; one BTreeSet of selected slugs grown from explicit requests by walking transitive deps (or not, when --no-deps is set); levels grouped by indeg=0 so siblings boot in parallel. Cycle detection mirrors the python-side check; useful as defense-in-depth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds: create_network, remove_network, start_service (long-lived container with port publishing + bridge network alias), inspect_ports (container -> host port map), stop_container, remove_container, list_containers_by_label. ServiceSpec + ServiceSpecBuilder give the call sites a fluent API without polluting docker_client with N-keyword args. All "not found" errors are swallowed so teardown is idempotent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stamps the canonical harmont.{worktree,session,driver} labels on
the network so `hm dev down --all` can find them later. Thin
wrapper around DockerClient::{create,remove}_network — kept here
rather than inlined in up.rs so down.rs can sweep with the same
shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PerSlug buffers partial chunks across docker-log frames and flushes on \n. owo-colors provides 6-color palette cycled by slug hash so colors are stable across runs. NO_COLOR / --no-color path drops the ANSI escapes entirely. Slug prefix is left-padded to a shared width for column alignment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves relative volume host paths against worktree_root, preserves :ro suffix on container paths, parses port_mapping container ports (filtering to entries with the __hm_dev_port__ sentinel — every entry in v1, but explicit so future pinned-int values can coexist), and stamps the canonical labels. ResolvedSpec owns its data so the up handler can hold it across the boot lifetime. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds build_image_from_pipeline to orchestrator/mod.rs and the required run_pipeline_v0_one_shot wrapper in commands/run/local.rs. STUB: run_pipeline_v0_one_shot returns Err(...) rather than a real container id. The existing orchestrator (scheduler::run_chain) commits each step's container to a SnapshotRef image tag and does not preserve the container id, so wiring the return value requires extending both StepResult and run_chain — a change that exceeds the 50-line threshold and is deferred to a dedicated task. Impact: raw image= deployments in hm dev up work end-to-end without this wrapper. Only from_=Step deployments are affected; they surface a clear error message directing users to image= for v1. The smoke test (build_image_from_pipeline_is_callable) verifies the function signature compiles. run/mod.rs re-exports run_pipeline_v0_one_shot so the orchestrator can call it without reaching into the private local module. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end flow: subprocess registry dump → topo boot plan → per- session bridge network → parallel boot per level → per-container log stream into the shared mux → wait SIGINT/SIGTERM → reverse-order teardown (stop+remove containers, remove network). Image resolution: raw image is pulled if missing; from_=Step lowers to a cache-keyed build tag (`hm-build-<wt>-<slug>:<key>`) and only rebuilds when --rebuild is set or the tag is absent. Also adds `inner_for_logs()` accessor to DockerClient for bollard log streaming in up.rs and future subcommands. Companion fixes to pass `cargo clippy --all-targets -p harmont-cli -- -D warnings`: logmux StdoutLock moved out of await (Send fix), builder methods gain #[must_use], match arms merged, naming module uses hex::encode, stub handlers gain # Errors docs and #[expect(clippy::unused_async)], topo uses map_or/ToString::to_string, service_spec doc fixes, creds_store uses is_some_and, build.rs use hoisted to module level. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Filters by harmont.driver=local (with --all) or harmont.worktree=<this> (default), optionally further filtered by --session and explicit slugs. stop+remove every match, then remove the corresponding per- session network. Idempotent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Walks the python-side registry and Docker container labels in parallel; prints one row per (slug, session) for live ones and one row per registered-but-not-running slug. Non-local drivers appear with a hint pointing at the matching driver's `up`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
stdout prints the bare integer (designed for $(...) use). Exit codes per spec § 2: 0 ok, 4 known-but-stopped, 5 unknown / multi-session ambiguity. Multi-session error enumerates sessions w/ host ports so the user can pick one with --session. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both share the multi-session ambiguity rule with port-of (exit 5) and the not-running rule (exit 4). logs streams docker logs to stdout; exec allocates a TTY and forwards the exit code. exec default cmd is \`sh -l\` for interactive shell. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers the end-to-end happy path: spawn `hm dev up` against an in-tmpdir .harmont/deploys.py, wait for the "all up." marker, query port-of in a second process, SIGINT to tear down, then verify port-of returns exit 4 on the stopped slug. Gated behind --features docker-integration so CI without Docker skips it. Document HARMONT_PYTHON in the test header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fix clippy doc_markdown nit on tests/dev_integration.rs (backticks around HARMONT_PYTHON). All gates pass: cargo clippy --all-targets -p harmont-cli -- -D warnings clean cargo test -p harmont-cli --lib 75 passed hm dev --help lists 6 subcommands The branch is feature-complete for v1 modulo from_=Step builds (stubbed in Task 9 with a clear error message). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks. Swap the heavy postgres+node example across all docs/tests in both repos for a hashicorp/http-echo:1.0 pair (5MB, boots in ~1s, still exercises @hm.deploy + hm.Dep[T] + bridge-net DNS). Add GitHub Actions CI in both repos: harmont-cli with a docker-gated integration job that boots the deployment and HTTP-GETs the host port; harmont-py with pytest + ruff + mypy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-planned per feedback: examples should use native language
facilities, not a third-party container image. Swap
hashicorp/http-echo:1.0 → python:3.12-alpine + `python -m http.server`
across every example site. Integration assertion changes from
exact-body match ("hi from harmont") to a substring match on the
literal title that Python's stdlib HTTP server always emits
("Directory listing").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap the postgres-based integration test for `python -m http.server` running inside `python:3.12-alpine` — pulls 50MB instead of 80MB, boots in <1s, and uses Python's stdlib HTTP server (no third-party image dependency). Add an actual HTTP GET against the host port + body assertion (the response is python http.server's directory listing, whose body always contains "Directory listing for /") so the test validates the whole chain: container start → bridge net → port publish → image CMD honored → server actually serving. ureq is the new dev-dep (default-features=false, just `tls` feature). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two jobs. `unit` runs cargo build/test/clippy on every PR + push; ~5 min w/ cache. `integration` is the deployment-goes-up gate: pulls python:3.12-alpine, runs `hm dev up hello` end-to-end via the docker-gated integration test (added in the prior commit), and HTTP-GETs the host port to confirm the python -m http.server inside the container is actually serving. The integration job checks out harmont-py from the matching branch (falling back to main) so PR-branch python-side changes participate in CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plans are scratchpads for implementation; once code lands the spec + tests + commit messages carry the load. Remove the plans dir entirely (including the pre-existing ones from prior work: harmont-cli-examples-passing, publishable-harmont-cli, readme-quickstart-rewrite, zig-js-parallel-demo). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
We removed all plans last commit. The one remaining file in docs/superpowers/specs/ is the pre-existing zig-js-parallel-demo design spec from prior work — symmetric cleanup; specs are implementation-time scaffolding the same as plans. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the Rust CLI half of local deployments:
hm dev up | down | ls | logs | port-of | exec. Together with harmont-py#1 this lands the v1 surface.crates/hm/src/commands/dev/subcommand tree.hm dev upshells out topython -m harmont.dev --dump-registry(provided by the py PR), computes a topo boot plan, creates a per-session bridge network, parallel-boots containers per level, multiplexes per-slug colored logs to stdout, tears down cleanly on SIGINT.hm-<worktree-hash>-<slug>-<session>(multiplehm dev upinstances in the same worktree coexist via 6-hex session-id discriminator). OS-assigned host ports via-p :CPORT.DockerClient(orchestrator/docker_client.rs) withcreate_network,remove_network,start_service,inspect_ports,commit_container,stop_container,remove_container,list_containers_by_label,exec_tty.from_=Stepdeployment builds stubbed in v1 with a clear error message — wiring through the existing local scheduler'sStepResultrequires refactor out of scope for v1. Rawimage="..."deployments work fully end-to-end..github/workflows/ci.yml:unitjob —cargo build --all-targets,cargo test --lib,cargo clippy -D warningson every PR + push.integrationjob — docker-gated. Checks out harmont-py from matching branch (with main fallback), pre-pullspython:3.12-alpine, runs the integration test: spawnshm dev up hello, pollspython -m http.server, HTTP-GETs the host port, asserts body containsDirectory listing, SIGINTs, asserts post-teardown exit code 4.Spec: cross-repo, lives in harmont-py.
Plan:
docs/superpowers/plans/2026-05-21-hm-dev-deploy-cli.mdTest plan
cargo build -p harmont-cli --all-targets— cleancargo test -p harmont-cli --lib— 75 passedcargo clippy --all-targets -p harmont-cli -- -D warnings— cleancargo build -p harmont-cli --tests --features docker-integration— cleanhm dev --help/hm dev up --help/hm dev port-of --helprender correctlyunitjob greenintegrationjob green (bootspython -m http.server, HTTP body assertion passes)hm dev upagainst a.harmont/deploys.pywith apython -m http.serverdeployment +curl localhost:\$(hm dev port-of hello 5678)returns Directory listingPrereq: harmont-py#1 must be merged (or the integration CI's matching-branch checkout picks up its `feat/hm-dev-deploy` branch).
🤖 Generated with Claude Code