Release the gate change as 0.3.0, and start a changelog #105
@@ -0,0 +1,56 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
The release version **is** the plugin contract version (`HOST_API_VERSION`), so a minor is a
|
||||||
|
contract break: a plugin's `apiVersion` must match the host's `major.minor` or discovery refuses it
|
||||||
|
at boot. Entries start at 0.3.0.
|
||||||
|
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
**Breaking.** Set `apiVersion: "0.3.0"`, and name a gate on every route and nav node.
|
||||||
|
|
||||||
|
### A session is a gate of its own
|
||||||
|
|
||||||
|
`session: true` takes any signed-in user, with no grant to hold — for a page whose data is the
|
||||||
|
visitor's own (their upstream account, their own tokens), where there is no distinction a permission
|
||||||
|
could name. An anonymous visitor is bounced to `/login` with the page as `return_to`, exactly as a
|
||||||
|
permission gate does.
|
||||||
|
|
||||||
|
Every route and nav node now names **exactly one** of `public: true`, `session: true` or
|
||||||
|
`permission: "<resource>:<action>"`, and a gate is spelled `true`:
|
||||||
|
|
||||||
|
- Naming **none** is refused. It used to mean public, so a forgotten gate published a page; it now
|
||||||
|
fails the boot instead.
|
||||||
|
- Naming **two** is refused, as before.
|
||||||
|
- Spelling one anything but `true` is refused — `public: false` and `session: "yes"` both set no gate
|
||||||
|
while reading as if they set one.
|
||||||
|
|
||||||
|
A section header gates nothing itself, so it takes `public: true` and lets each child decide; the
|
||||||
|
host still drops a header whose children all filtered out.
|
||||||
|
|
||||||
|
`Gate` is exported from `@plainpages/plugin-api`, and `Route` and `NavNode` extend it.
|
||||||
|
|
||||||
|
### Filter bars take a multi-select
|
||||||
|
|
||||||
|
The `filter-bar` partial gains a `multiselect` control — the same checkboxes on the same query
|
||||||
|
parameter as `chips`, but behind a button once the list is too long to lay on the bar. Config is
|
||||||
|
`{ name, legend?, note?, value?, options }`, and the panel says what a capped list left out.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- An identity carrying no email no longer yields a session at all. Login used to mint a JWT for one,
|
||||||
|
which every later request then rejected as anonymous — leaving the browser holding a dead cookie
|
||||||
|
and no way to tell why.
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- Node 24.20.0.
|
||||||
|
|
||||||
|
### Upgrading a plugin
|
||||||
|
|
||||||
|
1. Set `apiVersion: "0.3.0"`.
|
||||||
|
2. Give every route and nav node a gate. Anything that relied on omitting one was public — say
|
||||||
|
`public: true` outright.
|
||||||
|
|
||||||
|
A page that scopes rows to the signed-in visitor should join on `ctx.user.id`. An email address is
|
||||||
|
user-changeable and can be reassigned to someone else, who would then inherit the previous holder's
|
||||||
|
rows. The reference plugin's new `/scheduling/mine` page shows the shape.
|
||||||
@@ -47,7 +47,7 @@ folder under `plugins/` goes live after a restart. Create `plugins/hello/plugin.
|
|||||||
import { definePlugin } from "@plainpages/plugin-api";
|
import { definePlugin } from "@plainpages/plugin-api";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
apiVersion: "0.4.0",
|
apiVersion: "0.3.0",
|
||||||
nav: [{ href: "/hello", id: "hello", label: "Hello", public: true }],
|
nav: [{ href: "/hello", id: "hello", label: "Hello", public: true }],
|
||||||
routes: [
|
routes: [
|
||||||
{ method: "GET", path: "/", public: true, handler: () => ({ html: "<h1>Hello from my plugin</h1>" }) },
|
{ method: "GET", path: "/", public: true, handler: () => ({ html: "<h1>Hello from my plugin</h1>" }) },
|
||||||
@@ -350,7 +350,7 @@ import { definePlugin } from "@plainpages/plugin-api";
|
|||||||
import { listThings, createThings } from "./handlers.ts";
|
import { listThings, createThings } from "./handlers.ts";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
apiVersion: "0.4.0", // semver string of the host contract this plugin was built against (see Versioning)
|
apiVersion: "0.3.0", // semver string of the host contract this plugin was built against (see Versioning)
|
||||||
|
|
||||||
// Nav fragment, merged into the global menu and gate-filtered per user.
|
// Nav fragment, merged into the global menu and gate-filtered per user.
|
||||||
// `icon` is a Lucide icon by its sprite id (src/ui/icons.ts).
|
// `icon` is a Lucide icon by its sprite id (src/ui/icons.ts).
|
||||||
@@ -471,7 +471,7 @@ import { definePlugin } from "@plainpages/plugin-api";
|
|||||||
import { landing, board } from "./pages.ts";
|
import { landing, board } from "./pages.ts";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
apiVersion: "0.4.0",
|
apiVersion: "0.3.0",
|
||||||
home: landing, // owns "/" — the public front page
|
home: landing, // owns "/" — the public front page
|
||||||
dashboard: board, // owns "/dashboard" — the post-login app home
|
dashboard: board, // owns "/dashboard" — the post-login app home
|
||||||
});
|
});
|
||||||
@@ -626,6 +626,7 @@ provider/consumer semantics in `checkApiVersion`:
|
|||||||
The plugin pins one exact version (no ranges, per the project's pinning rules); the *host* supplies
|
The plugin pins one exact version (no ranges, per the project's pinning rules); the *host* supplies
|
||||||
the compatibility. One digit carries the whole release, so a **minor** means either the plugin
|
the compatibility. One digit carries the whole release, so a **minor** means either the plugin
|
||||||
contract changed or a dependency moved far enough to warrant one.
|
contract changed or a dependency moved far enough to warrant one.
|
||||||
|
[`CHANGELOG.md`](CHANGELOG.md) is what a minor sends you to: what broke, and what to change.
|
||||||
|
|
||||||
|
|
||||||
### Conflict rules
|
### Conflict rules
|
||||||
@@ -756,7 +757,7 @@ camel humps both becoming underscores — so `upstream` on the `scheduling` plug
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
apiVersion: "0.4.0",
|
apiVersion: "0.3.0",
|
||||||
settings: [
|
settings: [
|
||||||
{ key: "upstream", type: "url", required: true, description: "Base URL of the backend" },
|
{ key: "upstream", type: "url", required: true, description: "Base URL of the backend" },
|
||||||
{ key: "pageSize", type: "number", default: 25 },
|
{ key: "pageSize", type: "number", default: 25 },
|
||||||
@@ -810,7 +811,7 @@ import { definePlugin } from "@plainpages/plugin-api";
|
|||||||
let sql: ReturnType<typeof postgres>;
|
let sql: ReturnType<typeof postgres>;
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
apiVersion: "0.4.0",
|
apiVersion: "0.3.0",
|
||||||
storage: true,
|
storage: true,
|
||||||
hooks: {
|
hooks: {
|
||||||
onBoot: async (boot) => {
|
onBoot: async (boot) => {
|
||||||
@@ -1730,6 +1731,7 @@ e2e-tests/ Playwright specs + their Dockerfile and compose.{visual,aut
|
|||||||
release-tooling/ Everything the release runs: next-version (the bump math), contract-version
|
release-tooling/ Everything the release runs: next-version (the bump math), contract-version
|
||||||
(the HOST_API_VERSION↔tag gate), dockerhub-overview (+ its .md.tmpl)
|
(the HOST_API_VERSION↔tag gate), dockerhub-overview (+ its .md.tmpl)
|
||||||
registry-cleanup/ Nightly image pruning — the Gitea client plus what survives (select-versions.ts)
|
registry-cleanup/ Nightly image pruning — the Gitea client plus what survives (select-versions.ts)
|
||||||
|
CHANGELOG.md What changed per release, and how to upgrade a plugin across a minor
|
||||||
ci.sh The full gate: typecheck → unit tests → every E2E suite on a fresh stack
|
ci.sh The full gate: typecheck → unit tests → every E2E suite on a fresh stack
|
||||||
.gitea/workflows/ Gitea Actions — see CI/CD
|
.gitea/workflows/ Gitea Actions — see CI/CD
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const clients = on("oauth2-clients");
|
|||||||
const pluginSettings = on("plugin-settings");
|
const pluginSettings = on("plugin-settings");
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
apiVersion: "0.4.0", // the host contract this was built against — a literal, never HOST_API_VERSION
|
apiVersion: "0.3.0", // the host contract this was built against — a literal, never HOST_API_VERSION
|
||||||
|
|
||||||
nav: [ADMIN_NAV],
|
nav: [ADMIN_NAV],
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ let upstreamUrl = "";
|
|||||||
const upstream = createUpstream(() => upstreamUrl);
|
const upstream = createUpstream(() => upstreamUrl);
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
apiVersion: "0.4.0", // the host contract this was built against — a literal, never HOST_API_VERSION
|
apiVersion: "0.3.0", // the host contract this was built against — a literal, never HOST_API_VERSION
|
||||||
|
|
||||||
// onBoot runs after discovery, before the server listens — where a plugin receives its resolved
|
// onBoot runs after discovery, before the server listens — where a plugin receives its resolved
|
||||||
// settings. A malformed URL already failed the boot by then; the host validated the declared type.
|
// settings. A malformed URL already failed the boot by then; the host validated the declared type.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ test("readHostApiVersion pulls the constant out of the real source, and returns
|
|||||||
test("bumping HOST_API_VERSION is a deliberate act, so pin the shipped value", () => {
|
test("bumping HOST_API_VERSION is a deliberate act, so pin the shipped value", () => {
|
||||||
// Not a substitute for the release gate — this test cannot see a tag. It is the tripwire that
|
// Not a substitute for the release gate — this test cannot see a tag. It is the tripwire that
|
||||||
// makes an accidental edit fail here rather than at release time.
|
// makes an accidental edit fail here rather than at release time.
|
||||||
assert.equal(readHostApiVersion(readFileSync("src/plugin-host/plugin.ts", "utf8")), "0.4.0");
|
assert.equal(readHostApiVersion(readFileSync("src/plugin-host/plugin.ts", "utf8")), "0.3.0");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("every author-facing apiVersion sample matches the shipped contract", () => {
|
test("every author-facing apiVersion sample matches the shipped contract", () => {
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ into the app. Create `plugins/hello/plugin.ts`:
|
|||||||
import { definePlugin } from "@plainpages/plugin-api";
|
import { definePlugin } from "@plainpages/plugin-api";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
apiVersion: "0.4.0",
|
apiVersion: "0.3.0",
|
||||||
nav: [{ href: "/hello", id: "hello", label: "Hello", public: true }],
|
nav: [{ href: "/hello", id: "hello", label: "Hello", public: true }],
|
||||||
routes: [
|
routes: [
|
||||||
{ method: "GET", path: "/", public: true, handler: () => ({ html: "<h1>Hello from my plugin</h1>" }) },
|
{ method: "GET", path: "/", public: true, handler: () => ({ html: "<h1>Hello from my plugin</h1>" }) },
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { envName, type SettingDecl, type SettingsOf } from "./settings.ts";
|
|||||||
import type { StorageCredentials } from "./storage.ts";
|
import type { StorageCredentials } from "./storage.ts";
|
||||||
|
|
||||||
// The Plainpages release this contract ships in — see README → Contract versioning.
|
// The Plainpages release this contract ships in — see README → Contract versioning.
|
||||||
export const HOST_API_VERSION = "0.4.0";
|
export const HOST_API_VERSION = "0.3.0";
|
||||||
|
|
||||||
export type HttpMethod = "DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT";
|
export type HttpMethod = "DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT";
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@
|
|||||||
<p>${t("dashboard.starter.intro")}</p>
|
<p>${t("dashboard.starter.intro")}</p>
|
||||||
<p>${t("dashboard.starter.replace")}</p>
|
<p>${t("dashboard.starter.replace")}</p>
|
||||||
<pre class="code-block"><code>export default definePlugin({
|
<pre class="code-block"><code>export default definePlugin({
|
||||||
apiVersion: "0.4.0",
|
apiVersion: "0.3.0",
|
||||||
// view names plugins/<id>/views/<view>.ejs, rendered in this same shell
|
// view names plugins/<id>/views/<view>.ejs, rendered in this same shell
|
||||||
dashboard: (ctx) => ({ view: "dashboard", data: { /* … */ } }),
|
dashboard: (ctx) => ({ view: "dashboard", data: { /* … */ } }),
|
||||||
});</code></pre>
|
});</code></pre>
|
||||||
|
|||||||
Reference in New Issue
Block a user