Files
harmony/docs/adr/023-deploy-architecture.md
2026-05-20 12:03:19 -04:00

8.3 KiB

Architecture Decision Record: Deploy Architecture — Scores, Deploy Crates, and the E2E Contract

Initial Author: Jean-Gabriel Gill-Couture

Initial Date: 2026-05-18

Last Updated Date: 2026-05-20

Status

Accepted. Extends the Score-Topology-Interpret pattern documented in CLAUDE.md (ADR-002, ADR-003) with the deploy side of the contract: what a deploy crate is, how e2e harnesses relate to production deploys, how the CLI surface is shaped, and the smoke-test-on-deploy semantics.

Context

Three failure modes recur in tooling that ships infrastructure as code, and Harmony exists in part to defeat them. This ADR locks the deploy-time discipline that keeps them out.

  1. Manifests outside the type system. YAML/HCL configurations are validated at runtime, not compile time — the original "YAML mud pit" that ADR-005 names. A Rust framework that re-introduces raw Deployment / Service / ConfigMap structs in test harnesses, examples, or CLI helpers has only dressed up the same anti-pattern in Rust syntax: the typed Scores get a clean sample size of one (production), and everything else can silently diverge.

  2. Deploy logic with no canonical home. A "how to apply component X end-to-end" routine that lives in three places (an example crate, a CLI subcommand, ad-hoc orchestration in a test harness) will drift. The framework needs one address per deployable component, and every consumer of that component composes from there.

  3. "Applied" is not "working." helm install returns success the moment the API server accepts the manifest, and leaves the operator to debug downstream. Harmony's whole reason for existing is to shorten that feedback loop — a deploy primitive that doesn't itself verify the result keeps the loop open.

Decision

Nine principles, grouped.

Deployment as Scores

  1. Deploy with Scores, not handrolled manifests. Capability traits + compile-time bounds are the contract. No k8s_openapi::api::* structs outside of Score::interpret bodies. Test harnesses, examples, and CLI helpers compose *Score types — they never reimplement deploys.

  2. E2E uses the same Scores as production. Only the Topology instance changes (local k3d, remote OKD, bare-metal HA, …). A test harness is a Score-composer running against a test Topology. If e2e needs something prod doesn't, add the knob to the Score — don't fork the manifest in the harness.

  3. One Score per deployable component. Composition is the user-facing primitive: a MyAppScore pulls in PostgresScore, HttpServerScore, etc. Don't build monolithic "deploy everything" Scores. Each primitive Score must be independently testable and substitutable.

  4. Deploy returns only after smoke-test success. Every Score owns a readiness + smoke-test contract that the framework runs and blocks on. Convergence errors must be actionable, in the style of rustc's error messages, not "exit code 1 from helm". The implementation shape of the smoke-test contract is deferred (see §Out of scope); the principle is locked in.

Where deploy logic lives

  1. Deploy logic lives in a *-deploy crate that depends on both harmony and the runtime crate it deploys. Runtime binaries (the artefacts that ship to constrained devices and to in-cluster pods) stay free of the harmony dep. One deploy crate per app area, holding every component-Score for that app plus the main.rs that drives them via harmony_cli. The same crate is the single import for any consumer — CLI, e2e harness, future control planes.

    harmony core stays focused on framework primitives and reusable provider modules (DNS, K8s resources, Helm charts, NATS, PostgreSQL, …). It is not a parking lot for application-specific deploy Scores.

Topology selection

  1. Topologies are compile-time, selected at runtime. A deploy binary statically lists its supported topologies; the operator picks one at deploy time. Adding a brand-new topology backend (AWS, GCP, …) is a rebuild — acceptable cost, because dynamic-discovery topologies like K8sAnywhereTopology already cover "any physical place that runs k8s". No Box<dyn Topology> plugin loaders.

Framework evolution

  1. Extend Scores with companions, not API changes. New capabilities the framework wants to attach to Scores (planning, dry-run, observability, eventually smoke-test) default to a companion type or trait that wraps a Score rather than a new method on Score / Interpret. The base public API stays simple. The exception is principles every Score must honor (which may force a required method) — but only after the principle has been validated in practice via the companion-first iteration.

CLI

  1. CLI: hybrid, staged. Today (B): first-party tools ship as separate harmony-* binaries built on the existing harmony_cli crate. Tomorrow (C): a top-level harmony binary discovers harmony-* plugin binaries on $PATH (kubectl-style) so a third-party MyAppScore author gets harmony deploy my-app for free. The plugin protocol is deferred (see §Out of scope).

Error handling

  1. thiserror almost everywhere; anyhow only at binary glue. Library code, public crate boundaries, anything a caller might want to match on — typed errors via thiserror. anyhow is reserved for main.rs-level glue where the error is just printed.

Out of scope (deferred, not rejected)

  • Score derive macro / deployment DSL. Strategic intent from day one; the framework's value-add concentrates here. Separate design effort.
  • Score registry (Crichton-style: https://willcrichton.net/rust-api-type-patterns/registries.html). Real itch — examples and Scores are hard to discover today. Research + ADR first.
  • Inventory as capability-defined physical assets. Inventory is under-engineered today; the original idea is to represent physical infrastructure (building → cable → switch port → MAC) but most use cases ignore it. Decomposing inventory into a capability set is a deep redesign.
  • Plug-in CLI discovery layer (C in principle 8). The fix for the "too many disconnected CLIs" cohesion problem. Roadmap item, dedicated future effort.
  • Application featurescapabilities relationship. An in-progress concept the project lead is personally unsure about. Not resolved in this ADR.
  • Concrete smoke-test contract shape (principle 4). Whether smoke-test lives as a separate trait, a required method on Score, a companion struct, or a typestate is open. Until it's locked, deploy crates implement per-Score readiness checks inside interpret bodies — the principle is what travels with the Score, not yet the trait shape.

Consequences

  • New deployable components are authored as *Score types in a *-deploy crate, not in harmony core. harmony core is framework primitives plus reusable provider modules; it does not accumulate application-specific deploy logic.
  • Test harnesses are Score-composers. A harness that finds itself building Deployment / Service / ConfigMap structs is the signal that a Score is missing, not that the harness needs a special path.
  • Every Score owns its readiness story. Whatever shape the smoke-test contract eventually takes, the Score is the home for the logic — not a parallel test fixture.
  • Adding a new deploy backend (a new topology) is a deploy- binary rebuild. Dynamic loading of topologies is rejected by this ADR, and that posture is load-bearing for the compile-time-safety guarantees in CLAUDE.md.
  • New framework-level capabilities (dry-run, observability, smoke-test) ride in on companion types first. Only after a companion proves out does it earn a place in the Score / Interpret public API.

References

  • CLAUDE.md — Score-Topology-Interpret pattern, capability design rules.
  • docs/adr/002-hexagonal-architecture.md — domain/adapter split this builds on.
  • docs/adr/005-interactive-project.md — the original "no YAML-mud-pit" call (Rust DSL over YAML/HCL).
  • docs/adr/009-helm-and-kustomize-handling.md — established pattern: external charts inflate into the same Score pipeline.
  • harmony_agent/deploy*-deploy crate exemplar.