Files
cms-plugins/ARCHITECTURE.md
Oleks 67b07634ae initial scaffold: emdash catalog, helm chart, woodpecker pipeline, ddev
- app/: Emdash scaffold (Astro 6, node target) with cmses/plugins/pages collections
- app/seed/seed.json: WordPress→Emdash parity for kotkanagrilli.fi (~30 entries)
- Dockerfile + docker/entrypoint.sh: multi-stage build, single PVC at /app/state
- deploy/helm/: chart mirroring emdash-kotkanagrilli (single-replica, sqlite, kotkan)
- deploy/fleet-overlay/: HelmRelease/source/image-automation templates for
  anton-helm-workloads (staging + production)
- .woodpecker/container.yaml: arm64 build, three OCI tags per push
  (immutable 0.1.<pipeline> + floating <branch> + <branch>-latest)
- .ddev/: local dev with nginx proxy to emdash on :4321
- README/DEPLOYMENT/ARCHITECTURE/CLAUDE: docs covering the three-repo
  pipeline (cms-plugins + anton-helm-workloads + Gitea OCI registry)
2026-05-20 11:19:00 +03:00

129 lines
5.6 KiB
Markdown

# 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.<pipeline>` | Immutable | Audit trail. Use as a rollback target. |
| `<branch>` | Floating | Watched by FluxCD's `ImagePolicy`. Retagged on every build. |
| `<branch>-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: "<sha256:...>" # {"$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.