HarmonyApp<T> and Capability<T> are now generic over the target Topology
instead of hardwired to K8sAnywhereTopology. Each concrete capability is
implemented only for topologies that can host its Scores — Postgres for
T: K8sclient+HelmCommand, Monitoring for T: HelmCommand+TenantManager,
ZitadelAuth for T: K8sclient — so .with(X) is a compile error on a topology
that can't run X (the framework's compile-time-capability thesis, now applied
to the app layer). ComposeDeploy<T> and the deploy/ship verbs carry T through;
status/logs stay generic. The CLI front-end (app_main) pins K8sAnywhereTopology;
a different front-end can drive another topology. Example main.rs is unchanged
— T infers from app_main. 29+1 harmony_app, 11 harmony_cli tests green.
Move the compose importer, chart generator, ComposeAppScore, publish bricks,
and the ComposeDeploy builder out of the example into harmony_app (they needed
harmony_app::Profile/Capability, so harmony core would be circular — harmony_app
is the home, per decision A). The example is now a 23-line main.rs + app/ +
contexts; folk imports from harmony_app too. Tests + the helm lint/template
integration test moved along. 29 lib + 1 integration test green.
- app_main gains -C/--config <FILE> to point at the contexts file from any
cwd (the 'no .harmony/contexts.toml found' trap); error now names the flag.
- examples/compose_java_react/README rewritten to the declarative ComposeDeploy
+ verb/--context surface; drops removed flags (--host/--storage-class/
--published/--yes/compose-publish) and documents .with(...) capabilities.
(Both flagged by the fresh-eyes DX reviews.)
- ZitadelClientIdExportScore (harmony): reads a provisioned app's client_id
from the Zitadel cache and writes it to a ConfigMap — the Zitadel→app
bridge, by reference (client_id is non-secret under PKCE).
- ZitadelAuth capability (harmony_app): .with(ZitadelAuth::oidc(issuer)
.project(..).redirect(..)) provisions a PKCE app (USER_AGENT/auth NONE) via
ZitadelSetupScore + the export, and injects OIDC_ISSUER (plain) +
OIDC_CLIENT_ID (configMapKeyRef). Keeps .secret(...) for other config.
- Validated end-to-end against a live local Zitadel + k3d (gated #[ignore]
test): PKCE app created in Zitadel; client_id mirrored into the ConfigMap.
Adds ZitadelAppType::WebPkce { redirect_uris, post_logout_redirect_uris } —
appType=USER_AGENT, authMethod=NONE (Authorization Code + PKCE, no client
secret) — for React/SPA frontends. create/update refactored over shared
create_oidc_app/update_oidc_config helpers (DRY). The PKCE body was validated
against a live local Zitadel (Management API returned appId + clientId).
- docs/guides/application-capabilities.md: the .with(...) menu, by-reference
wiring, and a recipe for writing a capability; disambiguates the overloaded
'capability' (topology vs application) flagged in DX review; wired into the book.
- Deprecate ApplicationScore, features::Monitoring, features::PackagingDeployment
(+ doc-deprecate the ApplicationFeature trait) in favour of harmony_app +
ComposeDeploy/.with(...) (ADR-026). Legacy examples now warn; harmony lib is
warning-clean (internal self-refs allowed).
Capabilities were example-local (a discoverability trapdoor per review). Move
the Capability trait, AppRef, and the Postgres capability into
harmony_app::capabilities so .with(...) is a real, framework-level menu;
ComposeDeploy now consumes the framework trait. Pure capability tests live in
harmony_app; ComposeDeploy keeps the integration test. No behavior change.
Foundation for the Monitoring + ZitadelAuth capabilities (folk prod).
A Capability contributes Scores + injects env wired by k8s reference (service
DNS / secretKeyRef), so .with(Postgres::managed()) deploys a CNPG cluster and
wires DATABASE_URL from its generated <app>-db-app secret — the password never
passes through Harmony, so the chart stays publishable. No change to the
Score/Interpret/Outcome contract (typed outputs deferred until a non-reference
value needs wiring). TDD: capability tests; verified end-to-end on k3d
(cluster healthy, DATABASE_URL referenced, app pods ready).
Declare an app fluently — from_dir().expose().secret() — and it implements
HarmonyApp, so app_main gives ship/deploy/status/logs over any context. The
example's main is now ~20 lines of pure declaration; the flag soup is gone.
TDD: 4 tests pin the spec→Score derivation (local renders/RWO/HTTP, prod
publishes/replicated/TLS, defaults, secrets). Verified end-to-end on k3d.
--version no longer hijacked (it prints version again); the release tag is
--tag. User-facing help/errors no longer cite ADRs. deploy() clones scores
before register instead of reading them back through a lock.
Contexts are defined in .harmony/contexts.toml (found by walking up from cwd),
so collaborators and CI need zero local setup. A context owns cluster access:
k3d (local) or a kubeconfig brokered from OpenBao via harmony_config
(Zitadel-authenticated, the CI/prod path) as a ClusterAccess secret — separate
from the app's own secrets. --context required (ADR-026 §1).
Profile tag is now the shared harmony_app one; the k8s-knob mapping (storage/
replicas/RWX vs RWO/TLS) lives in DeployConfig::for_profile + cluster_issuer_for.
Parses argv, resolves a context (required, no default per ADR-026 §1), calls
the harmony_app verbs, renders. A per-app binary becomes: fn main() {
app_main(MyApp) }.
The home the modules::application feature was missing (ADR-026): a HarmonyApp
describes identity + how to build its Scores per context; ship/deploy/status/
logs are verbs returning structured results, never printed. CLI/TUI/web are
front-ends over these verbs — no UI coupling. AppContext is the selected
target+profile; converges the same Scores wherever it points.
K8sAnywhereConfig::kubeconfig(path, ctx) lets programmatic callers (the app
layer, TUI, web) target a cluster without mutating process env. K8sClient
gains pod_logs for operational read verbs.
One shared Zitadel project as universal audience; tenant scoping via the
groups claim; per-tenant onboarding is provisioning, not app/auth changes.
Index 026 + 027.
Human-friendly sibling of LocalFileSource: reads <base>/<key>.toml, so
multi-line values (kubeconfigs, PEM) need no escaping. Wired via an explicit
ConfigClient::new chain; cleartext on disk, local/offline use only.
Globbing private_repos/* into the public workspace leaked gitignored crate
names into the committed Cargo.lock. Each private crate is now its own
standalone workspace; the public root no longer references them.