app
Altium - the Altium Bridge
Public Made by Adomby adom
Adom Bridge for Altium Designer: a resident DelphiScript agent gives the AI eval, the 1,100-command process launcher, all 8 EDA surfaces with screenshots, compile/ERC, DRC, OutJob outputs, native SchLib/PcbLib authoring with STEP 3D bodies, and dialog handling by PID.
main
Colby Knox
Restore tools/symbol_modes.py from 4d4e030 (pin electrical types): the 0.2.52 release's pkg publish (cc812e5) wrote bac…
9c53d98
1d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
#!/usr/bin/env python3
"""Build + publish altium-bridge on the TWO-TIER release model (adom/altium-bridge#9;
pattern: adom/insiders skills + adom/pup-bridge tools/ship.sh, which did it first).
python3 scripts/release.py # build + lint only (dist/ artifact)
python3 scripts/release.py --publish "<log>" # -> INSIDERS tier (the default tier):
# release zip on THIS page, manifest to
# adom/insiders altium-bridge/version.json,
# release-log entry, source stamps pushed,
# skills pkg published in lockstep.
# The PUBLIC manifest is NEVER touched here.
python3 scripts/release.py --promote <ver> --attest "<human's exact words>" --by "<name>"
# -> PUBLIC tier. REFUSES without the words;
# they are recorded in the manifest
# (promotion_attestation/promoted_by/_at).
python3 scripts/release.py --rollback <ver> --attest "<words>" --by "<name>"
# re-point PUBLIC at an older archived build.
python3 scripts/release.py --stage "<log>" # build, sign, upload the release asset ONLY: no tier,
# no skills pkg (a build waiting for a live battery).
SIGNING: every PE file the zip ships (.exe AND .dll) is Authenticode-signed as "Adom Industries, Inc." before
packaging (Windows Smart App Control blocks unsigned exes, and unsigned DLLs a program loads). The signing identifiers live in a
PRIVATE config outside this public repo ($ADOM_SIGNING_DIR, default ~/.adom/signing/altium-bridge:
config.json + sign-verify.ps1); the exe is signed on the Windows host that config names, through
that host's signer login, verified there (Get-AuthenticodeSignature + signtool verify /pa), pulled
back with a hash check, and verified AGAIN here. build_zip refuses any unsigned .exe or .dll, so no
path through this script can package one. Signing is the last change to the file before packaging.
Rules (insiders-tier-publish, verbatim where it matters): every publish is insiders; public only
on a human's words; an AI never promotes on its own judgment; one plain semver per build in both
tiers; every insiders publish appends its release-log entry before it is called done.
Versions live in ONE place: src/bridge.json (BRIDGE_VERSION, main.rs, package.json, page.json are checked).
"""
import datetime
import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SRC = ROOT / "src"
RS = ROOT / "bridge-rs"
TARGET = "x86_64-pc-windows-gnu"
EXE = RS / "target" / TARGET / "release" / "altium-bridge.exe"
DIST = ROOT / "dist"
ARCHIVE = ROOT / "scripts" / "releases"
PAGE = "adom/altium-bridge"
PAGE_SLUG = "altium-bridge"
INSIDERS_PAGE = "adom/insiders"
INSIDERS_REL = "altium-bridge/version.json"
INSIDERS_LOG = "altium-bridge/README.md"
INSIDERS_URL = f"https://wiki.adom.inc/api/v1/pages/insiders/files/{INSIDERS_REL}"
PUBLIC_MANIFEST = ROOT / "adom-bridge-altium-manifest.json"
# (zip path, source path). bridge.json at the ROOT; everything else beside the exe exactly where
# the exe looks for it (agent/, resources/, extension/ relative to current_exe).
INCLUDE = [
("bridge.json", SRC / "bridge.json"),
("BRIDGE_VERSION", SRC / "BRIDGE_VERSION"),
("altium-bridge.exe", DIST / "signed" / "altium-bridge.exe"),
("agent/AdomAgent.pas.tmpl", SRC / "agent" / "AdomAgent.pas.tmpl"),
("agent/AdomAgent.PrjScr", SRC / "agent" / "AdomAgent.PrjScr"),
("resources/adom-overlay-32.png", SRC / "resources" / "adom-overlay-32.png"),
("extension/Install-AdomBridge.ps1", SRC / "extension" / "Install-AdomBridge.ps1"),
("extension/REMOVE-AdomBridge.cmd", SRC / "extension" / "REMOVE-AdomBridge.cmd"),
("extension/AdomBridge/AdomBridge.dll", DIST / "signed" / "AdomBridge.dll"),
("extension/AdomBridge/AdomBridge.ins", SRC / "extension" / "AdomBridge" / "AdomBridge.ins"),
("extension/AdomBridge/AdomBridge.rcs", SRC / "extension" / "AdomBridge" / "AdomBridge.rcs"),
("extension/AdomBridge/Help/ServerDlls.txt", SRC / "extension" / "AdomBridge" / "Help" / "ServerDlls.txt"),
]
def run(*a, **kw):
return subprocess.run(a, check=True, **kw)
def wiki_head():
out = subprocess.run(["adom-wiki", "repo", "log", PAGE, "--limit", "1", "--json"],
check=True, capture_output=True, text=True).stdout
d = json.loads(out)
return ((d.get("data") or d).get("log") or [{}])[0].get("hash", "")
def assert_source_is_head(source, when):
"""The working copy is a pull, and a pull records no commit, so the operator names the commit
they pulled. Another session can push a new source between that pull and this publish; building
then ships stale code, and the stamp push overwrites their CHANGELOG with ours."""
head = wiki_head()
if not source or not head.startswith(source):
raise SystemExit(f"REFUSED ({when}): the wiki head of {PAGE} is {head[:7]}, but --source says {source or '(none)'}. "
f"Pull again (adom-wiki repo pull {PAGE}), read what changed, and rerun with --source {head[:7]}.")
return head
def now_iso():
return datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
def lint_verbs():
"""bridge.json's verbs[] is a PROMISE ab routes on; bridge-rs/src/verbs.rs KNOWN is the
dispatch of record. Drift shipped live bugs on 2026-09-01, so the release refuses instead."""
rs = (RS / "src" / "verbs.rs").read_text(encoding="utf-8", errors="replace")
m = re.search(r"pub const KNOWN: &\[&str\] = &\[(.*?)\];", rs, re.S)
if not m:
raise SystemExit("cannot find KNOWN in bridge-rs/src/verbs.rs")
known = set(re.findall(r'"([a-z0-9_]+)"', m.group(1)))
declared = {v[len("altium_"):] for v in json.loads((SRC / "bridge.json").read_text())["verbs"]}
phantom, unreachable = sorted(declared - known), sorted(known - declared)
if phantom or unreachable:
raise SystemExit(
"bridge.json does not match bridge-rs/src/verbs.rs\n"
f" declared but NOT dispatchable: {phantom}\n"
f" dispatchable but NOT declared: {unreachable}")
# ab cuts a verb at bridge.json's timeouts (60 s unless listed), whatever its describe says: on
# 2026-10-15, 73 verbs documented 90-900 s were cut at 60 (a schematic build answered timed_out
# while it was still drawing). Every documented timeout over the default must be listed.
t = json.loads((SRC / "bridge.json").read_text())["timeouts"]
short = []
for m in re.finditer(r'verb_doc(?:_long)?\("([a-z_0-9]+)",', rs):
depth, cur, args, j = 1, "", [], m.end()
while j < len(rs) and depth > 0:
ch = rs[j]
depth += ch in "([{"
depth -= ch in ")]}"
if depth == 1 and ch == ",":
args.append(cur.strip()); cur = ""
elif depth >= 1:
cur += ch
j += 1
nums = [a for a in args if re.fullmatch(r"\d+", a)]
if nums and int(nums[0]) > t["verbs"].get(m.group(1), t["default"]):
short.append(f"{m.group(1)} documents {nums[0]} s, ab allows {t['verbs'].get(m.group(1), t['default'])}")
if short:
raise SystemExit("bridge.json timeouts are shorter than the verbs' documented timeouts:\n " + "\n ".join(short))
return len(declared)
def lint_versions(ver):
"""One version, four stamps beside bridge.json — refuse a half-bumped release. page.json is the
wiki page's own version (0.2.47 shipped with it at 0.2.46); main.rs is what the exe reports
(0.2.46 shipped reporting 0.2.45, adom/altium-bridge#22)."""
stamps = {
"src/BRIDGE_VERSION": (SRC / "BRIDGE_VERSION").read_text().strip(),
"package.json": json.loads((ROOT / "package.json").read_text())["version"],
"page.json": json.loads((ROOT / "page.json").read_text())["version"],
}
m = re.search(r'BRIDGE_VERSION: &str = "([^"]+)"', (RS / "src" / "main.rs").read_text())
stamps["bridge-rs/src/main.rs"] = m.group(1) if m else "?"
bad = {k: v for k, v in stamps.items() if v != ver}
if bad:
raise SystemExit(f"version drift against bridge.json {ver}: {bad}")
bj = json.loads((SRC / "bridge.json").read_text())
if bj.get("insidersManifestUrl") != INSIDERS_URL:
raise SystemExit(f"bridge.json insidersManifestUrl must be {INSIDERS_URL} (#9)")
# Identifiers MEASURED to halt Altium's DelphiScript engine (an unknown identifier is a RUNTIME
# error: an 'Error' box, then the script parks on a breakpoint and Altium will not even close).
# dev-skills/altium-bridge-dev lists how each was found. Add to this list whenever one is found;
# a build that contains one is refused, because the only other way to find it is a user's Altium.
ENGINE_HALTING = [
r"\.ImplementationCount\b", r"\.Implementation\[", r"\bBottomXSize\b", r"\bMidXSize\b",
r"\beProcessFreePrimitives\b", # (IPCB_Board.GroupIterator_Create also halts, but the same call is
# correct on a component/footprint and a regex cannot see the receiver: review those by hand)
r"\bRunProcessWithParameters\b", r"\.SuperClass\s*:=", r"\bCreateOleObject\b",
r"\.MinimumRing\b", # IPCB_MinimumAnnularRing has no MinimumRing on AD 26.11.1 (measured 2026-10-15)
r"\.Priority\s*:=", # writing a rule's Priority halts with an empty Error box (AD 26.11.1, 2026-10-15);
# a newly added rule already lands at priority 1
]
def lint_agent_script():
"""Refuse an agent template that uses an identifier known to halt the engine."""
src = (SRC / "agent" / "AdomAgent.pas.tmpl").read_text(encoding="utf-8", errors="replace")
code = re.sub(r"\{[^}]*\}", "", src) # brace comments may NAME the identifier
code = re.sub(r"//[^\n]*", "", code)
code = re.sub(r"'[^'\n]*'", "''", code) # and so may string literals
hits = []
for pat in ENGINE_HALTING:
for m in re.finditer(pat, code):
hits.append(f"{pat} -> ...{code[max(0, m.start() - 40):m.end() + 20].strip()}...")
# A raw StrToFloat/StrToInt on a caller value RAISES on malformed input, and a raise is an Error
# box on the user's screen. Only the AdomFloat/AdomInt helpers may call them.
body = re.sub(r"Function AdomFloat\(.*?^End;|Function AdomInt\(.*?^End;", "", code, flags=re.S | re.M)
for m in re.finditer(r"(?<![A-Za-z_])(StrToFloat|StrToInt)\(", body):
hits.append(f"raw {m.group(1)}( outside AdomFloat/AdomInt -> ...{body[max(0, m.start() - 40):m.end() + 25].strip()}...")
if hits:
raise SystemExit("agent template uses engine-halting identifiers (see ENGINE_HALTING):\n " + "\n ".join(hits))
def build_exe():
subprocess.run(["cargo", "test", "--quiet"], cwd=RS, check=True)
subprocess.run(["cargo", "build", "--release", "--target", TARGET], cwd=RS, check=True)
if not EXE.exists():
raise SystemExit(f"exe missing after build: {EXE}")
SIGNER_ORG = "Adom Industries, Inc."
def signing_dir():
return Path(os.environ.get("ADOM_SIGNING_DIR", Path.home() / ".adom" / "signing" / "altium-bridge"))
def authenticode_subjects(path):
"""Certificate subjects in a PE's Authenticode signature, read here without Windows: the
optional header's security data directory points at a WIN_CERTIFICATE holding PKCS#7 DER.
[] means unsigned."""
import struct
import tempfile
b = Path(path).read_bytes()
pe = struct.unpack_from("<I", b, 0x3C)[0]
if b[pe:pe + 4] != b"PE\0\0":
raise SystemExit(f"{path}: not a PE file")
opt = pe + 24
magic = struct.unpack_from("<H", b, opt)[0]
dirs = opt + (112 if magic == 0x20B else 96)
off, size = struct.unpack_from("<II", b, dirs + 4 * 8)
if not off or not size:
return []
length, _rev, ctype = struct.unpack_from("<IHH", b, off)
if ctype != 2:
return []
with tempfile.NamedTemporaryFile(suffix=".p7b", delete=False) as f:
f.write(b[off + 8:off + length])
r = subprocess.run(["openssl", "pkcs7", "-inform", "DER", "-in", f.name, "-print_certs", "-noout"],
capture_output=True, text=True)
os.unlink(f.name)
return [l[len("subject="):].strip() for l in r.stdout.splitlines() if l.startswith("subject=")]
def assert_signed(path):
subs = authenticode_subjects(path)
if not any(f'CN = "{SIGNER_ORG}"' in s for s in subs):
raise SystemExit(f"REFUSED: {path} is not Authenticode-signed by {SIGNER_ORG} "
f"(found {subs or 'no signature'}). Windows Smart App Control blocks unsigned exes and the "
f"unsigned DLLs a program loads; this script never packages one.")
return subs
def ab_json(*args, timeout=600):
out = subprocess.run(["adom-bridge", *args], capture_output=True, text=True, timeout=timeout).stdout
d = json.loads(out[out.find("{"):]) if "{" in out else {}
o = d.get("output")
if isinstance(o, str) and o.strip().startswith("{"):
try:
d = json.loads(o)
except ValueError:
pass
return d
# (published name, unsigned source). Each is signed on the host, then shipped from dist/signed/.
PE_SOURCES = [
("altium-bridge.exe", EXE),
("AdomBridge.dll", SRC / "extension" / "AdomBridge" / "AdomBridge.dll"),
]
def sign_pe_files(ver):
"""Sign every PE file the zip ships on the private config's Windows host, verify each there,
pull them back, verify each here."""
import base64
sd = signing_dir()
cfg_p, ps1 = sd / "config.json", sd / "sign-verify.ps1"
if not cfg_p.exists() or not ps1.exists():
raise SystemExit(f"REFUSED: private signing config missing at {sd} (config.json + sign-verify.ps1). "
f"It is deliberately not in this public repo; ask the release owner for it.")
cfg = json.loads(cfg_p.read_text())
host, thread = cfg["host"], "altium-bridge release signing"
stage = cfg["stageDir"].replace("%LOCALAPPDATA%", cfg.get("localAppData", r"C:\Users\cknox\AppData\Local")) \
+ "\\build-" + ver.replace(".", "_") # a dotted name reads as a file path to send_files
work = DIST / "signing"
shutil.rmtree(work, ignore_errors=True)
work.mkdir(parents=True)
for name, src in PE_SOURCES:
shutil.copy2(src, work / name)
shutil.copy2(ps1, work / "sign-verify.ps1")
names = [n for n, _ in PE_SOURCES]
print(f"== sign {', '.join(names)} on the signing host ==")
sent = ab_json("--target", host, "--ai-thread", thread, "send_files", json.dumps({
"filePaths": [str(work / n) for n in names] + [str(work / "sign-verify.ps1")], "dest": stage,
"reason": f"stage altium-bridge {ver} for Authenticode signing"}))
if int(sent.get("filesReceived") or 0) != len(names) + 1:
raise SystemExit(f"signing host did not receive the files: {sent.get('error') or sent}")
calls = "; ".join(
f"(& '{stage}\\sign-verify.ps1' -Exe '{stage}\\{n}' -Endpoint '{cfg['endpoint']}' -Account '{cfg['account']}' "
f"-Profile '{cfg['profile']}' -Tenant '{cfg['tenant']}' -Timestamp '{cfg['timestampUrl']}' | Out-String)"
for n in names)
cmd = f"$r = @({calls}); '[' + ($r -join ',') + ']'"
r = ab_json("--target", host, "--ai-thread", thread, "run_script", json.dumps({
"interpreter": "powershell", "scriptB64": base64.b64encode(cmd.encode()).decode(), "timeout": 540,
"reason": f"Authenticode-sign altium-bridge {ver} ({', '.join(names)}) as {SIGNER_ORG}, then Get-AuthenticodeSignature + signtool verify"}), timeout=900)
so = r.get("stdout") or ""
try:
verdicts = json.loads(so[so.find("["):])
except ValueError:
raise SystemExit(f"signing script gave no verdict: {so[:400]} {(r.get('stderr') or '')[:400]}")
reports = []
for name, v in zip(names, verdicts):
if v.get("error"):
raise SystemExit(f"REFUSED: signing host cannot sign ({v['error']}): {v.get('detail', '')}\n fix: {v.get('fix', '')}")
checks = {
"Status Valid": v.get("status") == "Valid",
"signer subject": cfg["expectSubject"] in (v.get("subject") or ""),
"timestamped": "Microsoft" in (v.get("timestamper") or ""),
"signtool verify /pa": "Successfully verified" in (v.get("signtoolVerify") or ""),
}
if not all(checks.values()):
raise SystemExit(f"REFUSED: signature checks failed for {name}: {checks}\n{json.dumps(v, indent=1)}")
pulled = ab_json("--target", host, "--ai-thread", thread, "pull_file", json.dumps({
"filePaths": [f"{stage}\\{name}"], "reason": f"pull the signed {name} ({ver}) back for packaging"}))
files = pulled.get("files") or []
if not files:
raise SystemExit(f"could not pull the signed {name} back: {pulled.get('error') or pulled}")
signed = DIST / "signed" / name
signed.parent.mkdir(parents=True, exist_ok=True)
shutil.move(files[0]["path"], signed)
local_sha = hashlib.sha256(signed.read_bytes()).hexdigest()
if local_sha != v.get("sha256"):
raise SystemExit(f"REFUSED: {name} changed in transit (host {v.get('sha256')}, here {local_sha})")
subs = assert_signed(signed)
reports.append({"file": name, "status": v["status"], "subject": v["subject"],
"timestamper": v["timestamper"], "signtoolVerify": v["signtoolVerify"].split(":")[0],
"signer": v.get("signer"), "sha256": local_sha, "localSubjects": subs})
print(f" {name}: " + ", ".join(f"{k} {'ok' if ok else 'FAIL'}" for k, ok in checks.items())
+ f"; verified again here, sha256 {local_sha[:12]}")
if len(reports) != len(names):
raise SystemExit(f"REFUSED: expected {len(names)} verdicts, got {len(reports)}")
(DIST / "signed" / "signing-report.json").write_text(json.dumps(reports, indent=2) + "\n")
return reports
def build_zip(ver):
DIST.mkdir(exist_ok=True)
zname = f"adom-bridge-altium-v{ver}.zip"
zpath = DIST / zname
for rel, p in INCLUDE:
if rel.lower().endswith((".exe", ".dll")):
assert_signed(p)
with zipfile.ZipFile(zpath, "w", zipfile.ZIP_DEFLATED) as z:
for rel, p in INCLUDE:
if not p.exists():
sys.exit(f"missing {p}")
z.write(p, rel)
sha = hashlib.sha256(zpath.read_bytes()).hexdigest()
print(f"built {zpath} ({zpath.stat().st_size} bytes) sha256={sha[:12]}")
return zname, zpath, sha
def manifest_for(ver, zname, zpath, sha, tier):
m = {
"name": "altium", "version": ver,
"url": f"https://wiki.adom.inc/download/{PAGE}/{ver}/{zname}",
"sha256": sha, "size": zpath.stat().st_size,
"verbPrefixes": ["altium_"], "healthEndpoint": "/health",
"updateManifestUrl": f"https://wiki.adom.inc/api/v1/pages/{PAGE_SLUG}/files/adom-bridge-altium-manifest.json",
"insidersManifestUrl": INSIDERS_URL,
"released_at": now_iso(),
}
if tier == "insiders":
m["tier"] = "insiders"
return m
def two_curl_split_check():
"""The tier's whole security story: the insiders manifest must be bearer-readable and
anonymously invisible. Refuse to call the publish done otherwise."""
import urllib.request
anon_ok = False
try:
urllib.request.urlopen(INSIDERS_URL, timeout=15)
except urllib.error.HTTPError as e:
anon_ok = e.code in (401, 403, 404)
if not anon_ok:
raise SystemExit(f"TIER LEAK: {INSIDERS_URL} is anonymously readable (or unreachable in an unexpected way)")
# the authenticated read goes through the CLI (its session auth is what insider desktops
# use); a raw container bearer gets 403 on private-page file endpoints (measured 2026-09-02)
out = subprocess.run(["adom-wiki", "api", "/api/v1/pages/insiders/files/altium-bridge/version.json"],
capture_output=True, text=True, check=True).stdout
served = json.loads(out[out.find("{"):])
print(f" two-curl split ok: anonymous blocked, bearer serves {served.get('version')}")
return served
def stage_only(log_line, source):
"""A build waiting for a live battery: signed and uploaded as a release asset, but NO tier moves
and no skills pkg row. Its manifest is archived so a later publish or pin can point at it."""
assert_source_is_head(source, "before build")
ver = json.loads((SRC / "bridge.json").read_text())["version"]
lint_versions(ver)
lint_agent_script()
n = lint_verbs()
print(f"verb lint ok: {n} verbs")
build_exe()
assert_source_is_head(source, "before signing")
sign_pe_files(ver)
zname, zpath, sha = build_zip(ver)
subprocess.run(["adom-wiki", "release", "create", PAGE, ver, "--title", f"altium-bridge {ver}",
"--changelog", f"STAGED, not on any tier: {log_line}"], check=False, capture_output=True)
run("adom-wiki", "release", "upload", PAGE, ver, str(zpath), "--platform", "windows", "--arch", "x64")
ARCHIVE.mkdir(exist_ok=True)
(ARCHIVE / f"{ver}.json").write_text(json.dumps(manifest_for(ver, zname, zpath, sha, "insiders"), indent=2) + "\n")
print(f"STAGED {ver}: signed zip sha256 {sha[:12]} uploaded; no tier moved. Archived scripts/releases/{ver}.json.")
def ship_insiders(log_line, source):
assert_source_is_head(source, "before build")
ver = json.loads((SRC / "bridge.json").read_text())["version"]
lint_versions(ver)
lint_agent_script()
n = lint_verbs()
print(f"verb lint ok: {n} verbs declared and dispatchable")
build_exe()
assert_source_is_head(source, "before signing")
sign_pe_files(ver)
zname, zpath, sha = build_zip(ver)
assert_source_is_head(source, "before upload")
print(f"== release {ver} on {PAGE} (bytes live on the product page, both tiers) ==")
subprocess.run(["adom-wiki", "release", "create", PAGE, ver, "--title", f"altium-bridge {ver}",
"--changelog", "see CHANGELOG.md"], check=False, capture_output=True)
run("adom-wiki", "release", "upload", PAGE, ver, str(zpath), "--platform", "windows", "--arch", "x64")
print(f"== insiders manifest + release log -> {INSIDERS_PAGE} ==")
ins = manifest_for(ver, zname, zpath, sha, "insiders")
work = DIST / "insiders-repo"
(work / "altium-bridge").mkdir(parents=True, exist_ok=True)
# NEVER edit a stale working copy: adom/insiders is a SHARED repo that parallel sessions
# (John's threads) write between our publishes. A Sept-8 working copy pushed on 2026-09-21
# clobbered 26 release-log entries for 12 minutes. Fetch the LIVE log first, always.
live = subprocess.run(["adom-wiki", "api", f"/api/v1/pages/insiders/files/{INSIDERS_LOG}"],
capture_output=True, text=True)
if live.returncode == 0 and '"raw"' in live.stdout:
raw = json.loads(live.stdout[live.stdout.find("{"):])["raw"]
(work / INSIDERS_LOG).write_text(raw)
(work / INSIDERS_REL).write_text(json.dumps(ins, indent=2) + "\n")
ARCHIVE.mkdir(exist_ok=True)
(ARCHIVE / f"{ver}.json").write_text(json.dumps(ins, indent=2) + "\n")
log_path = work / INSIDERS_LOG
today = datetime.date.today().isoformat()
entry = f"- **{ver}** insiders, {today}. {log_line} Not promoted.\n"
if log_path.exists():
s = log_path.read_text()
else:
s = (
"# altium-bridge on the insiders tier\n\n"
"**Publishing thread:** the altium-bridge thread owns this file. Every insiders publish "
"(`scripts/release.py --publish` in the altium-bridge repo) appends an entry to the release log "
"below before the publish is called done; a promote (`--promote`, which refuses without a human's "
"exact words) edits that version's entry with the date and the words. Newest first.\n\n"
"| | Insiders | Public |\n|---|---|---|\n"
f"| Manifest | [altium-bridge/version.json](https://wiki.adom.inc/adom/insiders/files/altium-bridge/version.json) | [adom-bridge-altium-manifest.json](https://wiki.adom.inc/adom/altium-bridge/files/adom-bridge-altium-manifest.json) |\n"
"| Bridge zip | [adom/altium-bridge releases](https://wiki.adom.inc/adom/altium-bridge/releases) (unlisted until promoted) | same files, same URLs |\n"
"| Skills pkg | `pkg install adom/altium-bridge` follows the newest row until the registry insiders dist-tag lands (adom/wiki#168) | same |\n\n"
"The bridge declares `insidersManifestUrl` in `src/bridge.json`; a desktop on ab 2.0.99+ whose "
"login can read this page follows the insiders manifest, everyone else follows the public one "
"and never sees this file.\n\n## Release log\n\n"
)
if f"- **{ver}**" not in s:
s = re.sub(r"(## Release log\n\n)", r"\1" + entry.replace("\\", "\\\\"), s, count=1)
log_path.write_text(s)
run("adom-wiki", "repo", "push", INSIDERS_PAGE, "--files", INSIDERS_REL, INSIDERS_LOG,
"-m", f"altium-bridge {ver} (insiders): {log_line}", cwd=work)
print("== source stamps to the product page (public manifest NOT touched) ==")
assert_source_is_head(source, "before the stamp push")
run("adom-wiki", "repo", "push", PAGE, "-m", f"v{ver} (insiders): {log_line}",
"--files", "CHANGELOG.md", "src/bridge.json", "src/BRIDGE_VERSION", "package.json", "page.json",
"bridge-rs/src/main.rs", cwd=ROOT)
stamped = wiki_head()
print("== skills pkg in lockstep (release rows share the version namespace, adom/wiki#170) ==")
# pkg publish writes the package payload (tools/, skills) back into the page repo from this
# working copy, so a source pushed since the stamp commit would be reverted file by file.
assert_source_is_head(stamped[:7], "before pkg publish")
run("adom-wiki", "pkg", "publish", "--org", "adom", cwd=ROOT)
print("== verify ==")
served = two_curl_split_check()
if served.get("version") != ver:
raise SystemExit(f"insiders manifest serves {served.get('version')}, expected {ver}")
import urllib.request
try:
with urllib.request.urlopen(
f"https://wiki.adom.inc/api/v1/pages/{PAGE_SLUG}/files/adom-bridge-altium-manifest.json",
timeout=15) as r:
pub = json.loads(r.read())
except Exception:
pub = json.loads(PUBLIC_MANIFEST.read_text())
print(f"SHIPPED {ver} to INSIDERS; public tier serves {pub['version']} (read LIVE - the local manifest can be stale when another session promoted).")
print(f'To promote: python3 scripts/release.py --promote {ver} --attest "<the human\'s exact words>" --by "<name>"')
def attestation_gate(words, by):
a = re.sub(r"\s+", " ", words or "").strip()
if len(a) < 8 or re.fullmatch(r"(?i)(ok|yes|promote|ship it|go|approved|test|placeholder|x+)", a):
raise SystemExit('REFUSED: the PUBLIC tier moves only on the human\'s exact words (>= 8 chars, not a placeholder). An AI never promotes on its own judgment.')
if not by:
raise SystemExit('REFUSED: say who promoted (--by "<name>").')
return a
def promote(ver, words, by, rollback=False):
a = attestation_gate(words, by)
arch = ARCHIVE / f"{ver}.json"
if not arch.exists():
raise SystemExit(f"no archived insiders manifest for {ver} ({arch}) - ship it first")
ins = json.loads(arch.read_text())
import urllib.request
with urllib.request.urlopen(ins["url"], timeout=120) as r:
remote_sha = hashlib.sha256(r.read()).hexdigest()
if remote_sha != ins["sha256"]:
raise SystemExit(f"REMOTE ASSET MISMATCH: {ins['url']} serves {remote_sha}, record says {ins['sha256']}")
print(f" asset verified: {ins['url']} sha={remote_sha[:12]}")
m = dict(ins)
m.pop("tier", None)
now = now_iso()
if rollback:
m.update({"rollback_attestation": a, "rolled_back_by": by, "rolled_back_at": now})
else:
m.update({"promotion_attestation": a, "promoted_by": by, "promoted_at": now,
"insiders_released_at": ins.get("released_at")})
PUBLIC_MANIFEST.write_text(json.dumps(m, indent=2) + "\n")
verb = "rolled back to" if rollback else "promoted to PUBLIC"
run("adom-wiki", "repo", "push", PAGE, "--files", "adom-bridge-altium-manifest.json",
"-m", f"v{ver}: {verb} - {by}: {a}", cwd=ROOT)
# mark the release-log entry
work = DIST / "insiders-repo"
log_path = work / INSIDERS_LOG
if log_path.exists() and not rollback:
s = log_path.read_text()
line = f"- **{ver}** insiders,"
if line in s and "Promoted to public" not in s.split(line)[1].split("\n")[0]:
s = s.replace(" Not promoted.\n", f' Promoted to public on {datetime.date.today().isoformat()} by {by}: "{a}".\n', 1)
log_path.write_text(s)
run("adom-wiki", "repo", "push", INSIDERS_PAGE, "--files", INSIDERS_LOG,
"-m", f"altium-bridge {ver}: promoted to public - {by}", cwd=work)
print(f"PUBLIC tier now serves {ver} ({'rollback' if rollback else 'promote'}), attested by {by}.")
def source_arg(args):
return args[args.index("--source") + 1] if "--source" in args and len(args) > args.index("--source") + 1 else ""
def main():
args = sys.argv[1:]
if "--promote" in args:
i = args.index("--promote")
ver = args[i + 1]
words = args[args.index("--attest") + 1] if "--attest" in args else ""
by = args[args.index("--by") + 1] if "--by" in args else ""
promote(ver, words, by)
elif "--rollback" in args:
i = args.index("--rollback")
ver = args[i + 1]
words = args[args.index("--attest") + 1] if "--attest" in args else ""
by = args[args.index("--by") + 1] if "--by" in args else ""
promote(ver, words, by, rollback=True)
elif "--publish" in args:
i = args.index("--publish")
log_line = args[i + 1] if len(args) > i + 1 and not args[i + 1].startswith("--") else ""
if not log_line:
raise SystemExit('usage: release.py --publish "<one changelog sentence for the release log>" --source <commit you pulled>')
ship_insiders(log_line, source_arg(args))
elif "--stage" in args:
i = args.index("--stage")
log_line = args[i + 1] if len(args) > i + 1 and not args[i + 1].startswith("--") else ""
if not log_line:
raise SystemExit('usage: release.py --stage "<one line: what this staged build is for>" --source <commit you pulled>')
stage_only(log_line, source_arg(args))
else:
# lint + build only: packaging needs a signed exe, so the zip is made by --stage/--publish
ver = json.loads((SRC / "bridge.json").read_text())["version"]
lint_versions(ver)
lint_agent_script()
n = lint_verbs()
print(f"verb lint ok: {n} verbs")
build_exe()
print("built the exe (unsigned). --stage or --publish signs, packages and uploads.")
if __name__ == "__main__":
main()