Merge ota-cut 83b8c8bd into release-0.3.22 (the interface version pair: ui/VERSION 1.0.2, pair-check.mjs, publish-manifest.sh stamps ui_version on new entries)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
75052d711f
6 changed files with 66 additions and 9 deletions
|
|
@ -1 +1 @@
|
|||
1.0.0
|
||||
1.0.2
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ test('the embedded interface says built in; an over-the-air one says so with its
|
|||
assert.equal(w.eyebrow, 'over the air');
|
||||
assert.equal(w.help, 'Built in: 1.0.0.');
|
||||
assert.equal(V.interfaceWords({}).line, '');
|
||||
assert.equal(readFileSync(join(here, 'VERSION'), 'utf8').trim(), '1.0.0', 'the embedded interface version is three-part and in ui/VERSION');
|
||||
assert.equal(readFileSync(join(here, 'VERSION'), 'utf8').trim(), '1.0.2', 'the embedded interface version is three-part and in ui/VERSION (1.0.2: the 0.3.22 tree, 7 October 2026)');
|
||||
});
|
||||
|
||||
test('the help line says what is pending, refused, held back by the switch, or skipped', () => {
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ while [ $# -gt 0 ]; do
|
|||
--drivers) DRIVERS_FILE="$2"; shift 2 ;;
|
||||
--no-drivers) NO_DRIVERS=1; shift ;;
|
||||
--ui) UI_FILE="$2"; shift 2 ;;
|
||||
--ui-pair-override) UI_PAIR_OVERRIDE="$2"; shift 2 ;; # ship an interface from another UI tree knowingly, with the reason logged (7 Oct 2026: 1.0.2 to 0.3.21 apps)
|
||||
--no-ui) NO_UI=1; shift ;;
|
||||
--base-url) BASE="$2"; shift 2 ;;
|
||||
--dest) DEST="$2"; shift 2 ;;
|
||||
|
|
@ -180,7 +181,7 @@ if [ -f "$OLD" ]; then
|
|||
OLD_VERSION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("version",""))' "$OLD" 2>/dev/null || true)"
|
||||
if [ "$OLD_VERSION" = "$VERSION" ]; then
|
||||
for p in mac windows; do
|
||||
carried="$(python3 -c 'import json,sys; e=json.load(open(sys.argv[1])).get("platforms",{}).get(sys.argv[2]); print(" ".join([e["url"],e["sha256"],str(e["size"]),e["kind"]]) if e else "")' "$OLD" "$p" 2>/dev/null || true)"
|
||||
carried="$(python3 -c 'import json,sys; e=json.load(open(sys.argv[1])).get("platforms",{}).get(sys.argv[2]); print(" ".join([e["url"],e["sha256"],str(e["size"]),e["kind"],e.get("ui_version") or "-"]) if e else "")' "$OLD" "$p" 2>/dev/null || true)"
|
||||
if [ "$p" = mac ] && [ -z "$MAC_ENTRY" ] && [ -n "$carried" ]; then MAC_ENTRY="$carried"; echo "mac: carried over from the current manifest"; fi
|
||||
if [ "$p" = windows ] && [ -z "$WIN_ENTRY" ] && [ -n "$carried" ]; then WIN_ENTRY="$carried"; echo "windows: carried over from the current manifest"; fi
|
||||
done
|
||||
|
|
@ -255,20 +256,28 @@ if [ -n "$ACTIVATION" ]; then
|
|||
echo "note: the live DAA could not be read from /api/live; the activation height $ACTIVATION is not checked against it" >&2
|
||||
fi
|
||||
fi
|
||||
python3 - "$NEW" "$VERSION" "$CHANNEL" "$NOTES" "$MIN_SUPPORTED" "$ACTIVATION" "$DEADLINE" "$MAC_ENTRY" "$WIN_ENTRY" "${OVERRIDE:-}" "${TUNING:-}" "${UI:-}" "${DRIVERS:-}" <<'PY'
|
||||
# the version pair: the UI tree this publish builds from stamps its interface version on every new app entry
|
||||
UI_TREE_VERSION="$(tr -d '[:space:]' < "$ROOT/app/igneum-app/ui/VERSION" 2>/dev/null || true)"
|
||||
python3 - "$NEW" "$VERSION" "$CHANNEL" "$NOTES" "$MIN_SUPPORTED" "$ACTIVATION" "$DEADLINE" "$MAC_ENTRY" "$WIN_ENTRY" "${OVERRIDE:-}" "${TUNING:-}" "${UI:-}" "${DRIVERS:-}" "${UI_TREE_VERSION:-}" <<'PY'
|
||||
import json, sys, datetime
|
||||
out, version, channel, notes, min_supported, activation, deadline, mac, win, override, tuning, ui, drivers = sys.argv[1:14]
|
||||
out, version, channel, notes, min_supported, activation, deadline, mac, win, override, tuning, ui, drivers, ui_tree = sys.argv[1:15]
|
||||
override = json.loads(override) if override else None
|
||||
if override is not None and (not isinstance(override, dict) or not override): raise SystemExit("--override must be a non-empty JSON object")
|
||||
def entry(s):
|
||||
def entry(s, ui_version=None):
|
||||
if not s: return None
|
||||
url, sha, size, kind = s.split()
|
||||
return {"url": url, "sha256": sha, "size": int(size), "kind": kind}
|
||||
parts = s.split()
|
||||
url, sha, size, kind = parts[:4]
|
||||
e = {"url": url, "sha256": sha, "size": int(size), "kind": kind}
|
||||
# the version pair (tools/ui-ota/pair-check.mjs): a NEW entry is built from this tree and carries its interface version;
|
||||
# a carried entry keeps its own ("-" = it had none, and none is stamped: the tree's version is not its proof)
|
||||
uv = (None if parts[4] == "-" else parts[4]) if len(parts) > 4 else ui_version
|
||||
if uv: e["ui_version"] = uv
|
||||
return e
|
||||
m = {
|
||||
"version": version,
|
||||
"published_at": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"channel": channel,
|
||||
"platforms": {k: v for k, v in (("mac", entry(mac)), ("windows", entry(win))) if v},
|
||||
"platforms": {k: v for k, v in (("mac", entry(mac, ui_tree)), ("windows", entry(win, ui_tree))) if v},
|
||||
"min_supported_version": min_supported,
|
||||
"notes": notes,
|
||||
"consensus": {"activation_height": int(activation) if activation else None, "deadline_note": deadline, **({"override": override} if override is not None else {})},
|
||||
|
|
@ -281,6 +290,12 @@ if drivers:
|
|||
m["drivers"] = json.loads(drivers)
|
||||
open(out, "w").write(json.dumps(m, sort_keys=True, separators=(",", ":"), ensure_ascii=False))
|
||||
PY
|
||||
# the version pair (7 October 2026): the interface bundle and every app entry must come from one UI tree; a carried-over
|
||||
# ui object against a newer tree, or an entry without its interface version, refuses the publish here
|
||||
if ! node "$ROOT/tools/ui-ota/pair-check.mjs" "$NEW"; then
|
||||
if [ -n "${UI_PAIR_OVERRIDE:-}" ]; then echo "the interface is not from this manifest's UI tree; published anyway on --ui-pair-override: $UI_PAIR_OVERRIDE" >&2
|
||||
else echo "refused: the manifest's interface and its app entries are not one UI tree (tools/ui-ota/pair-check.mjs); cut the interface from this tree with tools/ui-ota/publish.mjs --version $UI_TREE_VERSION, or --no-ui, or say why with --ui-pair-override \"<reason>\"" >&2; exit 2; fi
|
||||
fi
|
||||
"$SIGNER" sign "$KEY" "$NEW" > "$NEW.sig"
|
||||
"$SIGNER" verify "$PUB" "$NEW" "$NEW.sig"
|
||||
mv "$NEW" "$DEST/igneum-app-latest.json"
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ tree_checks() {
|
|||
run "explorer, emission and public stats unit tests" node --test site/lib/explorer.test.mjs site/lib/emission.test.mjs site/lib/money.test.mjs site/api/public-stats.test.mjs
|
||||
run "ship tool self-test" node tools/ship-app.mjs --self-test
|
||||
run "interface bundle publisher self-test (pack, and sign when the signer is built)" node tools/ui-ota/publish.mjs --self-test
|
||||
run "the interface and the app entries are one UI tree (version pair; self-test, then the staged manifest when one is given)" bash -c 'node tools/ui-ota/pair-check.mjs --self-test && { [ -z "${IGNEUM_MANIFEST:-}" ] || node tools/ui-ota/pair-check.mjs "$IGNEUM_MANIFEST"; }'
|
||||
run "heat gate reader self-test (a known hold passes, a known drift fails)" node tools/heat-gate.mjs --self-test
|
||||
run "relay unit tests" node --test relay/test/parse.test.mjs relay/test/auth.test.mjs relay/test/wake.test.mjs relay/test/ember.test.mjs
|
||||
run "miner app notice strip and update card tests" node --test app/igneum-app/ui/notices.test.mjs app/igneum-app/ui/update-card.test.mjs app/igneum-app/ui/view.test.mjs app/igneum-app/ui/tune-line.test.mjs app/igneum-app/ui/ui-ota.test.mjs app/igneum-app/ui/fold.test.mjs
|
||||
|
|
|
|||
38
tools/ui-ota/pair-check.mjs
Normal file
38
tools/ui-ota/pair-check.mjs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#!/usr/bin/env node
|
||||
// The version pair (7 October 2026, the project lead's Mac on 0.3.21: the Prove switch defect came back because interface 1.0.1, cut
|
||||
// from 0.3.20, was carried into the 0.3.21 manifest and served over the packaged 0.3.21 UI). The rule, mechanical: the
|
||||
// manifest's interface bundle is cut from the same UI tree as the app entry it ships with, so `ui.version` must equal the
|
||||
// `ui_version` every platform entry carries (the tree's app/igneum-app/ui/VERSION at the app's publish, written by
|
||||
// packaging/ota/publish-manifest.sh); a manifest with a `ui` object and no `ui_version` on an entry fails, as today's does.
|
||||
// node tools/ui-ota/pair-check.mjs <manifest.json> exit 1 with one line per mismatch
|
||||
// node tools/ui-ota/pair-check.mjs --self-test known-failed first (today's shape, a mismatch), then a good pair
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
export function pairProblems(m) {
|
||||
const out = [], ui = m && m.ui && typeof m.ui === 'object' ? m.ui : null;
|
||||
const plats = (m && m.platforms && typeof m.platforms === 'object') ? m.platforms : {};
|
||||
if (!ui) return out; // no interface channel: nothing can be mismatched (the kill switch state)
|
||||
if (!/^\d+\.\d+\.\d+$/.test(String(ui.version || ''))) out.push('ui.version is not three-part: ' + ui.version);
|
||||
for (const [name, e] of Object.entries(plats)) {
|
||||
if (!e || typeof e !== 'object') continue;
|
||||
if (!e.ui_version) out.push(`platforms.${name} (app ${m.version}) carries no ui_version: the entry cannot prove the interface ${ui.version} came from its tree`);
|
||||
else if (e.ui_version !== ui.version) out.push(`platforms.${name} (app ${m.version}) was built with interface ${e.ui_version}, the manifest ships interface ${ui.version}`);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === new URL(process.argv[1], 'file:').href) {
|
||||
if (process.argv.includes('--self-test')) {
|
||||
const today = { version: '0.3.21', ui: { version: '1.0.1', min_engine: '0.3.20' }, platforms: { mac: { url: 'x' }, windows: { url: 'y' } } };
|
||||
const mismatch = { version: '0.3.22', ui: { version: '1.0.1' }, platforms: { mac: { ui_version: '1.0.2' } } };
|
||||
const good = { version: '0.3.22', ui: { version: '1.0.2' }, platforms: { mac: { ui_version: '1.0.2' }, windows: { ui_version: '1.0.2' } } };
|
||||
const none = { version: '0.3.22', platforms: { mac: {} } };
|
||||
const a = pairProblems(today), b = pairProblems(mismatch), c = pairProblems(good), d = pairProblems(none);
|
||||
if (a.length !== 2 || b.length !== 1 || c.length !== 0 || d.length !== 0) { console.error('pair-check self-test: wrong verdicts', { a, b, c, d }); process.exit(1); }
|
||||
console.log('pair-check self-test: today\'s shape fails (' + a[0] + '), a mismatch fails, a matched pair passes, no ui passes'); process.exit(0);
|
||||
}
|
||||
const file = process.argv[2]; if (!file) { console.error('usage: pair-check.mjs <manifest.json> | --self-test'); process.exit(2); }
|
||||
const problems = pairProblems(JSON.parse(readFileSync(file, 'utf8')));
|
||||
if (problems.length) { for (const p of problems) console.error('pair-check: ' + p); process.exit(1); }
|
||||
console.log('pair-check: the interface and every app entry are one UI tree');
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
// which deploys from the live folder; a cut is staged in a scratch copy (IGNEUM_DLSITE) as every other publish.
|
||||
//
|
||||
// node tools/ui-ota/publish.mjs --version 1.0.1 --min-engine 0.3.19 [--notes "one line"] [--dry-run] [--deploy] [--public]
|
||||
// [--ui-pair-override "reason"] ship an interface from another UI tree knowingly (the pair rule)
|
||||
// node tools/ui-ota/publish.mjs --verify [--url https://dl.igneum.network/dl/<token>/igneum-app-latest.json]
|
||||
// the live manifest's ui entry against the bundle in the folder (the read-back)
|
||||
// node tools/ui-ota/publish.mjs --self-test pack, hash, sign and verify with a throwaway key in a scratch folder
|
||||
|
|
@ -161,6 +162,8 @@ function main() {
|
|||
const pm = path.join(root, 'packaging/ota/publish-manifest.sh');
|
||||
const args = ['--version', arg('--app-version', readFolderVersion(dest)), '--ui', uiJson, '--notes', notes || `interface ${version} over the air`];
|
||||
if (flag('--deploy')) args.push('--deploy');
|
||||
// the version pair (packaging/ota/publish-manifest.sh): an interface from another UI tree ships only with a logged reason
|
||||
if (arg('--ui-pair-override', '')) args.push('--ui-pair-override', arg('--ui-pair-override', ''));
|
||||
if (flag('--public')) args.push('--public');
|
||||
console.log(`publish-manifest.sh ${args.join(' ').split(token).join('<token>')}`);
|
||||
const r = spawnSync('bash', [pm, ...args], { stdio: 'inherit', env: { ...process.env, IGNEUM_DLSITE: dlsite } });
|
||||
|
|
|
|||
Loading…
Reference in a new issue