Compare commits
9 Commits
v0.0.10
...
7c66599f35
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c66599f35 | |||
| 6db0f57bf4 | |||
| 175717f04d | |||
| 6c850b8923 | |||
| af4a70d904 | |||
| a0244a32cd | |||
| 6559f40142 | |||
| d3154819f8 | |||
| 1cba6d470c |
@@ -8,6 +8,8 @@ jobs:
|
|||||||
runs-on: docker-host
|
runs-on: docker-host
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.2.2
|
- uses: actions/checkout@v4.2.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # ci.sh's docs-only check needs history; checkout defaults to depth 1
|
||||||
- run: bash ci.sh
|
- run: bash ci.sh
|
||||||
- name: Push app image tagged with the commit hash
|
- name: Push app image tagged with the commit hash
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
-e RENOVATE_PLATFORM=gitea \
|
-e RENOVATE_PLATFORM=gitea \
|
||||||
-e RENOVATE_REPOSITORIES=${{ github.repository }} \
|
-e RENOVATE_REPOSITORIES=${{ github.repository }} \
|
||||||
-e RENOVATE_TOKEN \
|
-e RENOVATE_TOKEN \
|
||||||
renovate/renovate:43.285.3
|
renovate/renovate:44.6.0
|
||||||
|
|
||||||
# After the renovate job, cut ONE tag covering the renovate-bot commits merged to main since the
|
# After the renovate job, cut ONE tag covering the renovate-bot commits merged to main since the
|
||||||
# last tag (batch per run). Targets origin/main — the real post-merge tip; the checkout SHA is the
|
# last tag (batch per run). Targets origin/main — the real post-merge tip; the checkout SHA is the
|
||||||
|
|||||||
@@ -127,6 +127,14 @@ When editing: put content in the section it belongs to (don't prepend rationale
|
|||||||
start); keep the ToC in sync when you add/rename/remove an `H2`/`H3`; and state each fact in
|
start); keep the ToC in sync when you add/rename/remove an `H2`/`H3`; and state each fact in
|
||||||
one home, linking to it rather than restating (credentials, env vars, rotation steps).
|
one home, linking to it rather than restating (credentials, env vars, rotation steps).
|
||||||
|
|
||||||
|
**Don't document internals here.** How a script reaches a decision, why one run behaved
|
||||||
|
differently from another, what a function guards — a developer doesn't need it day to day and
|
||||||
|
can read it off the code or a run's log in seconds. Prose like that only makes the README
|
||||||
|
longer and harder to consume, for humans and machines alike. It belongs in the code it
|
||||||
|
describes, or nowhere. The README earns its length on what you cannot dig out: how to use and
|
||||||
|
operate Plainpages, the external contracts, and one-time setup (secrets, accounts, tokens).
|
||||||
|
Same test before adding a row to a table or the file map — a clause, not a paragraph.
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
- Node 24 runs `.ts` directly (type stripping). Keep all TypeScript **erasable**
|
- Node 24 runs `.ts` directly (type stripping). Keep all TypeScript **erasable**
|
||||||
@@ -142,6 +150,10 @@ one home, linking to it rather than restating (credentials, env vars, rotation s
|
|||||||
- Tests use the built-in `node --test` runner — no test framework dependency.
|
- Tests use the built-in `node --test` runner — no test framework dependency.
|
||||||
- English everywhere. Keep code comments short and information-dense. Self explained code
|
- English everywhere. Keep code comments short and information-dense. Self explained code
|
||||||
without any comment at all is the preferred solution.
|
without any comment at all is the preferred solution.
|
||||||
|
- Do not comment about history in the code or README. Like "This function included X before,
|
||||||
|
but it moved to Y".
|
||||||
|
- Do not comment about the abscense of things, if it is not very undexpected. Banned is things
|
||||||
|
like "This function does not calculate pi, that is done in function Z".
|
||||||
- Pin all dependencies and Docker images to exact, human-readable **semantic
|
- Pin all dependencies and Docker images to exact, human-readable **semantic
|
||||||
versions** — never ranges (`^`, `~`) and never digests/hashes. npm deps are kept
|
versions** — never ranges (`^`, `~`) and never digests/hashes. npm deps are kept
|
||||||
exact by `.npmrc` (`save-exact=true`) + `npm ci`; the base image by tag (e.g.
|
exact by `.npmrc` (`save-exact=true`) + `npm ci`; the base image by tag (e.g.
|
||||||
@@ -161,4 +173,4 @@ one home, linking to it rather than restating (credentials, env vars, rotation s
|
|||||||
Skip this if the changes are purely documentation and/or comments.
|
Skip this if the changes are purely documentation and/or comments.
|
||||||
- Use well formed, standard compliant, rich URIs. Prefer state in the URL over POST:ing in for
|
- Use well formed, standard compliant, rich URIs. Prefer state in the URL over POST:ing in for
|
||||||
for example list pages with filters and pagination. Do: "ids=x&ids=y" and not "ids[]=x&ids[]=y"
|
for example list pages with filters and pagination. Do: "ids=x&ids=y" and not "ids[]=x&ids[]=y"
|
||||||
and not "ids=x,y".
|
and not "ids=x,y".
|
||||||
|
|||||||
@@ -1173,7 +1173,7 @@ Gitea Actions (`.gitea/workflows/`) runs the pipeline; the test job runs
|
|||||||
|
|
||||||
| Workflow | Trigger | Does |
|
| Workflow | Trigger | Does |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `ci.yml` | push, any branch except `main` | the full gate (`bash ci.sh`), then build + push the app image |
|
| `ci.yml` | push, any branch except `main` | the full gate (`bash ci.sh`, a no-op on a docs-only branch), then build + push the app image |
|
||||||
| `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 |
|
| `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) |
|
| `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 |
|
| `registry-cleanup.yml` | nightly cron, or manual | delete registry images that are neither release-tagged nor a branch head |
|
||||||
@@ -1203,12 +1203,9 @@ this step runs
|
|||||||
inside the required gate, a missing/expired token (or registry outage) fails every branch's
|
inside the required gate, a missing/expired token (or registry outage) fails every branch's
|
||||||
gate and blocks **all** merges until restored — set the secrets before this lands, and use a
|
gate and blocks **all** merges until restored — set the secrets before this lands, and use a
|
||||||
non-expiring token or track its expiry. Retention: hash tags accumulate one image per gated
|
non-expiring token or track its expiry. Retention: hash tags accumulate one image per gated
|
||||||
push, so the nightly `registry-cleanup.yml` prunes them precisely
|
push, so the nightly `registry-cleanup.yml` prunes them
|
||||||
([`registry-cleanup/cleanup.ts`](registry-cleanup/cleanup.ts), run in a `node:24` container):
|
([`registry-cleanup/cleanup.ts`](registry-cleanup/cleanup.ts) defines what survives).
|
||||||
a hash tag survives only while its commit is a **branch head** or carries a **`vX.Y.Z`
|
It reuses `DOCKER_REGISTRY_USER`/`DOCKER_REGISTRY_TOKEN` — no extra setup. Don't
|
||||||
release tag**; deleted alongside are the untagged `sha256:…` child manifests (arch image +
|
|
||||||
provenance) that no surviving tag references. Named tags (`1.2.3`, `latest`, …) are never
|
|
||||||
touched. It reuses `DOCKER_REGISTRY_USER`/`DOCKER_REGISTRY_TOKEN` — no extra setup. Don't
|
|
||||||
add a pattern-based org cleanup rule for this package (and remove it if one exists): its
|
add a pattern-based org cleanup rule for this package (and remove it if one exists): its
|
||||||
age/count heuristics can't see branch heads or release tags and would delete images the
|
age/count heuristics can't see branch heads or release tags and would delete images the
|
||||||
workflow protects.
|
workflow protects.
|
||||||
@@ -1259,12 +1256,10 @@ rejected).
|
|||||||
renovate`) cuts **one** `vX.Y.Z` tag per run covering the renovate-bot commits merged to `main`
|
renovate`) cuts **one** `vX.Y.Z` tag per run covering the renovate-bot commits merged to `main`
|
||||||
since the last tag (it targets `origin/main`, and **skips** when the tip isn't a Renovate commit —
|
since the last tag (it targets `origin/main`, and **skips** when the tip isn't a Renovate commit —
|
||||||
a human owns that release — or when nothing new merged). Renovate stamps every commit with a
|
a human owns that release — or when nothing new merged). Renovate stamps every commit with a
|
||||||
`Release-Bump: <updateType>` trailer (`commitBody` in `renovate.json`); the job takes the highest
|
`Release-Bump: <updateType>` trailer (`commitBody` in `renovate.json`), and
|
||||||
trailer on those commits — any dependency's `major`/`minor`/`patch` maps straight through,
|
[`auto-release/next-version.ts`](auto-release/next-version.ts) (unit-tested) turns the highest
|
||||||
defaulting to `patch`.
|
trailer on those commits into the next version — pre-1.0 it never auto-crosses into `1.0.0`,
|
||||||
**Pre-1.0 the level shifts down** — a dep major bumps the `0.x` minor, dep minor/patch bump the
|
which stays a deliberate hand-cut tag. It's
|
||||||
`0.x` patch (see [`auto-release/next-version.ts`](auto-release/next-version.ts), unit-tested) — so
|
|
||||||
routine bumps never auto-cross into `1.0.0`; `1.0.0` stays a deliberate hand-cut tag. It's
|
|
||||||
**tag-only** (no source commits): the tag hands off to `release.yml`, which promotes the
|
**tag-only** (no source commits): the tag hands off to `release.yml`, which promotes the
|
||||||
already-built image, and is pushed with renovate-bot's PAT so `release.yml` actually fires (a tag
|
already-built image, and is pushed with renovate-bot's PAT so `release.yml` actually fires (a tag
|
||||||
pushed by the built-in Actions token wouldn't trigger it). The plugin-contract version
|
pushed by the built-in Actions token wouldn't trigger it). The plugin-contract version
|
||||||
|
|||||||
@@ -12,6 +12,26 @@ cd "$(dirname "$0")"
|
|||||||
|
|
||||||
step() { printf '\n\033[1;34m==> %s\033[0m\n' "$1"; }
|
step() { printf '\n\033[1;34m==> %s\033[0m\n' "$1"; }
|
||||||
|
|
||||||
|
# Docs-only fast path: nothing but *.md changed since main, so there is nothing here to break.
|
||||||
|
# The working tree counts too — a dirty tree carrying real code must never skip. Anything
|
||||||
|
# undeterminable (no git, no reachable main, no merge-base) falls through to the gate, never a skip.
|
||||||
|
docs_only() {
|
||||||
|
local base changed
|
||||||
|
git rev-parse --git-dir >/dev/null 2>&1 || return 1
|
||||||
|
git fetch --no-tags --quiet origin +refs/heads/main:refs/remotes/origin/main 2>/dev/null || true
|
||||||
|
base=$(git merge-base refs/remotes/origin/main HEAD 2>/dev/null) || return 1
|
||||||
|
changed=$(
|
||||||
|
{ git diff --name-only "$base" HEAD && git status --porcelain --untracked-files=all | cut -c4-; } 2>/dev/null
|
||||||
|
) || return 1
|
||||||
|
[ -n "$changed" ] || return 1
|
||||||
|
! printf '%s\n' "$changed" | grep -qvE '\.md$'
|
||||||
|
}
|
||||||
|
|
||||||
|
if docs_only; then
|
||||||
|
step "Only *.md changed since main — nothing to test, skipping the gate"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# Pins that MUST move in lockstep: a browser/runner mismatch yields confusing E2E failures.
|
# Pins that MUST move in lockstep: a browser/runner mismatch yields confusing E2E failures.
|
||||||
step "Playwright pin lockstep (e2e-tests/Dockerfile image == e2e-tests/package.json @playwright/test)"
|
step "Playwright pin lockstep (e2e-tests/Dockerfile image == e2e-tests/package.json @playwright/test)"
|
||||||
# `|| true` so a no-match doesn't trip `set -e`/`pipefail` before the explicit check below can report.
|
# `|| true` so a no-match doesn't trip `set -e`/`pipefail` before the explicit check below can report.
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ services:
|
|||||||
# Dev mail catcher — Kratos recovery/verification emails land here (web UI on 8025).
|
# 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.
|
# kratos.yml points the courier at smtp://mailpit:1025; prod uses a real SMTP via env.
|
||||||
mailpit:
|
mailpit:
|
||||||
image: axllent/mailpit:v1.30.5
|
image: axllent/mailpit:v1.30.6
|
||||||
ports:
|
ports:
|
||||||
- "8025:8025"
|
- "8025:8025"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{"errors":null,"message":"not found","url":"https://gitea.larvit.se/api/swagger"}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Guards the docs-only fast path: `ci.sh` no-ops when nothing but *.md changed since main. The
|
||||||
|
// decision lives in ci.sh alone so `bash ci.sh` reproduces CI locally, and the workflow must still
|
||||||
|
// push the commit-hash image when it no-ops — release.yml re-tags that exact image, and
|
||||||
|
// fast-forward-only merges make every branch head a main commit.
|
||||||
|
import { test } from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
|
||||||
|
const read = (p: string) => readFileSync(new URL(`../${p}`, import.meta.url), "utf8");
|
||||||
|
const workflow = read(".gitea/workflows/ci.yml");
|
||||||
|
const gate = read("ci.sh");
|
||||||
|
const step = (needle: string) => {
|
||||||
|
const found = workflow.split("\n - ").slice(1).filter((s) => s.includes(needle));
|
||||||
|
assert.equal(found.length, 1, `exactly one workflow step contains ${needle}`);
|
||||||
|
return found[0]!;
|
||||||
|
};
|
||||||
|
|
||||||
|
test("the skip decision lives in ci.sh, so the workflow only runs it", () => {
|
||||||
|
assert.match(gate, /docs_only\(\)/);
|
||||||
|
assert.doesNotMatch(workflow, /docs_only|merge-base|GITHUB_OUTPUT/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkout is unshallow — the docs-only check needs the branch's history", () => {
|
||||||
|
assert.match(step("actions/checkout"), /fetch-depth: 0/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the commit-hash image is pushed even when the gate no-ops", () => {
|
||||||
|
assert.doesNotMatch(step("docker push"), /^\s*if:/m);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("only *.md counts as docs, and a dirty working tree counts as changed", () => {
|
||||||
|
assert.ok(gate.includes("\\.md$"), "the non-docs match is a *.md suffix test");
|
||||||
|
assert.match(gate, /git status --porcelain/, "uncommitted code can never be skipped over");
|
||||||
|
});
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
- [x] 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. Before implementing, explain in detail how you will solve this. (`renovate.yml` gains an `auto-release` job (`needs: renovate`) that cuts one `vX.Y.Z` tag per run for what Renovate merged; level = highest `Release-Bump:` trailer Renovate stamps via `commitBody`, any dep's major/minor/patch mapped straight through (default patch). Decoupled from `apiVersion` (tag-only, `HOST_API_VERSION` untouched — a "major" is just a bigger image tag, never a plugin break); pre-1.0 shifts down so nothing auto-crosses into 1.0.0. Pure `auto-release/next-version.ts` + unit tests; tag pushed with renovate-bot's PAT so `release.yml` fires; documented in README → CI/CD.)
|
- [x] 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. Before implementing, explain in detail how you will solve this. (`renovate.yml` gains an `auto-release` job (`needs: renovate`) that cuts one `vX.Y.Z` tag per run for what Renovate merged; level = highest `Release-Bump:` trailer Renovate stamps via `commitBody`, any dep's major/minor/patch mapped straight through (default patch). Decoupled from `apiVersion` (tag-only, `HOST_API_VERSION` untouched — a "major" is just a bigger image tag, never a plugin break); pre-1.0 shifts down so nothing auto-crosses into 1.0.0. Pure `auto-release/next-version.ts` + unit tests; tag pushed with renovate-bot's PAT so `release.yml` fires; documented in README → CI/CD.)
|
||||||
- [ ] 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.
|
- [ ] 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.
|
- [ ] Build and publish docker image as CI/CD.
|
||||||
|
- [ ] The human developer understands the security model in the auth in this project.
|
||||||
- [ ] Add i18n support.
|
- [ ] Add i18n support.
|
||||||
|
|
||||||
## Architectural review findings (2026-07-02)
|
## Architectural review findings (2026-07-02)
|
||||||
|
|||||||
Reference in New Issue
Block a user