docs(adr): add ADR-025 application lifecycle CLI #334
@@ -20,6 +20,7 @@
|
|||||||
## Developer Guides
|
## Developer Guides
|
||||||
|
|
||||||
- [Developer Guide](./guides/developer-guide.md)
|
- [Developer Guide](./guides/developer-guide.md)
|
||||||
|
- [Application CLI — Use Cases & Commands](./guides/application-cli.md)
|
||||||
- [Writing a Score](./guides/writing-a-score.md)
|
- [Writing a Score](./guides/writing-a-score.md)
|
||||||
- [Writing a Topology](./guides/writing-a-topology.md)
|
- [Writing a Topology](./guides/writing-a-topology.md)
|
||||||
- [Adding Capabilities](./guides/adding-capabilities.md)
|
- [Adding Capabilities](./guides/adding-capabilities.md)
|
||||||
|
|||||||
@@ -8,7 +8,15 @@ Last Updated Date: 2025-06-04
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Proposed
|
Superseded by [ADR-025](./025-application-lifecycle-cli.md).
|
||||||
|
|
||||||
|
This ADR set the original direction for delivery automation. ADR-025
|
||||||
|
revises it on three points: no mandatory staging (roll-forward only),
|
||||||
|
GitOps (Argo/Flux) is no longer the deploy interface (the CLI deploys
|
||||||
|
directly, context-targeted), and environments are selected by an explicit
|
||||||
|
context rather than relying on an ambient kubeconfig. The
|
||||||
|
auto-detect-and-bootstrap and CI-pipeline intents here remain valid and
|
||||||
|
are carried forward.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
|
|||||||
278
docs/adr/025-application-lifecycle-cli.md
Normal file
278
docs/adr/025-application-lifecycle-cli.md
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
# Architecture Decision Record: Application Lifecycle CLI — Dev-Facing Verbs, Contexts, and the Declarative Boundary
|
||||||
|
|
||||||
|
Initial Author: Sylvain Tremblay
|
||||||
|
|
||||||
|
Initial Date: 2026-06-10
|
||||||
|
|
||||||
|
Last Updated Date: 2026-06-10
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Proposed (draft)
|
||||||
|
|
||||||
|
Extends ADR-023 (deploy architecture) — whose CLI principle (8) only
|
||||||
|
covers *how binaries are discovered* — with the CLI's **experience
|
||||||
|
contract**: verb grammar, the context/identity model, the
|
||||||
|
declarative/operational boundary, and where "config" lives. Revises
|
||||||
|
ADR-012 (project delivery automation) on three points: no mandatory
|
||||||
|
staging, GitOps is not the deploy interface, and environment targeting
|
||||||
|
is an explicit context rather than an ambient kubeconfig.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
We need one CLI that lets a person *or* a machine drive the full
|
||||||
|
lifecycle of a client application running on a Harmony-managed tenant —
|
||||||
|
build, deploy, inspect, operate. The first concrete case — a small team
|
||||||
|
shipping a timesheet-style app — sets the shape of the problem: they
|
||||||
|
develop locally and ship **straight to production, roll-forward only** —
|
||||||
|
no staging, no rollback.
|
||||||
|
|
||||||
|
The audience is three-headed and **shares a single surface**:
|
||||||
|
developers (the priority for this phase), CI/automation, and autonomous
|
||||||
|
agents. The design is informed by a survey of kubectl, argocd, flux,
|
||||||
|
fly.io, heroku, pulumi, dagger, terraform, ansible, and gcloud, plus
|
||||||
|
current practice on CLIs built for AI agents. Two through-lines drove
|
||||||
|
every decision below:
|
||||||
|
|
||||||
|
1. **Harmony already owns the vocabulary the best tools bolt on.** A
|
||||||
|
*computed* idempotent result (`Outcome`: NOOP/RUNNING/…), declarative
|
||||||
|
desired state (Scores), a reconciling engine (Maestro), and named
|
||||||
|
targets (Topologies). The CLI's job is to *surface* these, not invent
|
||||||
|
machinery.
|
||||||
|
2. **The agent/CI contract is a strict-mode projection of good human
|
||||||
|
DX.** Non-interactive, JSON, idempotent, predictable exit codes — the
|
||||||
|
same features a careful human wants. Build the human CLI right and the
|
||||||
|
machine CLI falls out.
|
||||||
|
|
||||||
|
Constraints that bound the design:
|
||||||
|
|
||||||
|
- **Runs locally (k3d) and on any K8s** (`K8sAnywhereTopology`) — nothing
|
||||||
|
may be OKD- or vendor-specific.
|
||||||
|
- **Harmony's anti-"YAML mud pit" / compile-time-safety ethos** (ADR-005)
|
||||||
|
forbids reintroducing untyped, runtime-validated configuration in any
|
||||||
|
form, including a TOML or YAML file of deployment knobs.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Eleven principles, grouped.
|
||||||
|
|
||||||
|
### Targeting & safety
|
||||||
|
|
||||||
|
1. **No default context, ever.** Every command that touches a cluster
|
||||||
|
requires an explicit target: `--context <name>` flag, else the
|
||||||
|
`HARMONY_CONTEXT` env var, else a **hard error** that lists known
|
||||||
|
contexts. There is no "current context" and no implicit fallback —
|
||||||
|
*even local k3d* must be selected (`--context local`). This makes
|
||||||
|
"deployed to the wrong cluster" structurally impossible.
|
||||||
|
|
||||||
|
2. **Deploy is CLI-direct and context-targeted; GitOps is not the
|
||||||
|
interface.** A developer runs `harmony app deploy --context <env>`
|
||||||
|
explicitly. The *same verb and the same Scores* deploy to local k3d
|
||||||
|
(to test) or to a production tenant — only the Topology selected by
|
||||||
|
the context changes (ADR-023 §2). CI runs the identical command;
|
||||||
|
git-push-triggers-deploy is optional sugar a team may add, never a
|
||||||
|
requirement. A continuous in-cluster reconciler (GitOps-style) is a
|
||||||
|
possible future Topology, not the primary path. This supersedes
|
||||||
|
ADR-012's Argo/Flux-first stance.
|
||||||
|
|
||||||
|
### Project & identity
|
||||||
|
|
||||||
|
3. **Implicit app, explicit context.** The app identity comes from the
|
||||||
|
project (a tiny, identity-only `Harmony.toml` at the repo root, or
|
||||||
|
`[package.metadata.harmony]` in the deploy crate) — the app *name*,
|
||||||
|
and little else. Everything else derives by convention from
|
||||||
|
*name × context*: namespace = `{tenant-from-context}-{app}`, image =
|
||||||
|
`{registry}/{project}/{app}`, workload selector = by app label. **A
|
||||||
|
behavioral knob (replicas, env, resources) in this file is a defect**
|
||||||
|
— it belongs in a Score (see §6). Apps that break the naming
|
||||||
|
convention fall back to the deploy crate; that escape hatch is not
|
||||||
|
built until a real non-conventional app exists (Rule of Three).
|
||||||
|
|
||||||
|
### The declarative boundary
|
||||||
|
|
||||||
|
4. **Two verb classes.** This is the load-bearing distinction. It
|
||||||
|
honors ADR-023's "no handrolled manifests" while still giving
|
||||||
|
developers day-2 ergonomics.
|
||||||
|
|
||||||
|
| Class | Verbs | Path | Mutates desired state? | Needs compile? |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **Declarative** | `build` · `publish` · `deploy` · `ship` (= build+publish+deploy) · `check` · `run` (one-off Job) | through the project's **Scores**, re-converge | yes | yes (runs deploy crate) |
|
||||||
|
| **Operational** | `logs` · `status` · `exec` · `forward` · `restart` · `describe` · `history` | **direct kube client**, read-only / ephemeral | no | no (metadata only) |
|
||||||
|
|
||||||
|
`build`, `publish`, and `deploy` are independently invocable and
|
||||||
|
compose into `ship`. **`deploy` takes an explicit image
|
||||||
|
(`--image <digest>`) and only converges — it does not build.** `build`
|
||||||
|
produces a digest-pinned image, `publish` pushes it, and `ship` =
|
||||||
|
build + publish + deploy. This is what makes roll-forward recovery —
|
||||||
|
the only recovery a no-rollback team has — a plain
|
||||||
|
`harmony app deploy --image <prior-good-digest>` with no rebuild, and
|
||||||
|
it lets CI run the stages separately (digest handoff, debuggable
|
||||||
|
failures). **`publish` is Topology-specific:** against a remote context
|
||||||
|
it pushes the digest-pinned image to the registry; against a local
|
||||||
|
context it is coded to import the image directly into k3d
|
||||||
|
(`k3d image import`) instead of pushing. The verb is the same; the
|
||||||
|
Topology supplies the behavior (ADR-023 §2).
|
||||||
|
|
||||||
|
The local inner loop is simply `harmony app ship --context local`
|
||||||
|
(build + deploy to local k3d) — there is no separate `serve` verb,
|
||||||
|
because an explicit local context plus the standard verbs already cover
|
||||||
|
it (principle 11; a second verb would be ceremony). `check` is the only pre-deploy validation we have today: it
|
||||||
|
compiles and type-checks the Scores (there is **no** computed diff
|
||||||
|
against live state — see Out of scope).
|
||||||
|
|
||||||
|
5. **No imperative state mutation.** There is no `scale 3` or
|
||||||
|
`set-env`. Operational verbs are precisely the ones that *don't* touch
|
||||||
|
desired state — that is the basis of the split. Changing replicas,
|
||||||
|
env, resources, or routes means **editing the Score and
|
||||||
|
redeploying**. An imperative shortcut would only create drift that the
|
||||||
|
next deploy clobbers.
|
||||||
|
|
||||||
|
6. **Three homes for "config" — none of them a config file.**
|
||||||
|
- *Desired state / behavior* (replicas, env, resources, routes,
|
||||||
|
dependencies) → **typed Scores**, git-versioned, compile-checked.
|
||||||
|
- *Target + credentials* (which cluster/tenant, how to auth) →
|
||||||
|
**context / Topology**, selected explicitly.
|
||||||
|
- *Secrets* → **OpenBao** (`SecretVault`), referenced by Scores,
|
||||||
|
fetched at deploy. Never in code, never in a file.
|
||||||
|
|
||||||
|
7. **Per-environment values are a Score computed as a function of the
|
||||||
|
context.** Differences (prod = 3 replicas + managed Postgres; local =
|
||||||
|
1 replica + sqlite) are expressed in typed code that branches on a
|
||||||
|
**profile tag carried by the context** — the Pulumi-stack idea in pure
|
||||||
|
Rust, validated by the compiler. The profile is a **structured field on
|
||||||
|
the context, not encoded in its name**: a Score branches on
|
||||||
|
`ctx.profile()`, never on the context name (a free human handle —
|
||||||
|
`myapp-prod` and `otherapp-prod` may share `profile = Prod`). (A dedicated
|
||||||
|
typed `Profile` input is deferred until divergence justifies it — Rule
|
||||||
|
of Three.)
|
||||||
|
|
||||||
|
8. **Reconciliation is invocation-driven (today).** Convergence happens
|
||||||
|
when someone runs `harmony app deploy`, not continuously. Out-of-band
|
||||||
|
changes survive until the next deploy, then are overwritten. A
|
||||||
|
continuous controller that reverts drift in real time is a deliberate
|
||||||
|
future, not v1.
|
||||||
|
|
||||||
|
### One surface for humans, CI, and agents
|
||||||
|
|
||||||
|
9. **Machine contract, always on.** Human-readable output by default
|
||||||
|
(TTY-detected); `--json` opts into a **frozen, versioned** schema —
|
||||||
|
JSON to stdout, all logs/progress to stderr. No hidden prompts when
|
||||||
|
stdin is not a TTY. Verbs are idempotent. `Outcome` maps onto exit
|
||||||
|
codes (e.g. success/noop = 0, failure/blocked = non-zero) so CI and
|
||||||
|
agents branch programmatically without scraping text. An agent
|
||||||
|
skill/MCP surface is *derived from* this CLI later — never hand-built
|
||||||
|
in parallel.
|
||||||
|
|
||||||
|
### Credentials
|
||||||
|
|
||||||
|
10. **The credential source is part of the context, and degrades to
|
||||||
|
local.** A context is `{ cluster (endpoint + CA), tenant, credential
|
||||||
|
source / identity, profile }` — and carries **no `role`**:
|
||||||
|
authorization is a server-side property of the identity (Zitadel token
|
||||||
|
claims → OpenBao policy → RBAC), never a client-trusted field.
|
||||||
|
Locally, the context yields the ambient k3d/kubeconfig — no
|
||||||
|
identity infrastructure required. Remotely, the context carries a
|
||||||
|
brokered source: authenticate a Zitadel **service user** (machine
|
||||||
|
identity, role-scoped), exchange for an **OpenBao**-brokered,
|
||||||
|
short-lived, namespace-scoped Kubernetes token, and present that. The
|
||||||
|
CLI holds **nothing standing**; authorization is enforced
|
||||||
|
server-side (OpenBao policy + cluster RBAC). The broker is K8s-
|
||||||
|
agnostic by construction (standard `TokenRequest` + RBAC). Detailed
|
||||||
|
broker mechanics and tenant/identity provisioning may warrant a
|
||||||
|
follow-up ADR; provisioning is manual for now.
|
||||||
|
|
||||||
|
### Grammar
|
||||||
|
|
||||||
|
11. **Grammar: `harmony <scope> <verb>`.** A small, fixed set of scope
|
||||||
|
nouns organizes the surface by persona and object: **`app`** (the
|
||||||
|
developer lifecycle — `harmony app deploy`, `app logs`, `app ship`,
|
||||||
|
…), **`tenant`** (tenant-admin), **`cluster`** (cluster-admin), and
|
||||||
|
**`context`** (everyone). The *app instance stays implicit* — it is
|
||||||
|
the project's app (principle 3); `app` is the scope group, not an
|
||||||
|
instance argument, so it is `harmony app deploy`, never
|
||||||
|
`harmony app deploy myapp`. This uniform tree is predictable for humans
|
||||||
|
and breadth-first-discoverable for agents (`harmony --help` → scopes,
|
||||||
|
`harmony app --help` → verbs). We grow by adding verbs / resource
|
||||||
|
types under an existing scope, never by proliferating top-level
|
||||||
|
commands (argocd's sprawl is the anti-pattern), and add **no magic
|
||||||
|
top-level verb aliases** — there is no flat `harmony deploy` shortcut;
|
||||||
|
one way to do it.
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
- **No default context** trades a keystroke for the elimination of an
|
||||||
|
entire class of catastrophic mistake. For a tool whose first job is
|
||||||
|
shipping to production, that trade is obviously correct.
|
||||||
|
- **Declarative-only** is the single highest-leverage choice: every
|
||||||
|
surveyed tool that offers both imperative and declarative mutation
|
||||||
|
(kubectl `edit`/`scale` vs `apply`) suffers drift and clobbering. By
|
||||||
|
making operational verbs structurally incapable of touching desired
|
||||||
|
state, the conflict cannot occur.
|
||||||
|
- **Convention over config** keeps the project file at one line, which is
|
||||||
|
the only way to keep it from rotting into the mud pit ADR-005 rejects —
|
||||||
|
and it makes operational verbs instant and toolchain-free, since they
|
||||||
|
need only metadata + a kube client.
|
||||||
|
- **One surface** is cheaper and more correct than maintaining separate
|
||||||
|
human and machine tools, and it means an agent and a developer learn
|
||||||
|
the same verbs.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- `harmony_cli` grows from a flag-only score-runner into a verb-noun CLI;
|
||||||
|
deploy crates expose the standard verbs rather than ad-hoc clap.
|
||||||
|
- Developers never hand-edit live state; every change is code +
|
||||||
|
redeploy → reproducible, reviewable, diffable in git, with no drift
|
||||||
|
surprises (until the next deploy, per §8).
|
||||||
|
- The same command works local → prod; CI is just another caller, and
|
||||||
|
agents drive the JSON contract.
|
||||||
|
- A behavioral knob added to `Harmony.toml` is a review failure, not a
|
||||||
|
feature.
|
||||||
|
- Cost: no live "what will change" preview until a diff capability is
|
||||||
|
built (Out of scope); the v1 substitute is "deploy to local and
|
||||||
|
observe."
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
- **GitOps-first (Argo/Flux), per ADR-012.** Rejected as the primary
|
||||||
|
interface: it forces a git round-trip for every change, hides the build
|
||||||
|
behind a controller, and answers poorly to "deploy locally to test."
|
||||||
|
Retained only as a possible future Topology.
|
||||||
|
- **A config manifest with deployment knobs** (`Harmony.toml` holding
|
||||||
|
replicas/env/resources). Rejected: it is the YAML mud pit in TOML
|
||||||
|
clothing — untyped, runtime-validated, the exact anti-pattern of
|
||||||
|
ADR-005.
|
||||||
|
- **Ambient / current context** (kubectl/fly style). Rejected: a default
|
||||||
|
target is a standing invitation to deploy to the wrong cluster.
|
||||||
|
- **A standing service-account token as a CI secret.** Rejected: a
|
||||||
|
long-lived credential where Zitadel + OpenBao can mint short-lived,
|
||||||
|
identity-bound ones.
|
||||||
|
- **No project file — pure deploy-crate discovery.** Rejected: it forces
|
||||||
|
a `cargo` compile just to learn an app's own namespace for `logs`.
|
||||||
|
|
||||||
|
## Out of scope (deferred, not rejected)
|
||||||
|
|
||||||
|
- **`plan` / `diff` / delta.** No capability computes desired-vs-live
|
||||||
|
state today; `Outcome` is a per-Score *result*, not a preview. This is
|
||||||
|
the eventual differentiator and deserves its own design.
|
||||||
|
- **Continuous in-cluster reconciler** (real-time drift correction).
|
||||||
|
- **Step-0 provisioning via CLI** (tenant, app, identity triple) — manual
|
||||||
|
for now.
|
||||||
|
- **Tenant dashboard** (web UI for the tenant-admin persona).
|
||||||
|
- **MCP server / agent skill manifest** (derived from the CLI later).
|
||||||
|
- **CI-platform OIDC federation** replacing the handed-off deploy key.
|
||||||
|
- **Typed `Profile` input** for per-environment values (§7).
|
||||||
|
- **Non-convention app fallback** (multi-namespace / custom layout, §3).
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- `docs/adr/012-project-delivery-automation.md` — predecessor; this ADR
|
||||||
|
revises its staging/GitOps/kubeconfig assumptions.
|
||||||
|
- `docs/adr/023-deploy-architecture.md` — deploy crates, Scores, the E2E
|
||||||
|
contract; this ADR fills in its CLI principle (8).
|
||||||
|
- `docs/adr/005-interactive-project.md` — Rust DSL over YAML/HCL
|
||||||
|
("no mud pit").
|
||||||
|
- `docs/adr/020-1-zitadel-openbao-secure-config-store.md` — identity +
|
||||||
|
secret backends the credential model (§10) composes.
|
||||||
|
- `docs/adr/016-Harmony-Agent-And-Global-Mesh-…md` — the mesh that a
|
||||||
|
future remote-deploy control plane would ride on.
|
||||||
|
- `CLAUDE.md` — Score-Topology-Interpret, capability rules.
|
||||||
182
docs/guides/application-cli.md
Normal file
182
docs/guides/application-cli.md
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
# Harmony Application CLI — Use Cases & Commands
|
||||||
|
|
||||||
|
> **Status: design target (proposed).** Today `harmony_cli` runs Scores
|
||||||
|
> via flags; none of the verbs below are implemented yet. This is the
|
||||||
|
> living reference we are building toward. The *decisions and rationale*
|
||||||
|
> live in [ADR-025](../adr/025-application-lifecycle-cli.md) — read that
|
||||||
|
> for the "why"; this doc is the "what" and "how".
|
||||||
|
|
||||||
|
## Mental model
|
||||||
|
|
||||||
|
Four ideas carry the whole CLI:
|
||||||
|
|
||||||
|
- **`harmony <scope> <verb>`.** A small fixed set of scope nouns —
|
||||||
|
**`app`** (developer lifecycle), **`tenant`** (tenant-admin),
|
||||||
|
**`cluster`** (cluster-admin), **`context`** (everyone). What you *can*
|
||||||
|
run is decided server-side by your context's identity, not by the
|
||||||
|
command existing.
|
||||||
|
- **Implicit app, explicit context.** The app is *this project's app*
|
||||||
|
(inferred — you never name it). The **target is always explicit**:
|
||||||
|
`--context <name>` or `HARMONY_CONTEXT`. **There is no default context;
|
||||||
|
omitting it is a hard error.** Even local k3d is `--context local`.
|
||||||
|
- **Declarative vs operational.** Verbs that change desired state go
|
||||||
|
through the project's typed Scores and re-converge. Verbs that only
|
||||||
|
read or are ephemeral talk to the cluster directly and never mutate
|
||||||
|
desired state. You never imperatively edit live state — you edit a
|
||||||
|
Score and redeploy.
|
||||||
|
- **Config has three homes, none of them a config file.** Behavior →
|
||||||
|
typed Scores (in git). Target + credentials → the context. Secrets →
|
||||||
|
OpenBao.
|
||||||
|
|
||||||
|
### Contexts & profiles
|
||||||
|
|
||||||
|
A **context** is `{ cluster (endpoint + CA), tenant, credential source /
|
||||||
|
identity, profile }`. It carries **no role** — authorization is enforced
|
||||||
|
server-side from the identity's token (Zitadel → OpenBao → RBAC), never a
|
||||||
|
client-side setting. Contexts are defined out-of-band (user/CI config),
|
||||||
|
not in the project.
|
||||||
|
|
||||||
|
```
|
||||||
|
harmony context list # what can I target?
|
||||||
|
harmony context show myapp-prod # cluster, tenant, profile, identity (+ role, derived from token)
|
||||||
|
```
|
||||||
|
|
||||||
|
The **profile** (`local | staging | prod`) is a **structured field** the
|
||||||
|
context carries — *not* its name. A Score branches on `ctx.profile()` to
|
||||||
|
compute per-environment values (`prod` → 3 replicas + managed Postgres;
|
||||||
|
`local` → 1 replica + sqlite), and **never parses the context name**. The
|
||||||
|
name (`myapp-prod`) is a free human handle for picking `--context`; the
|
||||||
|
profile field is the authoritative value, so two differently-named contexts
|
||||||
|
can share `profile = Prod` (and `myapp-prod` vs `otherapp-prod` are two
|
||||||
|
different contexts at the same profile). See ADR-025 §7.
|
||||||
|
|
||||||
|
Credentials ride on the context and degrade: **local** uses the ambient
|
||||||
|
k3d/kubeconfig; **remote** mints a short-lived, namespace-scoped token via
|
||||||
|
Zitadel→OpenBao. The CLI holds nothing standing (ADR-025 §10).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `app` — the developer lifecycle
|
||||||
|
|
||||||
|
The verbs, by class:
|
||||||
|
|
||||||
|
| Verb | Class | Context? | What it does |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `check` | declarative | no | Compile + type-check the Scores. The only pre-deploy validation today (no live diff yet). |
|
||||||
|
| `build` | declarative | no | Build a digest-pinned OCI image; prints the digest. Environment-agnostic. |
|
||||||
|
| `publish` | declarative | **yes** | Push the image to the registry (remote) **or** `k3d image import` (local). Topology-specific. |
|
||||||
|
| `deploy` | declarative | **yes** | Converge the Scores against the context, pinned to `--image <digest>`. **Does not build.** Returns only after smoke-test. |
|
||||||
|
| `ship` | declarative | **yes** | `build` + `publish` + `deploy`, threading the digest. The everyday verb. |
|
||||||
|
| `run` | declarative | **yes** | Run a one-off Score-driven Job (migration, task). |
|
||||||
|
| `logs` | operational | **yes** | Stream/tail logs. Deep search lives in the observability tool. |
|
||||||
|
| `status` | operational | **yes** | Health, replicas, last deploy, recent events. |
|
||||||
|
| `exec` | operational | **yes** | Shell/exec into a running container. |
|
||||||
|
| `forward` | operational | **yes** | Port-forward a service to localhost. |
|
||||||
|
| `restart` | operational | **yes** | Rollout-restart a workload (pods return identical; no state change). |
|
||||||
|
| `describe` | operational | **yes** | Resource detail + events for diagnosis. |
|
||||||
|
| `history` | operational | **yes** | Deploy history — what shipped, when, which digest. |
|
||||||
|
|
||||||
|
### Develop (inner loop)
|
||||||
|
|
||||||
|
| I want to… | Command |
|
||||||
|
|---|---|
|
||||||
|
| Check my Scores compile/type-check | `harmony app check` |
|
||||||
|
| Build the image to test it builds | `harmony app build` |
|
||||||
|
| Deploy to local k3d and test | `harmony app ship --context local` |
|
||||||
|
| See it running locally | `harmony app status --context local` · `harmony app logs --context local -f` |
|
||||||
|
|
||||||
|
### Ship to production
|
||||||
|
|
||||||
|
| I want to… | Command |
|
||||||
|
|---|---|
|
||||||
|
| Build + publish + deploy to prod | `harmony app ship --context myapp-prod` |
|
||||||
|
| Deploy an already-built image | `harmony app deploy --context myapp-prod --image <digest>` |
|
||||||
|
| Roll **forward** to a prior good build (recovery) | `harmony app deploy --context myapp-prod --image <prior-digest>` |
|
||||||
|
| Build / publish as separate CI stages | `harmony app build` → `harmony app publish --context myapp-prod` → `harmony app deploy …` |
|
||||||
|
|
||||||
|
There is no rollback and no staging (the roll-forward-only model): you roll forward by
|
||||||
|
deploying a known-good digest.
|
||||||
|
|
||||||
|
### Operate (day-2)
|
||||||
|
|
||||||
|
| I want to… | Command |
|
||||||
|
|---|---|
|
||||||
|
| Tail logs | `harmony app logs --context myapp-prod -f` |
|
||||||
|
| Check health / what's deployed | `harmony app status --context myapp-prod` |
|
||||||
|
| Shell into the app | `harmony app exec --context myapp-prod -- /bin/sh` |
|
||||||
|
| Reach a service locally | `harmony app forward --context myapp-prod 8080:80` |
|
||||||
|
| Restart the app | `harmony app restart --context myapp-prod` |
|
||||||
|
| Run a DB migration | `harmony app run --context myapp-prod -- migrate` |
|
||||||
|
| Change replicas / env / resources | *edit the Score, then* `harmony app ship --context myapp-prod` |
|
||||||
|
|
||||||
|
> Changing replicas/env/config is **not** an imperative command — it's a
|
||||||
|
> Score edit + redeploy. An out-of-band `kubectl scale` survives only
|
||||||
|
> until the next deploy, then is reconciled away (ADR-025 §5, §8).
|
||||||
|
|
||||||
|
### Debug / diagnose
|
||||||
|
|
||||||
|
| I want to… | Command |
|
||||||
|
|---|---|
|
||||||
|
| Understand why a deploy failed | read the `deploy` output (convergence errors, rustc-style) |
|
||||||
|
| See why a workload is unhealthy | `harmony app describe --context myapp-prod` · `harmony app logs …` |
|
||||||
|
| See what shipped recently | `harmony app history --context myapp-prod` |
|
||||||
|
| Deep log search / traces / metrics | open the observability tool (HyperDX/SigNoz/Grafana) — the CLI deep-links |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `tenant` — tenant administration (tenant-admin)
|
||||||
|
|
||||||
|
> Mostly **manual today** (ADR-025 Out of scope: step-0 provisioning).
|
||||||
|
> Listed as the target surface; authorization is by the context's role.
|
||||||
|
|
||||||
|
| I want to… | Command (planned) |
|
||||||
|
|---|---|
|
||||||
|
| List apps / environments in my tenant | `harmony tenant list --context my-tenant` |
|
||||||
|
| Create an environment (namespace — billed) | `harmony tenant env create <name> --context my-tenant` |
|
||||||
|
| Add / remove a developer | `harmony tenant member add <user> --context my-tenant` |
|
||||||
|
| See usage vs quota | `harmony tenant usage --context my-tenant` (or the dashboard) |
|
||||||
|
| Rotate an app's deploy key | `harmony tenant key rotate <app> --context my-tenant` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `cluster` — cluster administration (platform operator)
|
||||||
|
|
||||||
|
> **Future / manual today.** Same noun-by-scope, authorization by role
|
||||||
|
> (only a cluster-admin context may run these).
|
||||||
|
|
||||||
|
| I want to… | Command (planned) |
|
||||||
|
|---|---|
|
||||||
|
| Provision a tenant | `harmony tenant create <name> --context <cluster-admin-ctx>` |
|
||||||
|
| Cluster-wide status | `harmony cluster status --context <cluster-admin-ctx>` |
|
||||||
|
| Manage platform operators | `harmony cluster operator …` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Machine & agent usage
|
||||||
|
|
||||||
|
Every verb is built for CI and autonomous agents — the same surface,
|
||||||
|
strict mode (ADR-025 §9):
|
||||||
|
|
||||||
|
- `--json` → a frozen, versioned schema on **stdout**; logs/progress on
|
||||||
|
**stderr**.
|
||||||
|
- Non-interactive when stdin isn't a TTY — no hidden prompts.
|
||||||
|
- Idempotent verbs; `Outcome` (SUCCESS/NOOP/FAILURE/RUNNING/BLOCKED)
|
||||||
|
mapped to **exit codes** so CI/agents branch without scraping text.
|
||||||
|
- A future agent skill / MCP surface is *derived from* this reference,
|
||||||
|
not authored separately.
|
||||||
|
|
||||||
|
Example CI release (digest pinned end-to-end):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
DIGEST=$(harmony app publish --context myapp-prod --json | jq -r .image)
|
||||||
|
harmony app deploy --context myapp-prod --image "$DIGEST" --json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status legend
|
||||||
|
|
||||||
|
Nothing here is implemented yet. As verbs land, tag them:
|
||||||
|
**`[impl]`** implemented · **`[wip]`** in progress · **`[planned]`** ·
|
||||||
|
**`[future]`** out of v1 scope (per ADR-025). Until the first verbs
|
||||||
|
ship, treat the whole document as `[planned]`.
|
||||||
Reference in New Issue
Block a user