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.
-
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/ConfigMapstructs 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. -
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.
-
"Applied" is not "working."
helm installreturns 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
-
Deploy with Scores, not handrolled manifests. Capability traits + compile-time bounds are the contract. No
k8s_openapi::api::*structs outside ofScore::interpretbodies. Test harnesses, examples, and CLI helpers compose*Scoretypes — they never reimplement deploys. -
E2E uses the same Scores as production. Only the
Topologyinstance changes (local k3d, remote OKD, bare-metal HA, …). A test harness is aScore-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. -
One Score per deployable component. Composition is the user-facing primitive: a
MyAppScorepulls inPostgresScore,HttpServerScore, etc. Don't build monolithic "deploy everything" Scores. Each primitive Score must be independently testable and substitutable. -
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
-
Deploy logic lives in a
*-deploycrate that depends on bothharmonyand the runtime crate it deploys. Runtime binaries (the artefacts that ship to constrained devices and to in-cluster pods) stay free of theharmonydep. One deploy crate per app area, holding every component-Score for that app plus themain.rsthat drives them viaharmony_cli. The same crate is the single import for any consumer — CLI, e2e harness, future control planes.harmonycore 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
- 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
K8sAnywhereTopologyalready cover "any physical place that runs k8s". NoBox<dyn Topology>plugin loaders.
Framework evolution
- 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
- CLI: hybrid, staged. Today (B): first-party tools ship as
separate
harmony-*binaries built on the existingharmony_clicrate. Tomorrow (C): a top-levelharmonybinary discoversharmony-*plugin binaries on$PATH(kubectl-style) so a third-partyMyAppScoreauthor getsharmony deploy my-appfor free. The plugin protocol is deferred (see §Out of scope).
Error handling
thiserroralmost everywhere;anyhowonly at binary glue. Library code, public crate boundaries, anything a caller might want to match on — typed errors viathiserror.anyhowis reserved formain.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 features↔capabilitiesrelationship. 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 insideinterpretbodies — the principle is what travels with the Score, not yet the trait shape.
Consequences
- New deployable components are authored as
*Scoretypes in a*-deploycrate, not inharmonycore.harmonycore 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/ConfigMapstructs 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/Interpretpublic 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—*-deploycrate exemplar.