Skip to content

Commit 904721f

Browse files
ci: run the full workspace test suite (CLI-37) (#109)
1 parent 6ec49ef commit 904721f

2 files changed

Lines changed: 55 additions & 35 deletions

File tree

.hm/ci.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,18 @@ def shared_base() -> hm.Step:
2020

2121
@hm.target()
2222
def rust_project(shared_base: hm.Target[hm.Step]) -> tuple[hm.Step, ...]:
23-
project = hm.rust.project(path=".", base=shared_base)
23+
# Build esbuild into the image first: hm-dsl-engine's build.rs shells out
24+
# to esbuild at compile time to embed the TypeScript SDK bundle. Without it
25+
# the build emits an 18-byte stub and the bundled_sources tests fail (CLI-37).
26+
# Installing Node here also lets the JS-runtime-gated render tests actually
27+
# run instead of self-skipping.
28+
ts_deps = hm.js.project(
29+
path="crates/hm-dsl-engine/harmont-ts",
30+
base=shared_base,
31+
).install()
32+
project = hm.rust.project(path=".", base=ts_deps)
2433
return hm.group([
25-
project.test(flags=("--lib",), packages=("harmont-cli",)),
26-
project.test(
27-
flags=("--test", "cmd_init"),
28-
packages=("harmont-cli",),
29-
label=":test_tube: init template roundtrip",
30-
),
34+
project.test(), # cargo test --workspace --locked — every package
3135
project.clippy(),
3236
project.fmt(),
3337
])

crates/hm/src/commands/run/mod.rs

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,8 @@ use crate::error::{ErrorCategory, HmError};
3131
/// the backend rejects the build, authentication fails, the network is
3232
/// unreachable, the local daemon is down, or the pipeline fails to render.
3333
pub async fn handle(args: RunArgs, ctx: RunContext) -> Result<i32> {
34-
// 1. Build the backend. Cloud needs auth + org resolution BEFORE any
35-
// (local) render work — fail fast on a missing token.
36-
// Resolution: explicit --backend > legacy --cloud alias > config.backend
37-
// (figment-layered default "docker").
34+
// 1. Resolve the backend name: explicit --backend > legacy --cloud alias >
35+
// config.backend (figment-layered default "docker").
3836
let backend_name = args
3937
.backend
4038
.clone()
@@ -47,7 +45,13 @@ pub async fn handle(args: RunArgs, ctx: RunContext) -> Result<i32> {
4745
})
4846
.unwrap_or_else(|| ctx.config.backend.clone());
4947

50-
let backend: Box<dyn hm_exec::ExecutionBackend> = if backend_name == "cloud" {
48+
// 2. Cloud needs auth + org resolution up front — fail fast on a missing
49+
// token before any render work. We resolve the credentials here but
50+
// defer *constructing* the backend (and, for local runs, *connecting* to
51+
// Docker) until after the pipeline renders, so an unknown slug or a
52+
// missing/ambiguous pipeline argument fails with a helpful message
53+
// instead of a daemon-connection error.
54+
let cloud_creds = if backend_name == "cloud" {
5155
let api_url = ctx.config.cloud.api_url.clone();
5256
let token = hm_config::creds::cloud_token(&api_url).context(
5357
"`hm run --backend cloud` requires authentication — run `hm cloud login` or set HARMONT_API_TOKEN",
@@ -57,23 +61,43 @@ pub async fn handle(args: RunArgs, ctx: RunContext) -> Result<i32> {
5761
.clone()
5862
.or_else(|| ctx.config.cloud.org.clone())
5963
.context("no organization — pass --org or set `[cloud] org = \"\"` in .hm/config.toml or ~/.config/hm/config.toml")?;
60-
let client = harmont_cloud::HarmontClient::with_base_url(token, &api_url);
61-
Box::new(hm_exec::CloudBackend::new(client, api_url, org))
64+
Some((api_url, token, org))
65+
} else if backend_name != "docker" {
66+
anyhow::bail!("unknown --backend '{backend_name}'\n available: docker, cloud");
6267
} else {
63-
// Local execution on a hm-vm VmBackend, selected by name.
64-
let vm_backend: std::sync::Arc<dyn hm_vm::VmBackend> = match backend_name.as_str() {
65-
"docker" => std::sync::Arc::new(
68+
None
69+
};
70+
71+
// 3. Render + parse the plan once (shared by every backend). This validates
72+
// the pipeline argument — unknown slug, or zero/many declared pipelines
73+
// — before we connect to any daemon.
74+
let (repo_root, slug, ir_json) = render_pipeline(&args, &ctx).await?;
75+
let plan = hm_exec::Plan::parse(ir_json).map_err(|e| backend_anyhow(&e))?;
76+
77+
// 4. Pick the renderer — this validates `--format` — before any daemon
78+
// connection, so an unknown format fails fast without a running Docker.
79+
let use_logs = args.logs
80+
|| std::env::var_os("CI").is_some_and(|v| !v.is_empty())
81+
|| !hm_render::stderr_interactive();
82+
let renderer = hm_render::renderer_for(&args.format, ctx.output.color_enabled(), use_logs)?;
83+
84+
// 5. Build the backend. For local runs this is where we connect to Docker.
85+
let backend: Box<dyn hm_exec::ExecutionBackend> =
86+
if let Some((api_url, token, org)) = cloud_creds {
87+
let client = harmont_cloud::HarmontClient::with_base_url(token, &api_url);
88+
Box::new(hm_exec::CloudBackend::new(client, api_url, org))
89+
} else {
90+
// Local execution on a hm-vm VmBackend (docker).
91+
let vm_backend: std::sync::Arc<dyn hm_vm::VmBackend> = std::sync::Arc::new(
6692
hm_vm::docker::DockerBackend::connect().map_err(|e| anyhow::anyhow!("{e:#}"))?,
67-
),
68-
other => anyhow::bail!("unknown --backend '{other}'\n available: docker, cloud"),
93+
);
94+
Box::new(hm_exec::LocalBackend::new(
95+
resolve_parallelism(&args),
96+
vm_backend,
97+
))
6998
};
70-
Box::new(hm_exec::LocalBackend::new(
71-
resolve_parallelism(&args),
72-
vm_backend,
73-
))
74-
};
7599

76-
// 2. Capability-driven flag validation (replaces the old silent ignoring).
100+
// 6. Capability-driven flag validation (replaces the old silent ignoring).
77101
let caps = backend.capabilities();
78102
if args.no_watch && !caps.supports_no_watch {
79103
anyhow::bail!(
@@ -94,9 +118,7 @@ pub async fn handle(args: RunArgs, ctx: RunContext) -> Result<i32> {
94118
);
95119
}
96120

97-
// 3. Render + parse the plan once (shared by every backend).
98-
let (repo_root, slug, ir_json) = render_pipeline(&args, &ctx).await?;
99-
let plan = hm_exec::Plan::parse(ir_json).map_err(|e| backend_anyhow(&e))?;
121+
// 7. Assemble the run request.
100122
let (branch, commit) = git_metadata(&repo_root, args.branch.clone());
101123
let req = hm_exec::RunRequest {
102124
plan,
@@ -116,13 +138,7 @@ pub async fn handle(args: RunArgs, ctx: RunContext) -> Result<i32> {
116138
},
117139
};
118140

119-
// 4. Renderer selection (unchanged): logs stream in CI or with --logs.
120-
let use_logs = args.logs
121-
|| std::env::var_os("CI").is_some_and(|v| !v.is_empty())
122-
|| !hm_render::stderr_interactive();
123-
let renderer = hm_render::renderer_for(&args.format, ctx.output.color_enabled(), use_logs)?;
124-
125-
// 5. Start, drive events, own Ctrl-C, await the outcome.
141+
// 8. Start, drive events, own Ctrl-C, await the outcome.
126142
let handle = backend.start(req).await.map_err(|e| backend_anyhow(&e))?;
127143
let (events, control) = handle.into_parts();
128144
let _ctrlc = crate::signal::install_ctrlc(control.cancel_token());

0 commit comments

Comments
 (0)