Skip to content

Commit fb27284

Browse files
Merge pull request #292 from Systemorph/feat/per-major-build-channels
feat(build-host): hand a set to every channel of its own major line
2 parents 46cd1d8 + 03d87b6 commit fb27284

3 files changed

Lines changed: 169 additions & 36 deletions

File tree

‎.github/build-hosts.json‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
2-
"$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.",
2+
"$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.",
33
"enabled": true,
44
"channels": [
5-
"latest"
5+
"latest",
6+
"latest3"
67
]
78
}

‎.github/scripts/build-host-announcement.py‎

Lines changed: 122 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,23 @@
2121
--core-sha <40 hex> [--plugins-sha <40 hex>] --instance <run url> \
2222
[--supersedes "sha256:… sha256:…"]
2323
→ the event JSON, one line (the bytes that are signed are the bytes that are sent)
24+
build-host-announcement.py channels --declaration .github/build-hosts.json --version 3.1.13071
25+
→ `channels=<the declared channels this set moves>` (GITHUB_OUTPUT line): its LINE's channel
26+
`latest<major>`, and `latest` for a set of the line it names (see THE LINES)
2427
build-host-announcement.py --self-test
2528
29+
🚨 THE LINES — A CHANNEL NEVER CROSSES A MAJOR (policy `major-lines-run-separately`, platform
30+
Doc/Architecture/MajorLinesRunSeparately; the incident of core #8129). Each platform major is its own
31+
line with its own channel, `latest<major>` (`latest3`, `latest4`) — an OPEN vocabulary: a new major
32+
needs a declared channel here and on the record, no change to this script. The bare `latest` is the
33+
v3 line's COMPATIBILITY name (`LATEST_ALIAS_MAJOR`), kept while any reader still follows it (the
34+
standing `build-host-latest`, a leased host's `follow-set.py`). A set is handed to EVERY declared
35+
channel of its own major — `latest3` and `latest` for a 3.x set — as ONE event per channel: each
36+
event moves only its own channel's key on the record, so nothing is applied twice, and the receiver
37+
answers a re-send of the set a channel already runs with "nothing changed". A set is never handed to
38+
a channel of another major: `body` refuses it here, and the receiver (MeshWeaver.Plugins
39+
`BuildHostUpdates.Decide`) refuses it again.
40+
2641
🚨 A NO-OP UNTIL DECLARED. `.github/build-hosts.json` says whether the platform's estate runs
2742
standing build hosts at all (`enabled`) and which channels CD rolls (`channels`). While it says
2843
`false` the CD job writes one summary line and sends nothing — and the RECEIVER guards the same
@@ -54,6 +69,11 @@
5469
DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$")
5570
SHA = re.compile(r"^[0-9a-f]{40}$")
5671
CHANNELS = ("latest", "stable")
72+
# A major line's channel: `latest<major>` (policy `major-lines-run-separately`). Open — any major.
73+
LINE_CHANNEL = re.compile(r"^latest([1-9][0-9]{0,3})$")
74+
# The line the compatibility name `latest` belongs to: the v3 line, which every reader followed before
75+
# the lines split. MeshWeaver.Plugins `BuildHostUpdates.CompatibilityAliasMajor` names the same major.
76+
LATEST_ALIAS_MAJOR = 3
5777
# Which channel each CD trigger may roll. `latest` follows the set CD ARMS (any trigger that arms);
5878
# `stable` — until core's stable promoter exists — is the main-passed ceiling, handed on by the
5979
# DAILY `schedule` of build-host-stable.yml only (§2: "by the daily full run, to the main-passed
@@ -65,25 +85,62 @@
6585
MAX_SUPERSEDED = 64
6686

6787

88+
def is_channel(channel: object) -> bool:
89+
"""A channel this lane may name: `latest`, `stable`, or a line's `latest<major>`. Pure."""
90+
return isinstance(channel, str) and (channel in CHANNELS or LINE_CHANNEL.match(channel) is not None)
91+
92+
93+
def channel_major(channel: str) -> int | None:
94+
"""The major a channel's NAME commits it to: `latest<N>` → N, `latest` → LATEST_ALIAS_MAJOR, any other
95+
(`stable`) → None — no line, no major guard. Pure."""
96+
if channel == "latest":
97+
return LATEST_ALIAS_MAJOR
98+
m = LINE_CHANNEL.match(channel or "")
99+
return int(m.group(1)) if m else None
100+
101+
102+
def set_major(version: str) -> int | None:
103+
"""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."""
104+
return int(version.split(".", 1)[0]) if SET_NAME.match(version or "") else None
105+
106+
68107
def load_declaration(path: Path) -> dict:
69-
d = json.loads(path.read_text())
108+
return load_declaration_doc(json.loads(path.read_text()), path)
109+
110+
111+
def load_declaration_doc(d: object, path: object = ".github/build-hosts.json") -> dict:
70112
if not isinstance(d, dict) or type(d.get("enabled")) is not bool:
71113
raise ValueError(f"{path}: `enabled` must be true or false")
72114
ch = d.get("channels")
73-
if not isinstance(ch, list) or not all(isinstance(c, str) and c in CHANNELS for c in ch):
74-
raise ValueError(f"{path}: `channels` must be a list drawn from {list(CHANNELS)}")
115+
if not isinstance(ch, list) or not all(is_channel(c) for c in ch) or len(set(ch)) != len(ch):
116+
raise ValueError(f"{path}: `channels` must be a list of distinct names drawn from {list(CHANNELS)} or latest<major>")
75117
return d
76118

77119

120+
def channels_for(declaration: dict, version: str) -> list[str]:
121+
"""The declared channels CD's newest armed set `version` moves, in send order: its line's own
122+
`latest<major>` first, then the compatibility `latest` when the set is of the line `latest` names.
123+
`stable` is never one (build-host-stable.yml's daily lane rolls it). Empty when the estate does not
124+
run build hosts or declares no channel of the set's major. Raises ValueError for no set name. Pure."""
125+
major = set_major(version)
126+
if major is None:
127+
raise ValueError(f"version {version!r} is not a platform set name")
128+
if not declaration["enabled"]:
129+
return []
130+
line = f"latest{major}"
131+
ordered = [line, "latest"]
132+
return [c for c in ordered if c in declaration["channels"] and channel_major(c) == major]
133+
134+
78135
def decide(declaration: dict, control: dict, channel: str, trigger: str = "") -> tuple[bool, str, str]:
79136
"""(send?, deployment, why). Pure."""
80137
dep = str(control.get("deployment") or "").strip()
81138
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._/-]*", dep):
82139
raise ValueError(f"control-instance.json names no plain `deployment` ({dep!r}) — the operations record "
83140
f"the build hosts live on")
84-
if channel not in CD_CHANNELS:
85-
raise ValueError(f"channel {channel!r} is not a channel CD rolls ({list(CD_CHANNELS)})")
86-
only_on = CD_CHANNELS[channel]
141+
if channel not in CD_CHANNELS and not LINE_CHANNEL.match(channel or ""):
142+
raise ValueError(f"channel {channel!r} is not a channel CD rolls ({list(CD_CHANNELS)} or latest<major>)")
143+
only_on = CD_CHANNELS.get(channel)
87144
if only_on is not None and trigger != only_on:
88145
raise ValueError(f"channel {channel!r} is rolled only by the {only_on!r} trigger (this is {trigger or 'no trigger'!r})")
89146
if not declaration["enabled"]:
@@ -99,10 +156,13 @@ def body(*, deployment: str, channel: str, version: str, tester_image: str, test
99156
plugins_sha: str, instance: str, detected_at: str, supersedes: list[str] | None = None) -> dict:
100157
"""The signed event. Pure; every field validated — a fact naming something malformed is worse
101158
than no fact."""
102-
if channel not in CHANNELS:
103-
raise ValueError(f"channel {channel!r} is not one of {list(CHANNELS)}")
159+
if not is_channel(channel):
160+
raise ValueError(f"channel {channel!r} is not one of {list(CHANNELS)} or latest<major>")
104161
if not SET_NAME.match(version or ""):
105162
raise ValueError(f"version {version!r} is not a platform set name")
163+
if (line := channel_major(channel)) is not None and set_major(version) != line:
164+
raise ValueError(f"{version} is not a {line}.x set, and build-{channel} is the {line}.x line's channel — "
165+
"a channel never crosses a major; refusing to sign it")
106166
for name, d in (("tester", tester_digest), ("portal", portal_digest), ("runner", runner_digest)):
107167
if not DIGEST.match(d or ""):
108168
raise ValueError(f"{name} digest {d!r} is not sha256:<64 hex>")
@@ -163,6 +223,9 @@ def main(argv: list[str]) -> int:
163223
d.add_argument("--control-instance", type=Path, default=Path(".github/control-instance.json"))
164224
d.add_argument("--channel", default="latest")
165225
d.add_argument("--trigger", default="")
226+
c = sub.add_parser("channels")
227+
c.add_argument("--declaration", type=Path, default=Path(".github/build-hosts.json"))
228+
c.add_argument("--version", required=True)
166229
b = sub.add_parser("body")
167230
for k in ("deployment", "channel", "version", "tester-image", "tester-digest", "portal-image",
168231
"portal-digest", "runner-image", "runner-digest", "instance"):
@@ -172,6 +235,9 @@ def main(argv: list[str]) -> int:
172235
b.add_argument("--supersedes", default="", help="space-separated digests of the set's OLDER runners")
173236
a = ap.parse_args(argv)
174237
try:
238+
if a.cmd == "channels":
239+
print(f"channels={' '.join(channels_for(load_declaration(a.declaration), a.version))}")
240+
return 0
175241
if a.cmd == "decide":
176242
send, dep, why = decide(load_declaration(a.declaration),
177243
json.loads(a.control_instance.read_text()), a.channel, a.trigger)
@@ -263,17 +329,63 @@ def check(name: str, cond: bool, detail: object = "") -> None:
263329
check(f"body/refuses {k}={v}", False, "accepted")
264330
except ValueError:
265331
pass
332+
# THE LINES (policy major-lines-run-separately; core #8129): a set moves every declared channel of its
333+
# own major, one event each, and never a channel of another major.
334+
lines = {"enabled": True, "channels": ["latest", "latest3", "latest4"]}
335+
check("lines/a 3.x set moves its line's channel first, then the compatibility latest",
336+
channels_for(lines, "3.1.13071") == ["latest3", "latest"], channels_for(lines, "3.1.13071"))
337+
check("lines/a 3.0.0-ci set is of the v3 line too", channels_for(lines, "3.0.0-ci.10052") == ["latest3", "latest"])
338+
check("lines/a 4.x set moves latest4 ALONE — never latest or latest3 (the incident)",
339+
channels_for(lines, "4.0.13200") == ["latest4"], channels_for(lines, "4.0.13200"))
340+
check("lines/a set of a line nobody declared moves nothing", channels_for(lines, "5.0.1") == [])
341+
check("lines/today's declaration (latest only) moves latest for a 3.x set — as before",
342+
channels_for({"enabled": True, "channels": ["latest"]}, "3.1.13071") == ["latest"])
343+
check("lines/…and nothing for a 4.x set (latest is the v3 line's name)",
344+
channels_for({"enabled": True, "channels": ["latest"]}, "4.0.13200") == [])
345+
check("lines/stable is never moved by the CD hand-over", channels_for({"enabled": True, "channels": ["latest3", "stable"]}, "3.1.13071") == ["latest3"])
346+
check("lines/off moves nothing", channels_for(dict(lines, enabled=False), "3.1.13071") == [])
347+
try:
348+
channels_for(lines, "latest")
349+
check("lines/a moving name is no set — refused", False, "accepted")
350+
except ValueError:
351+
pass
352+
check("lines/the vocabulary", channel_major("latest") == 3 and channel_major("latest3") == 3 and channel_major("latest4") == 4
353+
and channel_major("stable") is None and channel_major("latest03") is None and channel_major("latest-x") is None)
354+
send, _, _ = decide({"enabled": True, "channels": ["latest3"]}, control, "latest3")
355+
check("decide/a declared line channel follows any arming trigger", send is True)
356+
for v, ch in (("3.1.13071", "latest3"), ("3.1.13071", "latest"), ("4.0.13200", "latest4")):
357+
check(f"body/{v} is signed for {ch}", body(**dict(ok, version=v, channel=ch))["channel"] == ch)
358+
check("body/the record fields name the line's channel", "buildHosts.latest3.platformVersion" in body(**dict(ok, channel="latest3"))["recordFields"])
359+
# NEGATIVE CONTROLS — each would have moved a v3 reader onto v4 (or the reverse) under a lane that compared nothing.
360+
for v, ch in (("4.0.13200", "latest"), ("4.0.13200", "latest3"), ("3.1.13071", "latest4")):
361+
try:
362+
body(**dict(ok, version=v, channel=ch))
363+
check(f"body/refuses {v} on {ch}", False, "accepted")
364+
except ValueError as e:
365+
check(f"body/the refusal of {v} on {ch} is named", "never crosses a major" in str(e), str(e))
366+
check("body/stable takes any major (no line, no guard)", body(**dict(ok, version="4.0.13200", channel="stable"))["channel"] == "stable")
367+
for bad in (["latest", "latest"], ["latest-x"], ["latest0"], "latest"):
368+
try:
369+
load_declaration_doc({"enabled": True, "channels": bad})
370+
check(f"declaration/refuses channels={bad!r}", False, "accepted")
371+
except ValueError:
372+
pass
266373
shipped = Path(__file__).resolve().parents[1] / "build-hosts.json"
267374
try:
268-
load_declaration(shipped)
375+
shipped_decl = load_declaration(shipped)
376+
check("the shipped declaration hands a 3.x set to latest (the compatibility name every current reader follows)",
377+
"latest" in channels_for(shipped_decl, "3.1.13071"), channels_for(shipped_decl, "3.1.13071"))
378+
check("the shipped declaration never hands a 4.x set to latest or latest3",
379+
not {"latest", "latest3"} & set(channels_for(shipped_decl, "4.0.13200")), channels_for(shipped_decl, "4.0.13200"))
269380
except (ValueError, OSError) as e:
270381
check("the shipped .github/build-hosts.json validates", False, str(e))
271382
if failures:
272383
for f in failures:
273384
print(f"SELF-TEST FAILED: {f}")
274385
return 1
275386
print("build-host-announcement self-test: all cases passed (no-op while off, channel declaration, "
276-
"stable only on the daily schedule, record fields incl. the runner, the superseded runners, "
387+
"stable only on the daily schedule, the lines (a set moves only its own major's channels, one event each), "
388+
"record fields incl. the runner, the superseded runners, "
277389
"digest/repository/sha/version refusals, the shipped declaration)")
278390
return 0
279391

0 commit comments

Comments
 (0)