Lift the pre-release freeze — releases resume and the plugin contract goes live

This commit is contained in:
2026-08-20 22:02:29 +02:00
parent c16fb2449b
commit ecf33733a1
4 changed files with 27 additions and 50 deletions
-3
View File
@@ -30,12 +30,9 @@ jobs:
# authorship on the tip, so the author checks are reliable. Level = highest `Release-Bump:` trailer; # authorship on the tip, so the author checks are reliable. Level = highest `Release-Bump:` trailer;
# pre-1.0 shifts down (auto-release/next-version.ts). Tag-only — release.yml promotes the # pre-1.0 shifts down (auto-release/next-version.ts). Tag-only — release.yml promotes the
# already-built image; pushed with renovate-bot's PAT so release.yml fires (the built-in token won't). # already-built image; pushed with renovate-bot's PAT so release.yml fires (the built-in token won't).
# Off until the Actions variable AUTO_RELEASE is set to 'true': Plainpages is pre-announcement and
# deliberately carries no tags, so an automated bump would only invent a version nobody consumes.
auto-release: auto-release:
runs-on: docker-host runs-on: docker-host
needs: renovate needs: renovate
if: vars.AUTO_RELEASE == 'true'
steps: steps:
- uses: actions/checkout@v7.0.1 - uses: actions/checkout@v7.0.1
with: with:
+17 -31
View File
@@ -105,16 +105,15 @@ Revisit only if the stated reason stops holding.
`REVOKE CONNECT` from `bootstrap`. `REVOKE` only *warns* when the caller doesn't own the database, `REVOKE CONNECT` from `bootstrap`. `REVOKE` only *warns* when the caller doesn't own the database,
so under the least-privilege provisioning account the README recommends it would report success so under the least-privilege provisioning account the README recommends it would report success
while changing nothing, and it hard-fails whenever `PLUGIN_DB_ADMIN_URL` names a server with no while changing nothing, and it hard-fails whenever `PLUGIN_DB_ADMIN_URL` names a server with no
`kratos`. A volume created before that file gained the revokes keeps the default grant; `kratos`. It runs only on **first init**, so a revoke added to it later never reaches a volume that
`docker compose down -v` is the dev remedy. **Valid while pre-release, with no deployed volumes.** already exists — `docker compose down -v` is the dev remedy, a deployed install needs a migration.
- **`bootstrap.ts` stays under `src/auth/`** even though it now provisions plugin databases as well - **`bootstrap.ts` stays under `src/auth/`** even though it now provisions plugin databases as well
as seeding Ory. It is the one-shot service's entrypoint, not an auth module; moving it to as seeding Ory. It is the one-shot service's entrypoint, not an auth module; moving it to
`src/bootstrap.ts` would edit `compose.yml`, five e2e compose files and `src/compose.test.ts` for a `src/bootstrap.ts` would edit `compose.yml`, five e2e compose files and `src/compose.test.ts` for a
rename. Reconsider when a third seeding concern lands. rename. Reconsider when a third seeding concern lands.
- **`BootContext.storage` keeps all six credential fields, and there is no `onShutdown` hook.** While - **`BootContext.storage` keeps all six credential fields, and there is no `onShutdown` hook.** Adding
`HOST_API_VERSION` is frozen both are free to revisit; after the freeze, adding is compatible and to the context costs a minor bump and removing one a major, so the shape errs small elsewhere. Pools
removing is not, so the shape errs small elsewhere. Pools handed to a plugin are reaped on process handed to a plugin are reaped on process exit — revisit if a plugin ever needs an orderly drain.
exit — revisit if a plugin ever needs an orderly drain. **Valid while the freeze holds.**
- **`config/` is still a plain dir — no `package.json` of its own**, or `#menu-config` resolves - **`config/` is still a plain dir — no `package.json` of its own**, or `#menu-config` resolves
against that instead and boot fails loud. An operator's menu override has no use for against that instead and boot fails loud. An operator's menu override has no use for
dependencies; if that changes, it needs the same package treatment. dependencies; if that changes, it needs the same package treatment.
@@ -300,19 +299,6 @@ Revisit only if the stated reason stops holding.
file between login and logout. Same class: concurrent runs share the workspace dir, so ci.sh's file between login and logout. Same class: concurrent runs share the workspace dir, so ci.sh's
web-image build races another run's container creation on the `<project>-web` tag. Accepted for a web-image build races another run's container creation on the `<project>-web` tag. Accepted for a
single-maintainer cadence; serialize with a workflow `concurrency` group if it ever bites. single-maintainer cadence; serialize with a workflow `concurrency` group if it ever bites.
- **Plainpages is pre-announcement: no tags, no releases.** `auto-release` is gated behind the
`AUTO_RELEASE` Actions variable (unset ⇒ skipped, the fail-safe direction on every unknown-`vars`
path) — a version only communicates to consumers and there are none. Two couplings:
`registry-cleanup` keeps a hash image only while its commit is a branch head *or* release-tagged, so
with zero tags a hand-cut tag must sit on `main`'s tip; and `mirror.yml` pushes tags with `--prune`
(its `fetch-tags: true` is load-bearing), so a tag or Release created on GitHub is swept away and
releases are cut on Gitea only. Valid until the maintainer says Plainpages is ready to show people.
- **A stricter manifest rule breaks already-copied plugins**, and while `HOST_API_VERSION` is frozen
the failure names a symptom rather than the cause — `checkApiVersion` would refuse a stale plugin by
*version*, but only once the freeze lifts. Until then a stricter rule ships with a README →
Upgrading entry and a re-copy hint in the discovery error. Fail-loud stays right either way: the
alternative is a route gating on a name nobody can be granted, i.e. a permanent silent 403.
**Valid while `HOST_API_VERSION` stays frozen.**
## Docker only — no host tooling ## Docker only — no host tooling
@@ -361,15 +347,15 @@ one-time setup. A file-map or table row gets a clause, not a paragraph.
- Pin all dependencies and Docker images to exact, human-readable **semantic versions** — never - Pin all dependencies and Docker images to exact, human-readable **semantic versions** — never
ranges (`^`, `~`) and never digests. npm deps via `.npmrc` (`save-exact=true`) + `npm ci`; images ranges (`^`, `~`) and never digests. npm deps via `.npmrc` (`save-exact=true`) + `npm ci`; images
by tag. by tag.
- **`HOST_API_VERSION` is frozen at 1.0.0 until the first external install**, even for additive - **`HOST_API_VERSION` is a live promise as of the v0.1.0 release** (the app's version and the
contract changes. With no third-party plugin in the wild a bump can only produce noise. The contract's move independently). Bump it with every contract change, per the table in README →
promotion trigger is the first external plugin — from then on follow the versioning table in Contract versioning: major on a breaking one, minor on an additive one. **The contract surface
README → Contract versioning. **The frozen surface includes `views/partials/*.ejs`**: the view includes `views/partials/*.ejs`** — the view resolver makes every core partial an `include()` root
resolver makes every core partial an `include()` root for a plugin's views, so their option names for a plugin's views, so their option names and emitted markup are author-visible. Know the hole
and emitted markup are author-visible. Know the hole that leaves discovery fails loud on a bad that leaves: discovery fails loud on a bad `apiVersion`, but `include("menu", { open: true })`
`apiVersion`, but `include("menu", { open: true })` silently ignores a dropped option. Promotion silently ignores a dropped option, so the partial vocabulary is a surface the version check cannot
must cover the partial vocabulary too. police for you.
- **The frozen surface also includes the packaging promises** (README → Plugin dependencies): the - **The contract surface also includes the packaging promises** (README → Plugin dependencies): the
barrel is ambient at `/node_modules` with nothing for a plugin to declare, `"type": "module"` is barrel is ambient at `/node_modules` with nothing for a plugin to declare, `"type": "module"` is
mandatory, and the host neither upgrades nor dedupes a plugin's dependencies. Same hole as the mandatory, and the host neither upgrades nor dedupes a plugin's dependencies. Same hole as the
partials — move the publish point, rename the package or start hoisting and every installed plugin partials — move the publish point, rename the package or start hoisting and every installed plugin
@@ -377,9 +363,9 @@ one-time setup. A file-map or table row gets a clause, not a paragraph.
build-time dedupe for baked images stays open, module-instance sharing stays unpromised. build-time dedupe for baked images stays open, module-instance sharing stays unpromised.
- **Publishing `@plainpages/plugin-api` to a registry is deferred, not rejected.** Today it is - **Publishing `@plainpages/plugin-api` to a registry is deferred, not rejected.** Today it is
`private` and shaped as a shim — `index.ts` re-exports `../src/…`, so `npm pack` would ship a `private` and shaped as a shim — `index.ts` re-exports `../src/…`, so `npm pack` would ship a
broken tree. The trigger is the same as the freeze's: the first external plugin, which is also the broken tree. The trigger is the first plugin author outside this repo — the first who cannot
first author who cannot typecheck against a mounted host tree. Whoever does it must first make the typecheck against a mounted host tree. Whoever does it must first make the artifact self-contained
artifact self-contained (types-only `.d.ts`, or move the barrel into `plugin-api/`). (types-only `.d.ts`, or move the barrel into `plugin-api/`).
- A plugin's `apiVersion` is a **hand-written literal** semver — the host version it was built - A plugin's `apiVersion` is a **hand-written literal** semver — the host version it was built
against — bumped by hand on rebuild, **never** the host's `HOST_API_VERSION` constant. Importing against — bumped by hand on rebuild, **never** the host's `HOST_API_VERSION` constant. Importing
the constant makes every plugin always equal the host, so `checkApiVersion` can never fire. the constant makes every plugin always equal the host, so `checkApiVersion` can never fire.
+3 -3
View File
@@ -22,7 +22,7 @@ so there is nothing to clone. In an empty directory, save this as `compose.yml`:
```yaml ```yaml
services: services:
web: web:
image: larvit/plainpages:0.0.2 image: larvit/plainpages:0.1.0
ports: ports:
- "3000:3000" - "3000:3000"
environment: environment:
@@ -41,7 +41,7 @@ services:
# One-shot, idempotent seed: signing key if absent + the admin@plainpages.local / admin user. # One-shot, idempotent seed: signing key if absent + the admin@plainpages.local / admin user.
bootstrap: bootstrap:
image: larvit/plainpages:0.0.2 image: larvit/plainpages:0.1.0
command: node src/auth/bootstrap.ts command: node src/auth/bootstrap.ts
depends_on: depends_on:
kratos: kratos:
@@ -143,7 +143,7 @@ volumes:
Extract the Ory config the image ships, then start: Extract the Ory config the image ships, then start:
```bash ```bash
docker run --rm larvit/plainpages:0.0.2 tar -cf - ory | tar -xf - docker run --rm larvit/plainpages:0.1.0 tar -cf - ory | tar -xf -
mkdir -p plugins mkdir -p plugins
docker compose up -d docker compose up -d
``` ```
+7 -13
View File
@@ -1375,7 +1375,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 | | `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 (pruning deleted ones) to the [GitHub mirror](https://github.com/larvit/plainpages) | | `mirror.yml` | push to `main` or any tag, or manual | force-push `main` + tags (pruning deleted ones) 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 |
| `renovate.yml` | nightly cron, or manual | open dependency-update PRs, automerge them once the gate is green; the release-tag job only runs when `AUTO_RELEASE` is `true` | | `renovate.yml` | nightly cron, or manual | open dependency-update PRs, automerge them once the gate is green, then cut a release tag for what merged |
`main` is not re-tested on push — its commits are meant to arrive already green from a `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)`. gated branch, so the status check to gate a merge on is `CI / full-gate (push)`.
@@ -1417,15 +1417,9 @@ images, and the Playwright runner + its browser image — and every bump keeps t
exact. Each PR runs the normal gate on its `renovate/*` branch and automerges once exact. Each PR runs the normal gate on its `renovate/*` branch and automerges once
`CI / full-gate (push)` is green; only a red gate needs a human. `CI / full-gate (push)` is green; only a red gate needs a human.
**Releases are paused.** Plainpages is pre-announcement: the repository carries **no tags**, so **Auto-release on dependency updates** — a second job in `renovate.yml` (`auto-release`) cuts **one**
neither `release.yml` nor Docker Hub has a version to promote. Turn releasing back on by setting the `vX.Y.Z` tag per run covering the renovate-bot commits merged to `main` since the last tag, and
Actions variable `AUTO_RELEASE` to `true`, or cut a `vX.Y.Z` tag by hand **on `main`'s tip** — with **skips** when the tip isn't a Renovate commit or nothing new merged.
nothing tagged the nightly cleanup keeps only branch-head images, so an older commit's image is
already gone and `release.yml` would fail loud.
**Auto-release on dependency updates** — a second job in `renovate.yml` (`auto-release`, gated on
`AUTO_RELEASE`) cuts **one** `vX.Y.Z` tag per run covering the renovate-bot commits merged to `main`
since the last tag, and **skips** when the tip isn't a Renovate commit or nothing new merged.
Renovate stamps each commit with a `Release-Bump: <updateType>` trailer and Renovate stamps each commit with a `Release-Bump: <updateType>` trailer and
[`auto-release/next-version.ts`](auto-release/next-version.ts) turns the highest one into the next [`auto-release/next-version.ts`](auto-release/next-version.ts) turns the highest one into the next
version — pre-1.0 it never auto-crosses into `1.0.0`. It is **tag-only**: the tag hands off to version — pre-1.0 it never auto-crosses into `1.0.0`. It is **tag-only**: the tag hands off to
@@ -1502,9 +1496,9 @@ docker compose up -d --build
``` ```
Do the same for any other folder you copied out of `examples/`. A plugin you wrote yourself needs the Do the same for any other folder you copied out of `examples/`. A plugin you wrote yourself needs the
manifest change the error names. Once [`HOST_API_VERSION`](#contract-versioning) starts moving a manifest change the error names. A host contract change big enough to move
stale plugin will be refused by **version** instead; it is frozen at `1.0.0` until the first external [`HOST_API_VERSION`](#contract-versioning) shows up earlier and more precisely — discovery refuses the
plugin exists, so for now the error names the rule it tripped. plugin by **version** before any rule gets a chance to trip.
Two paths in the checkout are load-bearing and must stay clear of root-owned leftovers: Two paths in the checkout are load-bearing and must stay clear of root-owned leftovers:
`node_modules/` must not exist (deps live at `/node_modules`, and anything at `/app/node_modules` `node_modules/` must not exist (deps live at `/node_modules`, and anything at `/app/node_modules`