Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/build-hosts.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"$comment": "Whether the platform's estate runs STANDING BUILD HOSTS (platform Doc/Architecture/BuildHosts; MeshWeaver.Plugins Hosting/StandingBuildHosts), and which channels are rolled onto the set the platform's CD arms (`build-host.yml` job `handover`, on its 15-minute schedule that pulls the platform's sealed sets; `build-host-stable.yml` daily; `build-host-announcement.py`). The hosts live on the operations record the platform's `.github/control-instance.json` names. While `enabled` is false the hand-over sends NOTHING; the receiver guards the same fact again (it changes a record only when that record's `buildHosts.enabled` is true). Turning it on is two acts in this order: the record's `buildHosts` block (through its approval path), then `enabled: true` here. Turned on with the platform (core #7235): the operations record declares buildHosts.enabled with the `latest` channel, so the first act is done.",
"$comment": "Whether the platform's estate runs STANDING BUILD HOSTS (platform Doc/Architecture/BuildHosts; MeshWeaver.Plugins Hosting/StandingBuildHosts), and which channels are rolled onto the set the platform's CD arms (`build-host.yml` job `handover`, on its 15-minute schedule that pulls the platform's sealed sets; `build-host-stable.yml` daily; `build-host-announcement.py`). The hosts live on the operations record the platform's `.github/control-instance.json` names. While `enabled` is false the hand-over sends NOTHING; the receiver guards the same fact again (it changes a record only when that record's `buildHosts.enabled` is true). Turning it on is two acts in this order: the record's `buildHosts` block (through its approval path), then `enabled: true` here. Turned on with the platform (core #7235): the operations record declares buildHosts.enabled with the `latest` channel, so the first act is done. The LINES (policy major-lines-run-separately, platform Doc/Architecture/MajorLinesRunSeparately): each platform major has its own channel `latest<major>`, and the hand-over moves every declared channel of the armed set's OWN major, one event each (build-host-announcement.py `channels`) — `latest3` and the compatibility `latest` (the v3 line's name, kept while a reader follows it) for a 3.x set, `latest4` alone for a 4.x one once declared. A channel listed here that the operations record does not declare is refused there by name (an event never adds a channel), so the record declares a line's channel first.",
"enabled": true,
"channels": [
"latest"
"latest",
"latest3"
]
}
132 changes: 122 additions & 10 deletions .github/scripts/build-host-announcement.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,23 @@
--core-sha <40 hex> [--plugins-sha <40 hex>] --instance <run url> \
[--supersedes "sha256:… sha256:…"]
→ the event JSON, one line (the bytes that are signed are the bytes that are sent)
build-host-announcement.py channels --declaration .github/build-hosts.json --version 3.1.13071
→ `channels=<the declared channels this set moves>` (GITHUB_OUTPUT line): its LINE's channel
`latest<major>`, and `latest` for a set of the line it names (see THE LINES)
build-host-announcement.py --self-test
🚨 THE LINES — A CHANNEL NEVER CROSSES A MAJOR (policy `major-lines-run-separately`, platform
Doc/Architecture/MajorLinesRunSeparately; the incident of core #8129). Each platform major is its own
line with its own channel, `latest<major>` (`latest3`, `latest4`) — an OPEN vocabulary: a new major
needs a declared channel here and on the record, no change to this script. The bare `latest` is the
v3 line's COMPATIBILITY name (`LATEST_ALIAS_MAJOR`), kept while any reader still follows it (the
standing `build-host-latest`, a leased host's `follow-set.py`). A set is handed to EVERY declared
channel of its own major — `latest3` and `latest` for a 3.x set — as ONE event per channel: each
event moves only its own channel's key on the record, so nothing is applied twice, and the receiver
answers a re-send of the set a channel already runs with "nothing changed". A set is never handed to
a channel of another major: `body` refuses it here, and the receiver (MeshWeaver.Plugins
`BuildHostUpdates.Decide`) refuses it again.
🚨 A NO-OP UNTIL DECLARED. `.github/build-hosts.json` says whether the platform's estate runs
standing build hosts at all (`enabled`) and which channels CD rolls (`channels`). While it says
`false` the CD job writes one summary line and sends nothing — and the RECEIVER guards the same
Expand Down Expand Up @@ -54,6 +69,11 @@
DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$")
SHA = re.compile(r"^[0-9a-f]{40}$")
CHANNELS = ("latest", "stable")
# A major line's channel: `latest<major>` (policy `major-lines-run-separately`). Open — any major.
LINE_CHANNEL = re.compile(r"^latest([1-9][0-9]{0,3})$")
# The line the compatibility name `latest` belongs to: the v3 line, which every reader followed before
# the lines split. MeshWeaver.Plugins `BuildHostUpdates.CompatibilityAliasMajor` names the same major.
LATEST_ALIAS_MAJOR = 3
# Which channel each CD trigger may roll. `latest` follows the set CD ARMS (any trigger that arms);
# `stable` — until core's stable promoter exists — is the main-passed ceiling, handed on by the
# DAILY `schedule` of build-host-stable.yml only (§2: "by the daily full run, to the main-passed
Expand All @@ -65,25 +85,62 @@
MAX_SUPERSEDED = 64


def is_channel(channel: object) -> bool:
"""A channel this lane may name: `latest`, `stable`, or a line's `latest<major>`. Pure."""
return isinstance(channel, str) and (channel in CHANNELS or LINE_CHANNEL.match(channel) is not None)


def channel_major(channel: str) -> int | None:
"""The major a channel's NAME commits it to: `latest<N>` → N, `latest` → LATEST_ALIAS_MAJOR, any other
(`stable`) → None — no line, no major guard. Pure."""
if channel == "latest":
return LATEST_ALIAS_MAJOR
m = LINE_CHANNEL.match(channel or "")
return int(m.group(1)) if m else None


def set_major(version: str) -> int | None:
"""The MAJOR of a set name (`3.1.13071` → 3, `3.0.0-ci.10052` → 3, `4.0.13200` → 4); None for no set name. Pure."""
return int(version.split(".", 1)[0]) if SET_NAME.match(version or "") else None


def load_declaration(path: Path) -> dict:
d = json.loads(path.read_text())
return load_declaration_doc(json.loads(path.read_text()), path)


def load_declaration_doc(d: object, path: object = ".github/build-hosts.json") -> dict:
if not isinstance(d, dict) or type(d.get("enabled")) is not bool:
raise ValueError(f"{path}: `enabled` must be true or false")
ch = d.get("channels")
if not isinstance(ch, list) or not all(isinstance(c, str) and c in CHANNELS for c in ch):
raise ValueError(f"{path}: `channels` must be a list drawn from {list(CHANNELS)}")
if not isinstance(ch, list) or not all(is_channel(c) for c in ch) or len(set(ch)) != len(ch):
raise ValueError(f"{path}: `channels` must be a list of distinct names drawn from {list(CHANNELS)} or latest<major>")
return d


def channels_for(declaration: dict, version: str) -> list[str]:
"""The declared channels CD's newest armed set `version` moves, in send order: its line's own
`latest<major>` first, then the compatibility `latest` when the set is of the line `latest` names.
`stable` is never one (build-host-stable.yml's daily lane rolls it). Empty when the estate does not
run build hosts or declares no channel of the set's major. Raises ValueError for no set name. Pure."""
major = set_major(version)
if major is None:
raise ValueError(f"version {version!r} is not a platform set name")
if not declaration["enabled"]:
return []
line = f"latest{major}"
ordered = [line, "latest"]
return [c for c in ordered if c in declaration["channels"] and channel_major(c) == major]


def decide(declaration: dict, control: dict, channel: str, trigger: str = "") -> tuple[bool, str, str]:
"""(send?, deployment, why). Pure."""
dep = str(control.get("deployment") or "").strip()
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._/-]*", dep):
raise ValueError(f"control-instance.json names no plain `deployment` ({dep!r}) — the operations record "
f"the build hosts live on")
if channel not in CD_CHANNELS:
raise ValueError(f"channel {channel!r} is not a channel CD rolls ({list(CD_CHANNELS)})")
only_on = CD_CHANNELS[channel]
if channel not in CD_CHANNELS and not LINE_CHANNEL.match(channel or ""):
raise ValueError(f"channel {channel!r} is not a channel CD rolls ({list(CD_CHANNELS)} or latest<major>)")
only_on = CD_CHANNELS.get(channel)
if only_on is not None and trigger != only_on:
raise ValueError(f"channel {channel!r} is rolled only by the {only_on!r} trigger (this is {trigger or 'no trigger'!r})")
if not declaration["enabled"]:
Expand All @@ -99,10 +156,13 @@ def body(*, deployment: str, channel: str, version: str, tester_image: str, test
plugins_sha: str, instance: str, detected_at: str, supersedes: list[str] | None = None) -> dict:
"""The signed event. Pure; every field validated — a fact naming something malformed is worse
than no fact."""
if channel not in CHANNELS:
raise ValueError(f"channel {channel!r} is not one of {list(CHANNELS)}")
if not is_channel(channel):
raise ValueError(f"channel {channel!r} is not one of {list(CHANNELS)} or latest<major>")
if not SET_NAME.match(version or ""):
raise ValueError(f"version {version!r} is not a platform set name")
if (line := channel_major(channel)) is not None and set_major(version) != line:
raise ValueError(f"{version} is not a {line}.x set, and build-{channel} is the {line}.x line's channel — "
"a channel never crosses a major; refusing to sign it")
for name, d in (("tester", tester_digest), ("portal", portal_digest), ("runner", runner_digest)):
if not DIGEST.match(d or ""):
raise ValueError(f"{name} digest {d!r} is not sha256:<64 hex>")
Expand Down Expand Up @@ -163,6 +223,9 @@ def main(argv: list[str]) -> int:
d.add_argument("--control-instance", type=Path, default=Path(".github/control-instance.json"))
d.add_argument("--channel", default="latest")
d.add_argument("--trigger", default="")
c = sub.add_parser("channels")
c.add_argument("--declaration", type=Path, default=Path(".github/build-hosts.json"))
c.add_argument("--version", required=True)
b = sub.add_parser("body")
for k in ("deployment", "channel", "version", "tester-image", "tester-digest", "portal-image",
"portal-digest", "runner-image", "runner-digest", "instance"):
Expand All @@ -172,6 +235,9 @@ def main(argv: list[str]) -> int:
b.add_argument("--supersedes", default="", help="space-separated digests of the set's OLDER runners")
a = ap.parse_args(argv)
try:
if a.cmd == "channels":
print(f"channels={' '.join(channels_for(load_declaration(a.declaration), a.version))}")
return 0
if a.cmd == "decide":
send, dep, why = decide(load_declaration(a.declaration),
json.loads(a.control_instance.read_text()), a.channel, a.trigger)
Expand Down Expand Up @@ -263,17 +329,63 @@ def check(name: str, cond: bool, detail: object = "") -> None:
check(f"body/refuses {k}={v}", False, "accepted")
except ValueError:
pass
# THE LINES (policy major-lines-run-separately; core #8129): a set moves every declared channel of its
# own major, one event each, and never a channel of another major.
lines = {"enabled": True, "channels": ["latest", "latest3", "latest4"]}
check("lines/a 3.x set moves its line's channel first, then the compatibility latest",
channels_for(lines, "3.1.13071") == ["latest3", "latest"], channels_for(lines, "3.1.13071"))
check("lines/a 3.0.0-ci set is of the v3 line too", channels_for(lines, "3.0.0-ci.10052") == ["latest3", "latest"])
check("lines/a 4.x set moves latest4 ALONE — never latest or latest3 (the incident)",
channels_for(lines, "4.0.13200") == ["latest4"], channels_for(lines, "4.0.13200"))
check("lines/a set of a line nobody declared moves nothing", channels_for(lines, "5.0.1") == [])
check("lines/today's declaration (latest only) moves latest for a 3.x set — as before",
channels_for({"enabled": True, "channels": ["latest"]}, "3.1.13071") == ["latest"])
check("lines/…and nothing for a 4.x set (latest is the v3 line's name)",
channels_for({"enabled": True, "channels": ["latest"]}, "4.0.13200") == [])
check("lines/stable is never moved by the CD hand-over", channels_for({"enabled": True, "channels": ["latest3", "stable"]}, "3.1.13071") == ["latest3"])
check("lines/off moves nothing", channels_for(dict(lines, enabled=False), "3.1.13071") == [])
try:
channels_for(lines, "latest")
check("lines/a moving name is no set — refused", False, "accepted")
except ValueError:
pass
check("lines/the vocabulary", channel_major("latest") == 3 and channel_major("latest3") == 3 and channel_major("latest4") == 4
and channel_major("stable") is None and channel_major("latest03") is None and channel_major("latest-x") is None)
send, _, _ = decide({"enabled": True, "channels": ["latest3"]}, control, "latest3")
check("decide/a declared line channel follows any arming trigger", send is True)
for v, ch in (("3.1.13071", "latest3"), ("3.1.13071", "latest"), ("4.0.13200", "latest4")):
check(f"body/{v} is signed for {ch}", body(**dict(ok, version=v, channel=ch))["channel"] == ch)
check("body/the record fields name the line's channel", "buildHosts.latest3.platformVersion" in body(**dict(ok, channel="latest3"))["recordFields"])
# NEGATIVE CONTROLS — each would have moved a v3 reader onto v4 (or the reverse) under a lane that compared nothing.
for v, ch in (("4.0.13200", "latest"), ("4.0.13200", "latest3"), ("3.1.13071", "latest4")):
try:
body(**dict(ok, version=v, channel=ch))
check(f"body/refuses {v} on {ch}", False, "accepted")
except ValueError as e:
check(f"body/the refusal of {v} on {ch} is named", "never crosses a major" in str(e), str(e))
check("body/stable takes any major (no line, no guard)", body(**dict(ok, version="4.0.13200", channel="stable"))["channel"] == "stable")
for bad in (["latest", "latest"], ["latest-x"], ["latest0"], "latest"):
try:
load_declaration_doc({"enabled": True, "channels": bad})
check(f"declaration/refuses channels={bad!r}", False, "accepted")
except ValueError:
pass
shipped = Path(__file__).resolve().parents[1] / "build-hosts.json"
try:
load_declaration(shipped)
shipped_decl = load_declaration(shipped)
check("the shipped declaration hands a 3.x set to latest (the compatibility name every current reader follows)",
"latest" in channels_for(shipped_decl, "3.1.13071"), channels_for(shipped_decl, "3.1.13071"))
check("the shipped declaration never hands a 4.x set to latest or latest3",
not {"latest", "latest3"} & set(channels_for(shipped_decl, "4.0.13200")), channels_for(shipped_decl, "4.0.13200"))
except (ValueError, OSError) as e:
check("the shipped .github/build-hosts.json validates", False, str(e))
if failures:
for f in failures:
print(f"SELF-TEST FAILED: {f}")
return 1
print("build-host-announcement self-test: all cases passed (no-op while off, channel declaration, "
"stable only on the daily schedule, record fields incl. the runner, the superseded runners, "
"stable only on the daily schedule, the lines (a set moves only its own major's channels, one event each), "
"record fields incl. the runner, the superseded runners, "
"digest/repository/sha/version refusals, the shipped declaration)")
return 0

Expand Down
Loading
Loading