From af4a70d904242a538b50b26ec8bb96d5d5fd3702 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 2 Aug 2026 04:18:00 +0000 Subject: [PATCH 1/5] chore(deps): update renovate/renovate docker tag to v44.6.0 Release-Bump: minor --- .gitea/workflows/renovate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/renovate.yml b/.gitea/workflows/renovate.yml index 75acce3..49c9ea4 100644 --- a/.gitea/workflows/renovate.yml +++ b/.gitea/workflows/renovate.yml @@ -19,7 +19,7 @@ jobs: -e RENOVATE_PLATFORM=gitea \ -e RENOVATE_REPOSITORIES=${{ github.repository }} \ -e RENOVATE_TOKEN \ - renovate/renovate:44.5.3 + renovate/renovate:44.6.0 # 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 From 6c850b892383b7f49db0aa3ea32b6dcf6537564f Mon Sep 17 00:00:00 2001 From: lilleman Date: Sun, 2 Aug 2026 13:31:07 +0200 Subject: [PATCH 2/5] Skip the test gate on docs-only branches --- .gitea/workflows/ci.yml | 23 ++++++++++++++++++++++- README.md | 10 ++++++++-- src/ci-workflow.test.ts | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 src/ci-workflow.test.ts diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index c257534..b28fb5a 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -8,7 +8,28 @@ jobs: runs-on: docker-host steps: - uses: actions/checkout@v4.2.2 - - run: bash ci.sh + with: + fetch-depth: 0 # the merge-base below needs history; checkout defaults to depth 1 + - name: Detect a docs-only branch + id: scope + run: | + git fetch --no-tags --quiet origin +refs/heads/main:refs/remotes/origin/main + base=$(git merge-base refs/remotes/origin/main HEAD) \ + || { echo 'No merge-base with main - running the gate'; echo 'docs_only=false' >> "$GITHUB_OUTPUT"; exit 0; } + changed=$(git diff --name-only "$base" HEAD) + printf '%s\n' "$changed" + # Skip only when there IS a diff and every path is *.md. An empty diff, an unreadable + # range, anything else - fall through to the gate. + if [ -n "$changed" ] && ! printf '%s\n' "$changed" | grep -qvE '\.md$'; then + echo 'docs_only=true' >> "$GITHUB_OUTPUT" + else + echo 'docs_only=false' >> "$GITHUB_OUTPUT" + fi + - name: Full gate + if: steps.scope.outputs.docs_only != 'true' + run: bash ci.sh + # Never gated on docs_only: release.yml re-tags this exact image, and ff-only merges make + # every branch head a main commit - a main commit without an image is an unreleasable one. - name: Push app image tagged with the commit hash env: IMAGE: gitea.larvit.se/${{ github.repository }}:${{ github.sha }} diff --git a/README.md b/README.md index 6450ea1..b52dac9 100644 --- a/README.md +++ b/README.md @@ -1173,7 +1173,7 @@ Gitea Actions (`.gitea/workflows/`) runs the pipeline; the test job runs | 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`) — skipped 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 | | `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 | @@ -1188,6 +1188,12 @@ no repo files involved): direct pushes are blocked, changes land via PR only, th **fast-forward-only** — history stays linear and `main`'s head is the exact commit hash of the merged branch, which is why the branch's push-triggered status carries over. +**Docs-only branches skip the gate** — when every path a branch changes against its merge-base +with `main` ends in `.md`, `ci.yml` skips `bash ci.sh`. The job itself still runs and still +builds + pushes the commit-hash image, so `CI / full-gate (push)` reports green and neither the +merge gate nor `release.yml` notices the difference. Everything else in a diff — code, compose, +Ory config, the workflows themselves — runs the full gate, as does an empty or unreadable diff. + **Container images** — after a green gate, `ci.yml` builds the app image and pushes it to the Gitea container registry as `gitea.larvit.se/larvit/plainpages:`. Because merges are fast-forward-only, the image for any `main` commit already exists — it was built @@ -1494,7 +1500,7 @@ plugins/ Drop-in plugin folders (scanned at /app/plugins; bind-mount examples/ Copy-in reference material, mirroring the mount dirs: plugins/scheduling/ (the reference plugin — list/form over an upstream + permission-gated nav), plugins/admin/ (the system-admin plugin — Users/Groups/Roles/OAuth2-clients over Ory via ctx.system), both copied into plugins/; and config/menu.ts (the menu/branding template copied into config/); shifts-upstream/ is the dev mock backend the scheduling plugin reads/writes (stand-in for your real service) e2e-tests/ Playwright E2E: visual.spec (design system, Ory-free) + auth-refresh.spec (token timeout/re-mint) + oauth-login.spec (OAuth2 login + consent) + full-flow.spec (browser UI: password/SSO login, menu-by-role, admin CRUD, plugin page, logout) + devstack-login.spec (regression: login works from the banner's localhost URL and 127.0.0.1 is canonicalised, on the plain `docker compose up` topology); proxy.ts (same-origin gateway) + mock-oidc.ts (mock SSO provider) back full-flow. e2e-tests/Dockerfile + e2e-tests/compose.{visual,auth,oauth,full,devstack}.yml run them 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; +.gitea/workflows/ Gitea Actions: ci.yml — the full gate (ci.sh) on every branch push except main, skipped for docs-only branches; 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 ``` diff --git a/src/ci-workflow.test.ts b/src/ci-workflow.test.ts new file mode 100644 index 0000000..839d60a --- /dev/null +++ b/src/ci-workflow.test.ts @@ -0,0 +1,32 @@ +// Guards the docs-only fast path in .gitea/workflows/ci.yml: a branch that only touches *.md +// skips `ci.sh`, but must still publish its commit-hash image — 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 ci = readFileSync(new URL("../.gitea/workflows/ci.yml", import.meta.url), "utf8"); +const steps = ci.split("\n - ").slice(1); +const step = (needle: string) => { + const found = steps.filter((s) => s.includes(needle)); + assert.equal(found.length, 1, `exactly one step contains ${needle}`); + return found[0]!; +}; + +test("checkout is unshallow — the merge-base with main needs the branch's history", () => { + assert.match(step("actions/checkout"), /fetch-depth: 0/); +}); + +test("a docs-only branch skips the test gate", () => { + assert.match(step("bash ci.sh"), /if: steps\.scope\.outputs\.docs_only != 'true'/); +}); + +test("a docs-only branch still pushes its commit-hash image", () => { + assert.doesNotMatch(step("docker push"), /^\s*if:/m); +}); + +test("only *.md counts as docs, and anything unexpected runs the gate", () => { + const detect = step("docs_only="); + assert.ok(detect.includes("\\.md$"), "the non-docs match is a *.md suffix test"); + assert.ok(detect.includes("docs_only=false"), "the fallback is the full gate, never a skip"); +}); From 175717f04d493f23cc65ecdf341a752e14b80b90 Mon Sep 17 00:00:00 2001 From: lilleman Date: Sun, 2 Aug 2026 13:38:32 +0200 Subject: [PATCH 3/5] Move the docs-only decision into ci.sh so it runs locally --- .gitea/workflows/ci.yml | 25 ++++--------------------- README.md | 22 ++++++++++++++-------- ci.sh | 21 +++++++++++++++++++++ src/ci-gate.test.ts | 34 ++++++++++++++++++++++++++++++++++ src/ci-workflow.test.ts | 32 -------------------------------- 5 files changed, 73 insertions(+), 61 deletions(-) create mode 100644 src/ci-gate.test.ts delete mode 100644 src/ci-workflow.test.ts diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index b28fb5a..efa376b 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -9,27 +9,10 @@ jobs: steps: - uses: actions/checkout@v4.2.2 with: - fetch-depth: 0 # the merge-base below needs history; checkout defaults to depth 1 - - name: Detect a docs-only branch - id: scope - run: | - git fetch --no-tags --quiet origin +refs/heads/main:refs/remotes/origin/main - base=$(git merge-base refs/remotes/origin/main HEAD) \ - || { echo 'No merge-base with main - running the gate'; echo 'docs_only=false' >> "$GITHUB_OUTPUT"; exit 0; } - changed=$(git diff --name-only "$base" HEAD) - printf '%s\n' "$changed" - # Skip only when there IS a diff and every path is *.md. An empty diff, an unreadable - # range, anything else - fall through to the gate. - if [ -n "$changed" ] && ! printf '%s\n' "$changed" | grep -qvE '\.md$'; then - echo 'docs_only=true' >> "$GITHUB_OUTPUT" - else - echo 'docs_only=false' >> "$GITHUB_OUTPUT" - fi - - name: Full gate - if: steps.scope.outputs.docs_only != 'true' - run: bash ci.sh - # Never gated on docs_only: release.yml re-tags this exact image, and ff-only merges make - # every branch head a main commit - a main commit without an image is an unreleasable one. + fetch-depth: 0 # ci.sh's docs-only check needs history; checkout defaults to depth 1 + - run: bash ci.sh + # Runs even when ci.sh no-ops on a docs-only branch: release.yml re-tags this exact image, and + # ff-only merges make every branch head a main commit - one without an image is unreleasable. - name: Push app image tagged with the commit hash env: IMAGE: gitea.larvit.se/${{ github.repository }}:${{ github.sha }} diff --git a/README.md b/README.md index b52dac9..1d43a5a 100644 --- a/README.md +++ b/README.md @@ -1166,6 +1166,12 @@ Each E2E suite **owns a clean stack** — never point two suites at one backend revokes the admin's sessions; full-flow writes users/groups/roles to Keto), which is why the gate runs them serially, one stack up/down per suite. +It **no-ops on a docs-only change** — when every path that differs from `main` (committed *and* +uncommitted) ends in `.md`, there is nothing here to break, so it prints why and exits 0. +Anything it can't determine — no reachable `main`, no merge-base, offline — runs the full gate, +never a skip. The check lives here rather than in the workflow, so `bash ci.sh` on your branch +tells you exactly what CI will do. + ## CI/CD Gitea Actions (`.gitea/workflows/`) runs the pipeline; the test job runs @@ -1173,7 +1179,7 @@ Gitea Actions (`.gitea/workflows/`) runs the pipeline; the test job runs | Workflow | Trigger | Does | | --- | --- | --- | -| `ci.yml` | push, any branch except `main` | the full gate (`bash ci.sh`) — skipped on a docs-only branch — 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 | | `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 | @@ -1188,11 +1194,11 @@ no repo files involved): direct pushes are blocked, changes land via PR only, th **fast-forward-only** — history stays linear and `main`'s head is the exact commit hash of the merged branch, which is why the branch's push-triggered status carries over. -**Docs-only branches skip the gate** — when every path a branch changes against its merge-base -with `main` ends in `.md`, `ci.yml` skips `bash ci.sh`. The job itself still runs and still -builds + pushes the commit-hash image, so `CI / full-gate (push)` reports green and neither the -merge gate nor `release.yml` notices the difference. Everything else in a diff — code, compose, -Ory config, the workflows themselves — runs the full gate, as does an empty or unreadable diff. +**Docs-only branches skip the gate** — [`ci.sh` no-ops](#the-full-gate-one-command) when a branch +changes nothing but `*.md`. The job itself still runs and still builds + pushes the commit-hash +image, so `CI / full-gate (push)` reports green and neither the merge gate nor `release.yml` +notices the difference. This is why `ci.yml` checks out with `fetch-depth: 0` — a shallow clone +has no merge-base to compare against. **Container images** — after a green gate, `ci.yml` builds the app image and pushes it to the Gitea container registry as `gitea.larvit.se/larvit/plainpages:`. Because @@ -1499,8 +1505,8 @@ ory/ Ory service config (kratos/: identity schema, kratos.yml, o plugins/ Drop-in plugin folders (scanned at /app/plugins; bind-mount or bake in). Ships empty (.gitkeep, git-ignored otherwise) — mount your own; the E2E suites bind-mount the example plugins onto /app/plugins/scheduling and /app/plugins/admin examples/ Copy-in reference material, mirroring the mount dirs: plugins/scheduling/ (the reference plugin — list/form over an upstream + permission-gated nav), plugins/admin/ (the system-admin plugin — Users/Groups/Roles/OAuth2-clients over Ory via ctx.system), both copied into plugins/; and config/menu.ts (the menu/branding template copied into config/); shifts-upstream/ is the dev mock backend the scheduling plugin reads/writes (stand-in for your real service) e2e-tests/ Playwright E2E: visual.spec (design system, Ory-free) + auth-refresh.spec (token timeout/re-mint) + oauth-login.spec (OAuth2 login + consent) + full-flow.spec (browser UI: password/SSO login, menu-by-role, admin CRUD, plugin page, logout) + devstack-login.spec (regression: login works from the banner's localhost URL and 127.0.0.1 is canonicalised, on the plain `docker compose up` topology); proxy.ts (same-origin gateway) + mock-oidc.ts (mock SSO provider) back full-flow. e2e-tests/Dockerfile + e2e-tests/compose.{visual,auth,oauth,full,devstack}.yml run them -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, skipped for docs-only branches; +ci.sh The full CI gate: typecheck → unit tests → every E2E suite, each on a fresh, always-torn-down stack (`bash ci.sh`); no-ops on a docs-only change +.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 ``` diff --git a/ci.sh b/ci.sh index 0c1a048..0dfaa77 100755 --- a/ci.sh +++ b/ci.sh @@ -12,6 +12,27 @@ cd "$(dirname "$0")" step() { printf '\n\033[1;34m==> %s\033[0m\n' "$1"; } +# Docs-only fast path: when nothing but *.md changed since main there is nothing here to break, +# so the gate no-ops. Committed changes AND the working tree both count — a dirty tree carrying +# real code must never skip. Anything that can't be determined (no git, no reachable main, no +# merge-base, offline with no origin/main) falls through to the full gate, never to 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. 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. diff --git a/src/ci-gate.test.ts b/src/ci-gate.test.ts new file mode 100644 index 0000000..884a0f5 --- /dev/null +++ b/src/ci-gate.test.ts @@ -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"); +}); diff --git a/src/ci-workflow.test.ts b/src/ci-workflow.test.ts deleted file mode 100644 index 839d60a..0000000 --- a/src/ci-workflow.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Guards the docs-only fast path in .gitea/workflows/ci.yml: a branch that only touches *.md -// skips `ci.sh`, but must still publish its commit-hash image — 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 ci = readFileSync(new URL("../.gitea/workflows/ci.yml", import.meta.url), "utf8"); -const steps = ci.split("\n - ").slice(1); -const step = (needle: string) => { - const found = steps.filter((s) => s.includes(needle)); - assert.equal(found.length, 1, `exactly one step contains ${needle}`); - return found[0]!; -}; - -test("checkout is unshallow — the merge-base with main needs the branch's history", () => { - assert.match(step("actions/checkout"), /fetch-depth: 0/); -}); - -test("a docs-only branch skips the test gate", () => { - assert.match(step("bash ci.sh"), /if: steps\.scope\.outputs\.docs_only != 'true'/); -}); - -test("a docs-only branch still pushes its commit-hash image", () => { - assert.doesNotMatch(step("docker push"), /^\s*if:/m); -}); - -test("only *.md counts as docs, and anything unexpected runs the gate", () => { - const detect = step("docs_only="); - assert.ok(detect.includes("\\.md$"), "the non-docs match is a *.md suffix test"); - assert.ok(detect.includes("docs_only=false"), "the fallback is the full gate, never a skip"); -}); From 6db0f57bf4c1d37bb738e4dea4fb6e5b1b899e27 Mon Sep 17 00:00:00 2001 From: lilleman Date: Sun, 2 Aug 2026 13:55:50 +0200 Subject: [PATCH 4/5] Trim README and workflow prose that restates the code --- .gitea/workflows/ci.yml | 2 -- AGENTS.md | 8 ++++++++ README.md | 33 ++++++++------------------------- ci.sh | 7 +++---- json | 1 + 5 files changed, 20 insertions(+), 31 deletions(-) create mode 100644 json diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index efa376b..048b6cd 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -11,8 +11,6 @@ jobs: with: fetch-depth: 0 # ci.sh's docs-only check needs history; checkout defaults to depth 1 - run: bash ci.sh - # Runs even when ci.sh no-ops on a docs-only branch: release.yml re-tags this exact image, and - # ff-only merges make every branch head a main commit - one without an image is unreleasable. - name: Push app image tagged with the commit hash env: IMAGE: gitea.larvit.se/${{ github.repository }}:${{ github.sha }} diff --git a/AGENTS.md b/AGENTS.md index 4f6d95c..21d3178 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 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 - Node 24 runs `.ts` directly (type stripping). Keep all TypeScript **erasable** diff --git a/README.md b/README.md index 1d43a5a..776b3aa 100644 --- a/README.md +++ b/README.md @@ -1166,12 +1166,6 @@ Each E2E suite **owns a clean stack** — never point two suites at one backend revokes the admin's sessions; full-flow writes users/groups/roles to Keto), which is why the gate runs them serially, one stack up/down per suite. -It **no-ops on a docs-only change** — when every path that differs from `main` (committed *and* -uncommitted) ends in `.md`, there is nothing here to break, so it prints why and exits 0. -Anything it can't determine — no reachable `main`, no merge-base, offline — runs the full gate, -never a skip. The check lives here rather than in the workflow, so `bash ci.sh` on your branch -tells you exactly what CI will do. - ## CI/CD Gitea Actions (`.gitea/workflows/`) runs the pipeline; the test job runs @@ -1194,12 +1188,6 @@ no repo files involved): direct pushes are blocked, changes land via PR only, th **fast-forward-only** — history stays linear and `main`'s head is the exact commit hash of the merged branch, which is why the branch's push-triggered status carries over. -**Docs-only branches skip the gate** — [`ci.sh` no-ops](#the-full-gate-one-command) when a branch -changes nothing but `*.md`. The job itself still runs and still builds + pushes the commit-hash -image, so `CI / full-gate (push)` reports green and neither the merge gate nor `release.yml` -notices the difference. This is why `ci.yml` checks out with `fetch-depth: 0` — a shallow clone -has no merge-base to compare against. - **Container images** — after a green gate, `ci.yml` builds the app image and pushes it to the Gitea container registry as `gitea.larvit.se/larvit/plainpages:`. Because merges are fast-forward-only, the image for any `main` commit already exists — it was built @@ -1215,12 +1203,9 @@ this step runs 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 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 -([`registry-cleanup/cleanup.ts`](registry-cleanup/cleanup.ts), run in a `node:24` container): -a hash tag survives only while its commit is a **branch head** or carries a **`vX.Y.Z` -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 +push, so the nightly `registry-cleanup.yml` prunes them +([`registry-cleanup/cleanup.ts`](registry-cleanup/cleanup.ts) defines what survives). +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 age/count heuristics can't see branch heads or release tags and would delete images the workflow protects. @@ -1271,12 +1256,10 @@ rejected). 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 — a human owns that release — or when nothing new merged). Renovate stamps every commit with a -`Release-Bump: ` trailer (`commitBody` in `renovate.json`); the job takes the highest -trailer on those commits — any dependency's `major`/`minor`/`patch` maps straight through, -defaulting to `patch`. -**Pre-1.0 the level shifts down** — a dep major bumps the `0.x` minor, dep minor/patch bump the -`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 +`Release-Bump: ` trailer (`commitBody` in `renovate.json`), and +[`auto-release/next-version.ts`](auto-release/next-version.ts) (unit-tested) turns the highest +trailer on those commits into the next version — pre-1.0 it never auto-crosses into `1.0.0`, +which stays a deliberate hand-cut tag. It's **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 pushed by the built-in Actions token wouldn't trigger it). The plugin-contract version @@ -1505,7 +1488,7 @@ ory/ Ory service config (kratos/: identity schema, kratos.yml, o plugins/ Drop-in plugin folders (scanned at /app/plugins; bind-mount or bake in). Ships empty (.gitkeep, git-ignored otherwise) — mount your own; the E2E suites bind-mount the example plugins onto /app/plugins/scheduling and /app/plugins/admin examples/ Copy-in reference material, mirroring the mount dirs: plugins/scheduling/ (the reference plugin — list/form over an upstream + permission-gated nav), plugins/admin/ (the system-admin plugin — Users/Groups/Roles/OAuth2-clients over Ory via ctx.system), both copied into plugins/; and config/menu.ts (the menu/branding template copied into config/); shifts-upstream/ is the dev mock backend the scheduling plugin reads/writes (stand-in for your real service) e2e-tests/ Playwright E2E: visual.spec (design system, Ory-free) + auth-refresh.spec (token timeout/re-mint) + oauth-login.spec (OAuth2 login + consent) + full-flow.spec (browser UI: password/SSO login, menu-by-role, admin CRUD, plugin page, logout) + devstack-login.spec (regression: login works from the banner's localhost URL and 127.0.0.1 is canonicalised, on the plain `docker compose up` topology); proxy.ts (same-origin gateway) + mock-oidc.ts (mock SSO provider) back full-flow. e2e-tests/Dockerfile + e2e-tests/compose.{visual,auth,oauth,full,devstack}.yml run them -ci.sh The full CI gate: typecheck → unit tests → every E2E suite, each on a fresh, always-torn-down stack (`bash ci.sh`); no-ops on a docs-only change +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 diff --git a/ci.sh b/ci.sh index 0dfaa77..f78f8d2 100755 --- a/ci.sh +++ b/ci.sh @@ -12,10 +12,9 @@ cd "$(dirname "$0")" step() { printf '\n\033[1;34m==> %s\033[0m\n' "$1"; } -# Docs-only fast path: when nothing but *.md changed since main there is nothing here to break, -# so the gate no-ops. Committed changes AND the working tree both count — a dirty tree carrying -# real code must never skip. Anything that can't be determined (no git, no reachable main, no -# merge-base, offline with no origin/main) falls through to the full gate, never to a skip. +# 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 diff --git a/json b/json new file mode 100644 index 0000000..7dd567a --- /dev/null +++ b/json @@ -0,0 +1 @@ +{"errors":null,"message":"not found","url":"https://gitea.larvit.se/api/swagger"} \ No newline at end of file From 7c66599f35353b27fb58ac77ef4b9c2161e1f1b4 Mon Sep 17 00:00:00 2001 From: lilleman Date: Sun, 2 Aug 2026 13:24:20 +0200 Subject: [PATCH 5/5] Todo and agents updates --- AGENTS.md | 6 +++++- todo.md | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 21d3178..a51b79d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -150,6 +150,10 @@ Same test before adding a row to a table or the file map — a clause, not a par - Tests use the built-in `node --test` runner — no test framework dependency. - English everywhere. Keep code comments short and information-dense. Self explained code 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 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. @@ -169,4 +173,4 @@ Same test before adding a row to a table or the file map — a clause, not a par 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 for example list pages with filters and pagination. Do: "ids=x&ids=y" and not "ids[]=x&ids[]=y" - and not "ids=x,y". \ No newline at end of file + and not "ids=x,y". diff --git a/todo.md b/todo.md index bffea4c..9fcf88d 100644 --- a/todo.md +++ b/todo.md @@ -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.) - [ ] 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. +- [ ] The human developer understands the security model in the auth in this project. - [ ] Add i18n support. ## Architectural review findings (2026-07-02)