The Publication Names Its Platform

Policy publication-names-its-platform (Policy Not Prose). Refs MeshWeaver#7337.

The defect

A satellite gate (node-repo-gate.yml, called by every repository that seeds an upstream with upstream-seed) has two inputs:

  1. the platform set its host is composed on — the caller's preflight resolution, passed in as digests, or the set the lane resolves itself when nothing is pinned;
  2. the seed — the CURRENT sealed publication of each upstream for the framework identity (prebuilt-bundles/<identity>/<source>/, or the registry's /api/plugins/bundles/prebuilt/<identity>/<source>).

The identity is the compatibility key (c<major>e<epoch>, see Module Versioning), one per epoch, so the publication under it is resealed on whatever platform set the upstream last baked on, and every bundle carries that set as its platform FLOOR (producerPlatformVersion). The two inputs were resolved independently. When the upstream resealed on a newer set between the gate's preflight and the shard's seed fetch, every bundle was declined — its platform floor 3.1.11076 … is NEWER than the running platform 3.1.11069 — and the gate failed bake consumption on bytes no diff touched. Measured on MeshWeaver.Reinsurance PR #264, run 39025974143 (a "re-run failed jobs" that reused the first attempt's pins); the same window exists on a first attempt, because publish-bake runs overlap.

The rule

The publication records the set it was sealed on, and the gate takes its platform from the publication. One set identity for host and seed, so there is nothing to race.

The writer: platform-set.json

publish-bake-bundles.sh writes platform-set.json into the publication:

{"identity":"c003e001","portalDigest":"sha256:…","set":"3.1.11076","testerDigest":"sha256:…"}

node-repo-publish-bake.yml passes the three values (BAKE_PLATFORM_SET, BAKE_TESTER_REF, BAKE_PORTAL_REF). The record describes the bytes, so it is written only when bytes are: inside the publication, before the sentinel, in the manifest and verified by the postcondition like every other file, and uploaded before architecture.txt (Sealed Publication Generations). The sealed-skip leaves the incumbent's record in place, which is still the record of the incumbent's bytes. A publication that writes NO record removes any record already in its directory while that directory is unsealed, before its upload: the plan only adds and overwrites, so without the removal a flat directory republished record-less would seal the previous bake's set over bytes it does not describe, and consumers would compose on it instead of taking the no-record fallback. It is not a content marker for the convergence verdict: two bakes of one commit on two platforms are two publications.

Bundles carried forward from an earlier publication (a narrowed bake, a withheld package) were floored by the run that baked them, while the record names the set of the run that seals the whole publication. So the floor gate runs a second time over the ASSEMBLED publication, after both carries and before the publish: a bake frozen to an older set inside one compatibility epoch that would carry a bundle floored above the set it records is refused rather than sealed, because its consumers would decline that bundle. assert-bake-floor.py --self-test reads the lane and holds that order.

It is optional, and loudly so. A lane at a ref that predates it writes none and says so; a bake that followed a TAG writes none and warns (a record naming a tag could compose another build); a partial record is refused before anything is published. The OCI publication carries it as a sidecar without a change (Plugin Bundles in the Registry), and the registry's publication index serves it as platform.

The reader: the gate composes on the publication's set

Each gate shard reads every upstream's record before it composes anything (publication-platform.py choose, fetched at the lane's scripts-ref):

the caller passed the publication records the shard composes on
no digests set P P
no digests + platform-ceiling C P ≤ C P
no digests + platform-ceiling C P > C RED, naming P and C — a pull request does not run on a set main has not passed, and composing on C would decline every bundle floored above it
pinned digests = P set P P
pinned set older than P set P RED, naming both — this is the declined case, named before anything is composed
pinned set newer than P set P the pin, with a warning naming both (transitional, below)
anything no record its own platform, with a warning that the race stays open for that publication

Two upstreams sealed on different platforms are RED naming each: one host cannot be both.

The address comes before the host. The publication is found by framework identity, and the host that would report it does not exist yet. The identity is the compatibility key, so it is computed from the set name (the major) and core's declaration src/MeshWeaver.Compiler/platform-compatibility.json (the current epoch, and each declared break's previousEpochCeiling for the sets before it). The seed step then asserts that the composed host resolves the same key; a disagreement is RED by name, never a seed read for another identity.

The seed re-checks. When the seed step reads each publication (publication-platform.py verify, on every read including a re-read after a reseal mid-fetch), the record must still name the host the shard composed. A reseal on another set in the minutes between is RED naming both — a re-run reads the publication that then applies — instead of a wall of declined bundles.

What a caller passes

Transitional: a pin newer than the publication

Every satellite that seeds an upstream pins its preflight's resolution today, and that set is almost always newer than the set the upstream last baked on. Such a pin adopts every bundle (each floor is at or below it), so it composes as pinned, with a warning naming both sets. It turns RED once those callers pass no digests (and platform-ceiling on a pull request). A pin OLDER than the publication's set is RED from the start: that is exactly the declined case.

What this does not cover

Where it lives

piece file
the record's writer .github/scripts/publish-bake-bundles.sh (THE PLATFORM RECORD)
the values the lane passes .github/workflows/node-repo-publish-bake.yml, step Publish bundles to every portal target
the writer's cases .github/scripts/test-publish-bake-overlap.py ("platform record")
the registry index field MeshWeaver.Plugins PluginBundleEndpoints.PrebuiltIndex
the gate's decision and re-check .github/scripts/publication-platform.py, node-repo-gate.yml step publication-platform and the seed step