Skip to content
Closed
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
106 changes: 105 additions & 1 deletion .github/scripts/arm-promoted-set.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

python3 .github/scripts/arm-promoted-set.py select --armed-max N [--override SET]
python3 .github/scripts/arm-promoted-set.py pending # MeshWeaver.Plugins' poller
python3 .github/scripts/arm-promoted-set.py armed-base --manifests portal.json # promote's record base
python3 .github/scripts/arm-promoted-set.py --self-test

Policy `one-promotion-gate` (register: Doc/Architecture/PolicyNotProse). Design of record:
Expand Down Expand Up @@ -32,6 +33,26 @@
A set whose verdict is absent is WAITING, one whose verdict is red is REFUSED: neither is armed,
and a newer set with a green verdict supersedes both. Silence is never a pass.

THE BUNDLE'S BASE — the newest ARMED set, never the first parent. A promoted set usually carries
several core merges (the batch window; `pending` supersedes older pairs instead of queueing them), and
every one of them reaches the fleet when the set is armed. So the record's `base` — the commit the
Plugins run diffs from and re-runs its control arm at, and the one the verdict is pinned to — is the
core commit of the newest ARMED set: `base..candidate` is then exactly the merges the fleet has not
seen. `armed-base` resolves it from memex-portal-ai's manifests (the armed version tag shares its
manifest with the seven-character sha tag). The record says which rule produced `base` in
`base_kind`:

* `armed` — the newest armed set's commit (the normal case);
* `first-parent` — no set has ever been armed, or the candidate IS the armed set: nothing wider
exists to measure, said out loud;
* `unresolved` — the armed set could not be read. The record keeps the first parent so CI still
promotes, but `select` REFUSES to arm it and `pending` does not measure it: a
narrower bundle than the truth would arm merges nobody measured. The next
promoted set tries again.

A record written before this rule carries no `base_kind` and is judged as before (legacy, first
parent), so a set promoted in the transition is never stranded.

`pending` is the other consumer's half: MeshWeaver.Plugins' `promotion-candidate.yml` asks it for
the newest promoted-but-unarmed pair that has NO verdict yet, and runs its suites against it. So a
burst of merges coalesces: the poller always takes the newest pair, and the older ones are simply
Expand Down Expand Up @@ -67,6 +88,8 @@
RECORD_KEYS = ("run_number", "core_sha", "base", "plugins_sha", "short", "plugins_short", "staging",
"v_portal", "v_migration", "v_plugin", "key")
SHA = re.compile(r"^[0-9a-f]{40}$")
SHORT_SHA_TAG = re.compile(r"^[0-9a-f]{7}$")
BASE_KINDS = ("armed", "first-parent", "unresolved")
SET_NAME = re.compile(r"^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?[.-]ci\.(\d+)$")

# The waiter's validation is THE definition of a valid verdict — imported, never restated.
Expand Down Expand Up @@ -106,9 +129,39 @@ def check_record(rec: object) -> str | None:
return f"the key {rec['key']!r} is not {verdict_key(rec['short'], rec['plugins_short'])!r}"
if run_number_of(str(rec["v_portal"])) != int(rec["run_number"]):
return f"v_portal {rec['v_portal']!r} does not carry run number {rec['run_number']}"
if "base_kind" in rec and rec["base_kind"] not in BASE_KINDS:
return f"base_kind {rec['base_kind']!r} is not one of {BASE_KINDS}"
return None


def armed_commit(manifests: object) -> tuple[str, str] | None:
"""(the newest ARMED version, its seven-character core sha) from memex-portal-ai's manifest
metadata (`az acr manifest list-metadata -o json`), or None when no manifest carries a version
tag at all (nothing was ever armed). Pure.

The version tag is written by `arm` onto the promoted image's manifest, which already carries the
sha tag from `promote`, so both are on ONE manifest. A newest armed manifest WITHOUT a sha tag is
a fact this cannot interpret — ValueError, never a guess (a wrong base narrows the bundle)."""
if not isinstance(manifests, list):
raise ValueError("the manifest metadata is not a JSON list")
best: tuple[int, str, list[str]] | None = None
for row in manifests:
tags = row.get("tags") if isinstance(row, dict) else None
if not isinstance(tags, list):
continue
for t in tags:
n = run_number_of(str(t))
if n is not None and (best is None or n > best[0]):
best = (n, str(t), [str(x) for x in tags])
if best is None:
return None
shas = [t for t in best[2] if SHORT_SHA_TAG.match(t)]
if len(shas) != 1:
raise ValueError(f"the newest armed manifest {best[1]} carries {len(shas)} seven-character sha tag(s) "
f"({best[2]}) — cannot name its core commit")
return best[1], shas[0]


