# Architecture This file is the contract between the build (Dockerfile, Woodpecker, Helm chart) and the FluxCD reconciliation in `anton-helm-workloads`. See `DEPLOYMENT.md` for the operational walkthrough. ## Repo shape ``` cms-plugins/ ├── app/ ← Emdash/Astro source │ ├── astro.config.mjs ← node target, sqlite() + local() │ ├── package.json │ ├── src/ ← pages, layouts, components │ └── seed/seed.json ← collections + kotkanagrilli plugin entries ├── Dockerfile ← multi-stage build, single PVC at /app/state ├── docker/entrypoint.sh ← runs `emdash init` on first boot ├── deploy/ │ ├── helm/ ← chart consumed by Flux directly from this repo │ └── fleet-overlay/ ← HelmRelease/source/image-automation templates │ ├── cms-plugins-staging/ ← copy → anton-helm-workloads/cms-plugins-staging/ │ └── cms-plugins-production/ ← copy → anton-helm-workloads/cms-plugins-production/ ├── .woodpecker/container.yaml ← build pipeline └── .ddev/ ← local dev (DDEV nginx → emdash service on :4321) ``` ## Build pipeline A push to `develop` / `staging` / `production` runs `.woodpecker/container.yaml`. The pipeline labels itself `arch: arm64` because the deploy target (`kotkan`) is an arm64 node, so building natively avoids a cross-compile step. For each build the pipeline pushes three OCI tags to `git.oleks.space/oleks/cms-plugins`: | Tag | Mutability | Purpose | |---|---|---| | `0.1.` | Immutable | Audit trail. Use as a rollback target. | | `` | Floating | Watched by FluxCD's `ImagePolicy`. Retagged on every build. | | `-latest` | Floating | Cosmetic. Resolves the chart's `image.tag` fallback to a real ref when the digest path is unset. | Only the `staging` and `production` branches have an `ImagePolicy`, so only those move pods. ## Container shape Multi-stage Dockerfile: - `deps` — `node:22-bookworm-slim` + `python3 make g++` (better-sqlite3 build deps). Falls back to `npm install` if there's no committed lockfile. - `build` — `npm run build`. - `runtime` — `node:22-bookworm-slim` + `tini`. Runs as uid 1001. Persistent state symlinked from `/app/data.db` and `/app/uploads` → `/app/state/...` so the one PVC mounted at `/app/state` covers both SQLite and uploaded media. Entrypoint runs `emdash init` on first boot (idempotent), then `exec`s the Astro node server on port 4321. ## Helm chart `deploy/helm/` ships with the app. The chart is **not** packaged or pushed to an OCI registry — FluxCD's `GitRepository` source pulls it directly from the matching branch of this repo. The `ignore` rule in `source.yaml` restricts reconciliation to `/deploy/helm`, so app-source pushes don't trigger chart re-reconcile. Per-env values overlay `values.yaml` via `values-staging.yaml` / `values-production.yaml` (for `helm upgrade` direct use) and via the `values:` block in the FluxCD `HelmRelease`. Key shape decisions: - **`replicas: 1`, `strategy: Recreate`.** SQLite is single-writer. - **`nodeSelector: kotkan`.** `local-path` PV is sticky to one node; emdash is colocated with the legacy kotkanagrilli WP install. - **Pinned by digest, not tag.** The HelmRelease sets `values.image.digest: "" # {"$imagepolicy": ...}` so that `helm upgrade` detects a change when the floating tag is reassigned. Without digest pinning, `staging` stays the literal string `"staging"` through every build and Helm sees no spec change. - **Single PVC, `helm.sh/resource-policy: keep`.** Losing the PVC means losing all CMS data — uninstalls do not delete it. ## Flux contract Two repos cooperate: - **cms-plugins** (this one) exposes the chart at `deploy/helm/` on each branch. - **anton-helm-workloads** runs the show: `GitRepository` points at the matching branch of cms-plugins, `HelmRelease` references `chart: ./deploy/helm`, `ImagePolicy` + `ImageUpdateAutomation` watch the floating tag and rewrite the digest setter back into `helmrelease.yaml`. For the workloads repo to write digest commits, a `GitRepository` named `anton-workloads-image-automation` in `flux-system` namespace must exist with write access to `anton-helm-workloads:main`. The emdash-kotkanagrilli analogue is `oleks-fleet-image-automation` in the personal fleet repo. ## Data model Three Emdash collections (`app/seed/seed.json`): - **cmses** — CMS platforms. Seeded with WordPress + Emdash. - **plugins** — one entry per plugin. Fields: title, purpose, source_cms, target_cms, category, status (`port`/`built-in`/`saas`/`drop`/`gated`/`done`/`proposed`), source_repo_url, target_repo_url, notes (portableText). - **pages** — static content (About, Submit, etc.). The seeded plugin entries come from `~/projects/emdash.kotkanagrilli.fi/docs/parity.md` and the `mu-plugins/` / `plugins-custom/` directories in `~/projects/kotkanagrilli.fi/`. New entries are added through the Emdash admin (`/_emdash/admin`). ## What this does NOT solve - **Cloudflare Workers target.** The kotkanagrilli sister project has Cloudflare boundary files (`src/worker.ts`, `wrangler.jsonc`) for a later flip; this repo skips them. They can be added back from the emdash-kotkanagrilli pattern when needed. - **Authentication on the admin.** Emdash's default passkey auth runs; the first visit to `/_emdash/admin` redirects to `/setup` and creates the first admin. - **Multi-language.** No i18n — the catalog is English-only by default.