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)
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
name: cms-plugins
|
||||||
|
type: generic
|
||||||
|
docroot: ""
|
||||||
|
disable_settings_management: true
|
||||||
|
|
||||||
|
# DDEV's web container is used purely as a reverse proxy in front of
|
||||||
|
# the emdash service (defined in docker-compose.emdash.yaml). nginx
|
||||||
|
# is rewritten via .ddev/web-entrypoint.d/00-emdash-proxy.sh.
|
||||||
|
webserver_type: nginx-fpm
|
||||||
|
|
||||||
|
# No DB service — emdash uses SQLite inside its own container.
|
||||||
|
omit_containers:
|
||||||
|
- db
|
||||||
|
|
||||||
|
use_dns_when_possible: true
|
||||||
|
|
||||||
|
hooks:
|
||||||
|
post-start:
|
||||||
|
- exec: "echo 'cms-plugins up at https://${DDEV_PROJECT}.ddev.site/'"
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Development container for the emdash app. Bind-mounts ../app from
|
||||||
|
# the host and runs `npx emdash dev` so source edits hot-reload. The
|
||||||
|
# production image is built separately from the root Dockerfile and
|
||||||
|
# consumed by deploy/helm/ — DDEV is *not* a prod-parity smoke test.
|
||||||
|
|
||||||
|
services:
|
||||||
|
emdash:
|
||||||
|
container_name: ddev-${DDEV_SITENAME}-emdash
|
||||||
|
image: node:22-bookworm-slim
|
||||||
|
working_dir: /app
|
||||||
|
restart: "no"
|
||||||
|
expose:
|
||||||
|
- "4321"
|
||||||
|
env_file:
|
||||||
|
- ../app/.env
|
||||||
|
environment:
|
||||||
|
HOST: "0.0.0.0"
|
||||||
|
PORT: "4321"
|
||||||
|
NODE_ENV: "development"
|
||||||
|
EMDASH_ALLOWED_ORIGINS: "https://cms-plugins.ddev.site"
|
||||||
|
EMDASH_SITE_URL: "https://cms-plugins.ddev.site"
|
||||||
|
volumes:
|
||||||
|
- ../app:/app:cached
|
||||||
|
# Anonymous-ish named volume for node_modules so the host's
|
||||||
|
# (possibly empty / wrong-arch) directory doesn't shadow what
|
||||||
|
# the container installs.
|
||||||
|
- emdash-node-modules:/app/node_modules
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
set -eu
|
||||||
|
if [ ! -d node_modules ] || [ -z "$$(ls -A node_modules 2>/dev/null)" ]; then
|
||||||
|
echo "[emdash] installing deps"
|
||||||
|
apt-get update && apt-get install -y --no-install-recommends python3 make g++ ca-certificates >/dev/null
|
||||||
|
npm install
|
||||||
|
fi
|
||||||
|
if [ ! -f data.db ]; then
|
||||||
|
echo "[emdash] running emdash init"
|
||||||
|
node_modules/.bin/emdash init
|
||||||
|
fi
|
||||||
|
exec npx emdash dev
|
||||||
|
labels:
|
||||||
|
com.ddev.site-name: ${DDEV_SITENAME}
|
||||||
|
com.ddev.approot: $DDEV_APPROOT
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
emdash-node-modules:
|
||||||
Executable
+49
@@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Runs in DDEV's web container after start.sh has copied
|
||||||
|
# /mnt/ddev_config/nginx_full/ → /etc/nginx/sites-enabled/, but before
|
||||||
|
# supervisord boots nginx. We rewrite the generated site config to
|
||||||
|
# reverse-proxy everything to the emdash service.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
cat > /etc/nginx/sites-enabled/nginx-site.conf <<'NGINX'
|
||||||
|
# Forward `Connection: upgrade` only when the client is actually
|
||||||
|
# upgrading (Vite's HMR WebSocket lives at wss://<host>/?token=...).
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen 443 ssl default_server;
|
||||||
|
|
||||||
|
ssl_certificate /etc/ssl/certs/master.crt;
|
||||||
|
ssl_certificate_key /etc/ssl/certs/master.key;
|
||||||
|
|
||||||
|
include /etc/nginx/monitoring.conf;
|
||||||
|
|
||||||
|
sendfile off;
|
||||||
|
error_log /dev/stdout info;
|
||||||
|
access_log /var/log/nginx/access.log;
|
||||||
|
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
client_max_body_size 100m;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://ddev-cms-plugins-emdash:4321;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_read_timeout 86400s;
|
||||||
|
proxy_send_timeout 86400s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NGINX
|
||||||
|
|
||||||
|
rm -f /etc/nginx/sites-enabled/seconddocroot.conf.example \
|
||||||
|
/etc/nginx/sites-enabled/README.nginx_full.txt \
|
||||||
|
/etc/nginx/conf.d/connection-upgrade.conf
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
**/node_modules
|
||||||
|
**/.astro
|
||||||
|
**/dist
|
||||||
|
**/.env
|
||||||
|
**/.env.*
|
||||||
|
!**/.env.example
|
||||||
|
app/data.db
|
||||||
|
app/data.db-shm
|
||||||
|
app/data.db-wal
|
||||||
|
app/uploads
|
||||||
|
.git
|
||||||
|
.claude
|
||||||
|
.ddev
|
||||||
|
docs
|
||||||
|
deploy
|
||||||
|
*.md
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
**/dist/
|
||||||
|
**/.astro/
|
||||||
|
**/build/
|
||||||
|
|
||||||
|
# Astro generated types
|
||||||
|
**/emdash-env.d.ts
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!**/.env.example
|
||||||
|
|
||||||
|
# Local DB / uploads (emdash data)
|
||||||
|
**/data.db
|
||||||
|
**/data.db-shm
|
||||||
|
**/data.db-wal
|
||||||
|
**/uploads/
|
||||||
|
|
||||||
|
# DDEV
|
||||||
|
.ddev/.tarballs/
|
||||||
|
.ddev/db_snapshots/
|
||||||
|
.ddev/.global_commands/
|
||||||
|
.ddev/.imported-assets/
|
||||||
|
.ddev/import-db/
|
||||||
|
.ddev/import.yaml
|
||||||
|
.ddev/.bgsync*
|
||||||
|
.ddev/.ddev-docker-compose-base.yaml
|
||||||
|
.ddev/.ddev-docker-compose-full.yaml
|
||||||
|
.ddev/.bgsync-config.yaml
|
||||||
|
|
||||||
|
# Logs / OS / editor
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
.DS_Store
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
# patch-package
|
||||||
|
patches/*.patch.bak
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"default": true,
|
||||||
|
"MD013": false,
|
||||||
|
"MD032": false,
|
||||||
|
"MD033": false,
|
||||||
|
"MD040": false,
|
||||||
|
"MD060": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Build + push the cms-plugins container image.
|
||||||
|
# Triggered on push to develop / staging / production. Each push publishes
|
||||||
|
# three refs: 0.1.<pipeline> (immutable, audit), <branch> (the floating
|
||||||
|
# pointer Flux's ImagePolicy tracks → digest rewritten into the fleet
|
||||||
|
# repo → pod rolls), and <branch>-latest (same image; chart image.tag
|
||||||
|
# fallback). Only staging/production have an ImagePolicy, so only those
|
||||||
|
# move pods.
|
||||||
|
|
||||||
|
labels:
|
||||||
|
# kotkan (the deploy target) is an arm64 host, so we build natively on
|
||||||
|
# arm64 — no cross-compile needed.
|
||||||
|
arch: arm64
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: push
|
||||||
|
branch: [develop, staging, production]
|
||||||
|
|
||||||
|
clone:
|
||||||
|
- name: clone
|
||||||
|
image: woodpeckerci/plugin-git
|
||||||
|
environment:
|
||||||
|
CI_NETRC_MACHINE: git.oleks.space
|
||||||
|
CI_NETRC_USERNAME: oleks
|
||||||
|
CI_NETRC_PASSWORD:
|
||||||
|
from_secret: gitea_clone_token
|
||||||
|
PLUGIN_TAGS: "false"
|
||||||
|
PLUGIN_DEPTH: "1"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build-and-push
|
||||||
|
image: git.oleks.space/oleks/nix-ci:latest-arm64
|
||||||
|
environment:
|
||||||
|
REGISTRY_TOKEN:
|
||||||
|
from_secret: registry_token
|
||||||
|
commands:
|
||||||
|
- BRANCH="$CI_COMMIT_BRANCH"
|
||||||
|
- SHA=$(echo "$CI_COMMIT_SHA" | cut -c1-12)
|
||||||
|
# Semver-shaped immutable tag, one per build. First two components
|
||||||
|
# stay 0.1 (no real semver discipline yet); patch is the pipeline
|
||||||
|
# number, monotonic across the whole repo.
|
||||||
|
- VERSION="0.1.$CI_PIPELINE_NUMBER"
|
||||||
|
- IMAGE="git.oleks.space/oleks/cms-plugins"
|
||||||
|
- 'echo "Building $IMAGE:$VERSION (branch=$BRANCH sha=$SHA linux/arm64)"'
|
||||||
|
|
||||||
|
# Wait for the in-cluster buildkit to be reachable (it can be cold).
|
||||||
|
- |
|
||||||
|
BUILDER_HOST="buildkit-rootless-arm64.infra.svc.cluster.local"
|
||||||
|
BUILDER_PORT="1234"
|
||||||
|
echo "Waiting for buildkit at $BUILDER_HOST:$BUILDER_PORT..."
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
if echo >/dev/tcp/$BUILDER_HOST/$BUILDER_PORT 2>/dev/null; then
|
||||||
|
echo "Builder ready"; break
|
||||||
|
fi
|
||||||
|
[ "$i" -eq 30 ] && echo "Builder not available" && exit 1
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
|
||||||
|
- echo "$REGISTRY_TOKEN" | docker login git.oleks.space -u oleks --password-stdin
|
||||||
|
- docker buildx create --name cms-plugins-builder --driver remote "tcp://$BUILDER_HOST:$BUILDER_PORT"
|
||||||
|
|
||||||
|
# Tagging scheme — every build pushes three refs:
|
||||||
|
# $VERSION — semver-shaped, one per build, immutable (audit).
|
||||||
|
# $BRANCH — floating channel pointer. THIS is what Flux's
|
||||||
|
# ImagePolicy tracks (filterTags `^staging$` /
|
||||||
|
# `^production$`, digestReflectionPolicy: Always);
|
||||||
|
# retagging it onto the new image is what makes
|
||||||
|
# ImageUpdateAutomation rewrite the pinned digest
|
||||||
|
# in the workloads repo and roll the pod.
|
||||||
|
# $BRANCH-latest — same image, kept only so the chart's cosmetic
|
||||||
|
# `image.tag` fallback (used when image.digest is
|
||||||
|
# unset) resolves to a real ref.
|
||||||
|
# All branches publish all three; only staging/production have an
|
||||||
|
# ImagePolicy, so only those actually move pods.
|
||||||
|
- |
|
||||||
|
TAGS="-t $IMAGE:$VERSION -t $IMAGE:$BRANCH -t $IMAGE:$BRANCH-latest"
|
||||||
|
docker buildx build \
|
||||||
|
--builder cms-plugins-builder \
|
||||||
|
--platform linux/arm64 \
|
||||||
|
$TAGS \
|
||||||
|
--push \
|
||||||
|
.
|
||||||
|
- 'echo "Pushed $IMAGE:$VERSION + floated $IMAGE:$BRANCH and $IMAGE:$BRANCH-latest"'
|
||||||
|
|
||||||
|
backend_options:
|
||||||
|
kubernetes:
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/arch: arm64
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 4Gi
|
||||||
|
limits:
|
||||||
|
memory: 4Gi
|
||||||
|
labels:
|
||||||
|
commit-branch: "${CI_COMMIT_BRANCH}"
|
||||||
|
commit-sha: "${CI_COMMIT_SHA}"
|
||||||
|
pipeline-number: "${CI_PIPELINE_NUMBER}"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
extends: default
|
||||||
|
|
||||||
|
rules:
|
||||||
|
line-length: disable
|
||||||
|
truthy: disable
|
||||||
|
document-start: disable
|
||||||
|
comments-indentation: disable
|
||||||
|
braces: disable
|
||||||
|
brackets: disable
|
||||||
|
indentation: disable
|
||||||
|
|
||||||
|
ignore: |
|
||||||
|
deploy/helm/templates/
|
||||||
|
**/secrets.enc.yaml
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
Guidance for Claude Code agents working in this repository.
|
||||||
|
|
||||||
|
## What this repo is
|
||||||
|
|
||||||
|
A browseable catalog of CMS plugins built on [Emdash](https://github.com/emdash-cms/emdash).
|
||||||
|
Phase 0 — scaffold + chart + pipeline are in place, no live deploy yet.
|
||||||
|
Seeded with the WordPress → Emdash plugin parity matrix from
|
||||||
|
`~/projects/kotkanagrilli.fi/` and `~/projects/emdash.kotkanagrilli.fi/docs/parity.md`.
|
||||||
|
|
||||||
|
## Linked repos
|
||||||
|
|
||||||
|
- `~/projects/kotkanagrilli.fi/` — legacy WordPress site. **Do not edit
|
||||||
|
from here.** Source of the seeded plugin entries.
|
||||||
|
- `~/projects/emdash.kotkanagrilli.fi/` — Emdash replacement for that site.
|
||||||
|
The Dockerfile, Helm chart, Woodpecker pipeline, and DDEV setup in this
|
||||||
|
repo are deliberate copies of that one. Treat it as the reference
|
||||||
|
implementation; deviations should be justified.
|
||||||
|
- `~/projects/servers/anton/anton-helm-workloads/` — where the FluxCD
|
||||||
|
`HelmRelease`s for `cms-plugins-{staging,production}.kotkanagrilli.fi`
|
||||||
|
live. `deploy/fleet-overlay/` in this repo is the template set to copy
|
||||||
|
into that repo. **Do not commit to anton-helm-workloads without
|
||||||
|
explicit confirmation** — secrets need sops-encryption with the right
|
||||||
|
age recipients.
|
||||||
|
- `~/projects/servers/fleet/` — personal fleet repo. Owns
|
||||||
|
`emdash-kotkanagrilli-{staging,production}` but NOT cms-plugins.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
- `app/` — Emdash scaffold (node target, no i18n, no Cloudflare boundary).
|
||||||
|
Three collections in `seed/seed.json`: `cmses`, `plugins`, `pages`.
|
||||||
|
- `Dockerfile` + `docker/entrypoint.sh` — production image.
|
||||||
|
- `deploy/helm/` — the chart Flux pulls from `./deploy/helm` on the
|
||||||
|
matching branch.
|
||||||
|
- `deploy/fleet-overlay/cms-plugins-{staging,production}/` — HelmRelease
|
||||||
|
+ GitRepository + image-automation + secrets templates ready to drop
|
||||||
|
into `anton-helm-workloads`.
|
||||||
|
- `.woodpecker/container.yaml` — build pipeline (arm64; deploy target is
|
||||||
|
`kotkan`).
|
||||||
|
- `.ddev/` — local dev.
|
||||||
|
- `DEPLOYMENT.md` — full pipeline walkthrough.
|
||||||
|
- `ARCHITECTURE.md` — chart / image / Flux contracts.
|
||||||
|
|
||||||
|
## Common commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Local dev
|
||||||
|
ddev start # https://cms-plugins.ddev.site/
|
||||||
|
# or, without DDEV:
|
||||||
|
cd app && npm install && npm run bootstrap && npx emdash dev
|
||||||
|
|
||||||
|
# Build production image
|
||||||
|
docker build -t cms-plugins:dev .
|
||||||
|
|
||||||
|
# Typecheck
|
||||||
|
cd app && npm run typecheck
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architectural constraints to respect
|
||||||
|
|
||||||
|
- **One repo, one app, node target.** No Cloudflare boundary files yet;
|
||||||
|
adding them is a deliberate Phase-N call, not casual work.
|
||||||
|
- **SQLite single-writer.** One replica, pinned to `kotkan`, `local-path`
|
||||||
|
PVC. No StatefulSet, no horizontal scale.
|
||||||
|
- **Chart pulled directly from git by Flux** — no `helm push` step. Chart
|
||||||
|
changes ship in the same commit as the code that needs them.
|
||||||
|
- **Image is pinned by digest in the HelmRelease.** `ImageUpdateAutomation`
|
||||||
|
rewrites the digest setter; `helm upgrade` only sees a change because
|
||||||
|
of that. The floating `<branch>` tag alone wouldn't roll the pod.
|
||||||
|
- **The legacy WP site keeps running.** This repo doesn't migrate
|
||||||
|
kotkanagrilli.fi — it's a catalog ABOUT plugins, not the site itself.
|
||||||
|
|
||||||
|
## Emdash gotchas (from the kotkanagrilli reference)
|
||||||
|
|
||||||
|
- All content pages must be server-rendered (`output: "server"`); no
|
||||||
|
`getStaticPaths()` for CMS content.
|
||||||
|
- `entry.id` is the slug (URLs); `entry.data.id` is the database ULID
|
||||||
|
(used for API calls / cross-collection refs).
|
||||||
|
- Image fields are `{ src, alt }` objects, not strings.
|
||||||
|
- Always `Astro.cache.set(cacheHint)` on pages that query content.
|
||||||
|
- Taxonomy names in queries match `seed.json`'s `name` field exactly.
|
||||||
|
|
||||||
|
## What NOT to do
|
||||||
|
|
||||||
|
- Don't `npm install` random kotkanagrilli plugins (the e-commerce ones).
|
||||||
|
This is a catalog, not a store.
|
||||||
|
- Don't push to `anton-helm-workloads` without explicit confirmation —
|
||||||
|
unencrypted secrets would leak.
|
||||||
|
- Don't change branch promotion semantics (fast-forward only across
|
||||||
|
`develop` → `staging` → `production`). Mirroring emdash-kotkanagrilli's
|
||||||
|
flow is intentional.
|
||||||
+257
@@ -0,0 +1,257 @@
|
|||||||
|
# Deployment
|
||||||
|
|
||||||
|
How a code change in this repo becomes a running pod at
|
||||||
|
`cms-plugins-{staging,production}.kotkanagrilli.fi`.
|
||||||
|
|
||||||
|
## Repos involved
|
||||||
|
|
||||||
|
| Repo | Lives at | Owns |
|
||||||
|
|---|---|---|
|
||||||
|
| **cms-plugins** (this one) | `git.oleks.space/oleks/cms-plugins` | App source, Dockerfile, Helm chart, Woodpecker pipeline |
|
||||||
|
| **anton-helm-workloads** | `git.oleks.space/anton/helm-workloads` | The FluxCD `HelmRelease`s for each subdomain on `kotkanagrilli.fi`. The legacy WP `kotkanagrilli/` and the dev `hello-kotkan/` releases also live here. |
|
||||||
|
| Gitea OCI registry | `git.oleks.space/oleks/cms-plugins` (image) | Container images pushed by CI |
|
||||||
|
|
||||||
|
The `cms-plugins` repo and `anton-helm-workloads` repo are linked by:
|
||||||
|
|
||||||
|
1. **The Helm chart path.** The HelmRelease in `anton-helm-workloads`
|
||||||
|
references `chart: ./deploy/helm` from a `GitRepository` source that
|
||||||
|
points back at the `staging` or `production` branch of `cms-plugins`.
|
||||||
|
Flux pulls the chart directly from this repo — there is no
|
||||||
|
"publish chart" step in CI.
|
||||||
|
2. **The image policy.** A FluxCD `ImagePolicy` in `anton-helm-workloads`
|
||||||
|
watches the `staging` / `production` floating tag in the Gitea OCI
|
||||||
|
registry and rewrites the resolved digest into `helmrelease.yaml`,
|
||||||
|
which is what makes `helm upgrade` see a change when CI retags.
|
||||||
|
|
||||||
|
## Branch → environment
|
||||||
|
|
||||||
|
Three branches, three environments. Promotion is fast-forward only — no PR
|
||||||
|
ceremony.
|
||||||
|
|
||||||
|
| Branch | Environment | URL | Where it runs |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `develop` | dev | `https://cms-plugins.ddev.site/` | Local DDEV |
|
||||||
|
| `staging` | staging | `https://cms-plugins-staging.kotkanagrilli.fi/` | k3s on `kotkan` |
|
||||||
|
| `production` | production | `https://cms-plugins-production.kotkanagrilli.fi/` | k3s on `kotkan` |
|
||||||
|
|
||||||
|
## What happens on a push
|
||||||
|
|
||||||
|
A push to `staging` or `production` triggers `.woodpecker/container.yaml`:
|
||||||
|
|
||||||
|
1. Spins up a remote buildkit at `buildkit-rootless-arm64.infra.svc.cluster.local:1234`.
|
||||||
|
2. Builds the root `Dockerfile`.
|
||||||
|
3. Publishes **three refs** for each build:
|
||||||
|
- `git.oleks.space/oleks/cms-plugins:0.1.<pipeline>` — immutable audit tag.
|
||||||
|
- `git.oleks.space/oleks/cms-plugins:<branch>` — floating channel pointer; this is what FluxCD's `ImagePolicy` watches.
|
||||||
|
- `git.oleks.space/oleks/cms-plugins:<branch>-latest` — cosmetic; falls back to a real ref if the chart's `image.tag` is consulted (which the digest-pinning path normally bypasses).
|
||||||
|
|
||||||
|
FluxCD then takes over (it polls every minute):
|
||||||
|
|
||||||
|
1. The `ImagePolicy` in `anton-helm-workloads/cms-plugins-<env>/image-automation.yaml` resolves the new digest behind the `<branch>` tag.
|
||||||
|
2. `ImageUpdateAutomation` writes that digest back into `helmrelease.yaml` and commits/pushes to `anton-helm-workloads:main`.
|
||||||
|
3. The `GitRepository` in `anton-helm-workloads/cms-plugins-<env>/source.yaml` syncs the `cms-plugins` branch and exposes `deploy/helm/`.
|
||||||
|
4. The `HelmRelease` reconciles: `helm upgrade` sees a new digest in `values.image.digest` and rolls the Deployment.
|
||||||
|
|
||||||
|
End-to-end deploy time: ≈4 min (build ≈3 min + Flux interval ≈1 min).
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/projects/cms-plugins
|
||||||
|
ddev start
|
||||||
|
# → https://cms-plugins.ddev.site/ (admin at /_emdash/admin)
|
||||||
|
```
|
||||||
|
|
||||||
|
This is **not** a prod-parity smoke test — DDEV runs `npx emdash dev` with
|
||||||
|
hot reload; production runs the built `node ./dist/server/entry.mjs`. The
|
||||||
|
shared artifact is the Astro source under `app/`. To smoke-test the
|
||||||
|
production image standalone:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t cms-plugins:dev .
|
||||||
|
docker run --rm -p 4321:4321 \
|
||||||
|
-e EMDASH_ENCRYPTION_KEY=$(openssl rand -hex 32) \
|
||||||
|
-v cms-plugins-state:/app/state \
|
||||||
|
cms-plugins:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Promoting develop → staging
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git switch staging
|
||||||
|
git merge --ff-only develop
|
||||||
|
git push origin staging
|
||||||
|
```
|
||||||
|
|
||||||
|
The Woodpecker pipeline triggers automatically. Watch it at the Woodpecker
|
||||||
|
UI; once green, Flux reconciles within ≤1 min.
|
||||||
|
|
||||||
|
## Promoting staging → production
|
||||||
|
|
||||||
|
After staging looks good:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git switch production
|
||||||
|
git merge --ff-only staging
|
||||||
|
git push origin production
|
||||||
|
```
|
||||||
|
|
||||||
|
If the fast-forward fails, something is out of order — investigate before
|
||||||
|
forcing.
|
||||||
|
|
||||||
|
## Rolling back
|
||||||
|
|
||||||
|
The chart pins by **digest** (set by FluxCD's
|
||||||
|
`ImageUpdateAutomation`), with `<branch>-latest` as a fallback tag. To
|
||||||
|
roll back:
|
||||||
|
|
||||||
|
1. Identify the previous good build's immutable tag from the Gitea
|
||||||
|
registry — it is `0.1.<N>` where `<N>` is the pipeline number.
|
||||||
|
2. In `anton-helm-workloads/cms-plugins-<env>/helmrelease.yaml`, hard-code
|
||||||
|
the previous digest under `values.image.digest` and remove the
|
||||||
|
`{"$imagepolicy": ...}` marker (so IUA stops overwriting it). Commit
|
||||||
|
and push.
|
||||||
|
3. Flux reconciles in ≤1 min.
|
||||||
|
|
||||||
|
To resume tracking the floating tag, restore the marker and the
|
||||||
|
auto-update resumes.
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
|
||||||
|
All secrets are sops-encrypted in `anton-helm-workloads`:
|
||||||
|
|
||||||
|
- `anton-helm-workloads/cms-plugins-staging/secrets.yaml`
|
||||||
|
- `anton-helm-workloads/cms-plugins-production/secrets.yaml`
|
||||||
|
|
||||||
|
Each env has two distinct Secrets:
|
||||||
|
|
||||||
|
| Secret | Namespace | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `cms-plugins-deploy-key` | `flux-system` | SSH deploy key for Flux to clone `cms-plugins` (one pair shared between staging + production — same key reads both branches). |
|
||||||
|
| `cms-plugins-<env>-secrets` | `kotkan` | Env vars the pod consumes via `existingSecret`. Required key: `EMDASH_ENCRYPTION_KEY`. |
|
||||||
|
|
||||||
|
To rotate a credential:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/projects/servers/anton/anton-helm-workloads/cms-plugins-staging
|
||||||
|
sops secrets.yaml # opens decrypted in $EDITOR
|
||||||
|
git add . && git commit -m "rotate cms-plugins staging credential"
|
||||||
|
git push # Flux re-applies in ≤1 min
|
||||||
|
```
|
||||||
|
|
||||||
|
The Deployment does NOT restart on Secret change (k8s `envFrom` doesn't
|
||||||
|
watch). Force a roll after rotating:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n kotkan rollout restart deployment/cms-plugins-staging
|
||||||
|
```
|
||||||
|
|
||||||
|
## First-time setup checklist
|
||||||
|
|
||||||
|
This is the once-per-environment dance to bring staging or production
|
||||||
|
online.
|
||||||
|
|
||||||
|
### A. In this (cms-plugins) repo
|
||||||
|
|
||||||
|
1. Push to `git.oleks.space/oleks/cms-plugins` with three branches:
|
||||||
|
`develop`, `staging`, `production`. The initial commit lives on
|
||||||
|
`develop`; the other two are fast-forward copies.
|
||||||
|
2. Add the Woodpecker secrets at Repo → Settings → Secrets in the
|
||||||
|
Woodpecker UI (`ci.oleks.space`), scoped to event `push`:
|
||||||
|
|
||||||
|
| Secret | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `gitea_clone_token` | Gitea PAT for cloning during the build step. |
|
||||||
|
| `registry_token` | Gitea PAT with write access to `git.oleks.space/oleks/cms-plugins` (container packages). |
|
||||||
|
|
||||||
|
### B. In the anton-helm-workloads repo
|
||||||
|
|
||||||
|
1. Generate an SSH deploy key (one pair, shared across envs):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh-keygen -t ed25519 -f /tmp/cms-plugins-deploy -N ""
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Upload `/tmp/cms-plugins-deploy.pub` to the `cms-plugins` repo on
|
||||||
|
Gitea: Settings → Deploy Keys → "cms-plugins Flux deploy",
|
||||||
|
**read-only**.
|
||||||
|
3. Generate one `EMDASH_ENCRYPTION_KEY` per env (do NOT reuse):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl rand -hex 32 # staging
|
||||||
|
openssl rand -hex 32 # production
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Copy this repo's `deploy/fleet-overlay/cms-plugins-staging/` and
|
||||||
|
`cms-plugins-production/` directories into the root of
|
||||||
|
`anton-helm-workloads`, then edit each `secrets.yaml` to put the
|
||||||
|
real key material in, and sops-encrypt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd anton-helm-workloads/cms-plugins-staging
|
||||||
|
sops --encrypt --age <recipient> secrets.yaml > secrets.enc.yaml
|
||||||
|
mv secrets.enc.yaml secrets.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Add both directories to `anton-helm-workloads/kustomization.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
resources:
|
||||||
|
- hello-kotkan
|
||||||
|
- kotkanagrilli
|
||||||
|
- max-openclaw
|
||||||
|
- terminal-agent
|
||||||
|
- cms-plugins-staging # ← add
|
||||||
|
- cms-plugins-production # ← add
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Ensure a writable `GitRepository` named
|
||||||
|
`anton-workloads-image-automation` exists in `flux-system` for the
|
||||||
|
`ImageUpdateAutomation` to push digest commits to
|
||||||
|
`anton-helm-workloads:main`. If it doesn't exist yet, create it
|
||||||
|
(this is a one-time setup for the whole workloads repo). The
|
||||||
|
`emdash-kotkanagrilli` analogue is `oleks-fleet-image-automation`.
|
||||||
|
7. Commit and push `anton-helm-workloads`. Flux picks it up in ≤1 min.
|
||||||
|
|
||||||
|
### C. DNS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dig +short cms-plugins-staging.kotkanagrilli.fi
|
||||||
|
dig +short cms-plugins-production.kotkanagrilli.fi
|
||||||
|
```
|
||||||
|
|
||||||
|
Both should resolve to the `kotkan` ingress IP (the same one
|
||||||
|
`emdash-staging.kotkanagrilli.fi` already uses).
|
||||||
|
|
||||||
|
### D. First deploy
|
||||||
|
|
||||||
|
1. Push to `staging` to trigger the first build. Watch Woodpecker.
|
||||||
|
2. Once the image lands, Flux reconciles. Watch the rollout:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n kotkan get pods -w | grep cms-plugins
|
||||||
|
flux get all -n flux-system | grep cms-plugins
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Once `cms-plugins-staging.kotkanagrilli.fi` returns a 200, fast-forward
|
||||||
|
`production` to `staging` and push.
|
||||||
|
|
||||||
|
## Why this shape
|
||||||
|
|
||||||
|
- **Single replica, pinned to `kotkan`.** Emdash uses local SQLite
|
||||||
|
(single-writer); horizontal scaling isn't a thing here. The PVC is
|
||||||
|
`local-path`, so the pod can only run where the volume lives.
|
||||||
|
- **Mutable `<branch>` tag.** The sha-tagged ref (`0.1.<pipeline>`) gives
|
||||||
|
audit trail; the mutable `<branch>` tag is what Flux watches. Together
|
||||||
|
they give visibility without forcing an `anton-helm-workloads` commit
|
||||||
|
per deploy (the `ImageUpdateAutomation` does that automatically).
|
||||||
|
- **Flux pulls the chart from this repo (no OCI step).** Keeps the chart
|
||||||
|
in the same git history as the app code; a chart change rolls with the
|
||||||
|
same release.
|
||||||
|
- **HelmRelease lives in `anton-helm-workloads`, not in fleet.** Matches
|
||||||
|
the convention that the kotkanagrilli.fi subdomain pool (legacy
|
||||||
|
`kotkanagrilli/`, `hello-kotkan/`, future subdomains) is reconciled
|
||||||
|
out of the workloads repo, not the personal fleet repo.
|
||||||
|
- **kotkan, not armer.** The legacy `kotkanagrilli.fi` site and the
|
||||||
|
emdash replacement both live on `kotkan`; colocating reduces DNS /
|
||||||
|
ingress drift while the WP → Emdash story plays out.
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
|
|
||||||
|
FROM node:22-bookworm-slim AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends python3 make g++ ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY app/package.json app/package-lock.json* ./
|
||||||
|
# package-lock.json may not exist on the first commit — fall back to `npm install`
|
||||||
|
# so the image still builds; once a lockfile is committed, npm ci kicks in.
|
||||||
|
RUN if [ -f package-lock.json ]; then npm ci --include=dev; else npm install --include=dev; fi
|
||||||
|
|
||||||
|
FROM deps AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY app/ ./
|
||||||
|
RUN rm -f data.db data.db-shm data.db-wal && rm -rf uploads
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:22-bookworm-slim AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production \
|
||||||
|
HOST=0.0.0.0 \
|
||||||
|
PORT=4321
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends ca-certificates tini \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& useradd --system --uid 1001 --home /app emdash \
|
||||||
|
&& mkdir -p /app/state/uploads \
|
||||||
|
&& chown -R emdash:emdash /app
|
||||||
|
|
||||||
|
COPY --from=build --chown=emdash:emdash /app/package.json ./
|
||||||
|
COPY --from=build --chown=emdash:emdash /app/node_modules ./node_modules
|
||||||
|
COPY --from=build --chown=emdash:emdash /app/dist ./dist
|
||||||
|
COPY --from=build --chown=emdash:emdash /app/seed ./seed
|
||||||
|
|
||||||
|
# Persistent state lives in /app/state (single PVC in k3s).
|
||||||
|
# data.db and uploads/ are symlinked from the working directory so the
|
||||||
|
# default emdash paths (./data.db, ./uploads) resolve into the volume.
|
||||||
|
RUN ln -s /app/state/data.db /app/data.db \
|
||||||
|
&& ln -s /app/state/uploads /app/uploads
|
||||||
|
|
||||||
|
COPY --chown=emdash:emdash docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
|
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||||
|
|
||||||
|
USER emdash
|
||||||
|
EXPOSE 4321
|
||||||
|
VOLUME ["/app/state"]
|
||||||
|
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"]
|
||||||
|
CMD ["node", "./dist/server/entry.mjs"]
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# cms-plugins
|
||||||
|
|
||||||
|
A browseable catalog of CMS plugins and how they map across platforms,
|
||||||
|
built on [Emdash](https://github.com/emdash-cms/emdash) (Astro 6 +
|
||||||
|
TypeScript + SQLite). The seed dataset is the WordPress → Emdash plugin
|
||||||
|
parity matrix from `kotkanagrilli.fi`; new entries can be added through
|
||||||
|
the Emdash admin.
|
||||||
|
|
||||||
|
| Repo | URL | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| **cms-plugins** (this repo) | `git@git.oleks.space:oleks/cms-plugins.git` | App, Dockerfile, Helm chart, Woodpecker pipeline |
|
||||||
|
| **emdash.kotkanagrilli.fi** | `git@git.oleks.space:oleks/emdash-kotkanagrilli.git` | Reference Emdash deployment whose pattern this repo mirrors |
|
||||||
|
| **kotkanagrilli.fi** (WordPress) | `git@git.oleks.space:oleks/kotkanagrilli.fi.git` (+ `github.com:retif/kotkanagrilli.fi.git`) | Source of the seeded plugin entries (parity matrix) |
|
||||||
|
| **anton-helm-workloads** | `https://git.oleks.space/anton/helm-workloads` | Houses the FluxCD `HelmRelease`s for each environment (kotkanagrilli.fi subdomains) |
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Phase 0 — Emdash scaffold + Helm chart + Woodpecker pipeline. No content
|
||||||
|
beyond the seeded WordPress plugin entries. Not yet running anywhere.
|
||||||
|
|
||||||
|
## What's in here
|
||||||
|
|
||||||
|
- `app/` — Emdash scaffold (Astro 6, node target). Three collections: `cmses`,
|
||||||
|
`plugins`, `pages`. Seed at `app/seed/seed.json`.
|
||||||
|
- `Dockerfile` + `docker/entrypoint.sh` — production image. Single PVC at
|
||||||
|
`/app/state` holds `data.db` and `uploads/`.
|
||||||
|
- `deploy/helm/` — the Helm chart that runs the pod in k3s. Same shape as
|
||||||
|
`~/projects/emdash.kotkanagrilli.fi/deploy/helm/`.
|
||||||
|
- `deploy/fleet-overlay/` — `HelmRelease` + `GitRepository` + image-automation
|
||||||
|
templates ready to drop into the `anton-helm-workloads` repo. Not consumed
|
||||||
|
from this repo directly.
|
||||||
|
- `.woodpecker/container.yaml` — build + push pipeline (develop / staging /
|
||||||
|
production branches).
|
||||||
|
- `.ddev/` — DDEV setup for local development (`https://cms-plugins.ddev.site/`).
|
||||||
|
- `DEPLOYMENT.md` — full pipeline from `git push` to a running pod, plus
|
||||||
|
the first-time setup checklist.
|
||||||
|
- `ARCHITECTURE.md` — chart / image / Flux contracts.
|
||||||
|
- `docs/` — additional notes.
|
||||||
|
|
||||||
|
## Quickstart (local)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/projects/cms-plugins
|
||||||
|
ddev start
|
||||||
|
# → https://cms-plugins.ddev.site/ (admin at /_emdash/admin)
|
||||||
|
```
|
||||||
|
|
||||||
|
Or without DDEV:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd app
|
||||||
|
npm install
|
||||||
|
npm run bootstrap # emdash init: migrations + seed
|
||||||
|
npx emdash dev # http://localhost:4321/
|
||||||
|
```
|
||||||
|
|
||||||
|
Reset to a blank state:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -f app/data.db app/data.db-shm app/data.db-wal
|
||||||
|
rm -rf app/uploads
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploying
|
||||||
|
|
||||||
|
Push to the matching branch:
|
||||||
|
|
||||||
|
| Branch | Environment | URL |
|
||||||
|
|---|---|---|
|
||||||
|
| `develop` | dev (local DDEV only) | `https://cms-plugins.ddev.site/` |
|
||||||
|
| `staging` | staging | `https://cms-plugins-staging.kotkanagrilli.fi/` |
|
||||||
|
| `production` | production | `https://cms-plugins-production.kotkanagrilli.fi/` |
|
||||||
|
|
||||||
|
Woodpecker builds + pushes the image to `git.oleks.space/oleks/cms-plugins`,
|
||||||
|
FluxCD reconciles the chart from `deploy/helm/` against the matching
|
||||||
|
`HelmRelease` in `anton-helm-workloads/cms-plugins-{staging,production}/`.
|
||||||
|
|
||||||
|
See **[DEPLOYMENT.md](./DEPLOYMENT.md)** for the full flow, the relationship
|
||||||
|
between this repo and `anton-helm-workloads`, the first-time setup
|
||||||
|
checklist, and how rollbacks work.
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import node from "@astrojs/node";
|
||||||
|
import react from "@astrojs/react";
|
||||||
|
import { defineConfig, memoryCache } from "astro/config";
|
||||||
|
import emdash, { local } from "emdash/astro";
|
||||||
|
import { sqlite } from "emdash/db";
|
||||||
|
|
||||||
|
// Persistent state directory. Defaults to the working directory for dev/DDEV
|
||||||
|
// (so data.db + uploads/ stay where you'd expect them). The k8s deploy sets
|
||||||
|
// STATE_DIR=/app/state and mounts a PVC there so SQLite + uploads survive
|
||||||
|
// pod replacement.
|
||||||
|
const stateDir = process.env.STATE_DIR ?? ".";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
output: "server",
|
||||||
|
adapter: node({ mode: "standalone" }),
|
||||||
|
trailingSlash: "ignore",
|
||||||
|
server: { host: true, port: 4321 },
|
||||||
|
vite: {
|
||||||
|
server: {
|
||||||
|
// Dev runs behind DDEV's nginx (https://cms-plugins.ddev.site/).
|
||||||
|
// Vite's host check must allow the public hostname.
|
||||||
|
allowedHosts: ["cms-plugins.ddev.site", ".ddev.site"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
layout: "constrained",
|
||||||
|
responsiveStyles: true,
|
||||||
|
},
|
||||||
|
integrations: [
|
||||||
|
react(),
|
||||||
|
emdash({
|
||||||
|
database: sqlite({ url: `file:${stateDir}/data.db` }),
|
||||||
|
storage: local({
|
||||||
|
directory: `${stateDir}/uploads`,
|
||||||
|
baseUrl: "/_emdash/api/media/file",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
experimental: {
|
||||||
|
cache: { provider: memoryCache() },
|
||||||
|
},
|
||||||
|
devToolbar: { enabled: false },
|
||||||
|
});
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "cms-plugins-app",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"emdash": {
|
||||||
|
"label": "CMS plugins catalog",
|
||||||
|
"seed": "seed/seed.json"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"start": "node ./dist/server/entry.mjs",
|
||||||
|
"bootstrap": "emdash init",
|
||||||
|
"typecheck": "astro check"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/node": "^10.0.5",
|
||||||
|
"@astrojs/react": "^5.0.0",
|
||||||
|
"astro": "~6.2.2",
|
||||||
|
"better-sqlite3": "^12.8.0",
|
||||||
|
"emdash": "^0.10.0",
|
||||||
|
"react": "19.2.4",
|
||||||
|
"react-dom": "19.2.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@astrojs/check": "^0.9.7"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"better-sqlite3",
|
||||||
|
"esbuild"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,572 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://emdashcms.com/seed.schema.json",
|
||||||
|
"version": "1",
|
||||||
|
"meta": {
|
||||||
|
"name": "CMS plugins catalog",
|
||||||
|
"description": "WordPress → Emdash plugin parity catalog, seeded with kotkanagrilli.fi entries",
|
||||||
|
"author": "oleks"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "CMS plugins catalog",
|
||||||
|
"tagline": "WordPress → Emdash plugin parity, one entry at a time"
|
||||||
|
},
|
||||||
|
"collections": [
|
||||||
|
{
|
||||||
|
"slug": "cmses",
|
||||||
|
"label": "CMS platforms",
|
||||||
|
"labelSingular": "CMS platform",
|
||||||
|
"urlPattern": "/cms/{slug}",
|
||||||
|
"supports": ["search", "seo"],
|
||||||
|
"fields": [
|
||||||
|
{ "slug": "title", "label": "Name", "type": "string", "required": true, "searchable": true },
|
||||||
|
{ "slug": "website", "label": "Website URL", "type": "string" },
|
||||||
|
{ "slug": "description", "label": "Description", "type": "text", "searchable": true }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "plugins",
|
||||||
|
"label": "Plugins",
|
||||||
|
"labelSingular": "Plugin",
|
||||||
|
"urlPattern": "/plugins/{slug}",
|
||||||
|
"supports": ["drafts", "search", "seo"],
|
||||||
|
"fields": [
|
||||||
|
{ "slug": "title", "label": "Plugin name", "type": "string", "required": true, "searchable": true },
|
||||||
|
{ "slug": "purpose", "label": "Purpose", "type": "text", "searchable": true,
|
||||||
|
"description": "One-line description of what the plugin does." },
|
||||||
|
{ "slug": "source_cms", "label": "Source CMS", "type": "string", "required": true, "searchable": true,
|
||||||
|
"description": "Name of the CMS this plugin runs on today (e.g. WordPress)." },
|
||||||
|
{ "slug": "target_cms", "label": "Target CMS", "type": "string", "searchable": true,
|
||||||
|
"description": "Name of the CMS we're porting to, if applicable." },
|
||||||
|
{ "slug": "category", "label": "Category", "type": "string",
|
||||||
|
"description": "e-commerce, SEO, content, performance, etc." },
|
||||||
|
{
|
||||||
|
"slug": "status",
|
||||||
|
"label": "Migration status",
|
||||||
|
"type": "select",
|
||||||
|
"required": true,
|
||||||
|
"defaultValue": "proposed",
|
||||||
|
"searchable": true,
|
||||||
|
"options": [
|
||||||
|
{ "value": "port", "label": "Port — reimplement on target" },
|
||||||
|
{ "value": "built-in", "label": "Built-in — covered by target core" },
|
||||||
|
{ "value": "saas", "label": "SaaS — replaced by external service" },
|
||||||
|
{ "value": "drop", "label": "Drop — not needed" },
|
||||||
|
{ "value": "gated", "label": "Gated — pending decision" },
|
||||||
|
{ "value": "done", "label": "Done — ported and shipped" },
|
||||||
|
{ "value": "proposed", "label": "Proposed — newly submitted" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "slug": "source_repo_url", "label": "Source repo URL", "type": "string" },
|
||||||
|
{ "slug": "target_repo_url", "label": "Target repo URL", "type": "string" },
|
||||||
|
{ "slug": "notes", "label": "Migration notes", "type": "portableText", "searchable": true }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "pages",
|
||||||
|
"label": "Pages",
|
||||||
|
"labelSingular": "Page",
|
||||||
|
"urlPattern": "/{slug}",
|
||||||
|
"supports": ["drafts", "revisions", "search", "seo"],
|
||||||
|
"fields": [
|
||||||
|
{ "slug": "title", "label": "Title", "type": "string", "required": true, "searchable": true },
|
||||||
|
{ "slug": "content", "label": "Content", "type": "portableText", "searchable": true },
|
||||||
|
{ "slug": "excerpt", "label": "Excerpt", "type": "text" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": {
|
||||||
|
"cmses": [
|
||||||
|
{
|
||||||
|
"slug": "wordpress",
|
||||||
|
"data": {
|
||||||
|
"title": "WordPress",
|
||||||
|
"website": "https://wordpress.org",
|
||||||
|
"description": "PHP/MariaDB CMS with the largest plugin ecosystem. Source platform for the kotkanagrilli.fi migration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "emdash",
|
||||||
|
"data": {
|
||||||
|
"title": "Emdash",
|
||||||
|
"website": "https://github.com/emdash-cms/emdash",
|
||||||
|
"description": "TypeScript/Astro/SQLite CMS positioned as a spiritual successor to WordPress. Target platform for the kotkanagrilli.fi migration."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"slug": "about",
|
||||||
|
"data": {
|
||||||
|
"title": "About this catalog",
|
||||||
|
"excerpt": "What this catalog is and how it's organized.",
|
||||||
|
"content": [
|
||||||
|
{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "This catalog tracks plugins from one CMS and how they map to another. The seeded entries come from the kotkanagrilli.fi WordPress → Emdash migration, where ~30 third-party and custom plugins needed to be classified before the rebuild." }] },
|
||||||
|
{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Each entry has a migration status (port, built-in, saas, drop, gated, done, proposed) and free-form notes. New entries can be added through the Emdash admin at /_emdash/admin." }] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"slug": "woocommerce",
|
||||||
|
"data": {
|
||||||
|
"title": "WooCommerce",
|
||||||
|
"purpose": "E-commerce / order management for WordPress.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "e-commerce",
|
||||||
|
"status": "gated",
|
||||||
|
"source_repo_url": "https://wordpress.org/plugins/woocommerce/",
|
||||||
|
"notes": [
|
||||||
|
{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Fate depends on the kotkanagrilli WooCommerce decision gate: (a) keep WP on a subdomain for ordering, (b) build an Emdash orders+SumUp plugin, (c) outsource to an ordering SaaS." }] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "woocommerce-payments",
|
||||||
|
"data": {
|
||||||
|
"title": "WooCommerce Payments",
|
||||||
|
"purpose": "WC native payment gateway.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "e-commerce",
|
||||||
|
"status": "gated",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Tied to the WooCommerce decision." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "sumup-payment-gateway-for-woocommerce",
|
||||||
|
"data": {
|
||||||
|
"title": "SumUp Payment Gateway for WooCommerce",
|
||||||
|
"purpose": "SumUp checkout integration for WC.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "e-commerce",
|
||||||
|
"status": "gated",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Tied to WC decision. SumUp REST API is callable directly from a custom Emdash plugin if option (b) is chosen." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "fluid-checkout",
|
||||||
|
"data": {
|
||||||
|
"title": "Fluid Checkout",
|
||||||
|
"purpose": "Improved WooCommerce checkout UX.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "e-commerce",
|
||||||
|
"status": "gated"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "woo-checkout-field-editor-pro",
|
||||||
|
"data": {
|
||||||
|
"title": "Woo Checkout Field Editor Pro",
|
||||||
|
"purpose": "Custom checkout fields for WooCommerce.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "e-commerce",
|
||||||
|
"status": "gated"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "woocommerce-customizer",
|
||||||
|
"data": {
|
||||||
|
"title": "WooCommerce Customizer",
|
||||||
|
"purpose": "Miscellaneous WooCommerce tweaks.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "e-commerce",
|
||||||
|
"status": "gated"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "woc-open-close",
|
||||||
|
"data": {
|
||||||
|
"title": "WooCommerce Open/Close",
|
||||||
|
"purpose": "Show/hide the store as open or closed.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "e-commerce",
|
||||||
|
"status": "port",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Reimplement as a small Emdash plugin or Astro middleware reading a business-hours config. Not actually WC-coupled despite the name." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "google-listings-and-ads",
|
||||||
|
"data": {
|
||||||
|
"title": "Google Listings & Ads",
|
||||||
|
"purpose": "Merchant Center feed for WooCommerce.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "marketing",
|
||||||
|
"status": "gated"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "polylang",
|
||||||
|
"data": {
|
||||||
|
"title": "Polylang",
|
||||||
|
"purpose": "FI/EN translation for WordPress.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "i18n",
|
||||||
|
"status": "port",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Native Emdash collections + locale routing in Astro (src/pages/[lang]/...) replaces Polylang." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "connect-polylang-elementor",
|
||||||
|
"data": {
|
||||||
|
"title": "Connect Polylang for Elementor",
|
||||||
|
"purpose": "Glue between Polylang and Elementor.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "i18n",
|
||||||
|
"status": "drop",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Dropped together with Elementor." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "elementor",
|
||||||
|
"data": {
|
||||||
|
"title": "Elementor",
|
||||||
|
"purpose": "Visual page builder for WordPress.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "page-builder",
|
||||||
|
"status": "drop",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Rebuild pages as Astro components / Emdash portable-text blocks." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "cafe-eatery",
|
||||||
|
"data": {
|
||||||
|
"title": "Cafe Eatery",
|
||||||
|
"purpose": "WordPress block theme (parent of the kotkanagrilli child theme).",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "theme",
|
||||||
|
"status": "drop",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Rebuild as an Astro theme (Phase 2)." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "autoptimize",
|
||||||
|
"data": {
|
||||||
|
"title": "Autoptimize",
|
||||||
|
"purpose": "Asset minification + concatenation.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "performance",
|
||||||
|
"status": "built-in",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Astro/Vite handles bundling natively." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "jetpack-boost",
|
||||||
|
"data": {
|
||||||
|
"title": "Jetpack Boost",
|
||||||
|
"purpose": "Performance hints for WordPress.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "performance",
|
||||||
|
"status": "built-in",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Astro + Cloudflare handle this." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "ewww-image-optimizer",
|
||||||
|
"data": {
|
||||||
|
"title": "EWWW Image Optimizer",
|
||||||
|
"purpose": "Image optimization for WordPress media.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "performance",
|
||||||
|
"status": "built-in",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Astro Image integration + responsive styles already wired in astro.config.mjs." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "jetpack",
|
||||||
|
"data": {
|
||||||
|
"title": "Jetpack",
|
||||||
|
"purpose": "Stats, hardening, related-posts, and more.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "ops",
|
||||||
|
"status": "saas",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Replaced by Cloudflare analytics + WAF." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "redis-cache",
|
||||||
|
"data": {
|
||||||
|
"title": "Redis Object Cache",
|
||||||
|
"purpose": "PHP object cache for WordPress.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "performance",
|
||||||
|
"status": "drop",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "No PHP, no need; Astro + Emdash memoryCache provider already wired." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "wp-mail-smtp",
|
||||||
|
"data": {
|
||||||
|
"title": "WP Mail SMTP",
|
||||||
|
"purpose": "Outbound mail via external SMTP.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "mail",
|
||||||
|
"status": "saas",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "External SMTP configured at the platform layer, or via an Emdash mailer plugin." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "cb-change-mail-sender",
|
||||||
|
"data": {
|
||||||
|
"title": "CB Change Mail Sender",
|
||||||
|
"purpose": "Override the sender address on outgoing mail.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "mail",
|
||||||
|
"status": "port",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Handled by the same Emdash mailer plugin that replaces wp-mail-smtp." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "wpforms-lite",
|
||||||
|
"data": {
|
||||||
|
"title": "WPForms Lite",
|
||||||
|
"purpose": "Contact form builder.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "forms",
|
||||||
|
"status": "port",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Small Astro form action posting to a server endpoint, or a Cloudflare Worker on the CF target." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "wp-google-maps",
|
||||||
|
"data": {
|
||||||
|
"title": "WP Google Maps",
|
||||||
|
"purpose": "Map embed for posts/pages.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "content",
|
||||||
|
"status": "port",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Static iframe embed in the theme — trivial port." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "sticky-chat-widget",
|
||||||
|
"data": {
|
||||||
|
"title": "Sticky Chat Widget",
|
||||||
|
"purpose": "Floating WhatsApp/chat button.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "content",
|
||||||
|
"status": "port",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Static component in the theme — trivial port." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "shortcodes-ultimate",
|
||||||
|
"data": {
|
||||||
|
"title": "Shortcodes Ultimate",
|
||||||
|
"purpose": "Shortcode library for the WP editor.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "content",
|
||||||
|
"status": "drop",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Astro components / portable-text block types replace it." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "tinymce-advanced",
|
||||||
|
"data": {
|
||||||
|
"title": "TinyMCE Advanced",
|
||||||
|
"purpose": "Editor enhancement for WordPress.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "editor",
|
||||||
|
"status": "drop",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Emdash has its own editor." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "lara-google-analytics",
|
||||||
|
"data": {
|
||||||
|
"title": "Lara Google Analytics",
|
||||||
|
"purpose": "GA4 tag for WordPress.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "analytics",
|
||||||
|
"status": "port",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Static <script> in the theme head — trivial port." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "social-login",
|
||||||
|
"data": {
|
||||||
|
"title": "Social Login",
|
||||||
|
"purpose": "OAuth login for WordPress.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "auth",
|
||||||
|
"status": "drop",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Drop unless actually needed; Emdash uses passkeys by default." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "twentytwentyfive",
|
||||||
|
"data": {
|
||||||
|
"title": "Twenty Twenty-Five",
|
||||||
|
"purpose": "Default WordPress fallback theme.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "theme",
|
||||||
|
"status": "drop"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "store-closed-button",
|
||||||
|
"data": {
|
||||||
|
"title": "store-closed-button (mu-plugin)",
|
||||||
|
"purpose": "Toggle the store between open and closed.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "ops",
|
||||||
|
"status": "port",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Becomes an Emdash plugin: business-hours config + an admin badge. Replaces store.py CLI." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "store-override-admin-bar",
|
||||||
|
"data": {
|
||||||
|
"title": "store-override-admin-bar (mu-plugin)",
|
||||||
|
"purpose": "Admin-bar indicator for the open/close override.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "ops",
|
||||||
|
"status": "port",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Folded into the same business-hours Emdash plugin." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "nginx-cache-indicator",
|
||||||
|
"data": {
|
||||||
|
"title": "nginx-cache-indicator (mu-plugin)",
|
||||||
|
"purpose": "Visual UX hint for nginx cache hits.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "ops",
|
||||||
|
"status": "port",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Folded into the same admin badge that store-closed-button becomes." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "out-of-stock-display",
|
||||||
|
"data": {
|
||||||
|
"title": "out-of-stock-display (mu-plugin)",
|
||||||
|
"purpose": "WC UX fix for out-of-stock items.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "e-commerce",
|
||||||
|
"status": "gated"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "cart-fragment-cache-fix",
|
||||||
|
"data": {
|
||||||
|
"title": "cart-fragment-cache-fix (mu-plugin)",
|
||||||
|
"purpose": "Fix for WC cart fragment caching.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "e-commerce",
|
||||||
|
"status": "gated"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "checkout-button-fix",
|
||||||
|
"data": {
|
||||||
|
"title": "checkout-button-fix (mu-plugin)",
|
||||||
|
"purpose": "WC checkout button UX fix.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "e-commerce",
|
||||||
|
"status": "gated"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "checkout-local-pickup",
|
||||||
|
"data": {
|
||||||
|
"title": "checkout-local-pickup (mu-plugin)",
|
||||||
|
"purpose": "Local-pickup behavior at checkout.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "e-commerce",
|
||||||
|
"status": "gated"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "sumup-payment-verify",
|
||||||
|
"data": {
|
||||||
|
"title": "sumup-payment-verify (mu-plugin)",
|
||||||
|
"purpose": "Manual SumUp payment verification step.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "e-commerce",
|
||||||
|
"status": "gated"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "sumup-webhook-fix",
|
||||||
|
"data": {
|
||||||
|
"title": "sumup-webhook-fix (mu-plugin)",
|
||||||
|
"purpose": "Patch for the SumUp webhook handler.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "e-commerce",
|
||||||
|
"status": "gated"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "load-elementor-fonts",
|
||||||
|
"data": {
|
||||||
|
"title": "load-elementor-fonts (mu-plugin)",
|
||||||
|
"purpose": "Force-load Elementor fonts.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "theme",
|
||||||
|
"status": "drop",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Dropped together with Elementor." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "remove-theme-ads",
|
||||||
|
"data": {
|
||||||
|
"title": "remove-theme-ads (mu-plugin)",
|
||||||
|
"purpose": "Strip upsell ads from the Cafe Eatery theme.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "theme",
|
||||||
|
"status": "drop",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Dropped together with the Cafe Eatery theme." }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "security-hardening",
|
||||||
|
"data": {
|
||||||
|
"title": "security-hardening (mu-plugin)",
|
||||||
|
"purpose": "Response headers + login lockdown.",
|
||||||
|
"source_cms": "WordPress",
|
||||||
|
"target_cms": "Emdash",
|
||||||
|
"category": "security",
|
||||||
|
"status": "port",
|
||||||
|
"notes": [{ "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Cloudflare WAF rules cover the WAF half; Astro middleware sets response headers for the rest." }] }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
import StatusBadge from "./StatusBadge.astro";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
entry: {
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
title: string;
|
||||||
|
purpose?: string | null;
|
||||||
|
status?: string | null;
|
||||||
|
source_cms?: string | null;
|
||||||
|
target_cms?: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { entry } = Astro.props;
|
||||||
|
const d = entry.data;
|
||||||
|
---
|
||||||
|
<li class="plugin-card">
|
||||||
|
<h3><a href={`/plugins/${entry.id}`}>{d.title}</a></h3>
|
||||||
|
<div class="meta">
|
||||||
|
<StatusBadge status={d.status} />
|
||||||
|
{d.source_cms && <span>{d.source_cms} → {d.target_cms ?? "—"}</span>}
|
||||||
|
</div>
|
||||||
|
{d.purpose && <p>{d.purpose}</p>}
|
||||||
|
</li>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
status: string | null | undefined;
|
||||||
|
}
|
||||||
|
const { status } = Astro.props;
|
||||||
|
const value = (status ?? "proposed").toLowerCase();
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
port: "Port",
|
||||||
|
"built-in": "Built-in",
|
||||||
|
drop: "Drop",
|
||||||
|
saas: "SaaS",
|
||||||
|
gated: "Gated",
|
||||||
|
done: "Done",
|
||||||
|
proposed: "Proposed",
|
||||||
|
};
|
||||||
|
const label = labels[value] ?? value;
|
||||||
|
---
|
||||||
|
<span class={`badge badge--${value}`}>{label}</span>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
import StatusBadge from "./StatusBadge.astro";
|
||||||
|
const items = [
|
||||||
|
{ status: "port", desc: "must be reimplemented on the target CMS" },
|
||||||
|
{ status: "built-in", desc: "covered by target CMS core / framework" },
|
||||||
|
{ status: "saas", desc: "replaced by an external service" },
|
||||||
|
{ status: "drop", desc: "not needed on the target CMS" },
|
||||||
|
{ status: "gated", desc: "fate depends on an unresolved decision" },
|
||||||
|
{ status: "done", desc: "ported and shipped" },
|
||||||
|
{ status: "proposed", desc: "submitted for cataloging" },
|
||||||
|
];
|
||||||
|
---
|
||||||
|
<div class="legend">
|
||||||
|
{items.map((i) => (
|
||||||
|
<span class="item"><StatusBadge status={i.status} /> {i.desc}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
import { getSiteSettings } from "emdash";
|
||||||
|
import { EmDashHead, EmDashBodyStart, EmDashBodyEnd } from "emdash/ui";
|
||||||
|
import { createPublicPageContext } from "emdash/page";
|
||||||
|
import "../styles/global.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
description?: string | null;
|
||||||
|
content?: { collection: string; id: string; slug?: string | null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, description, content } = Astro.props;
|
||||||
|
const settings = await getSiteSettings();
|
||||||
|
const siteTitle = settings?.title ?? "CMS plugins catalog";
|
||||||
|
const siteTagline = settings?.tagline ?? "WordPress → Emdash plugin parity catalog";
|
||||||
|
|
||||||
|
const fullTitle = title === siteTitle ? siteTitle : `${title} — ${siteTitle}`;
|
||||||
|
const pageDescription = description ?? siteTagline;
|
||||||
|
|
||||||
|
const pageCtx = createPublicPageContext({
|
||||||
|
Astro,
|
||||||
|
kind: content ? "content" : "custom",
|
||||||
|
pageType: "website",
|
||||||
|
title: fullTitle,
|
||||||
|
pageTitle: title,
|
||||||
|
description: pageDescription,
|
||||||
|
content,
|
||||||
|
siteName: siteTitle,
|
||||||
|
});
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{fullTitle}</title>
|
||||||
|
<meta name="description" content={pageDescription} />
|
||||||
|
<EmDashHead page={pageCtx} />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<EmDashBodyStart page={pageCtx} />
|
||||||
|
<header class="site">
|
||||||
|
<div class="container row">
|
||||||
|
<a href="/" class="brand">{siteTitle}</a>
|
||||||
|
<nav>
|
||||||
|
<a href="/">Plugins</a>
|
||||||
|
<a href="/cms">By CMS</a>
|
||||||
|
<a href="/about">About</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div class="container">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer class="site">
|
||||||
|
<div class="container">
|
||||||
|
<p>{siteTagline}</p>
|
||||||
|
<p><a href="/_emdash/admin">Admin</a></p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<EmDashBodyEnd page={pageCtx} />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { defineLiveCollection } from "astro:content";
|
||||||
|
import { emdashLoader } from "emdash/runtime";
|
||||||
|
|
||||||
|
export const collections = {
|
||||||
|
_emdash: defineLiveCollection({ loader: emdashLoader() }),
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
import Base from "../layouts/Base.astro";
|
||||||
|
---
|
||||||
|
<Base title="Not found">
|
||||||
|
<h1>Not found</h1>
|
||||||
|
<p>The page you're looking for doesn't exist. <a href="/">Back to the catalog.</a></p>
|
||||||
|
</Base>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
import { getEmDashEntry, decodeSlug } from "emdash";
|
||||||
|
import { PortableText } from "emdash/ui";
|
||||||
|
import Base from "../layouts/Base.astro";
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
const slug = decodeSlug(Astro.params.slug);
|
||||||
|
if (!slug) return Astro.redirect("/404");
|
||||||
|
|
||||||
|
const { entry, cacheHint } = await getEmDashEntry("pages", slug);
|
||||||
|
if (!entry) return Astro.redirect("/404");
|
||||||
|
Astro.cache.set(cacheHint);
|
||||||
|
---
|
||||||
|
<Base title={entry.data.title} description={entry.data.excerpt ?? undefined} content={{ collection: "pages", id: entry.data.id, slug: entry.id }}>
|
||||||
|
<article class="detail">
|
||||||
|
<h1>{entry.data.title}</h1>
|
||||||
|
{entry.data.content && <div class="notes"><PortableText value={entry.data.content} /></div>}
|
||||||
|
</article>
|
||||||
|
</Base>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
import { getEmDashCollection, getEmDashEntry, decodeSlug } from "emdash";
|
||||||
|
import Base from "../../layouts/Base.astro";
|
||||||
|
import PluginCard from "../../components/PluginCard.astro";
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
const slug = decodeSlug(Astro.params.slug);
|
||||||
|
if (!slug) return Astro.redirect("/404");
|
||||||
|
|
||||||
|
const { entry: cms, cacheHint } = await getEmDashEntry("cmses", slug);
|
||||||
|
if (!cms) return Astro.redirect("/404");
|
||||||
|
Astro.cache.set(cacheHint);
|
||||||
|
|
||||||
|
const { entries: all } = await getEmDashCollection("plugins", {
|
||||||
|
orderBy: { title: "asc" },
|
||||||
|
limit: 9999,
|
||||||
|
});
|
||||||
|
const cmsName = cms.data.title as string;
|
||||||
|
const plugins = all.filter((p) => p.data.source_cms === cmsName);
|
||||||
|
---
|
||||||
|
<Base title={cmsName} description={cms.data.description ?? undefined}>
|
||||||
|
<p class="crumbs"><a href="/cms">By CMS</a></p>
|
||||||
|
<h1>{cmsName}</h1>
|
||||||
|
{cms.data.website && <p><a href={cms.data.website}>{cms.data.website}</a></p>}
|
||||||
|
{cms.data.description && <p>{cms.data.description}</p>}
|
||||||
|
|
||||||
|
<h2 style="margin-top:2rem">Plugins ({plugins.length})</h2>
|
||||||
|
{plugins.length === 0 ? (
|
||||||
|
<p class="empty">No plugins cataloged for this CMS yet.</p>
|
||||||
|
) : (
|
||||||
|
<ul class="plugin-grid">
|
||||||
|
{plugins.map((entry) => <PluginCard entry={entry} />)}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</Base>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
import { getEmDashCollection } from "emdash";
|
||||||
|
import Base from "../../layouts/Base.astro";
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
const { entries: cmses, cacheHint } = await getEmDashCollection("cmses", {
|
||||||
|
orderBy: { title: "asc" },
|
||||||
|
});
|
||||||
|
Astro.cache.set(cacheHint);
|
||||||
|
|
||||||
|
const { entries: plugins } = await getEmDashCollection("plugins", { limit: 9999 });
|
||||||
|
const countBySource = new Map<string, number>();
|
||||||
|
for (const p of plugins) {
|
||||||
|
const s = p.data.source_cms;
|
||||||
|
if (s) countBySource.set(s, (countBySource.get(s) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
---
|
||||||
|
<Base title="By CMS" description="Browse plugins grouped by source CMS.">
|
||||||
|
<h1>By CMS</h1>
|
||||||
|
<ul class="plugin-grid">
|
||||||
|
{cmses.map((c) => (
|
||||||
|
<li class="plugin-card">
|
||||||
|
<h3><a href={`/cms/${c.id}`}>{c.data.title}</a></h3>
|
||||||
|
<p class="meta">{countBySource.get(c.data.title) ?? 0} plugins</p>
|
||||||
|
{c.data.description && <p>{c.data.description}</p>}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Base>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
import { getEmDashCollection } from "emdash";
|
||||||
|
import Base from "../layouts/Base.astro";
|
||||||
|
import PluginCard from "../components/PluginCard.astro";
|
||||||
|
import StatusLegend from "../components/StatusLegend.astro";
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
const { entries: plugins, cacheHint } = await getEmDashCollection("plugins", {
|
||||||
|
orderBy: { title: "asc" },
|
||||||
|
});
|
||||||
|
Astro.cache.set(cacheHint);
|
||||||
|
|
||||||
|
const url = new URL(Astro.request.url);
|
||||||
|
const q = (url.searchParams.get("q") ?? "").toLowerCase().trim();
|
||||||
|
const statusFilter = url.searchParams.get("status") ?? "";
|
||||||
|
const sourceFilter = url.searchParams.get("source") ?? "";
|
||||||
|
|
||||||
|
const sources = Array.from(new Set(plugins.map((p) => p.data.source_cms).filter(Boolean))).sort();
|
||||||
|
const statuses = ["port", "built-in", "saas", "drop", "gated", "done", "proposed"];
|
||||||
|
|
||||||
|
const filtered = plugins.filter((p) => {
|
||||||
|
const d = p.data;
|
||||||
|
if (q && !(`${d.title} ${d.purpose ?? ""}`.toLowerCase().includes(q))) return false;
|
||||||
|
if (statusFilter && d.status !== statusFilter) return false;
|
||||||
|
if (sourceFilter && d.source_cms !== sourceFilter) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
---
|
||||||
|
<Base title="Plugins" description="Browse cataloged CMS plugins and their migration status.">
|
||||||
|
<h1>Plugins</h1>
|
||||||
|
<p>{plugins.length} entries cataloged. Filter by status, source CMS, or search by name.</p>
|
||||||
|
|
||||||
|
<StatusLegend />
|
||||||
|
|
||||||
|
<form class="toolbar" method="get">
|
||||||
|
<input type="search" name="q" placeholder="Search…" value={q} />
|
||||||
|
<select name="source">
|
||||||
|
<option value="">All source CMSes</option>
|
||||||
|
{sources.map((s) => (
|
||||||
|
<option value={s} selected={s === sourceFilter}>{s}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select name="status">
|
||||||
|
<option value="">All statuses</option>
|
||||||
|
{statuses.map((s) => (
|
||||||
|
<option value={s} selected={s === statusFilter}>{s}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button type="submit">Apply</button>
|
||||||
|
{(q || statusFilter || sourceFilter) && <a href="/">Reset</a>}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{filtered.length === 0 ? (
|
||||||
|
<p class="empty">No plugins match the current filters.</p>
|
||||||
|
) : (
|
||||||
|
<ul class="plugin-grid">
|
||||||
|
{filtered.map((entry) => <PluginCard entry={entry} />)}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</Base>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
import { getEmDashEntry, decodeSlug } from "emdash";
|
||||||
|
import { PortableText } from "emdash/ui";
|
||||||
|
import Base from "../../layouts/Base.astro";
|
||||||
|
import StatusBadge from "../../components/StatusBadge.astro";
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
const slug = decodeSlug(Astro.params.slug);
|
||||||
|
if (!slug) return Astro.redirect("/404");
|
||||||
|
|
||||||
|
const { entry, cacheHint } = await getEmDashEntry("plugins", slug);
|
||||||
|
if (!entry) return Astro.redirect("/404");
|
||||||
|
Astro.cache.set(cacheHint);
|
||||||
|
|
||||||
|
const d = entry.data;
|
||||||
|
---
|
||||||
|
<Base title={d.title} description={d.purpose ?? undefined} content={{ collection: "plugins", id: entry.data.id, slug: entry.id }}>
|
||||||
|
<article class="detail">
|
||||||
|
<header>
|
||||||
|
<p class="crumbs">
|
||||||
|
<a href="/">Plugins</a>
|
||||||
|
{d.source_cms && (<> · <a href={`/cms/${d.source_cms.toLowerCase()}`}>{d.source_cms}</a></>)}
|
||||||
|
</p>
|
||||||
|
<h1>{d.title}</h1>
|
||||||
|
<p>
|
||||||
|
<StatusBadge status={d.status} />
|
||||||
|
{d.source_cms && <span style="margin-left:.75rem;color:var(--c-muted)">{d.source_cms}{d.target_cms ? ` → ${d.target_cms}` : ""}</span>}
|
||||||
|
</p>
|
||||||
|
{d.purpose && <p style="font-size:1.05rem">{d.purpose}</p>}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
{d.category && (<><dt>Category</dt><dd>{d.category}</dd></>)}
|
||||||
|
{d.source_cms && (<><dt>Source CMS</dt><dd>{d.source_cms}</dd></>)}
|
||||||
|
{d.target_cms && (<><dt>Target CMS</dt><dd>{d.target_cms}</dd></>)}
|
||||||
|
{d.source_repo_url && (<><dt>Source repo</dt><dd><a href={d.source_repo_url}>{d.source_repo_url}</a></dd></>)}
|
||||||
|
{d.target_repo_url && (<><dt>Target repo</dt><dd><a href={d.target_repo_url}>{d.target_repo_url}</a></dd></>)}
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
{d.notes && (
|
||||||
|
<section class="notes">
|
||||||
|
<h2>Migration notes</h2>
|
||||||
|
<PortableText value={d.notes} />
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
</Base>
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
:root {
|
||||||
|
--c-bg: #ffffff;
|
||||||
|
--c-bg-alt: #f6f7f9;
|
||||||
|
--c-border: #e2e4e9;
|
||||||
|
--c-heading: #0b1320;
|
||||||
|
--c-body: #2a2f3a;
|
||||||
|
--c-muted: #5b6371;
|
||||||
|
--c-link: #2b5cff;
|
||||||
|
--c-accent: #6b21a8;
|
||||||
|
|
||||||
|
/* status palette */
|
||||||
|
--c-status-port: #2563eb;
|
||||||
|
--c-status-builtin: #16a34a;
|
||||||
|
--c-status-drop: #6b7280;
|
||||||
|
--c-status-saas: #d97706;
|
||||||
|
--c-status-gated: #b91c1c;
|
||||||
|
--c-status-done: #047857;
|
||||||
|
--c-status-proposed: #7c3aed;
|
||||||
|
|
||||||
|
--fs-base: 1rem;
|
||||||
|
--fs-sm: 0.875rem;
|
||||||
|
--fs-h1: clamp(1.75rem, 4vw, 2.5rem);
|
||||||
|
--fs-h2: clamp(1.4rem, 3vw, 1.9rem);
|
||||||
|
--fs-h3: 1.25rem;
|
||||||
|
--fs-h4: 1.1rem;
|
||||||
|
|
||||||
|
--radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
html { font-size: 16px; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
||||||
|
color: var(--c-body);
|
||||||
|
background: var(--c-bg);
|
||||||
|
line-height: 1.55;
|
||||||
|
font-size: var(--fs-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4 { color: var(--c-heading); margin: 0 0 0.5rem; line-height: 1.25; }
|
||||||
|
h1 { font-size: var(--fs-h1); }
|
||||||
|
h2 { font-size: var(--fs-h2); }
|
||||||
|
h3 { font-size: var(--fs-h3); }
|
||||||
|
h4 { font-size: var(--fs-h4); }
|
||||||
|
|
||||||
|
a { color: var(--c-link); text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 clamp(1rem, 3vw, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
header.site {
|
||||||
|
border-bottom: 1px solid var(--c-border);
|
||||||
|
padding: 1rem 0;
|
||||||
|
background: var(--c-bg);
|
||||||
|
}
|
||||||
|
header.site .row {
|
||||||
|
display: flex; align-items: center; justify-content: space-between; gap: 1.5rem;
|
||||||
|
}
|
||||||
|
header.site .brand { font-weight: 700; color: var(--c-heading); }
|
||||||
|
header.site .brand:hover { text-decoration: none; }
|
||||||
|
header.site nav a { color: var(--c-muted); margin-left: 1.25rem; font-size: var(--fs-sm); }
|
||||||
|
header.site nav a:hover { color: var(--c-heading); }
|
||||||
|
|
||||||
|
main { padding: clamp(1.5rem, 4vw, 3rem) 0; min-height: 60vh; }
|
||||||
|
|
||||||
|
footer.site {
|
||||||
|
border-top: 1px solid var(--c-border);
|
||||||
|
padding: 2rem 0;
|
||||||
|
color: var(--c-muted);
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* status badge */
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: white;
|
||||||
|
background: var(--c-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.badge--port { background: var(--c-status-port); }
|
||||||
|
.badge--built-in { background: var(--c-status-builtin); }
|
||||||
|
.badge--drop { background: var(--c-status-drop); }
|
||||||
|
.badge--saas { background: var(--c-status-saas); }
|
||||||
|
.badge--gated { background: var(--c-status-gated); }
|
||||||
|
.badge--done { background: var(--c-status-done); }
|
||||||
|
.badge--proposed { background: var(--c-status-proposed); }
|
||||||
|
|
||||||
|
/* plugin grid + cards */
|
||||||
|
.toolbar {
|
||||||
|
display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center;
|
||||||
|
padding: 0.75rem 0 1.5rem;
|
||||||
|
}
|
||||||
|
.toolbar input[type="search"], .toolbar select {
|
||||||
|
font: inherit;
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
border: 1px solid var(--c-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
.toolbar input[type="search"] { min-width: 240px; }
|
||||||
|
|
||||||
|
.plugin-grid {
|
||||||
|
list-style: none; padding: 0; margin: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.plugin-card {
|
||||||
|
border: 1px solid var(--c-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 1rem 1.1rem;
|
||||||
|
background: var(--c-bg);
|
||||||
|
display: flex; flex-direction: column; gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.plugin-card h3 { margin: 0; font-size: 1.05rem; }
|
||||||
|
.plugin-card h3 a { color: var(--c-heading); }
|
||||||
|
.plugin-card .meta { font-size: 0.78rem; color: var(--c-muted); display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
|
||||||
|
.plugin-card p { margin: 0; font-size: 0.92rem; color: var(--c-body); }
|
||||||
|
|
||||||
|
/* detail page */
|
||||||
|
.detail header { border-bottom: 1px solid var(--c-border); padding-bottom: 1rem; margin-bottom: 1.5rem; }
|
||||||
|
.detail .crumbs { color: var(--c-muted); font-size: var(--fs-sm); margin-bottom: 0.25rem; }
|
||||||
|
.detail dl { display: grid; grid-template-columns: max-content 1fr; gap: 0.4rem 1rem; margin: 0 0 1.5rem; }
|
||||||
|
.detail dt { color: var(--c-muted); font-size: var(--fs-sm); }
|
||||||
|
.detail dd { margin: 0; }
|
||||||
|
.detail .notes { line-height: 1.65; }
|
||||||
|
.detail .notes pre { background: var(--c-bg-alt); padding: 0.6rem 0.8rem; border-radius: var(--radius); overflow-x: auto; }
|
||||||
|
|
||||||
|
/* status legend */
|
||||||
|
.legend { display: flex; flex-wrap: wrap; gap: 0.5rem; font-size: var(--fs-sm); color: var(--c-muted); padding: 0 0 1rem; }
|
||||||
|
.legend .item { display: inline-flex; gap: 0.4rem; align-items: center; }
|
||||||
|
|
||||||
|
.empty { padding: 2rem; text-align: center; color: var(--c-muted); }
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/base",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"include": ["src", ".astro/types.d.ts", "emdash-env.d.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# deploy/
|
||||||
|
|
||||||
|
Two sibling directories with very different lifecycles:
|
||||||
|
|
||||||
|
- **`helm/`** — the Helm chart that runs the pod. FluxCD pulls it
|
||||||
|
directly from this repo on the branch matching each environment
|
||||||
|
(no `helm push` step). Edit this in lockstep with the app code that
|
||||||
|
depends on it.
|
||||||
|
- **`fleet-overlay/`** — templates for the FluxCD manifests that live in
|
||||||
|
the `anton-helm-workloads` repo. Not consumed from here — they're
|
||||||
|
versioned alongside the chart so the chart's contract with Flux stays
|
||||||
|
legible.
|
||||||
|
|
||||||
|
See `../DEPLOYMENT.md` for the end-to-end pipeline.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# Fleet overlay templates
|
||||||
|
|
||||||
|
The YAMLs under `cms-plugins-staging/` and `cms-plugins-production/` are the
|
||||||
|
FluxCD manifests that drive each environment. They are **not** consumed from
|
||||||
|
this repo — they live here as a versioned blueprint, intended to be copied
|
||||||
|
into the workloads repo that Flux watches:
|
||||||
|
|
||||||
|
```
|
||||||
|
git.oleks.space/anton/helm-workloads
|
||||||
|
├─ cms-plugins-staging/ ← copy from deploy/fleet-overlay/cms-plugins-staging/
|
||||||
|
├─ cms-plugins-production/ ← copy from deploy/fleet-overlay/cms-plugins-production/
|
||||||
|
└─ kustomization.yaml ← add both directories to `resources:`
|
||||||
|
```
|
||||||
|
|
||||||
|
See `../../DEPLOYMENT.md` for the full pipeline and the first-time setup
|
||||||
|
checklist (deploy keys, sops secrets, Woodpecker secrets, DNS).
|
||||||
|
|
||||||
|
## Shape
|
||||||
|
|
||||||
|
Each env directory contains five files, mirroring the emdash-kotkanagrilli
|
||||||
|
layout in `~/projects/servers/fleet/apps/base/`:
|
||||||
|
|
||||||
|
- `source.yaml` — `GitRepository` pointing at this repo on the matching
|
||||||
|
branch (`staging` / `production`), restricted to `/deploy/helm` via the
|
||||||
|
`ignore` rule so Flux only pulls the chart.
|
||||||
|
- `helmrelease.yaml` — `HelmRelease` consuming the chart from `./deploy/helm`
|
||||||
|
in that `GitRepository`. Pinned by digest (see image-automation.yaml).
|
||||||
|
- `image-automation.yaml` — `ImageRepository` + `ImagePolicy` +
|
||||||
|
`ImageUpdateAutomation`. Watches the floating `staging` / `production`
|
||||||
|
tag in the Gitea OCI registry, resolves the current digest, and rewrites
|
||||||
|
the digest setter in `helmrelease.yaml` (which is what actually makes
|
||||||
|
`helm upgrade` see a change when CI retags the image).
|
||||||
|
- `secrets.yaml` — two Secrets per env: the SSH deploy key Flux uses to
|
||||||
|
clone this repo (`cms-plugins-deploy-key`, in `flux-system`), and the
|
||||||
|
pod's env-var secret (`cms-plugins-{staging,production}-secrets`, in
|
||||||
|
`kotkan`). **Templates here are NOT encrypted** — sops-encrypt them
|
||||||
|
before pushing to anton-helm-workloads.
|
||||||
|
- `kustomization.yaml` — bundles the above.
|
||||||
|
|
||||||
|
## Why this lives in two repos
|
||||||
|
|
||||||
|
The chart (`deploy/helm/`) ships with the app — that way a chart change
|
||||||
|
is reviewed and tagged alongside the code that depends on it. The
|
||||||
|
HelmRelease references the chart as a path inside a `GitRepository`,
|
||||||
|
not as an OCI artifact, so there's no "publish chart" step in CI.
|
||||||
|
|
||||||
|
The HelmRelease itself lives in the workloads repo because that repo is
|
||||||
|
the source of truth for what runs on the kotkanagrilli.fi subdomain
|
||||||
|
pool. Same convention as the existing `kotkanagrilli/` (legacy WP) and
|
||||||
|
`hello-kotkan/` entries there.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||||
|
kind: HelmRelease
|
||||||
|
metadata:
|
||||||
|
name: cms-plugins-production
|
||||||
|
namespace: flux-system
|
||||||
|
spec:
|
||||||
|
interval: 5m
|
||||||
|
chart:
|
||||||
|
spec:
|
||||||
|
chart: ./deploy/helm
|
||||||
|
sourceRef:
|
||||||
|
kind: GitRepository
|
||||||
|
name: cms-plugins-production
|
||||||
|
namespace: flux-system
|
||||||
|
reconcileStrategy: Revision
|
||||||
|
releaseName: cms-plugins-production
|
||||||
|
targetNamespace: kotkan
|
||||||
|
install:
|
||||||
|
disableWait: true
|
||||||
|
remediation:
|
||||||
|
retries: 3
|
||||||
|
upgrade:
|
||||||
|
disableWait: true
|
||||||
|
remediation:
|
||||||
|
retries: 3
|
||||||
|
values:
|
||||||
|
existingSecret: cms-plugins-production-secrets
|
||||||
|
image:
|
||||||
|
# `tag` stays human-readable. The chart prefers `digest` when set
|
||||||
|
# and renders `repository@<digest>` — that's what actually pins
|
||||||
|
# the pod. Without digest pinning, helm upgrade would see no spec
|
||||||
|
# change when CI retags the floating `production` tag.
|
||||||
|
tag: production
|
||||||
|
digest: "" # {"$imagepolicy": "kotkan:cms-plugins-production:digest"}
|
||||||
|
pullPolicy: Always
|
||||||
|
ingress:
|
||||||
|
host: cms-plugins-production.kotkanagrilli.fi
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/hostname: kotkan
|
||||||
|
persistence:
|
||||||
|
size: 10Gi
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 384Mi
|
||||||
|
limits:
|
||||||
|
cpu: "1"
|
||||||
|
memory: 1Gi
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
# Watch the Gitea OCI registry for the floating `production` tag. Every
|
||||||
|
# push to the production branch retags the new image as `production`,
|
||||||
|
# overwriting the previous binding (OCI tag→manifest is single-valued).
|
||||||
|
# The image's immutable `0.1.<N>` tag stays in the registry as audit.
|
||||||
|
apiVersion: image.toolkit.fluxcd.io/v1
|
||||||
|
kind: ImageRepository
|
||||||
|
metadata:
|
||||||
|
name: cms-plugins-production
|
||||||
|
namespace: kotkan
|
||||||
|
spec:
|
||||||
|
image: git.oleks.space/oleks/cms-plugins
|
||||||
|
interval: 1m
|
||||||
|
secretRef:
|
||||||
|
name: gitea-registry-creds
|
||||||
|
---
|
||||||
|
# Only the `production` floating tag is in scope. There's at most one
|
||||||
|
# match at a time, so alphabetical ordering is a no-op — the policy
|
||||||
|
# just resolves to that single tag's current digest.
|
||||||
|
apiVersion: image.toolkit.fluxcd.io/v1
|
||||||
|
kind: ImagePolicy
|
||||||
|
metadata:
|
||||||
|
name: cms-plugins-production
|
||||||
|
namespace: kotkan
|
||||||
|
spec:
|
||||||
|
interval: 1m
|
||||||
|
imageRepositoryRef:
|
||||||
|
name: cms-plugins-production
|
||||||
|
filterTags:
|
||||||
|
pattern: '^production$'
|
||||||
|
# Extract and reflect the resolved digest into helmrelease.yaml.
|
||||||
|
# This enables IUA to pin by digest, which makes helm upgrade detect
|
||||||
|
# changes when the floating tag is reassigned.
|
||||||
|
digestReflectionPolicy: Always
|
||||||
|
policy:
|
||||||
|
alphabetical:
|
||||||
|
order: asc
|
||||||
|
---
|
||||||
|
# IUA writes the resolved digest into helmrelease.yaml — pinning by
|
||||||
|
# digest is what makes `helm upgrade` see a change when the floating
|
||||||
|
# tag is reassigned (without digest, tag stays `production` literal and
|
||||||
|
# helm upgrade is a no-op).
|
||||||
|
#
|
||||||
|
# NOTE: `sourceRef` must reference a GitRepository that points at
|
||||||
|
# THIS workloads repo (anton-helm-workloads) with write access. If it
|
||||||
|
# doesn't exist yet, create one alongside this manifest. The
|
||||||
|
# emdash-kotkanagrilli equivalent uses `oleks-fleet-image-automation`
|
||||||
|
# because its HelmReleases live in the fleet repo.
|
||||||
|
apiVersion: image.toolkit.fluxcd.io/v1
|
||||||
|
kind: ImageUpdateAutomation
|
||||||
|
metadata:
|
||||||
|
name: cms-plugins-production
|
||||||
|
namespace: kotkan
|
||||||
|
spec:
|
||||||
|
interval: 1m
|
||||||
|
sourceRef:
|
||||||
|
kind: GitRepository
|
||||||
|
name: anton-workloads-image-automation
|
||||||
|
namespace: flux-system
|
||||||
|
git:
|
||||||
|
checkout:
|
||||||
|
ref:
|
||||||
|
branch: main
|
||||||
|
commit:
|
||||||
|
author:
|
||||||
|
email: flux-bot@oleks.space
|
||||||
|
name: flux-bot
|
||||||
|
messageTemplate: |
|
||||||
|
chore(cms-plugins-production): pin new digest
|
||||||
|
Files:
|
||||||
|
{{ range $filename, $_ := .Changed.FileChanges -}}
|
||||||
|
- {{ $filename }}
|
||||||
|
{{ end -}}
|
||||||
|
push:
|
||||||
|
branch: main
|
||||||
|
update:
|
||||||
|
path: ./cms-plugins-production
|
||||||
|
strategy: Setters
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- source.yaml
|
||||||
|
- helmrelease.yaml
|
||||||
|
- secrets.yaml
|
||||||
|
- image-automation.yaml
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Two secrets per environment:
|
||||||
|
# 1. cms-plugins-deploy-key — Flux's SSH key for cloning the production branch
|
||||||
|
# of cms-plugins (only `read` on this Gitea repo).
|
||||||
|
# One pair is shared between production + production;
|
||||||
|
# commit it under whichever env directory is
|
||||||
|
# applied first.
|
||||||
|
# 2. cms-plugins-production-secrets — env vars consumed by the pod via the
|
||||||
|
# chart's `existingSecret`. EMDASH_ENCRYPTION_KEY
|
||||||
|
# is required; everything else is optional.
|
||||||
|
#
|
||||||
|
# These are TEMPLATES — encrypt them with sops before committing to the
|
||||||
|
# anton-helm-workloads repo:
|
||||||
|
#
|
||||||
|
# sops --encrypt --age <recipient-key> secrets.yaml > secrets.enc.yaml
|
||||||
|
# mv secrets.enc.yaml secrets.yaml
|
||||||
|
#
|
||||||
|
# Generation:
|
||||||
|
# ssh-keygen -t ed25519 -f /tmp/cms-plugins-deploy -N ""
|
||||||
|
# → upload /tmp/cms-plugins-deploy.pub to Gitea: Repo Settings → Deploy
|
||||||
|
# Keys → "cms-plugins Flux deploy", read-only.
|
||||||
|
# openssl rand -hex 32 → EMDASH_ENCRYPTION_KEY (one per env, do not reuse).
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: cms-plugins-deploy-key
|
||||||
|
namespace: flux-system
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
identity: |
|
||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
REPLACE_WITH_PRIVATE_KEY
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
|
identity.pub: |
|
||||||
|
ssh-ed25519 REPLACE_WITH_PUBLIC_KEY flux@cms-plugins
|
||||||
|
known_hosts: |
|
||||||
|
git.oleks.space REPLACE_WITH_HOST_KEY
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: cms-plugins-production-secrets
|
||||||
|
namespace: kotkan
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
EMDASH_ENCRYPTION_KEY: REPLACE_WITH_RANDOM_HEX_32
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
# Flux pulls the chart from the `production` branch of cms-plugins on Gitea.
|
||||||
|
# The `ignore` rule restricts reconciliation to /deploy/helm so app-code
|
||||||
|
# pushes don't trigger chart re-reconcile.
|
||||||
|
apiVersion: source.toolkit.fluxcd.io/v1
|
||||||
|
kind: GitRepository
|
||||||
|
metadata:
|
||||||
|
name: cms-plugins-production
|
||||||
|
namespace: flux-system
|
||||||
|
spec:
|
||||||
|
interval: 1m0s
|
||||||
|
url: ssh://git@git.oleks.space/oleks/cms-plugins.git
|
||||||
|
ref:
|
||||||
|
branch: production
|
||||||
|
secretRef:
|
||||||
|
name: cms-plugins-deploy-key
|
||||||
|
ignore: |
|
||||||
|
/*
|
||||||
|
!/deploy/helm
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||||
|
kind: HelmRelease
|
||||||
|
metadata:
|
||||||
|
name: cms-plugins-staging
|
||||||
|
namespace: flux-system
|
||||||
|
spec:
|
||||||
|
interval: 5m
|
||||||
|
chart:
|
||||||
|
spec:
|
||||||
|
chart: ./deploy/helm
|
||||||
|
sourceRef:
|
||||||
|
kind: GitRepository
|
||||||
|
name: cms-plugins-staging
|
||||||
|
namespace: flux-system
|
||||||
|
reconcileStrategy: Revision
|
||||||
|
releaseName: cms-plugins-staging
|
||||||
|
targetNamespace: kotkan
|
||||||
|
install:
|
||||||
|
disableWait: true
|
||||||
|
remediation:
|
||||||
|
retries: 3
|
||||||
|
upgrade:
|
||||||
|
disableWait: true
|
||||||
|
remediation:
|
||||||
|
retries: 3
|
||||||
|
values:
|
||||||
|
existingSecret: cms-plugins-staging-secrets
|
||||||
|
image:
|
||||||
|
# `tag` stays human-readable. The chart prefers `digest` when set
|
||||||
|
# and renders `repository@<digest>` — that's what actually pins
|
||||||
|
# the pod. Without digest pinning, helm upgrade would see no spec
|
||||||
|
# change when CI retags the floating `staging` tag.
|
||||||
|
tag: staging
|
||||||
|
digest: "" # {"$imagepolicy": "kotkan:cms-plugins-staging:digest"}
|
||||||
|
pullPolicy: Always
|
||||||
|
ingress:
|
||||||
|
host: cms-plugins-staging.kotkanagrilli.fi
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/hostname: kotkan
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 192Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 768Mi
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
# Watch the Gitea OCI registry for the floating `staging` tag. Every
|
||||||
|
# push to the staging branch retags the new image as `staging`,
|
||||||
|
# overwriting the previous binding (OCI tag→manifest is single-valued).
|
||||||
|
# The image's immutable `0.1.<N>` tag stays in the registry as audit.
|
||||||
|
apiVersion: image.toolkit.fluxcd.io/v1
|
||||||
|
kind: ImageRepository
|
||||||
|
metadata:
|
||||||
|
name: cms-plugins-staging
|
||||||
|
namespace: kotkan
|
||||||
|
spec:
|
||||||
|
image: git.oleks.space/oleks/cms-plugins
|
||||||
|
interval: 1m
|
||||||
|
secretRef:
|
||||||
|
name: gitea-registry-creds
|
||||||
|
---
|
||||||
|
# Only the `staging` floating tag is in scope. There's at most one
|
||||||
|
# match at a time, so alphabetical ordering is a no-op — the policy
|
||||||
|
# just resolves to that single tag's current digest.
|
||||||
|
apiVersion: image.toolkit.fluxcd.io/v1
|
||||||
|
kind: ImagePolicy
|
||||||
|
metadata:
|
||||||
|
name: cms-plugins-staging
|
||||||
|
namespace: kotkan
|
||||||
|
spec:
|
||||||
|
interval: 1m
|
||||||
|
imageRepositoryRef:
|
||||||
|
name: cms-plugins-staging
|
||||||
|
filterTags:
|
||||||
|
pattern: '^staging$'
|
||||||
|
# Extract and reflect the resolved digest into helmrelease.yaml.
|
||||||
|
# This enables IUA to pin by digest, which makes helm upgrade detect
|
||||||
|
# changes when the floating tag is reassigned.
|
||||||
|
digestReflectionPolicy: Always
|
||||||
|
policy:
|
||||||
|
alphabetical:
|
||||||
|
order: asc
|
||||||
|
---
|
||||||
|
# IUA writes the resolved digest into helmrelease.yaml — pinning by
|
||||||
|
# digest is what makes `helm upgrade` see a change when the floating
|
||||||
|
# tag is reassigned (without digest, tag stays `staging` literal and
|
||||||
|
# helm upgrade is a no-op).
|
||||||
|
#
|
||||||
|
# NOTE: `sourceRef` must reference a GitRepository that points at
|
||||||
|
# THIS workloads repo (anton-helm-workloads) with write access. If it
|
||||||
|
# doesn't exist yet, create one alongside this manifest. The
|
||||||
|
# emdash-kotkanagrilli equivalent uses `oleks-fleet-image-automation`
|
||||||
|
# because its HelmReleases live in the fleet repo.
|
||||||
|
apiVersion: image.toolkit.fluxcd.io/v1
|
||||||
|
kind: ImageUpdateAutomation
|
||||||
|
metadata:
|
||||||
|
name: cms-plugins-staging
|
||||||
|
namespace: kotkan
|
||||||
|
spec:
|
||||||
|
interval: 1m
|
||||||
|
sourceRef:
|
||||||
|
kind: GitRepository
|
||||||
|
name: anton-workloads-image-automation
|
||||||
|
namespace: flux-system
|
||||||
|
git:
|
||||||
|
checkout:
|
||||||
|
ref:
|
||||||
|
branch: main
|
||||||
|
commit:
|
||||||
|
author:
|
||||||
|
email: flux-bot@oleks.space
|
||||||
|
name: flux-bot
|
||||||
|
messageTemplate: |
|
||||||
|
chore(cms-plugins-staging): pin new digest
|
||||||
|
Files:
|
||||||
|
{{ range $filename, $_ := .Changed.FileChanges -}}
|
||||||
|
- {{ $filename }}
|
||||||
|
{{ end -}}
|
||||||
|
push:
|
||||||
|
branch: main
|
||||||
|
update:
|
||||||
|
path: ./cms-plugins-staging
|
||||||
|
strategy: Setters
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- source.yaml
|
||||||
|
- helmrelease.yaml
|
||||||
|
- secrets.yaml
|
||||||
|
- image-automation.yaml
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Two secrets per environment:
|
||||||
|
# 1. cms-plugins-deploy-key — Flux's SSH key for cloning the staging branch
|
||||||
|
# of cms-plugins (only `read` on this Gitea repo).
|
||||||
|
# One pair is shared between staging + production;
|
||||||
|
# commit it under whichever env directory is
|
||||||
|
# applied first.
|
||||||
|
# 2. cms-plugins-staging-secrets — env vars consumed by the pod via the
|
||||||
|
# chart's `existingSecret`. EMDASH_ENCRYPTION_KEY
|
||||||
|
# is required; everything else is optional.
|
||||||
|
#
|
||||||
|
# These are TEMPLATES — encrypt them with sops before committing to the
|
||||||
|
# anton-helm-workloads repo:
|
||||||
|
#
|
||||||
|
# sops --encrypt --age <recipient-key> secrets.yaml > secrets.enc.yaml
|
||||||
|
# mv secrets.enc.yaml secrets.yaml
|
||||||
|
#
|
||||||
|
# Generation:
|
||||||
|
# ssh-keygen -t ed25519 -f /tmp/cms-plugins-deploy -N ""
|
||||||
|
# → upload /tmp/cms-plugins-deploy.pub to Gitea: Repo Settings → Deploy
|
||||||
|
# Keys → "cms-plugins Flux deploy", read-only.
|
||||||
|
# openssl rand -hex 32 → EMDASH_ENCRYPTION_KEY (one per env, do not reuse).
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: cms-plugins-deploy-key
|
||||||
|
namespace: flux-system
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
identity: |
|
||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
REPLACE_WITH_PRIVATE_KEY
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
|
identity.pub: |
|
||||||
|
ssh-ed25519 REPLACE_WITH_PUBLIC_KEY flux@cms-plugins
|
||||||
|
known_hosts: |
|
||||||
|
git.oleks.space REPLACE_WITH_HOST_KEY
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: cms-plugins-staging-secrets
|
||||||
|
namespace: kotkan
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
EMDASH_ENCRYPTION_KEY: REPLACE_WITH_RANDOM_HEX_32
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
# Flux pulls the chart from the `staging` branch of cms-plugins on Gitea.
|
||||||
|
# The `ignore` rule restricts reconciliation to /deploy/helm so app-code
|
||||||
|
# pushes don't trigger chart re-reconcile.
|
||||||
|
apiVersion: source.toolkit.fluxcd.io/v1
|
||||||
|
kind: GitRepository
|
||||||
|
metadata:
|
||||||
|
name: cms-plugins-staging
|
||||||
|
namespace: flux-system
|
||||||
|
spec:
|
||||||
|
interval: 1m0s
|
||||||
|
url: ssh://git@git.oleks.space/oleks/cms-plugins.git
|
||||||
|
ref:
|
||||||
|
branch: staging
|
||||||
|
secretRef:
|
||||||
|
name: cms-plugins-deploy-key
|
||||||
|
ignore: |
|
||||||
|
/*
|
||||||
|
!/deploy/helm
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.DS_Store
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
*.swp
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
*.orig
|
||||||
|
README.md
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: cms-plugins
|
||||||
|
description: CMS plugins catalog — Emdash-based catalog of WordPress→Emdash plugin parity
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
||||||
|
appVersion: "0.1.0"
|
||||||
|
keywords:
|
||||||
|
- emdash
|
||||||
|
- cms
|
||||||
|
- astro
|
||||||
|
- catalog
|
||||||
|
home: https://git.oleks.space/oleks/cms-plugins
|
||||||
|
maintainers:
|
||||||
|
- name: oleks
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{{/* Expand the name of the chart. */}}
|
||||||
|
{{- define "cms-plugins.name" -}}
|
||||||
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/* Create a default fully qualified app name. */}}
|
||||||
|
{{- define "cms-plugins.fullname" -}}
|
||||||
|
{{- if .Values.fullnameOverride }}
|
||||||
|
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||||
|
{{- if contains $name .Release.Name }}
|
||||||
|
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/* Common labels. Flux appends `+<sha>` to chart versions for GitRepository
|
||||||
|
sources; `+` is illegal in k8s labels, so we replace it with `_`. */}}
|
||||||
|
{{- define "cms-plugins.labels" -}}
|
||||||
|
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||||
|
app.kubernetes.io/name: {{ include "cms-plugins.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/* Selector labels */}}
|
||||||
|
{{- define "cms-plugins.selectorLabels" -}}
|
||||||
|
app.kubernetes.io/name: {{ include "cms-plugins.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "cms-plugins.fullname" . }}
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
|
labels:
|
||||||
|
{{- include "cms-plugins.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
# SQLite is single-writer; do not scale beyond 1.
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "cms-plugins.selectorLabels" . | nindent 6 }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
{{- include "cms-plugins.selectorLabels" . | nindent 8 }}
|
||||||
|
app.kubernetes.io/version: {{ .Values.image.tag | quote }}
|
||||||
|
spec:
|
||||||
|
{{- with .Values.imagePullSecrets }}
|
||||||
|
imagePullSecrets:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.podSecurityContext }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
containers:
|
||||||
|
- name: cms-plugins
|
||||||
|
# When `image.digest` is provided, pin by digest so a floating
|
||||||
|
# tag (staging, production) doesn't confuse Helm into a no-op
|
||||||
|
# upgrade when the underlying image changes. Tag stays as a
|
||||||
|
# human-readable hint via the imagePullPolicy fallback path.
|
||||||
|
image: "{{ .Values.image.repository }}{{- if .Values.image.digest -}}@{{ .Values.image.digest }}{{- else -}}:{{ .Values.image.tag }}{{- end }}"
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
{{- with .Values.containerSecurityContext }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml . | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: {{ .Values.service.port }}
|
||||||
|
env:
|
||||||
|
{{- range $key, $val := .Values.env }}
|
||||||
|
- name: {{ $key }}
|
||||||
|
value: {{ $val | quote }}
|
||||||
|
{{- end }}
|
||||||
|
# EMDASH_SITE_URL gates the CSRF check on plugin POST routes.
|
||||||
|
# Astro inside the pod sees http://localhost:4321/, so without
|
||||||
|
# this any browser request from https://<ingress.host>/ trips
|
||||||
|
# the same-origin check. Derived from the ingress host so we
|
||||||
|
# don't need to set it per-environment.
|
||||||
|
{{- if and .Values.ingress.enabled .Values.ingress.host }}
|
||||||
|
- name: EMDASH_SITE_URL
|
||||||
|
value: "https://{{ .Values.ingress.host }}"
|
||||||
|
{{- end }}
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: {{ .Values.existingSecret | default (printf "%s-secrets" (include "cms-plugins.fullname" .)) }}
|
||||||
|
volumeMounts:
|
||||||
|
- name: state
|
||||||
|
mountPath: {{ .Values.persistence.mountPath }}
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: {{ .Values.probes.liveness.path }}
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
|
||||||
|
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
|
||||||
|
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: {{ .Values.probes.readiness.path }}
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
|
||||||
|
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
|
||||||
|
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
|
volumes:
|
||||||
|
- name: state
|
||||||
|
{{- if .Values.persistence.enabled }}
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ include "cms-plugins.fullname" . }}-state
|
||||||
|
{{- else }}
|
||||||
|
emptyDir: {}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.nodeSelector }}
|
||||||
|
nodeSelector:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{{- if .Values.ingress.enabled -}}
|
||||||
|
{{- $fullName := include "cms-plugins.fullname" . -}}
|
||||||
|
# Plain Ingress object (no TLS) — TLS terminates at the Caddy reverse-proxy
|
||||||
|
# at the cluster edge (matches the woodpecker / emdash-kotkanagrilli pattern).
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ $fullName }}
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
|
annotations:
|
||||||
|
caddy.oleks.space/ingress: "true"
|
||||||
|
labels:
|
||||||
|
{{- include "cms-plugins.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
ingressClassName: {{ .Values.ingress.className | default "kube-system-traefik" }}
|
||||||
|
rules:
|
||||||
|
- host: {{ .Values.ingress.host | quote }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: {{ $fullName }}
|
||||||
|
port:
|
||||||
|
number: {{ .Values.service.port }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{{- if .Values.persistence.enabled }}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: {{ include "cms-plugins.fullname" . }}-state
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
|
labels:
|
||||||
|
{{- include "cms-plugins.labels" . | nindent 4 }}
|
||||||
|
annotations:
|
||||||
|
# Holds the single-writer SQLite DB and uploaded media. Keep it on
|
||||||
|
# `helm uninstall` / chart-name changes — losing it is unrecoverable
|
||||||
|
# data loss, not a redeployable artifact.
|
||||||
|
helm.sh/resource-policy: keep
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
storageClassName: {{ .Values.persistence.storageClass | quote }}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .Values.persistence.size | quote }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "cms-plugins.fullname" . }}
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
|
labels:
|
||||||
|
{{- include "cms-plugins.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: {{ .Values.service.port }}
|
||||||
|
targetPort: http
|
||||||
|
selector:
|
||||||
|
{{- include "cms-plugins.selectorLabels" . | nindent 4 }}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Production overrides — applied via the FluxCD HelmRelease (or directly with
|
||||||
|
# `helm upgrade -f values-production.yaml`).
|
||||||
|
|
||||||
|
image:
|
||||||
|
tag: production-latest
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
host: cms-plugins-production.kotkanagrilli.fi
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
size: 10Gi
|
||||||
|
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 384Mi
|
||||||
|
limits:
|
||||||
|
cpu: "1"
|
||||||
|
memory: 1Gi
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Staging overrides — applied via the FluxCD HelmRelease (or directly with
|
||||||
|
# `helm upgrade -f values-staging.yaml`).
|
||||||
|
|
||||||
|
image:
|
||||||
|
tag: staging-latest
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
host: cms-plugins-staging.kotkanagrilli.fi
|
||||||
|
|
||||||
|
# Slimmer staging — non-critical workload, can run lean.
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 192Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 768Mi
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# Defaults for the cms-plugins chart.
|
||||||
|
# Per-env overrides come from values-staging.yaml / values-production.yaml
|
||||||
|
# and from the FluxCD HelmRelease's `values:` block.
|
||||||
|
|
||||||
|
image:
|
||||||
|
repository: git.oleks.space/oleks/cms-plugins
|
||||||
|
tag: develop-latest
|
||||||
|
# The tag is a mutable floating pointer (CI retags <branch>-latest onto
|
||||||
|
# each new build), so kubelet must always re-pull — IfNotPresent would
|
||||||
|
# pin the node to whatever digest it cached first and never roll.
|
||||||
|
pullPolicy: Always
|
||||||
|
|
||||||
|
service:
|
||||||
|
port: 4321
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
host: cms-plugins.kotkanagrilli.fi
|
||||||
|
# TLS terminates at the Caddy reverse-proxy at the cluster edge
|
||||||
|
# (matches the woodpecker / emdash-kotkanagrilli pattern). The
|
||||||
|
# Ingress object is plain — no inline TLS, no cert-manager Certificate.
|
||||||
|
className: kube-system-traefik
|
||||||
|
|
||||||
|
# SQLite is single-writer — pin to one node so the local-path PV is sticky.
|
||||||
|
# kotkan hosts the kotkanagrilli subdomain pool, matching the
|
||||||
|
# anton-helm-workloads convention (hello-kotkan, kotkanagrilli, etc.).
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/hostname: kotkan
|
||||||
|
|
||||||
|
tolerations: []
|
||||||
|
affinity: {}
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
storageClass: local-path
|
||||||
|
size: 5Gi
|
||||||
|
# Mounted at /app/state. The image symlinks /app/data.db and /app/uploads
|
||||||
|
# into this volume, so a single PVC covers SQLite + uploaded media.
|
||||||
|
mountPath: /app/state
|
||||||
|
|
||||||
|
# Plain env values (non-secret).
|
||||||
|
env:
|
||||||
|
HOST: "0.0.0.0"
|
||||||
|
PORT: "4321"
|
||||||
|
NODE_ENV: production
|
||||||
|
DEPLOY_TARGET: node
|
||||||
|
STATE_DIR: /app/state
|
||||||
|
EMDASH_ALLOWED_ORIGINS: ""
|
||||||
|
|
||||||
|
# All secrets project from one Secret. Keys expected:
|
||||||
|
# - EMDASH_ENCRYPTION_KEY (required)
|
||||||
|
existingSecret: cms-plugins-secrets
|
||||||
|
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: gitea-registry-creds
|
||||||
|
|
||||||
|
probes:
|
||||||
|
liveness:
|
||||||
|
# /_emdash/api/health requires auth (401 to unauthenticated requests),
|
||||||
|
# so kubelet probes fail and the pod gets killed. The site root is
|
||||||
|
# public and a 200 from it is a reasonable proxy for "the server is up".
|
||||||
|
path: /
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 30
|
||||||
|
timeoutSeconds: 5
|
||||||
|
readiness:
|
||||||
|
path: /
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
cpu: "1"
|
||||||
|
memory: 1Gi
|
||||||
|
|
||||||
|
podSecurityContext:
|
||||||
|
fsGroup: 1001
|
||||||
|
containerSecurityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1001
|
||||||
|
runAsGroup: 1001
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: [ALL]
|
||||||
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# Ensure persistent state dirs exist (volume may be empty on first boot).
|
||||||
|
mkdir -p /app/state/uploads
|
||||||
|
|
||||||
|
# Bootstrap on first run: create data.db and apply migrations.
|
||||||
|
# emdash init is expected to be idempotent on subsequent boots.
|
||||||
|
if [ ! -f /app/state/data.db ]; then
|
||||||
|
echo "[entrypoint] no data.db found in /app/state, running emdash init"
|
||||||
|
node_modules/.bin/emdash init
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
Reference in New Issue
Block a user