# ───────────────────────────────── transport ──────────────────────────────────────────────

class _NoRedirect(urllib.request.HTTPRedirectHandler):
Expand Down Expand Up @@ -194,6 +247,9 @@ def read_verdict(get: Get, plugins_token: str, key: str) -> tuple[object | None,

def judge(rec: dict, verdict: object | None, why: str) -> tuple[str, str]:
"""(state, sentence): state is `green`, `waiting` or `refused`. Pure."""
if rec.get("base_kind") == "unresolved":
return "refused", ("its bundle base could not be resolved when it was promoted (the newest armed set was "
"unreadable), so its verdict would cover fewer merges than the fleet would receive")
if verdict is None:
return ("waiting" if why == "no verdict yet" else "refused"), why
ok, text = validate_verdict(verdict, rec["key"], rec["core_sha"], rec["base"])
Expand Down Expand Up @@ -236,6 +292,8 @@ def pending(records: list[dict], verdicts: dict[str, tuple[object | None, str]],
if not newer:
return None
top = newer[0]
if top.get("base_kind") == "unresolved":
return None
v, why = verdicts.get(top["key"], (None, "not read"))
return top if v is None and why == "no verdict yet" else None

Expand Down Expand Up @@ -353,6 +411,35 @@ def fake(url: str, token: str, raw: bool = False):
[r["run_number"] for r in got] == [9460], str(got))
check("read_records never reads a run at or below the armed set",
not any("runs/0/" in u for u in seen), str(seen))
# the bundle base — the newest ARMED set
live = [ # the shape measured on memex-portal-ai, 2026-09-28
{"tags": ["3-latest", "3.0-latest", "3.0.0-ci.9538", "3.0.0-latest", "b9fe5ed", "b9fe5ed-pb7390a0", "main"]},
{"tags": ["staging-9357060-36379800125"]},
{"tags": ["3.0.0-ci.9535", "9357060", "9357060-p90f70cd"]},
{"tags": None},
]
check("armed-base: the NEWEST version tag's manifest names the base (its seven-character sha tag)",
armed_commit(live) == ("3.0.0-ci.9538", "b9fe5ed"), str(armed_commit(live)))
check("armed-base: newest by RUN NUMBER, not by listing order",
armed_commit(list(reversed(live))) == ("3.0.0-ci.9538", "b9fe5ed"))
check("armed-base: no version tag anywhere means nothing was ever armed (None, not a guess)",
armed_commit([{"tags": ["main", "b9fe5ed"]}]) is None)
for bad, why in (([{"tags": ["3.0.0-ci.9538", "main"]}], "no sha tag"),
([{"tags": ["3.0.0-ci.9538", "b9fe5ed", "a1b2c3d"]}], "two sha tags"),
({"tags": []}, "not a list")):
try:
armed_commit(bad)
check(f"armed-base: an uninterpretable manifest ({why}) is RED", False)
except ValueError:
check(f"armed-base: an uninterpretable manifest ({why}) is RED, never a guess", True)
u = {**c, "base_kind": "unresolved"}
check("a record with an unknown base_kind is refused", check_record({**a, "base_kind": "nope"}) is not None)
check("an `armed` and a legacy (no base_kind) record are both accepted",
check_record({**a, "base_kind": "armed"}) is None and check_record(a) is None)
chosen, lines = select([u, b], {u["key"]: green(u), b["key"]: green(b)}, armed_max=9458)
check("an UNRESOLVED base is never armed, even green — the next older green set is taken", chosen is b, "\n".join(lines))
check("pending: an UNRESOLVED newest record is not measured (the next promotion retries)",
pending([u, b, a], {u["key"]: (None, "no verdict yet")}, armed_max=9458) is None)
print(f"arm-promoted-set self-test: {failures} failure(s)")
return 1 if failures else 0

Expand All @@ -362,7 +449,8 @@ def fake(url: str, token: str, raw: bool = False):
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
ap.add_argument("--self-test", action="store_true")
ap.add_argument("command", nargs="?", choices=("select", "pending"))
ap.add_argument("command", nargs="?", choices=("select", "pending", "armed-base"))
ap.add_argument("--manifests", type=Path, help="armed-base: memex-portal-ai manifest metadata (JSON list)")
ap.add_argument("--armed-max", type=int, default=0,
help="run number of the newest ARMED set (memex-portal-ai's newest version tag)")
ap.add_argument("--override", default="", help="arm exactly this set name, verdict or not")
Expand All @@ -371,6 +459,22 @@ def main() -> int:
return self_test()
if not a.command:
ap.error("a command is required")
if a.command == "armed-base":
# Offline and pure: the caller reads the registry; this only interprets. Exit 1 = cannot say.
try:
got = armed_commit(json.loads(a.manifests.read_text())) if a.manifests else None
except (OSError, ValueError) as e:
print(f"::error::the newest armed set cannot be named: {e}")
return 1
if a.manifests is None:
ap.error("armed-base needs --manifests")
rows = {"armed_version": got[0], "armed_short": got[1]} if got else {"armed_version": "", "armed_short": ""}
out = os.environ.get("GITHUB_OUTPUT")
if out:
with open(out, "a") as f:
f.writelines(f"{k}={v}\n" for k, v in rows.items())
print(json.dumps(rows))
return 0
core_token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN", "")
plugins_token = os.environ.get("PLUGINS_TOKEN") or core_token
if not core_token:
Expand Down
38 changes: 34 additions & 4 deletions .github/workflows/main-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2541,6 +2541,13 @@ jobs:
# moved to `arm`, which writes them only for a set whose MeshWeaver.Plugins dependent suites
# passed against exactly this PAIR (core + the Plugins commit gate built it with). This step
# hands `arm` — in this run or any later one — the facts it needs, as a record.
# 🚨 THE BUNDLE'S BASE IS THE NEWEST ARMED SET, not the first parent. A promoted set usually
# carries several core merges, and ALL of them reach the fleet when it is armed; a first-parent
# base would diff, and control-arm, only the last one. `arm-promoted-set.py armed-base` names the
# newest armed set from memex-portal-ai's manifests (its version tag shares a manifest with the
# sha tag). `base_kind` records which rule produced `base`, and an UNRESOLVED base still promotes
# for CI but is never armed and never measured (see the script's header) — CI is never held, and
# the fleet never receives merges nobody measured.
- name: "Promotion record (what `arm` needs to arm this set later)"
env:
GH_TOKEN: ${{ github.token }}
Expand All @@ -2549,13 +2556,36 @@ jobs:
RUN_NUMBER: ${{ github.run_number }}
run: |
set -euo pipefail
base=$(gh api "repos/$GITHUB_REPOSITORY/commits/$CORE_SHA" --jq '.parents[0].sha')
[[ "$base" =~ ^[0-9a-f]{40}$ ]] || { echo "::error::cannot read the first parent of $CORE_SHA ('$base')"; exit 1; }
parent=$(gh api "repos/$GITHUB_REPOSITORY/commits/$CORE_SHA" --jq '.parents[0].sha')
[[ "$parent" =~ ^[0-9a-f]{40}$ ]] || { echo "::error::cannot read the first parent of $CORE_SHA ('$parent')"; exit 1; }
base="$parent"; kind=unresolved; why=""
if az acr manifest list-metadata --registry meshweaver --name memex-portal-ai --top 300 --orderby time_desc \
-o json --only-show-errors > "$RUNNER_TEMP/portal-manifests.json" \
&& armed=$(python3 .github/scripts/arm-promoted-set.py armed-base --manifests "$RUNNER_TEMP/portal-manifests.json"); then
a_short=$(printf '%s' "$armed" | jq -r '.armed_short'); a_version=$(printf '%s' "$armed" | jq -r '.armed_version')
if [ -z "$a_short" ]; then
kind=first-parent; why="no set has ever been armed"
elif a_full=$(gh api "repos/$GITHUB_REPOSITORY/commits/$a_short" --jq .sha) && [[ "$a_full" =~ ^[0-9a-f]{40}$ ]]; then
if [ "$a_full" = "$CORE_SHA" ]; then
kind=first-parent; why="this commit IS the armed set $a_version"
elif st=$(gh api "repos/$GITHUB_REPOSITORY/compare/$a_full...$CORE_SHA" --jq .status) && [ "$st" = ahead ]; then
base="$a_full"; kind=armed; why="newest armed set $a_version"
else
why="the armed set $a_version ($a_short) is not an ancestor of $CORE_SHA (compare: ${st:-unreadable})"
fi
else
why="the armed set's commit $a_short could not be resolved"
fi
else
why="the newest armed set could not be read from memex-portal-ai"
fi
[ "$kind" != unresolved ] || echo "::warning title=Promotion base unresolved::$why — this set is promoted for CI but will NOT be armed or measured; the next promoted set retries"
echo "bundle base: $kind — ${why}; base $base"
mkdir -p "$RUNNER_TEMP/promotion-record"
jq -n --argjson n "$RUN_NUMBER" --arg c "$CORE_SHA" --arg b "$base" --arg p "$PLUGINS_SHA" \
jq -n --argjson n "$RUN_NUMBER" --arg c "$CORE_SHA" --arg b "$base" --arg bk "$kind" --arg p "$PLUGINS_SHA" \
--arg s "$SHA" --arg ps "$PLUGINS_SHORT" --arg st "$STAGING" \
--arg vp "$V_PORTAL" --arg vm "$V_MIGRATION" --arg vt "$V_PLUGIN" \
'{run_number: $n, core_sha: $c, base: $b, plugins_sha: $p, short: $s, plugins_short: $ps,
'{run_number: $n, core_sha: $c, base: $b, base_kind: $bk, plugins_sha: $p, short: $s, plugins_short: $ps,
staging: $st, v_portal: $vp, v_migration: $vm, v_plugin: $vt, key: ("pair-" + $s + "-p" + $ps)}' \
> "$RUNNER_TEMP/promotion-record/promotion-record.json"
cat "$RUNNER_TEMP/promotion-record/promotion-record.json"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ and arms the newest promoted set that is

