From 7d2caa12de4697edc7c1f8ae888a3f08df502050 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sat, 25 Jul 2026 20:06:57 -0700 Subject: [PATCH] chore: remove dead plugin-era code and stale WASM-plugin references The WASM/extism plugin runtime was already replaced by in-process static DI. Remove the residual debt it left behind: - hm-plugin-protocol: drop unused schemars/JsonSchema derives + dep and unused IsVariant/Deref derives; rewrite docstrings that referenced the defunct plugin/host_fn/hm_archive_read machinery. - hm-plugin-cloud: delete the uncalled argv dispatch fn (+ CloudCli, ExitCode::UsageError); replace the blanket pedantic/nursery/cargo allow-block with narrow, justified allows and a cents_to_dollars helper; strip stale plugin/plan-task comments. - Rewrite stale plugin/WASM docstrings across hm-exec, hm-render, hm, hm-config, hm-util, hm-pipeline-ir, hm-vm; drop the false from_mins-is-nightly suppression; drop unused schemars dep from hm. - Remove dead plugin-authoring docs and hm-fixtures references from README, CLAUDE.md, CONTRIBUTING.md, RELEASING.md, release.yml, and the stale crates/hm/.gitignore. Generated by an AI agent. --- .github/workflows/release.yml | 4 +- CLAUDE.md | 1 - CONTRIBUTING.md | 2 +- Cargo.lock | 2 - RELEASING.md | 2 +- crates/hm-config/src/creds.rs | 9 ++- crates/hm-exec/src/local/archive.rs | 8 +-- crates/hm-exec/src/local/events.rs | 8 +-- crates/hm-exec/src/local/runner/mod.rs | 9 +-- crates/hm-pipeline-ir/src/graph.rs | 4 +- ...apshot__command_step_schema_is_stable.snap | 4 +- crates/hm-plugin-cloud/src/auth/login.rs | 4 +- crates/hm-plugin-cloud/src/auth/logout.rs | 4 ++ crates/hm-plugin-cloud/src/auth/mod.rs | 3 +- crates/hm-plugin-cloud/src/cli.rs | 59 +++---------------- crates/hm-plugin-cloud/src/lib.rs | 10 ---- crates/hm-plugin-cloud/src/settings.rs | 26 ++++---- crates/hm-plugin-cloud/src/verbs/billing.rs | 15 ++++- crates/hm-plugin-cloud/src/verbs/mod.rs | 3 +- crates/hm-plugin-cloud/src/verbs/run.rs | 6 +- crates/hm-plugin-protocol/Cargo.toml | 3 +- crates/hm-plugin-protocol/src/events.rs | 31 +++------- crates/hm-plugin-protocol/src/executor.rs | 26 +++----- crates/hm-plugin-protocol/src/ir.rs | 5 +- crates/hm-render/src/human.rs | 5 +- crates/hm-render/src/json.rs | 5 +- crates/hm-render/src/lib.rs | 6 +- crates/hm-util/src/os/fs.rs | 5 +- crates/hm-vm/src/docker.rs | 6 +- crates/hm/.gitignore | 4 -- crates/hm/Cargo.toml | 1 - crates/hm/README.md | 37 +----------- crates/hm/src/cli/run.rs | 3 +- crates/hm/src/context.rs | 7 +-- crates/hm/tests/chain_failure_render.rs | 5 +- crates/hm/tests/cmd_cloud_gate.rs | 9 ++- crates/hm/tests/cmd_run_local_format.rs | 2 +- crates/hm/tests/cmd_run_local_orchestrated.rs | 10 +--- crates/hm/tests/default_image_inheritance.rs | 2 +- 39 files changed, 110 insertions(+), 245 deletions(-) delete mode 100644 crates/hm/.gitignore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e3c42d07..be803e3f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,7 +57,7 @@ jobs: sed -i "s|hm-render = { path = \"crates/hm-render\", version = \"0.0.0-dev\" }|hm-render = { path = \"crates/hm-render\", version = \"$VERSION\" }|" Cargo.toml sed -i "s|hm-vm = { path = \"crates/hm-vm\", version = \"0.0.0-dev\" }|hm-vm = { path = \"crates/hm-vm\", version = \"$VERSION\" }|" Cargo.toml - cargo check --workspace --exclude hm-fixtures + cargo check --workspace - name: Publishability guard (dry-run package the whole graph) # Fail fast before any real `cargo publish` if the dependency graph @@ -68,7 +68,7 @@ jobs: # would fail), so it catches exactly the publish=false / unpublished-dep # class of regression. `--no-verify` skips the per-crate rebuild; # `cargo check --workspace` above already proved it compiles. - run: cargo package --workspace --exclude hm-fixtures --allow-dirty --no-verify + run: cargo package --workspace --allow-dirty --no-verify - name: Publish hm-util run: | diff --git a/CLAUDE.md b/CLAUDE.md index 9daa33df..5f6542e2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,6 @@ The `cli/` directory is a Cargo workspace. - `crates/hm-pipeline-ir/` — pipeline IR schema (serde structs only, no runtime). - `crates/hm-util/` — shared OS and filesystem utilities. - `crates/hm-plugin-protocol/` — wire types (serde structs only). -- `crates/hm-plugin-sdk/` — authoring SDK for plugin writers. Run `cargo build` from the workspace root. For cross-cutting doctrine see [PRINCIPLES.md](../PRINCIPLES.md). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 246cfc81..5d2687a6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -181,7 +181,7 @@ cargo insta review | `crates/hm-pipeline-ir` | The pipeline IR wire-format schema. | | `crates/hm-config` | Layered configuration and credential storage. | | `crates/hm-plugin-cloud` | Cloud client library. | -| `crates/hm-plugin-protocol` | Wire types shared between `hm` and plugins. | +| `crates/hm-plugin-protocol` | Wire types shared between `hm` crate internals. | | `crates/hm-util` | Shared OS and filesystem utilities. | A useful mental model: the DSL engine evaluates your `.hm/*.py` pipeline diff --git a/Cargo.lock b/Cargo.lock index bceb2f8b..27d0abad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1141,7 +1141,6 @@ dependencies = [ "rand 0.8.6", "reqwest", "rstest", - "schemars 0.8.22", "semver", "serde", "serde_json", @@ -1375,7 +1374,6 @@ dependencies = [ "derive_more", "hm-pipeline-ir", "rstest", - "schemars 0.8.22", "serde", "serde_json", "uuid", diff --git a/RELEASING.md b/RELEASING.md index 338f9422..ab33e891 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -51,7 +51,7 @@ hm-util → hm-pipeline-ir → hm-config → hm-plugin-protocol → hm-render `harmont-cli` (the `hm` binary) depends on every other crate, so they must all reach crates.io first — including `hm-vm`, which carries the -local Docker backend. `hm-fixtures` is test-only and not published. +local Docker backend. ### Prerequisites (one-time) diff --git a/crates/hm-config/src/creds.rs b/crates/hm-config/src/creds.rs index 7008256d..b8efcfa8 100644 --- a/crates/hm-config/src/creds.rs +++ b/crates/hm-config/src/creds.rs @@ -1,8 +1,8 @@ //! File-backed credential store at `~/.config/hm/credentials.toml`. //! -//! Replaces the OS keyring as the sole backend. The file is written with -//! mode 0o600 (parent dir 0o700) via [`hm_util::os::fs::blocking::write_atomic_restricted`]. -//! Keyed by `(service, account)` to match the host-fn ABI plugins use. +//! The file is written with mode 0o600 (parent dir 0o700) via +//! [`hm_util::os::fs::blocking::write_atomic_restricted`], keyed by +//! `(service, account)`. use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; @@ -50,8 +50,7 @@ pub fn get(service: &str, account: &str) -> Option { load().entries.get(service)?.get(account).cloned() } -/// Write a credential. Silently no-ops on I/O failure so plugin callers -/// match the prior keyring-backed best-effort semantics. +/// Write a credential. Silently no-ops on I/O failure (best-effort). pub fn set(service: &str, account: &str, secret: &str) { let mut f = load(); f.entries diff --git a/crates/hm-exec/src/local/archive.rs b/crates/hm-exec/src/local/archive.rs index 9e9e59ab..7b7a90d6 100644 --- a/crates/hm-exec/src/local/archive.rs +++ b/crates/hm-exec/src/local/archive.rs @@ -2,10 +2,10 @@ //! //! On build start the orchestrator tar.gzs the user's working //! directory once (via [`crate::local::build_archive_bytes`]) -//! and registers the bytes under an opaque `ArchiveId`. Step-executor -//! plugins receive that ID in their `ExecutorInput` and pull bytes -//! via `hm_archive_read`. The host caches archives in memory keyed -//! by ID for the duration of a single `local::run` invocation. +//! and registers the bytes under an opaque `ArchiveId`. Runners receive +//! that ID in their `ExecutorInput` and read the bytes back from this +//! store. Archives are cached in memory keyed by ID for the duration of +//! a single `local::run` invocation. use std::collections::HashMap; use std::sync::Mutex; diff --git a/crates/hm-exec/src/local/events.rs b/crates/hm-exec/src/local/events.rs index 9c205cfd..2786b233 100644 --- a/crates/hm-exec/src/local/events.rs +++ b/crates/hm-exec/src/local/events.rs @@ -1,9 +1,7 @@ //! Build-event broadcast channel. //! -//! Subscribers (output formatter plugin, lifecycle hook plugins, -//! the human-readable progress sink) all subscribe to the same -//! channel; the host's `emit_event` / `emit_step_log` host fns -//! publish into it. +//! Subscribers (the output renderers and progress sink) subscribe to the +//! same channel; runners and the scheduler publish [`BuildEvent`]s into it. // `new()` returning `Arc` is intentional (the bus is always // shared); `subscribe()` returns a tokio receiver that callers must @@ -39,7 +37,7 @@ impl EventBus { /// received it. A return of 0 is normal (no subscribers yet). pub fn emit(&self, event: BuildEvent) { // We intentionally drop the error: zero-subscriber sends are - // not interesting and we don't want host_fn impls to fail + // not interesting and we don't want publishers to fail // because nobody is listening. let _ = self.tx.send(event); } diff --git a/crates/hm-exec/src/local/runner/mod.rs b/crates/hm-exec/src/local/runner/mod.rs index 6d9651e4..7df919a5 100644 --- a/crates/hm-exec/src/local/runner/mod.rs +++ b/crates/hm-exec/src/local/runner/mod.rs @@ -1,8 +1,7 @@ //! Static runner interface. //! -//! This module replaces the old WASM plugin system with a static DI -//! approach. Step executors implement [`StepRunner`]. A [`RunnerRegistry`] -//! maps runner names to concrete implementations at startup. +//! Step executors implement [`StepRunner`]; a [`RunnerRegistry`] maps +//! runner names to concrete implementations at startup. use std::collections::HashMap; use std::fmt; @@ -21,9 +20,7 @@ pub mod vm; /// Shared context threaded into every runner invocation. /// -/// Replaces the monolithic `OrchestratorState` that the old plugin -/// system passed as opaque host memory. All fields are cheaply -/// cloneable (`Arc` / `CancellationToken`). +/// All fields are cheaply cloneable (`Arc` / `CancellationToken`). #[derive(Clone, Debug)] pub struct StepContext { pub event_bus: Arc, diff --git a/crates/hm-pipeline-ir/src/graph.rs b/crates/hm-pipeline-ir/src/graph.rs index cc9212f7..38c9ef7b 100644 --- a/crates/hm-pipeline-ir/src/graph.rs +++ b/crates/hm-pipeline-ir/src/graph.rs @@ -36,11 +36,11 @@ pub struct CommandStep { /// Cache configuration for this step's committed snapshot. #[serde(default)] pub cache: Option, - /// Step-executor plugin name. `None` falls back to the default + /// Step-executor (runner) name. `None` falls back to the default /// runner (Docker in the shipped configuration). #[serde(default, skip_serializing_if = "Option::is_none")] pub runner: Option, - /// Plugin-specific extra fields passed verbatim to the runner. + /// Runner-specific extra fields passed verbatim to the runner. #[serde(default, skip_serializing_if = "Option::is_none")] pub runner_args: Option, } diff --git a/crates/hm-pipeline-ir/tests/snapshots/schema_snapshot__command_step_schema_is_stable.snap b/crates/hm-pipeline-ir/tests/snapshots/schema_snapshot__command_step_schema_is_stable.snap index 83725012..d08307ce 100644 --- a/crates/hm-pipeline-ir/tests/snapshots/schema_snapshot__command_step_schema_is_stable.snap +++ b/crates/hm-pipeline-ir/tests/snapshots/schema_snapshot__command_step_schema_is_stable.snap @@ -70,14 +70,14 @@ expression: schema ] }, "runner": { - "description": "Step-executor plugin name. `None` falls back to the default runner (Docker in the shipped configuration).", + "description": "Step-executor (runner) name. `None` falls back to the default runner (Docker in the shipped configuration).", "type": [ "string", "null" ] }, "runner_args": { - "description": "Plugin-specific extra fields passed verbatim to the runner." + "description": "Runner-specific extra fields passed verbatim to the runner." } }, "definitions": { diff --git a/crates/hm-plugin-cloud/src/auth/login.rs b/crates/hm-plugin-cloud/src/auth/login.rs index 668d7844..97b18423 100644 --- a/crates/hm-plugin-cloud/src/auth/login.rs +++ b/crates/hm-plugin-cloud/src/auth/login.rs @@ -83,7 +83,7 @@ async fn login_loopback(client: &HarmontClient, app: &str) -> Result { } }; // Give the browser up to 3 minutes to complete sign-in and redirect. - let _ = tokio::time::timeout(Duration::from_secs(180), accept).await; + let _ = tokio::time::timeout(Duration::from_mins(3), accept).await; // Poll the claim endpoint. The SPA parks the token under our nonce; until // then the endpoint returns 400 `cli_code_invalid`, which we retry. @@ -92,7 +92,7 @@ async fn login_loopback(client: &HarmontClient, app: &str) -> Result { /// Poll `claim_token` until the token is parked or the ~60s window elapses. async fn poll_claim(client: &HarmontClient, nonce: &str) -> Result { - let deadline = std::time::Instant::now() + Duration::from_secs(60); + let deadline = std::time::Instant::now() + Duration::from_mins(1); loop { match client.claim_token(nonce).await { Ok(token) => return Ok(token), diff --git a/crates/hm-plugin-cloud/src/auth/logout.rs b/crates/hm-plugin-cloud/src/auth/logout.rs index 9cd5d788..57478847 100644 --- a/crates/hm-plugin-cloud/src/auth/logout.rs +++ b/crates/hm-plugin-cloud/src/auth/logout.rs @@ -6,6 +6,10 @@ use anyhow::Result; use crate::settings; +#[allow( + clippy::unused_async, + reason = "async to match the uniform await arm in cli::dispatch_command" +)] pub(crate) async fn run(_env: &BTreeMap) -> Result<()> { let (_client, api) = settings::anon_client()?; hm_config::creds::forget_cloud_token(&api); diff --git a/crates/hm-plugin-cloud/src/auth/mod.rs b/crates/hm-plugin-cloud/src/auth/mod.rs index c0f553e8..663c73b8 100644 --- a/crates/hm-plugin-cloud/src/auth/mod.rs +++ b/crates/hm-plugin-cloud/src/auth/mod.rs @@ -1,7 +1,6 @@ //! `hm cloud login | logout | whoami`. //! -//! Each submodule exposes a single `run(env, ...)` entry point that -//! the plugin's `cli::dispatch` calls once it has parsed argv. +//! Each submodule exposes a single `run(env, ...)` entry point. pub(crate) mod login; pub(crate) mod logout; diff --git a/crates/hm-plugin-cloud/src/cli.rs b/crates/hm-plugin-cloud/src/cli.rs index bb2b519a..a6ccbf10 100644 --- a/crates/hm-plugin-cloud/src/cli.rs +++ b/crates/hm-plugin-cloud/src/cli.rs @@ -3,21 +3,16 @@ use std::collections::BTreeMap; use anyhow::Result; -use clap::{Parser, Subcommand}; +use clap::Subcommand; use crate::{auth, verbs}; /// Process exit status for the cloud subcommands. -/// -/// Centralizes the otherwise-magic 0/1/2 integers so each status is -/// self-documenting and the mapping lives in exactly one place. enum ExitCode { /// The command completed successfully. Success, /// The command ran but failed at runtime. RuntimeError, - /// The arguments could not be parsed. - UsageError, } impl From for i32 { @@ -25,22 +20,10 @@ impl From for i32 { match code { ExitCode::Success => 0, ExitCode::RuntimeError => 1, - ExitCode::UsageError => 2, } } } -#[derive(Debug, Parser)] -#[command( - name = "hm cloud", - about = "Talk to the Harmont cloud API", - disable_help_subcommand = true -)] -struct CloudCli { - #[command(subcommand)] - command: CloudCommand, -} - #[derive(Debug, Clone, Subcommand)] pub enum CloudCommand { /// Authenticate this CLI against the Harmont API. @@ -169,40 +152,12 @@ pub enum BillingCommand { Redeem { code: String }, } -/// Dispatch from raw argv (used if calling from an external-subcommand -/// pattern). Returns an exit code. -pub async fn dispatch(argv: Vec, env: BTreeMap) -> Result { - let mut full: Vec = vec!["hm cloud".to_string()]; - full.extend(argv.into_iter().skip(1)); - let parsed = match CloudCli::try_parse_from(&full) { - Ok(p) => p, - Err(e) => { - use clap::error::ErrorKind; - let msg = e.to_string(); - return match e.kind() { - ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => { - #[allow(clippy::print_stdout)] - { - use std::io::Write; - std::io::stdout().write_all(msg.as_bytes()).ok(); - } - Ok(ExitCode::Success.into()) - } - _ => { - #[allow(clippy::print_stderr)] - { - use std::io::Write; - std::io::stderr().write_all(msg.as_bytes()).ok(); - } - Ok(ExitCode::UsageError.into()) - } - }; - } - }; - dispatch_command(parsed.command, env).await -} - -/// Dispatch from a pre-parsed `CloudCommand`. Returns an exit code. +/// Dispatch a parsed `CloudCommand`, returning its process exit code. +/// +/// # Errors +/// +/// Returns an error only if dispatch itself fails; a verb's own runtime +/// failure is logged and mapped to a non-zero exit code. pub async fn dispatch_command(command: CloudCommand, env: BTreeMap) -> Result { let result = match command { CloudCommand::Login { paste } => auth::login::run(&env, paste).await, diff --git a/crates/hm-plugin-cloud/src/lib.rs b/crates/hm-plugin-cloud/src/lib.rs index 3734f391..9af9d239 100644 --- a/crates/hm-plugin-cloud/src/lib.rs +++ b/crates/hm-plugin-cloud/src/lib.rs @@ -2,16 +2,6 @@ //! //! Implements `hm cloud {login,logout,whoami,org,pipeline,build,job,billing,run}`. -#![allow( - clippy::pedantic, - clippy::nursery, - clippy::cargo, - clippy::multiple_crate_versions, - clippy::cargo_common_metadata, - clippy::missing_errors_doc, - reason = "quick migration from plugin crate; polish later" -)] - pub mod cli; pub mod settings; diff --git a/crates/hm-plugin-cloud/src/settings.rs b/crates/hm-plugin-cloud/src/settings.rs index d3a2a2bc..036a48ab 100644 --- a/crates/hm-plugin-cloud/src/settings.rs +++ b/crates/hm-plugin-cloud/src/settings.rs @@ -43,7 +43,7 @@ impl ResolvedCtx { /// /// Returns an error if config can't be loaded or no token is available. pub fn client() -> Result<(HarmontClient, ResolvedCtx)> { - let cfg = hm_config::Config::load(None).context("loading config")?; // user + env layering + let cfg = hm_config::Config::load(None).context("loading config")?; let api = cfg.cloud.api_url.clone(); let token = hm_config::creds::cloud_token(&api) .context("not logged in — run `hm cloud login` or set HM_API_TOKEN")?; @@ -64,7 +64,7 @@ pub fn client() -> Result<(HarmontClient, ResolvedCtx)> { /// Returns an error if config can't be loaded. pub fn anon_client() -> Result<(HarmontClient, String)> { let cfg = hm_config::Config::load(None).context("loading config")?; - let api = cfg.cloud.api_url.clone(); + let api = cfg.cloud.api_url; Ok((HarmontClient::anonymous(&api), api)) } @@ -75,10 +75,6 @@ pub fn anon_client() -> Result<(HarmontClient, String)> { #[derive(Debug, Clone, Copy)] pub struct RenderPrefs { /// ANSI enabled when `NO_COLOR` is unset and stderr is a TTY. - /// - /// The plugin verbs have no `--no-color` flag, so we pass `false` for the - /// flag; the `--no-color` asymmetry vs. the host `hm run` path is explicit - /// here at the call site. pub color: bool, /// Force the streaming `HumanRenderer` over the live `ProgressRenderer`. /// @@ -88,11 +84,7 @@ pub struct RenderPrefs { } impl RenderPrefs { - /// Detect render preferences from the live environment via `hm-render`'s - /// shared TTY/color helpers — the single source of truth, also used by - /// `hm/src/context.rs`. This inspects `NO_COLOR` and the stdout/stderr - /// TTY state at call time, so it is a deliberate observation of the - /// environment rather than a constant default. + /// Detect render preferences from the current `NO_COLOR` and TTY state. #[must_use] pub fn detect() -> Self { Self { @@ -102,10 +94,14 @@ impl RenderPrefs { } } -/// Map a raw generated-client error into an `anyhow` error with a readable -/// message. The raw `Error` renders the server's error body (status, -/// headers, decoded value) via its `Display` impl, which holds for any -/// `E: Debug` — true of the generated `types::Error` body. +/// Map a raw generated-client error into a readable `anyhow` error. +/// +/// The server's error body (status, headers, decoded value) is rendered +/// via the raw `Error`'s `Display` impl. +#[allow( + clippy::needless_pass_by_value, + reason = "by-value signature lets this be used directly as `.map_err(map_raw)`" +)] pub fn map_raw(e: harmont_cloud_raw::Error) -> anyhow::Error { anyhow::anyhow!("{e}") } diff --git a/crates/hm-plugin-cloud/src/verbs/billing.rs b/crates/hm-plugin-cloud/src/verbs/billing.rs index dea99a28..2f1ba7e7 100644 --- a/crates/hm-plugin-cloud/src/verbs/billing.rs +++ b/crates/hm-plugin-cloud/src/verbs/billing.rs @@ -8,6 +8,15 @@ use harmont_cloud::HarmontClient; use crate::cli::BillingCommand; use crate::settings; +/// Convert an integer cent amount to dollars for display. +#[allow( + clippy::cast_precision_loss, + reason = "display-only; cent magnitudes stay far below the f64 mantissa limit" +)] +fn cents_to_dollars(cents: i64) -> f64 { + cents as f64 / 100.0 +} + pub(crate) async fn run(_env: &BTreeMap, cmd: BillingCommand) -> Result<()> { let (client, ctx) = settings::client()?; let org = ctx.org()?; @@ -33,7 +42,7 @@ async fn balance(client: &HarmontClient, org: &str) -> Result<()> { .await .map_err(settings::map_raw)? .into_inner(); - let dollars = b.balance_cents as f64 / 100.0; + let dollars = cents_to_dollars(b.balance_cents); tracing::info!("${dollars:.2}"); Ok(()) } @@ -84,7 +93,7 @@ async fn usage( u.cpu_seconds, u.memory_gb_seconds, u.disk_gb_seconds, - u.total_cents as f64 / 100.0 + cents_to_dollars(u.total_cents) ); Ok(()) } @@ -122,7 +131,7 @@ async fn redeem(client: &HarmontClient, org: &str, code: &str) -> Result<()> { .await .map_err(settings::map_raw)? .into_inner(); - let dollars = r.credit_cents as f64 / 100.0; + let dollars = cents_to_dollars(r.credit_cents); tracing::info!("credited ${dollars:.2}"); Ok(()) } diff --git a/crates/hm-plugin-cloud/src/verbs/mod.rs b/crates/hm-plugin-cloud/src/verbs/mod.rs index f8a9a693..cbfe40dc 100644 --- a/crates/hm-plugin-cloud/src/verbs/mod.rs +++ b/crates/hm-plugin-cloud/src/verbs/mod.rs @@ -1,6 +1,5 @@ //! Verb implementations for `hm cloud `. Each module -//! exposes a `run(env, cmd)` entry point that `cli::dispatch` calls -//! after argv has been parsed. +//! exposes a `run(env, cmd)` entry point. pub(crate) mod billing; pub(crate) mod build; diff --git a/crates/hm-plugin-cloud/src/verbs/run.rs b/crates/hm-plugin-cloud/src/verbs/run.rs index 2ff682cd..1958d51c 100644 --- a/crates/hm-plugin-cloud/src/verbs/run.rs +++ b/crates/hm-plugin-cloud/src/verbs/run.rs @@ -4,8 +4,8 @@ //! This is the minimal, file-based path: the caller supplies a pre-rendered v0 //! IR plan via `--plan-file` (or `plan.json` by convention) and **no source //! archive** is uploaded. The full local-worktree flow — rendering the DSL and -//! archiving the working tree — is implemented by `hm run --cloud` (plan task -//! E2), which lives in the `hm` crate where the renderer and archiver are. +//! archiving the working tree — is implemented by `hm run --cloud`, which +//! lives in the `hm` crate where the renderer and archiver are. use std::collections::BTreeMap; @@ -59,7 +59,7 @@ pub(crate) async fn run(env: &BTreeMap, args: RunArgs) -> Result commit: args.commit.clone(), message: args.message.clone(), pipeline_ir, - // Full worktree archiving lands in `hm run --cloud` (task E2). + // Full worktree archiving lands in `hm run --cloud`. source_tgz: Vec::new(), env: env .iter() diff --git a/crates/hm-plugin-protocol/Cargo.toml b/crates/hm-plugin-protocol/Cargo.toml index ab58d898..3c864396 100644 --- a/crates/hm-plugin-protocol/Cargo.toml +++ b/crates/hm-plugin-protocol/Cargo.toml @@ -4,13 +4,12 @@ version = "0.0.0-dev" edition.workspace = true license.workspace = true repository.workspace = true -description = "Wire-level data types shared between the hm binary and hm plugins." +description = "Wire-level data types shared between hm crate internals." [dependencies] hm-pipeline-ir = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -schemars = { workspace = true } uuid = { workspace = true } chrono = { workspace = true } derive_more = { workspace = true } diff --git a/crates/hm-plugin-protocol/src/events.rs b/crates/hm-plugin-protocol/src/events.rs index e8a5f9eb..1243e975 100644 --- a/crates/hm-plugin-protocol/src/events.rs +++ b/crates/hm-plugin-protocol/src/events.rs @@ -1,25 +1,12 @@ -//! Build-time events. Produced by the orchestrator (host) and fanned -//! out to output formatters, lifecycle hooks, and (via the host -//! re-broadcast of `hm_emit_step_log`) any subscriber. +//! Build-time events produced by the orchestrator and consumed by renderers. use chrono::{DateTime, Utc}; -use schemars::JsonSchema as DeriveJsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::executor::SnapshotRef; -#[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - Serialize, - Deserialize, - DeriveJsonSchema, - derive_more::IsVariant, -)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum StdStream { Stdout, @@ -27,7 +14,7 @@ pub enum StdStream { } #[derive( - Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema, derive_more::IsVariant, + Debug, Clone, PartialEq, Eq, Serialize, Deserialize, derive_more::IsVariant, )] #[serde(tag = "kind", rename_all = "snake_case")] #[non_exhaustive] @@ -37,8 +24,8 @@ pub enum BuildEvent { plan: PlanSummary, started_at: DateTime, }, - /// Emitted once, early, when the build has an identity. Replaces the - /// ad-hoc "Build #N submitted" log line. `watch_url` is `Some` for cloud. + /// Emitted once, early, when the build first has an identity. + /// `watch_url` is `Some` for cloud builds. BuildAccepted { build: BuildRef, watch_url: Option, @@ -77,7 +64,7 @@ pub enum BuildEvent { snapshot: Option, }, /// Emitted when any step in a chain returns non-zero. Carries the - /// failing step's identity so output plugins can render a precise + /// failing step's identity so renderers can show a precise /// diagnostic. Distinct from `StepEnd` (per-step) and `BuildEnd` /// (per-run). ChainFailed { @@ -96,7 +83,7 @@ pub enum BuildEvent { /// Stable identity for a build, shared by `BuildAccepted` and `hm_exec::BuildOutcome`. /// Local builds have a `run_id` only; cloud builds also have `number`/`org`. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BuildRef { pub run_id: Uuid, pub number: Option, @@ -105,8 +92,8 @@ pub struct BuildRef { } /// Compact summary of the resolved IR included in `BuildStart`. Lets -/// output formatters print a header without needing the full pipeline. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema)] +/// renderers print a header without needing the full pipeline. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct PlanSummary { pub step_count: usize, pub chain_count: usize, diff --git a/crates/hm-plugin-protocol/src/executor.rs b/crates/hm-plugin-protocol/src/executor.rs index 75323192..5bda2fb4 100644 --- a/crates/hm-plugin-protocol/src/executor.rs +++ b/crates/hm-plugin-protocol/src/executor.rs @@ -1,15 +1,14 @@ -//! Wire types passed to and returned by step-executor plugins. +//! Wire types passed to and returned by the step executor. use std::collections::BTreeMap; -use schemars::JsonSchema as DeriveJsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::ir::CommandStep; -/// Opaque archive handle. The plugin streams bytes via -/// `hm_archive_read(id, offset, max)`. +/// Opaque handle to a registered workspace archive. The holder reads the +/// bytes back by id; the contents are never inspected through this handle. #[derive( Debug, Clone, @@ -19,17 +18,14 @@ use crate::ir::CommandStep; Hash, Serialize, Deserialize, - DeriveJsonSchema, derive_more::From, - derive_more::Deref, derive_more::Display, )] #[serde(transparent)] pub struct ArchiveId(pub Uuid); -/// Opaque snapshot reference. For the docker plugin this is an image -/// tag; other plugins are free to encode their own format. The host -/// never inspects the contents. +/// Snapshot reference whose format is chosen by the runner (for the +/// Docker runner, an image tag). #[derive( Debug, Clone, @@ -38,15 +34,13 @@ pub struct ArchiveId(pub Uuid); Hash, Serialize, Deserialize, - DeriveJsonSchema, derive_more::From, - derive_more::Deref, derive_more::Display, )] #[serde(transparent)] pub struct SnapshotRef(pub String); -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ArtifactRef { pub key: String, pub mime: String, @@ -55,9 +49,7 @@ pub struct ArtifactRef { /// Host-decided cache outcome. The executor honours this; it does /// not re-decide. -#[derive( - Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema, derive_more::IsVariant, -)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum CacheDecision { /// Boot from `tag`; skip running `cmd`. @@ -69,7 +61,7 @@ pub enum CacheDecision { MissNoCommit, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ExecutorInput { pub step: CommandStep, @@ -91,7 +83,7 @@ pub struct ExecutorInput { pub parent_snapshot: Option, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct StepResult { pub exit_code: i32, /// `Some(tag)` when the executor wrote a snapshot for this step diff --git a/crates/hm-plugin-protocol/src/ir.rs b/crates/hm-plugin-protocol/src/ir.rs index 727be450..e00c9340 100644 --- a/crates/hm-plugin-protocol/src/ir.rs +++ b/crates/hm-plugin-protocol/src/ir.rs @@ -1,5 +1,4 @@ -//! Re-exports from `hm_pipeline_ir`. The canonical definitions now -//! live in the `hm-pipeline-ir` crate; this module keeps the -//! `hm_plugin_protocol::ir::*` import path working. +//! Pipeline IR types re-exported from `hm_pipeline_ir`, so the wire types +//! in this crate and their consumers share a single definition. pub use hm_pipeline_ir::{Cache, CommandStep}; diff --git a/crates/hm-render/src/human.rs b/crates/hm-render/src/human.rs index 652f5f19..3305df6c 100644 --- a/crates/hm-render/src/human.rs +++ b/crates/hm-render/src/human.rs @@ -1,6 +1,5 @@ -//! Human-readable [`OutputRenderer`] — replaces the former -//! `hm-plugin-output-human` WASM plugin with a plain struct that -//! writes formatted lines to any [`std::io::Write`] target. +//! Human-readable [`OutputRenderer`]: writes formatted lines to any +//! [`std::io::Write`] target. use std::collections::HashMap; use std::fmt; diff --git a/crates/hm-render/src/json.rs b/crates/hm-render/src/json.rs index 94bd8863..6b3b2821 100644 --- a/crates/hm-render/src/json.rs +++ b/crates/hm-render/src/json.rs @@ -1,6 +1,5 @@ -//! JSON-lines [`OutputRenderer`] — replaces the former -//! `hm-plugin-output-json` WASM plugin. Serialises each -//! [`BuildEvent`] as a single JSON line. +//! JSON-lines [`OutputRenderer`]: serialises each [`BuildEvent`] as a +//! single JSON line. use std::fmt; use std::io::Write; diff --git a/crates/hm-render/src/lib.rs b/crates/hm-render/src/lib.rs index 77d98a33..6ae7a7b7 100644 --- a/crates/hm-render/src/lib.rs +++ b/crates/hm-render/src/lib.rs @@ -1,4 +1,4 @@ -//! Build-event renderers shared by the `hm` CLI and the cloud plugin. +//! Build-event renderers shared by the `hm` CLI and `hm-plugin-cloud`. //! //! This crate owns the output layer: the [`OutputRenderer`] trait, the //! [`OutputMode`] selection enum, and the concrete renderers @@ -14,8 +14,8 @@ use hm_plugin_protocol::BuildEvent; /// Whether ANSI color should be used: honors an explicit no-color flag, /// the `NO_COLOR` env convention, and whether stderr is a TTY. /// -/// Single source of truth for the color rule, shared by the `hm` host -/// context and the cloud plugin's render preferences. +/// Single source of truth for the color rule, shared by the `hm` run +/// context and `hm-plugin-cloud`'s render preferences. #[must_use] pub fn color_enabled(no_color_flag: bool) -> bool { !no_color_flag && std::env::var_os("NO_COLOR").is_none() && std::io::stderr().is_terminal() diff --git a/crates/hm-util/src/os/fs.rs b/crates/hm-util/src/os/fs.rs index db83a988..19441ca4 100644 --- a/crates/hm-util/src/os/fs.rs +++ b/crates/hm-util/src/os/fs.rs @@ -2,8 +2,7 @@ //! //! The main entry point is [`write_atomic_restricted`]. A synchronous //! wrapper is available at [`blocking::write_atomic_restricted`] for -//! callers that run inside a tokio runtime but cannot use async -//! (e.g. extism `host_fn` callbacks). +//! callers that run inside a tokio runtime but cannot use async. //! //! Both guarantee that readers observe either the full old contents or //! the full new contents — never a truncated file — and that Unix @@ -202,7 +201,7 @@ async fn write_file_with_mode(path: &Path, contents: &[u8], mode: u32) -> io::Re /// Synchronous wrappers that shell out to the async API via /// `tokio::task::block_in_place`. Safe to call from sync contexts -/// that run inside a tokio runtime (e.g. extism `host_fn` callbacks). +/// that run inside a tokio runtime. pub mod blocking { use super::{DirMode, FileMode}; use std::io; diff --git a/crates/hm-vm/src/docker.rs b/crates/hm-vm/src/docker.rs index 87f776b2..093fdb87 100644 --- a/crates/hm-vm/src/docker.rs +++ b/crates/hm-vm/src/docker.rs @@ -333,14 +333,10 @@ impl Vm for DockerVm { }; // docker commit can be slow for containers with large filesystems; // use a dedicated long-timeout client for this operation. - #[allow( - clippy::duration_suboptimal_units, - reason = "from_mins is nightly-only" - )] let commit_client = self .client .clone() - .with_timeout(std::time::Duration::from_secs(600)); + .with_timeout(std::time::Duration::from_mins(10)); commit_client .commit_container(opts, Config::::default()) .await diff --git a/crates/hm/.gitignore b/crates/hm/.gitignore deleted file mode 100644 index 4ff74fed..00000000 --- a/crates/hm/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Pre-built WASM plugins staged by .github/workflows/release.yml -# before `cargo publish` runs. Generated on every release; never -# committed. -embedded/*.wasm diff --git a/crates/hm/Cargo.toml b/crates/hm/Cargo.toml index a51d4f9b..c506e0cf 100644 --- a/crates/hm/Cargo.toml +++ b/crates/hm/Cargo.toml @@ -69,7 +69,6 @@ hm-dsl-engine = { workspace = true } hm-render = { workspace = true } hm-exec = { workspace = true } async-trait = { workspace = true } -schemars = { workspace = true } semver = { workspace = true } smart-default = { workspace = true } once_cell = "1" diff --git a/crates/hm/README.md b/crates/hm/README.md index 1f0821f8..50c84c27 100644 --- a/crates/hm/README.md +++ b/crates/hm/README.md @@ -133,8 +133,8 @@ hm run --help # full flag reference ## Cloud `hm cloud ` talks to the hosted Harmont API at `api.harmont.dev`. -Every cloud verb is delivered by the embedded `hm-plugin-cloud` WASM -plugin (no separate install step): +Every cloud verb is delivered by the embedded `hm-plugin-cloud` crate +(no separate install step): ```sh hm cloud login # browser-loopback OAuth (or --paste to @@ -184,36 +184,3 @@ Dual-licensed under either of - MIT license ([`LICENSE-MIT`](LICENSE-MIT)) at your option. - -## Plugin authoring - -`hm` is plugin-driven via [Extism](https://extism.org). To write a plugin: - -```bash -cargo new --lib my-plugin -cd my-plugin -cargo add --git https://github.com/harmont-dev/harmont-cli hm-plugin-sdk -``` - -Implement one of `StepExecutor`, `SubcommandPlugin`, `LifecycleHook`, or -`OutputFormatter`, declare a `PluginManifest`, and call -`register_plugin!(...)`. Build with: - -```bash -cargo build --target wasm32-wasip1 --release -``` - -The output `.wasm` can be installed with: - -```bash -hm plugin install ./target/wasm32-wasip1/release/my_plugin.wasm -``` - -See `cli/crates/hm-fixtures/src/bin/` for minimal working examples. - -### Output formatter - -Implement `OutputFormatter::on_event` to render each `BuildEvent`. -Plugins emit bytes via `host::write_stdout` or `host::write_stderr`. -Built-in formatters: `human` (default), `json`. Select with -`hm run --format `. diff --git a/crates/hm/src/cli/run.rs b/crates/hm/src/cli/run.rs index d3eea291..223a0f8e 100644 --- a/crates/hm/src/cli/run.rs +++ b/crates/hm/src/cli/run.rs @@ -42,8 +42,7 @@ pub struct RunArgs { #[arg(short = 'k', long)] pub keep_going: bool, - /// Output formatter (matches an installed output-formatter plugin - /// `name`). Built-ins: `human`, `json`. Default: `human`. + /// Output format. One of `human` or `json`. Default: `human`. #[arg(long, value_name = "NAME", default_value = "human", global = false)] pub format: String, diff --git a/crates/hm/src/context.rs b/crates/hm/src/context.rs index 82194497..eaf4c1e7 100644 --- a/crates/hm/src/context.rs +++ b/crates/hm/src/context.rs @@ -8,10 +8,9 @@ use hm_render::OutputMode; /// Runtime context that bundles resolved config and output preferences. /// -/// After the plan-4 cloud-plugin cutover this is intentionally thin: -/// API client, credential store, and active-org resolution moved into -/// `hm-plugin-cloud`. The host context retains the config file (for -/// future use) and the output mode. +/// Deliberately thin: the API client, credential store, and active-org +/// resolution live in `hm-plugin-cloud`. This context carries only the +/// resolved config and output mode. #[derive(Debug)] pub struct RunContext { pub config: Config, diff --git a/crates/hm/tests/chain_failure_render.rs b/crates/hm/tests/chain_failure_render.rs index 3f941885..c80a5b9a 100644 --- a/crates/hm/tests/chain_failure_render.rs +++ b/crates/hm/tests/chain_failure_render.rs @@ -1,6 +1,5 @@ //! End-to-end: a failing pipeline step routes through `BuildEvent::ChainFailed` -//! and both output plugins render it. See plan task B5 in -//! docs/superpowers/plans/2026-05-19-pr22-followups.md. +//! and both output renderers render it. #![allow( clippy::unwrap_used, @@ -38,7 +37,7 @@ fn human_format_renders_chain_failure_to_stderr() { .assert() .failure(); let stderr = String::from_utf8_lossy(&assert.get_output().stderr); - // The human plugin renders ChainFailed via: + // The human renderer renders ChainFailed via: // "chain {chain_idx}: FAILED at step '{failed_step_key}' (exit={exit_code}): {message}\n" // The step's `key` is the slugified label, so `label="oops"` => key="oops". assert!( diff --git a/crates/hm/tests/cmd_cloud_gate.rs b/crates/hm/tests/cmd_cloud_gate.rs index 6365606e..c50736dd 100644 --- a/crates/hm/tests/cmd_cloud_gate.rs +++ b/crates/hm/tests/cmd_cloud_gate.rs @@ -1,5 +1,5 @@ -//! Integration tests proving that `hm cloud` is wired to the real plugin -//! (the old waitlist gate has been removed). +//! Integration tests: any authenticated `hm cloud` verb fails fast with +//! an auth-required error when no token is present. #![allow(clippy::unwrap_used, reason = "test setup and assertions")] @@ -9,9 +9,8 @@ use predicates::str::contains; use rstest::rstest; /// Any authenticated verb must fail fast with an auth-required error when -/// no token is present. The command must NOT succeed (proving the gate is -/// gone and the plugin is reached), and the error message must tell the -/// user exactly how to fix it. +/// no token is present. The command must NOT succeed, and the error +/// message must tell the user exactly how to fix it. /// /// Hermetic: `HOME` is overridden to a fresh temp dir (no /// `~/.config/hm/credentials.toml`) and `HM_API_TOKEN` is explicitly diff --git a/crates/hm/tests/cmd_run_local_format.rs b/crates/hm/tests/cmd_run_local_format.rs index 89c1fb8a..fc58b9d9 100644 --- a/crates/hm/tests/cmd_run_local_format.rs +++ b/crates/hm/tests/cmd_run_local_format.rs @@ -1,5 +1,5 @@ //! End-to-end: `hm run --local --format ` exercises both -//! output plugins against a real Docker daemon. +//! output renderers against a real Docker daemon. #![allow( clippy::unwrap_used, diff --git a/crates/hm/tests/cmd_run_local_orchestrated.rs b/crates/hm/tests/cmd_run_local_orchestrated.rs index a43b6a24..eb02bc02 100644 --- a/crates/hm/tests/cmd_run_local_orchestrated.rs +++ b/crates/hm/tests/cmd_run_local_orchestrated.rs @@ -1,5 +1,5 @@ //! End-to-end: `hm run --local` against a real Docker daemon, driving -//! the new orchestrator + embedded docker plugin. +//! the orchestrator and the docker VM runner. //! //! Gated `#[ignore]` because it shells out to a real Docker daemon — //! opt-in with `cargo test -p harmont-cli --test cmd_run_local_orchestrated -- --ignored`. @@ -17,12 +17,6 @@ use rstest::rstest; /// A trivial one-step pipeline that doesn't need any user source — just /// runs a single `echo` in an alpine container. -/// -/// Note: the plan's pseudocode used a `hm.command(...)` API that does -/// not exist on the cidsl/py public surface. The real entry point is -/// `hm.sh(cmd, image="...")` (== `scratch().sh(...)`), wrapped in the -/// `@hm.pipeline(slug)` decorator (mirrors the existing -/// `tests/fixtures/pipelines/scratch.py`). const PIPELINE_PY: &str = r#" import harmont as hm @@ -34,7 +28,7 @@ def orchestrated() -> hm.Step: /// Chain-lineage regression: a 2-step chain (`b.builds_in = a`) must /// inherit `a`'s filesystem mutations into `b`'s container. Pre-fix, -/// the docker plugin booted a fresh container per step, losing +/// the docker runner booted a fresh container per step, losing /// `/tmp/a`. const CHAIN_PIPELINE_PY: &str = r#" import harmont as hm diff --git a/crates/hm/tests/default_image_inheritance.rs b/crates/hm/tests/default_image_inheritance.rs index 2de621f9..d5da3ebd 100644 --- a/crates/hm/tests/default_image_inheritance.rs +++ b/crates/hm/tests/default_image_inheritance.rs @@ -1,5 +1,5 @@ //! Regression test: root steps with no per-step `image` must inherit -//! the pipeline's `default_image`. Without this, the docker plugin's +//! the pipeline's `default_image`. Without this, the docker runner's //! `resolve_image` falls back to `alpine:latest` and any apt-get //! command in a ubuntu-targeted example dies with //! `sh: apt-get: not found`.