From 2d62553a10fa72fbbbd1a916472550aa1e0d3157 Mon Sep 17 00:00:00 2001 From: lilleman Date: Sat, 22 Aug 2026 12:13:41 +0200 Subject: [PATCH] Refuse a 0.x minor mismatch, and republish the overview from the named release's tree --- .gitea/workflows/release.yml | 6 +++++- README.md | 15 +++++++++------ src/plugin-host/plugin.test.ts | 3 +++ src/plugin-host/plugin.ts | 6 ++++++ 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 1b3f84f..ddb4477 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -5,7 +5,7 @@ on: workflow_dispatch: inputs: overview_version: - description: 'Version the overview should tell adopters to pull (e.g. 0.1.0)' + description: 'Released version to republish the overview for, without the leading v (e.g. 0.1.0)' required: true jobs: @@ -72,7 +72,11 @@ jobs: needs: [retag-image] runs-on: docker-host steps: + # Publish the named release's own tree, so the page never pairs one Plainpages tag with another + # release's sidecar pins. A version that was never released fails here. - uses: actions/checkout@v7.0.1 + with: + ref: ${{ inputs.overview_version && format('refs/tags/v{0}', inputs.overview_version) || github.ref }} - name: Publish the Docker Hub overview env: DOCKERHUB_OVERVIEW_TOKEN: ${{ secrets.DOCKERHUB_OVERVIEW_TOKEN }} diff --git a/README.md b/README.md index 969e35b..fef3f21 100644 --- a/README.md +++ b/README.md @@ -607,17 +607,20 @@ provider/consumer semantics in `checkApiVersion`: | --- | --- | --- | | same major, same minor (patch ignored) | `ok` | load | | same major, plugin minor **<** host minor | `warn` | load, log — built against an older release; check that release's notes | +| **major `0`**, any minor mismatch | `refuse` | **abort boot** — pre-1.0 the minor is the breaking slot | | same major, plugin minor **>** host minor | `refuse` | **abort boot** — plugin needs a newer host | | different major | `refuse` | **abort boot** — incompatible contract | | missing / not a valid semver | `refuse` | **abort boot** — must be declared | The plugin pins one exact version (no ranges, per the project's pinning rules); the *host* supplies -the caret-style 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. A `warn` therefore says -"built against an older release" rather than promising new plugin features; check that release's -notes before assuming there is anything to adopt. While Plainpages is `0.x` every release shares major `0`, so a plugin -built against `0.1.0` still loads on a `0.9.0` host with a `warn`; reaching `1.0.0` refuses everything -built against `0.x`, which is the point of that milestone. +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. + +While Plainpages is `0.x` the major stays `0`, which leaves the minor as the only slot a breaking +change can use — so a minor mismatch is refused rather than warned: a plugin declaring `0.1.0` will +not boot on a `0.2.0` host until it is rebuilt against it. Once `1.0.0` lands the minor becomes +additive and the `warn` row applies, saying "built against an older release" rather than promising +new features. ### Conflict rules diff --git a/src/plugin-host/plugin.test.ts b/src/plugin-host/plugin.test.ts index ebd9330..ceae298 100644 --- a/src/plugin-host/plugin.test.ts +++ b/src/plugin-host/plugin.test.ts @@ -86,6 +86,9 @@ test("checkApiVersion: semver compat — equal/patch ok, older minor warns, newe assert.equal(checkApiVersion("1.3.0", "1.2.0").level, "refuse"); // needs features a newer host has assert.equal(checkApiVersion("2.0.0", "1.5.0").level, "refuse"); // incompatible major (newer) assert.equal(checkApiVersion("1.0.0", "2.0.0").level, "refuse"); // incompatible major (older) + assert.equal(checkApiVersion("0.1.0", "0.1.9").level, "ok"); // pre-1.0 patch is still ignored + assert.equal(checkApiVersion("0.1.0", "0.2.0").level, "refuse"); // pre-1.0 the minor IS the breaking slot + assert.equal(checkApiVersion("0.2.0", "0.1.0").level, "refuse"); for (const bad of ["1", "1.2", "v1.2.3", "01.2.3", "1.2.x", "", 1, undefined, null]) { assert.equal(checkApiVersion(bad).level, "refuse", `${String(bad)} must refuse`); } diff --git a/src/plugin-host/plugin.ts b/src/plugin-host/plugin.ts index c544d39..9ff9f5a 100644 --- a/src/plugin-host/plugin.ts +++ b/src/plugin-host/plugin.ts @@ -156,6 +156,12 @@ export function checkApiVersion(pluginVersion: unknown, hostVersion: string = HO if (plugin.major !== host.major) { return { level: "refuse", message: `plugin targets apiVersion ${pluginVersion}; host is ${hostVersion} — incompatible major` }; } + // Pre-1.0 the major is pinned at 0 until the 1.0.0 milestone, so a breaking change can only land + // as a minor (release-tooling/next-version.ts shifts every level down). Treating that as additive + // would let a stale plugin boot and fail at runtime instead of at discovery. + if (host.major === 0 && plugin.minor !== host.minor) { + return { level: "refuse", message: `plugin targets apiVersion ${pluginVersion}; host is ${hostVersion} — pre-1.0 a minor is a contract break, rebuild against ${hostVersion}` }; + } if (plugin.minor > host.minor) { return { level: "refuse", message: `plugin targets apiVersion ${pluginVersion} but host is ${hostVersion}; upgrade the host` }; }