1. newer than every set already armed (never backwards — re-checked at the pointer write), and
2. carries a `success` verdict from MeshWeaver.Plugins for exactly its pair — key, core commit, base
(the core commit's first parent) and Plugins commit all checked
(the core commit of the newest ARMED set — below) and Plugins commit all checked
(`.github/scripts/arm-promoted-set.py`, `--self-test`).

A missing verdict WAITS, a red one is REFUSED, a newer green one supersedes both. The verdict is
Expand All @@ -101,6 +101,31 @@ finishes it dispatches main-cd so the arming does not wait for the hourly reconc
nothing to Plugins for this (`CoreDispatchesToNoRepository` stands); it only READS the verdict ref,
which is ledgered in `PlatformNeverDependsOnPluginsGuard.ApiReadLedger`.

### The bundle's base is the newest ARMED set, never the first parent

A promoted set usually carries several core merges — the batch window coalesces them, and `pending`
takes only the newest unarmed pair and supersedes the older ones instead of queueing them — and ALL
of them reach the fleet the moment the set is armed. The record's `base` is what MeshWeaver.Plugins
diffs from to SELECT its suites and re-runs its control arm AT, and the verdict is pinned to it. A
first-parent base would therefore measure one merge of the bundle and arm every other merge
unmeasured — exactly how #5635/#5647/#5655 would reach the fleet if they landed in one set.

So `promote`'s record step takes `base` from the newest armed set: `arm-promoted-set.py armed-base`
reads memex-portal-ai's manifests (the armed `<version>` tag shares its manifest with the
seven-character sha tag — measured on `3.0.0-ci.9538` / `b9fe5ed`), the commit is resolved and
checked to be an ANCESTOR of the candidate, and `base..candidate` is then exactly the merges the fleet
has not seen. The record says which rule produced `base` in `base_kind`:

| `base_kind` | when | what happens |
|---|---|---|
| `armed` | the normal case | measured and armed as usual |
| `first-parent` | nothing was ever armed, or this commit IS the armed set | measured and armed; nothing wider exists |
| `unresolved` | the armed set could not be read, resolved, or is not an ancestor | promoted for CI as always, but `select` REFUSES to arm it and `pending` does not measure it (a warning names why); the next promoted set retries |

An `unresolved` base never holds CI and never arms merges nobody measured. A record written before
this rule carries no `base_kind` and is judged as before, so no set promoted during the transition is
stranded.

**What the gate does not hold:** promote, `verify-images`, the platform bake, the Plugins seal, the
satellites' compatibility legs and every CI resolver. `delivery-verdict` judges platform delivery
without `arm` or `notify-platform-update` (`PlatformDeliveryNeverWaitsOnPluginsGuard.
Expand Down
Loading