Why
Dispatch currently deploys via bjw-s app-template in home-ops. That works, but three problems are chart-shaped:
- Boot-time migrations block HA.
docker-entrypoint.sh runs prisma migrate deploy on every pod start — two replicas racing migrations is the audit's latent multi-replica hazard. A chart moves migrations to a pre-install,pre-upgrade hook Job; pods just run node server.js (SKIP_DB_MIGRATIONS=true).
- The env contract is stringly-typed.
DISPATCH_LANE_CONFIG_JSON is a giant quoted JSON blob in a values file; scheduler intervals, auth mode, and groomer knobs are undocumented free-form env. A chart gives them structured values + values.schema.json validation, documented next to the code that reads them.
- Chart/app version skew. Deploy config lives in a different repo from the app, so image bumps and config changes land separately (recent examples: the HOSTNAME loopback workaround shipping before the image fix; scheduler env landing before 0.5.16). Chart version = app version kills the skew class.
Scope (thin chart, not a framework)
charts/dispatch/
Chart.yaml # version == appVersion == release tag
values.yaml
values.schema.json # validates lanes, intervals, auth mode, etc.
templates/
deployment.yaml # node server.js, SKIP_DB_MIGRATIONS=true
migrate-job.yaml # helm.sh/hook: pre-install,pre-upgrade — prisma migrate deploy
service.yaml
httproute.yaml # optional (gateway API), off by default
serviceaccount.yaml
secret-contract.md # documented expected Secret keys (DISPATCH_AGENT_TOKEN, GITHUB_*, LLM key) — chart references, never creates
Values sketch:
image: {repository: ghcr.io/misospace/dispatch, tag: ""} # tag defaults to appVersion
env: {} # escape hatch, merged last
config:
authMode: basic # basic|oidc|disabled
excludedLabels: [renovate]
llmBaseUrl: ""
groomer: {enabled: true, model: "", dryRun: false, repoContext: true}
scheduler:
enabled: true
intervals: {syncMs: 900000, groomerMs: 600000, prFollowupMs: 900000, pruneClosedMs: 86400000}
lanes: # structured -> templated into DISPATCH_LANE_CONFIG_JSON
- {id: local, title: Local, claimable: true, role: default}
- {id: frontier, title: Frontier, claimable: true, role: escalation}
- {id: backlog, title: Backlog, claimable: false}
laneAliases: {normal: local, escalated: frontier}
existingSecret: dispatch # envFrom
database: {existingSecret: "", key: uri}
migrations: {enabled: true} # hook Job; disable for platforms handling it elsewhere
Release wiring
helm package + helm push oci://ghcr.io/misospace/charts as a step in the existing manual-release workflow (after the image build succeeds), chart version stamped from the release version.
- Lint/template in CI (
helm lint, helm template against schema, kubeconform).
- Renovate: home-ops already has an OCIRepository per app — point dispatch's at the chart artifact; one bump updates chart+app atomically.
home-ops migration path
- Ship chart in release N; keep app-template values as-is.
- Swap home-ops OCIRepository from app-template to
ghcr.io/misospace/charts/dispatch, translate values (lanes JSON → structured), set SKIP_DB_MIGRATIONS path via migrations.enabled: true.
- Verify hook Job runs migrations before rollout; delete the entrypoint migration once stable (follow-up release).
Non-goals
- No bundled Postgres (CNPG/postgres component stays platform-side).
- No secret creation (ExternalSecret/SOPS stays platform-side).
- No ingress-controller matrix — HTTPRoute + plain Service only.
Acceptance
helm install dispatch oci://ghcr.io/misospace/charts/dispatch + a Secret + a DATABASE_URL yields a working instance on a kind cluster.
- Migration hook Job runs before pod rollout on upgrades; pods never migrate.
values.schema.json rejects an invalid lane config at install time.
- home-ops runs on the chart with no behavior change.
Why
Dispatch currently deploys via bjw-s app-template in home-ops. That works, but three problems are chart-shaped:
docker-entrypoint.shrunsprisma migrate deployon every pod start — two replicas racing migrations is the audit's latent multi-replica hazard. A chart moves migrations to apre-install,pre-upgradehook Job; pods just runnode server.js(SKIP_DB_MIGRATIONS=true).DISPATCH_LANE_CONFIG_JSONis a giant quoted JSON blob in a values file; scheduler intervals, auth mode, and groomer knobs are undocumented free-form env. A chart gives them structured values +values.schema.jsonvalidation, documented next to the code that reads them.Scope (thin chart, not a framework)
Values sketch:
Release wiring
helm package+helm push oci://ghcr.io/misospace/chartsas a step in the existing manual-release workflow (after the image build succeeds), chart version stamped from the release version.helm lint,helm templateagainst schema, kubeconform).home-ops migration path
ghcr.io/misospace/charts/dispatch, translate values (lanes JSON → structured), setSKIP_DB_MIGRATIONSpath viamigrations.enabled: true.Non-goals
Acceptance
helm install dispatch oci://ghcr.io/misospace/charts/dispatch+ a Secret + a DATABASE_URL yields a working instance on a kind cluster.values.schema.jsonrejects an invalid lane config at install time.