Scope the release signal, collapse the contract onto the release version, publish the Hub overview #83

Merged
lilleman merged 17 commits from auto-release-only-when-affected into main 2026-08-22 22:19:59 +02:00
7 changed files with 19 additions and 19 deletions
Showing only changes of commit 5820264885 - Show all commits
+1 -1
View File
@@ -7,7 +7,7 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
overview_version: overview_version:
description: 'Version the overview should tell adopters to pull (e.g. 0.2.0)' description: 'Version the overview should tell adopters to pull (e.g. 0.1.0)'
required: true required: true
jobs: jobs:
+4 -4
View File
@@ -293,7 +293,7 @@ Revisit only if the stated reason stops holding.
docs and skip the gate over a source file that was gone. `src/ci-gate.test.ts` locks the flags as a docs and skip the gate over a source file that was gone. `src/ci-gate.test.ts` locks the flags as a
*text* guard — the test image ships neither `git` nor `bash`. This is why the Docker Hub overview is *text* guard — the test image ships neither `git` nor `bash`. This is why the Docker Hub overview is
`release-tooling/dockerhub-overview.md.tmpl` and not a `.md`: a release reads it and a unit test `release-tooling/dockerhub-overview.md.tmpl` and not a `.md`: a release reads it and a unit test
guards it, so renaming it back would let a broken `{{VERSION}}` merge with its own guard skipped. guards it, so giving it a `.md` name would let a broken `{{VERSION}}` merge with its own guard skipped.
- **CI docker logins share the runner host's Docker config.** The act_runner is host-mode, so - **CI docker logins share the runner host's Docker config.** The act_runner is host-mode, so
`docker login`/`logout` in the workflows mutate one shared `~/.docker/config.json`: concurrent jobs `docker login`/`logout` in the workflows mutate one shared `~/.docker/config.json`: concurrent jobs
can race (one job's logout can 401 another's push — recover by re-running), and tokens sit in that can race (one job's logout can 401 another's push — recover by re-running), and tokens sit in that
@@ -353,9 +353,9 @@ one-time setup. A file-map or table row gets a clause, not a paragraph.
trailer, so a dependency added anywhere else never escalates the release version and nothing fails to trailer, so a dependency added anywhere else never escalates the release version and nothing fails to
say so. A new manifest, compose file, custom manager or dep type is a decision: can it reach a say so. A new manifest, compose file, custom manager or dep type is a decision: can it reach a
running Plainpages? If yes it needs a rule; if no, record nothing and let it ride the next patch. running Plainpages? If yes it needs a rule; if no, record nothing and let it ride the next patch.
- **`HOST_API_VERSION` *is* the release version — one number, not two.** Its `major.minor` must equal - **`HOST_API_VERSION` *is* the release version.** Its `major.minor` must equal the release tag's, and
the release tag's, and both release paths refuse a tag that disagrees both release paths refuse a tag that disagrees (`release-tooling/contract-version.ts`). The patch
(`release-tooling/contract-version.ts`). The patch digit may lag on purpose: `checkApiVersion` digit may lag on purpose: `checkApiVersion`
ignores patch, and auto-release cuts patch releases with no commit to bump a constant in. So a ignores patch, and auto-release cuts patch releases with no commit to bump a constant in. So a
dependency update big enough to force a **minor** is plugin-visible by definition — `auto-release` dependency update big enough to force a **minor** is plugin-visible by definition — `auto-release`
stops rather than tagging, and the fix is to bump `HOST_API_VERSION` to that `X.Y.0` in a PR, merge stops rather than tagging, and the fix is to bump `HOST_API_VERSION` to that `X.Y.0` in a PR, merge
+7 -7
View File
@@ -594,9 +594,9 @@ works without editing host config.
### Contract versioning ### Contract versioning
Each manifest declares `apiVersion` — a **semver** string naming the **Plainpages release** it was Each manifest declares `apiVersion` — a **semver** string naming the **Plainpages release** it was
built against. There is no second number to track: the host's `HOST_API_VERSION` *is* its release built against. The host's `HOST_API_VERSION` *is* its release version, so a plugin author reads one
version, so a plugin author reads one version off the image they run and writes it down. Both release version off the image they run and writes it downthere is no separate contract number. Both
paths refuse a tag whose `major.minor` disagrees with the constant, so the two cannot drift. release paths refuse a tag whose `major.minor` disagrees with the constant, so the two cannot drift.
Patch releases are invisible here — `checkApiVersion` ignores the patch digit, which is what lets Patch releases are invisible here — `checkApiVersion` ignores the patch digit, which is what lets
dependency updates ship continuously without touching any plugin. At discovery the host parses both dependency updates ship continuously without touching any plugin. At discovery the host parses both
@@ -612,10 +612,10 @@ provider/consumer semantics in `checkApiVersion`:
| missing / not a valid semver | `refuse` | **abort boot** — must be declared | | 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 plugin pins one exact version (no ranges, per the project's pinning rules); the *host* supplies
the caret-style compatibility. Note what a **minor** means now that one digit carries the whole the caret-style compatibility. One digit carries the whole release, so a **minor** means either the
release: either the plugin contract changed, or a dependency moved far enough to warrant one. A plugin contract changed or a dependency moved far enough to warrant one. A `warn` therefore says
`warn` therefore says "built against an older release", not "new plugin features exist" — check the "built against an older release" rather than promising new plugin features; check that release's
Upgrading section for that release before assuming there is anything to adopt. While Plainpages is `0.x` every release shares major `0`, so a plugin 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.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. built against `0.x`, which is the point of that milestone.
+1 -1
View File
@@ -17,6 +17,6 @@ test("the real template renders clean and pins no literal image tag", () => {
const rendered = renderOverview(readFileSync("release-tooling/dockerhub-overview.md.tmpl", "utf8"), "9.9.9"); const rendered = renderOverview(readFileSync("release-tooling/dockerhub-overview.md.tmpl", "utf8"), "9.9.9");
assert.deepEqual(leftoverPlaceholders(rendered), []); assert.deepEqual(leftoverPlaceholders(rendered), []);
assert.match(rendered, /larvit\/plainpages:9\.9\.9/); // the placeholder actually reaches the examples assert.match(rendered, /larvit\/plainpages:9\.9\.9/); // the placeholder actually reaches the examples
// A hardcoded version here is what went stale on the live page; the release must own every one. // The release owns every image tag on the page, so a literal one must not survive rendering.
assert.doesNotMatch(rendered, /larvit\/plainpages:\d+\.\d+\.\d+(?<!9\.9\.9)/); assert.doesNotMatch(rendered, /larvit\/plainpages:\d+\.\d+\.\d+(?<!9\.9\.9)/);
}); });
+2 -2
View File
@@ -1,6 +1,6 @@
// Publishes the Docker Hub repository overview from dockerhub-overview.md.tmpl. The page is the // Publishes the Docker Hub repository overview from dockerhub-overview.md.tmpl. The page is the
// first thing an adopter copies, and its image tags were maintained by hand — they pointed at a // first thing an adopter copies, so `{{VERSION}}` is rendered from the release being published
// version that no longer existed. `{{VERSION}}` is rendered from the release, so they cannot. // rather than written by hand.
const HUB = "https://hub.docker.com/v2"; const HUB = "https://hub.docker.com/v2";
+1 -1
View File
@@ -6,7 +6,7 @@
"automerge": true, "automerge": true,
"packageRules": [ "packageRules": [
{ {
"description": "The host's own runtime deps. Release-Bump is opt-in per surface (README → CI/CD) because updateType rates the dependency's own jump, not its effect here — unscoped, the bot's self-update bumped the product", "description": "The host's own runtime deps. Release-Bump is opt-in per surface (README → CI/CD): updateType rates the dependency's own jump, not whether it reaches a running Plainpages",
"matchDepTypes": ["dependencies"], "matchDepTypes": ["dependencies"],
"matchFileNames": ["package.json"], "matchFileNames": ["package.json"],
"matchManagers": ["npm"], "matchManagers": ["npm"],
+3 -3
View File
@@ -8,9 +8,9 @@ import type { RequestContext } from "../http/context.ts";
import type { NavNode } from "../ui/nav.ts"; import type { NavNode } from "../ui/nav.ts";
import type { StorageCredentials } from "./storage.ts"; import type { StorageCredentials } from "./storage.ts";
// The Plainpages release this contract ships in — one version, not a second one to track. Its // The Plainpages release this contract ships in. Its major.minor must equal the release tag's, and
// major.minor must equal the release tag's; `release.yml` refuses to promote a tag that disagrees. // both release paths refuse a tag that disagrees. The patch digit may lag: checkApiVersion ignores
// The patch digit may lag, since checkApiVersion ignores patch and auto-release cuts patches itself. // patch, and auto-release cuts patch releases with no commit to bump this in.
export const HOST_API_VERSION = "0.1.0"; export const HOST_API_VERSION = "0.1.0";
export type HttpMethod = "DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT"; export type HttpMethod = "DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT";