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

5.6 KiB

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:

  • depsnode:22-bookworm-slim + python3 make g++ (better-sqlite3 build deps). Falls back to npm install if there's no committed lockfile.
  • buildnpm run build.
  • runtimenode: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 execs 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.