Compare commits
5 Commits
v0.0.2
...
7c39056188
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c39056188 | |||
| 9719586f51 | |||
| 67d8a095a5 | |||
| 476ef6fce2 | |||
| 93fa751d6d |
@@ -19,4 +19,4 @@ jobs:
|
||||
run: |
|
||||
docker run --rm -v "$PWD:/repo" -w /repo \
|
||||
-e REGISTRY_TOKEN -e REGISTRY_USER -e REPO_TOKEN -e REPOSITORY -e SERVER_URL \
|
||||
node:24.16.0-alpine3.24 node registry-cleanup/cleanup.ts
|
||||
node:24.18.0-alpine3.24 node registry-cleanup/cleanup.ts
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Renovate
|
||||
on:
|
||||
schedule:
|
||||
- cron: '17 4 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
renovate:
|
||||
runs-on: docker-host
|
||||
steps:
|
||||
- name: Run Renovate against this repo
|
||||
env:
|
||||
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e LOG_LEVEL=info \
|
||||
-e RENOVATE_ENDPOINT=https://gitea.larvit.se/api/v1 \
|
||||
-e RENOVATE_GIT_AUTHOR="Renovate Bot <renovate@larvit.se>" \
|
||||
-e RENOVATE_PLATFORM=gitea \
|
||||
-e RENOVATE_REPOSITORIES=${{ github.repository }} \
|
||||
-e RENOVATE_TOKEN \
|
||||
renovate/renovate:43.251.0
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# Node 24 runs TypeScript directly (type stripping) — no build step. Pinned exact tag.
|
||||
FROM node:24.16.0-alpine3.24
|
||||
FROM node:24.18.0-alpine3.24
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
# Plainpages
|
||||
|
||||
A self-hostable foundation for server-rendered web apps — public or gated pages from a
|
||||
zero-JS design system, with a config-driven menu and auth/permissions (Ory) baked in.
|
||||
Every domain feature is a drop-in plugin folder; the app is stateless, no build step.
|
||||
|
||||
**Source, docs & issues: <https://gitea.larvit.se/larvit/plainpages>**
|
||||
([GitHub mirror](https://github.com/larvit/plainpages))
|
||||
|
||||
## Tags
|
||||
|
||||
`X.Y.Z` · `X.Y` · `X` · `latest` — each is a release promoted from a CI-gated build.
|
||||
Pin the exact `X.Y.Z` you deploy.
|
||||
|
||||
## Quick start
|
||||
|
||||
This image is the Plainpages web app plus its one-shot bootstrap seeder. It runs
|
||||
alongside its Ory sidecars (Kratos, Keto) and Postgres — and it **ships their config**,
|
||||
so there is nothing to clone. In an empty directory, save this as `compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
image: larvit/plainpages:0.0.2
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
APP_URL: http://localhost:3000
|
||||
depends_on:
|
||||
bootstrap:
|
||||
condition: service_completed_successfully
|
||||
kratos:
|
||||
condition: service_healthy
|
||||
keto:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./ory/kratos/tokenizer:/etc/config/kratos/tokenizer:ro
|
||||
- ./plugins:/app/plugins
|
||||
restart: unless-stopped
|
||||
|
||||
# One-shot, idempotent seed: signing key if absent + the admin@plainpages.local / admin user.
|
||||
bootstrap:
|
||||
image: larvit/plainpages:0.0.2
|
||||
command: node src/auth/bootstrap.ts
|
||||
depends_on:
|
||||
kratos:
|
||||
condition: service_healthy
|
||||
keto:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./ory/kratos/tokenizer:/etc/config/kratos/tokenizer
|
||||
- ./plugins:/app/plugins:ro
|
||||
restart: "on-failure:5"
|
||||
|
||||
postgres:
|
||||
image: postgres:18.4-alpine3.23
|
||||
environment:
|
||||
POSTGRES_DB: ory
|
||||
POSTGRES_PASSWORD: ory
|
||||
POSTGRES_USER: ory
|
||||
volumes:
|
||||
- ./ory/postgres/init:/docker-entrypoint-initdb.d:ro
|
||||
- pgdata:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ory -d ory"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
restart: unless-stopped
|
||||
|
||||
kratos-migrate:
|
||||
image: oryd/kratos:v26.2.0
|
||||
command: -c /etc/config/kratos/kratos.yml migrate sql -e --yes
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DSN: postgres://ory:ory@postgres:5432/kratos?sslmode=disable
|
||||
volumes:
|
||||
- ./ory/kratos:/etc/config/kratos:ro
|
||||
restart: on-failure
|
||||
|
||||
kratos:
|
||||
image: oryd/kratos:v26.2.0
|
||||
command: serve -c /etc/config/kratos/kratos.yml --watch-courier
|
||||
ports:
|
||||
- "4433:4433" # the login form POSTs straight to Kratos from the browser
|
||||
depends_on:
|
||||
kratos-migrate:
|
||||
condition: service_completed_successfully
|
||||
environment:
|
||||
DSN: postgres://ory:ory@postgres:5432/kratos?sslmode=disable
|
||||
volumes:
|
||||
- ./ory/kratos:/etc/config/kratos:ro
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:4433/health/ready"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
restart: unless-stopped
|
||||
|
||||
keto-migrate:
|
||||
image: oryd/keto:v26.2.0
|
||||
command: -c /etc/config/keto/keto.yml migrate up -y
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DSN: postgres://ory:ory@postgres:5432/keto?sslmode=disable
|
||||
volumes:
|
||||
- ./ory/keto:/etc/config/keto:ro
|
||||
restart: on-failure
|
||||
|
||||
keto:
|
||||
image: oryd/keto:v26.2.0
|
||||
command: serve -c /etc/config/keto/keto.yml
|
||||
depends_on:
|
||||
keto-migrate:
|
||||
condition: service_completed_successfully
|
||||
environment:
|
||||
DSN: postgres://ory:ory@postgres:5432/keto?sslmode=disable
|
||||
volumes:
|
||||
- ./ory/keto:/etc/config/keto:ro
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:4466/health/ready"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
restart: unless-stopped
|
||||
|
||||
# Catches Kratos' recovery/verification emails — UI on http://localhost:8025
|
||||
mailpit:
|
||||
image: axllent/mailpit:v1.30.1
|
||||
ports:
|
||||
- "8025:8025"
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
```
|
||||
|
||||
Extract the Ory config the image ships, then start:
|
||||
|
||||
```bash
|
||||
docker run --rm larvit/plainpages:0.0.2 tar -cf - ory | tar -xf -
|
||||
mkdir -p plugins
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Open <http://localhost:3000> and sign in as `admin@plainpages.local` / `admin`.
|
||||
|
||||
This quick start runs http-on-localhost with dev-throwaway secrets, and omits Hydra (the
|
||||
OAuth2 provider — only needed when other apps log in *through* Plainpages). For
|
||||
production — https, real secrets (`CSRF_SECRET`, Postgres credentials, a fresh JWT
|
||||
signing key), Hydra — see the repo README → Production & deployment.
|
||||
|
||||
## Configuration
|
||||
|
||||
Every behaviour is an explicit env toggle read at boot — no `NODE_ENV`. The common ones:
|
||||
|
||||
| Var | Default | What |
|
||||
| --- | --- | --- |
|
||||
| `ADMIN_EMAIL` / `ADMIN_PASSWORD` | `admin@plainpages.local` / `admin` | the seeded first admin (bootstrap service) |
|
||||
| `APP_URL` | unset | canonical public URL; off-host visitors are redirected to it |
|
||||
| `CACHE_TEMPLATES` | `false` | cache compiled templates (`true` in prod) |
|
||||
| `CSRF_SECRET` | dev throwaway | signs the CSRF token — set a real one in prod |
|
||||
| `KRATOS_*` / `KETO_*` / `HYDRA_*` URLs | compose defaults | the Ory sidecar endpoints |
|
||||
| `LOG_FORMAT` / `LOG_LEVEL` | `text` / `info` | `json` for structured prod logs |
|
||||
| `OTLP_ENDPOINT` | unset | export logs + traces to an OpenTelemetry Collector |
|
||||
| `REQUIRE_SECURE_SECRETS` | `false` | `true` ⇒ refuse to boot on a missing/throwaway `CSRF_SECRET` |
|
||||
| `SECURE_COOKIES` | `false` | mark cookies `Secure` (`true` behind https) |
|
||||
|
||||
Full list (JWT/JWKS, timeouts, instant revoke): repo README → Configuration.
|
||||
|
||||
## Your first plugin
|
||||
|
||||
Everything domain-specific is a plugin folder — the compose above mounts `./plugins`
|
||||
into the app. Create `plugins/hello/plugin.ts`:
|
||||
|
||||
```ts
|
||||
import { definePlugin } from "#plugin-api";
|
||||
|
||||
export default definePlugin({
|
||||
apiVersion: "1.0.0",
|
||||
nav: [{ href: "/hello", id: "hello", label: "Hello", public: true }],
|
||||
routes: [
|
||||
{ method: "GET", path: "/", public: true, handler: () => ({ html: "<h1>Hello from my plugin</h1>" }) },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Restart (`docker compose restart web`) and visit <http://localhost:3000/hello>. Views,
|
||||
forms, permissions, and the runnable reference plugin: repo README → Building plugins.
|
||||
@@ -1177,6 +1177,7 @@ Gitea Actions (`.gitea/workflows/`) runs the pipeline; the test job runs
|
||||
| `release.yml` | push of a `vX.Y.Z` tag | re-tag that commit's image as `X.Y.Z`, `X.Y`, `X`, `latest`; sync those tags to Docker Hub |
|
||||
| `mirror.yml` | push to `main` or any tag, or manual | force-push `main` + tags to the [GitHub mirror](https://github.com/larvit/plainpages) |
|
||||
| `registry-cleanup.yml` | nightly cron, or manual | delete registry images that are neither release-tagged nor a branch head |
|
||||
| `renovate.yml` | nightly cron, or manual | open dependency-update PRs and automerge them once the gate is green |
|
||||
|
||||
`main` is not re-tested on push — its commits are meant to arrive already green from a
|
||||
gated branch, so the status check to gate a merge on is `CI / full-gate (push)`.
|
||||
@@ -1224,7 +1225,10 @@ repository on Docker Hub, generate a read/write access token **scoped to that re
|
||||
account-wide PAT can push to every repo under the account), and store the account name as
|
||||
the Actions **variable** `DOCKERHUB_USER` and the token as the Actions **secret**
|
||||
`DOCKERHUB_TOKEN`. Until they exist, a release run fails at the Docker Hub step — after the
|
||||
Gitea re-tag has succeeded — so set them, then re-run the workflow.
|
||||
Gitea re-tag has succeeded — so set them, then re-run the workflow. The Docker Hub
|
||||
repository **description** is maintained by hand: its source is
|
||||
[`README-dockerhub.md`](README-dockerhub.md) — paste it into the repository overview on
|
||||
Docker Hub when it changes.
|
||||
|
||||
**GitHub mirror** — [github.com/larvit/plainpages](https://github.com/larvit/plainpages) is a
|
||||
read-only mirror; after every merge, `mirror.yml` force-pushes `main` and all tags there,
|
||||
@@ -1235,6 +1239,22 @@ as the Gitea Actions secret `MIRROR_GITHUB_TOKEN` (repo Settings → Actions →
|
||||
rejects secret names starting with `GITHUB_`/`GITEA_`). Trigger the workflow manually for
|
||||
the first sync — until the secret exists, the mirror job fails loud on each merge.
|
||||
|
||||
**Dependency updates** — `renovate.yml` runs [Renovate](https://docs.renovatebot.com)
|
||||
nightly (self-hosted, this repo only) against [`renovate.json`](renovate.json), opening PRs
|
||||
that bump npm deps (both `package.json`s), Docker base images (both Dockerfiles +
|
||||
`compose*.yml`), Gitea action versions, and the image tags pinned inside workflow `run:`
|
||||
steps (a custom regex manager, so nothing pinned drifts unmanaged). Version-locked sets move
|
||||
together in one PR — the Ory images (kratos/keto/hydra) and the Playwright runner + its
|
||||
browser image — and every bump keeps the existing **exact semver pin** exact, never widening
|
||||
to a range or adding a digest. Each PR
|
||||
runs through the normal gate on its `renovate/*` branch and, with `"automerge": true`,
|
||||
Renovate merges it once `CI / full-gate (push)` is green (rebasing stale branches so the
|
||||
fast-forward-only merge still holds) — routine bumps land untouched; only a red gate needs a
|
||||
human. One-time setup: reuse the shared `renovate@larvit.se` bot — give it write access to
|
||||
this repo and store its Gitea PAT as the Actions **secret** `RENOVATE_TOKEN`. Until it
|
||||
exists, the nightly job fails loud (and, like the other secrets, a `GITEA_` prefix is
|
||||
rejected).
|
||||
|
||||
**One-time server setup** — register an
|
||||
[act_runner](https://docs.gitea.com/usage/actions/act-runner) in host mode with the label
|
||||
`docker-host` (config: `labels: ["docker-host:host"]`) on a machine with Docker Engine +
|
||||
@@ -1460,6 +1480,7 @@ e2e-tests/ Playwright E2E: visual.spec (design system, Ory-free) + aut
|
||||
ci.sh The full CI gate: typecheck → unit tests → every E2E suite, each on a fresh, always-torn-down stack (`bash ci.sh`)
|
||||
.gitea/workflows/ Gitea Actions: ci.yml — the full gate (ci.sh) on every branch push except main;
|
||||
mirror.yml — force-sync main + tags to the GitHub mirror; see CI/CD
|
||||
README-dockerhub.md The Docker Hub repository description (docker.io/larvit/plainpages) — pasted into the Docker Hub overview by hand when it changes; see CI/CD
|
||||
```
|
||||
|
||||
## Extending the core
|
||||
|
||||
@@ -27,7 +27,7 @@ services:
|
||||
# backs it (SCHEDULING_UPSTREAM above points here). Stand-in for the customer's real service —
|
||||
# stdlib-only, in-memory, no auth. Prod points SCHEDULING_UPSTREAM at the real backend instead.
|
||||
shifts-upstream:
|
||||
image: node:24.16.0-alpine3.24
|
||||
image: node:24.18.0-alpine3.24
|
||||
command: node /srv/server.ts
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
@@ -36,7 +36,7 @@ services:
|
||||
# Dev mail catcher — Kratos recovery/verification emails land here (web UI on 8025).
|
||||
# kratos.yml points the courier at smtp://mailpit:1025; prod uses a real SMTP via env.
|
||||
mailpit:
|
||||
image: axllent/mailpit:v1.30.1
|
||||
image: axllent/mailpit:v1.30.3
|
||||
ports:
|
||||
- "8025:8025"
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -54,7 +54,7 @@ services:
|
||||
|
||||
# The reference plugin's upstream (examples/shifts-upstream) so /scheduling/shifts shows real rows.
|
||||
shifts-upstream:
|
||||
image: node:24.16.0-alpine3.24
|
||||
image: node:24.18.0-alpine3.24
|
||||
command: ["node", "/server.ts"]
|
||||
volumes:
|
||||
- ./examples/shifts-upstream/server.ts:/server.ts:ro
|
||||
@@ -67,7 +67,7 @@ services:
|
||||
# Mock OIDC provider for the SSO login test — stdlib Node, auto-approves, signs an id_token Kratos
|
||||
# verifies via its jwks. Reachable as the same host (mock-oidc:9000) by both the browser and Kratos.
|
||||
mock-oidc:
|
||||
image: node:24.16.0-alpine3.24
|
||||
image: node:24.18.0-alpine3.24
|
||||
command: ["node", "/mock-oidc.ts"]
|
||||
environment:
|
||||
ISSUER: http://mock-oidc:9000
|
||||
@@ -82,7 +82,7 @@ services:
|
||||
|
||||
# Same-origin gateway: Kratos-owned paths → kratos, everything else → web (e2e-tests/proxy.ts).
|
||||
proxy:
|
||||
image: node:24.16.0-alpine3.24
|
||||
image: node:24.18.0-alpine3.24
|
||||
command: ["node", "/proxy.ts"]
|
||||
depends_on:
|
||||
web:
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"automerge": true,
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Ory services share one release train - update kratos, keto and hydra together",
|
||||
"matchDatasources": ["docker"],
|
||||
"matchPackageNames": ["oryd/kratos", "oryd/keto", "oryd/hydra"],
|
||||
"groupName": "Ory stack"
|
||||
},
|
||||
{
|
||||
"description": "Playwright runner and its browser image are version-locked - bump together",
|
||||
"matchPackageNames": ["@playwright/test", "mcr.microsoft.com/playwright"],
|
||||
"groupName": "Playwright"
|
||||
}
|
||||
],
|
||||
"customManagers": [
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "Pin the Renovate image the Renovate workflow runs",
|
||||
"managerFilePatterns": [".gitea/workflows/renovate.yml"],
|
||||
"matchStrings": ["renovate/renovate:(?<currentValue>[0-9][^\\s\"']*)"],
|
||||
"depNameTemplate": "renovate/renovate",
|
||||
"datasourceTemplate": "docker"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "Pin the node image registry-cleanup runs cleanup.ts in",
|
||||
"managerFilePatterns": [".gitea/workflows/registry-cleanup.yml"],
|
||||
"matchStrings": ["\\snode:(?<currentValue>[0-9][^\\s\"']*)"],
|
||||
"depNameTemplate": "node",
|
||||
"datasourceTemplate": "docker"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9,7 +9,9 @@
|
||||
- [x] CI/CD - Build docker images as part of the requirements to be able to merge to main. Push them with the git commit hash as docker tag. Push to container registry at Gitea. (`ci.yml` builds + pushes `gitea.larvit.se/larvit/plainpages:<commit hash>` after a green gate — with ff-only merges that is the main commit's image; auth via the `DOCKER_REGISTRY_USER` variable + `DOCKER_REGISTRY_TOKEN` secret, retention via an org cleanup rule; documented in README → CI/CD.)
|
||||
- [x] CI/CD - Re-tag docker images from git hash to semver when a semver git tag is pushed. (`release.yml` on a `vX.Y.Z` tag pulls the commit-hash image and re-tags it `X.Y.Z`/`X.Y`/`X`/`latest`, failing loud if the gated image is missing; tag pushes also trigger the GitHub mirror; documented in README → CI/CD.)
|
||||
- [x] CI/CD - Sync docker images to docker hub after each re-tag to git tags. (`release.yml` pushes the same `X.Y.Z`/`X.Y`/`X`/`latest` tags to `docker.io/larvit/plainpages` after the Gitea re-tag — releases only, no hash tags; auth via the `DOCKERHUB_USER` variable + `DOCKERHUB_TOKEN` secret; documented in README → CI/CD.)
|
||||
- [ ] CI/CD - Setup renovate bot.
|
||||
- [x] Write a short text on how to use this docker image to publish on docker hub and save it to README-dockerhub.md (tagline, tags, clone-free quick start — the image ships the Ory config, extracted via `docker run … tar` + a self-contained compose.yml — env table, first plugin; pasted into the Docker Hub overview by hand — noted in README → CI/CD.)
|
||||
- [x] CI/CD - Setup renovate bot. Check how other repos on this Gitea is setup you can get access to, there should be a number of renovate bot activated ones. (`renovate.yml` runs the self-hosted `renovate/renovate` image nightly against `renovate.json` — this repo only, via the shared `renovate@larvit.se` bot + `RENOVATE_TOKEN` secret, mirroring the `pwrpln/core` pattern; standard managers cover npm/Dockerfiles/compose/gitea-action pins, two custom regex managers cover the image tags embedded in workflow `run:` steps, the Ory + Playwright lockstep sets are grouped, every bump stays an exact pin, and each PR automerges once the gate is green; documented in README → CI/CD.)
|
||||
- [ ] CI/CD - When renovate updates a dependency - also release a new version of plainpages based on what got updated with Renovate. Major typescript? New apiVersion + new major. A tiny patch to ejs? Only patch release etc.
|
||||
- [ ] Add an e2e test for the admin plugin's OAuth2-clients (Hydra) screen. The full-flow e2e suite runs without Hydra (compose.full.yml), so /admin/clients register/detail/delete is only unit-covered (src/http/app.test.ts); wire Hydra into an e2e stack and drive the screen in the browser.
|
||||
- [ ] Build and publish docker image as CI/CD.
|
||||
- [ ] Add i18n support.
|
||||
|
||||
Reference in New Issue
Block a user