app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
#!/usr/bin/env bash
#
# tools/release.sh <version> (e.g. tools/release.sh 1.9.175)
# tools/release.sh --bump (auto: max of all current version files, patch + 1)
#
# THE SINGLE LOCKSTEP RELEASE for the Fusion bridge. The bridge (Python), the dashboard
# (Rust binary), and the wiki PACKAGE all move to ONE version, together, in one command.
#
# Why this exists (John, 2026-08-23): the three artifacts used to be versioned independently
# on a shared 1.9.x line, so they LOOKED lockstep but silently drifted - and a dashboard ship
# that did `release upload` + `repo push` but forgot `pkg publish` left every `pkg install`
# resolving a stale dashboard binary (the DASH_VERSION publish-lag, caught twice by the dock
# thread). Field best-practice for one-product-installed-as-a-unit is lockstep versioning; a
# convention a human must remember is exactly what failed, so this script IS the convention.
#
# It also fixes the class of "silent publish failure": EVERY upload/publish/install step is
# checked for success and the script EXITS NONZERO on the first failure - it never sails past
# a failed publish and reports success.
set -euo pipefail
REPO="adom/fusion-bridge"
MANIFEST="adom-bridge-fusion-manifest.json"
RELEASE_FILES="tools/release-files.txt"
DASH_BIN="dashboard/target/release/fusion-dashboard"
DL_BASE="https://wiki.adom.inc/download/${REPO}"
API_MANIFEST="https://wiki.adom.inc/api/v1/pages/fusion-bridge/files/${MANIFEST}"
# Adom's own boxes follow the INSIDERS tier - the opt-in RELEASE_INSTALL below must install
# what this release just published (the insiders manifest), not the public one, which only
# moves on tools/promote.sh with a human's attested words.
INSIDERS_MANIFEST="https://wiki.adom.inc/api/v1/pages/insiders/files/fusion-bridge/version.json"
BRIDGE_TARGET="${BRIDGE_TARGET:-AdomLapper}"
AITHREAD="fusion-bridge dashboard"
cd "$(dirname "$0")/.." # repo root
die() { echo "❌ release.sh: $*" >&2; exit 1; }
step() { echo; echo "▶ $*"; }
# --- 0. resolve the target version (lockstep = ONE number for everything) ----------------
cur_versions() {
cat BRIDGE_VERSION 2>/dev/null; echo # BRIDGE_VERSION has no trailing newline
sed -n 's/.*"version": "\([0-9.]*\)".*/\1/p' bridge.json page.json package.json 2>/dev/null
sed -n 's/^version = "\([0-9.]*\)".*/\1/p' dashboard/Cargo.toml 2>/dev/null
}
[ $# -ge 1 ] || die "usage: tools/release.sh <version> | --bump"
if [ "$1" = "--bump" ]; then
MAX=$(cur_versions | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)
[ -n "$MAX" ] || die "could not read any current version"
IFS=. read -r MA MI PA <<<"$MAX"; VERSION="${MA}.${MI}.$((PA+1))"
echo "auto-bump: max current $MAX -> $VERSION"
else
VERSION="$1"
fi
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "version must be X.Y.Z, got '$VERSION'"
echo "== LOCKSTEP RELEASE $VERSION =="
# RALPH WIDTHS (fleet lesson, 2026-08-25): John's dock pane measures ~744px and the pane
# band is ~744-853px. A breakpoint bug is INVISIBLE to anyone whose test widths straddle it
# (pup tested 1280 and 390, either side of 744, and never saw a two-column panel fail to
# render for months). Any dashboard layout work must be ralph-tested AT 744px, measuring
# window.innerWidth inside the ah webview rather than assuming the pane width.
# --- 1. preflight gates (the outage lesson: never publish code that won't import) ---------
step "Preflight gates (syntax / shadow / import)"
echo " gates OK"
# ⚠ LIVE-WORK GUARD (John, 2026-08-23: "that was really rude"): bridge_install makes AD REAP
# the running bridge process. That kills any in-flight create_library chain, and on bridges
# older than 1.9.183 it killed FUSION ITSELF (Fusion was a child of the bridge process -
# EIGHT releases shipped during a build matrix = eight dead Fusions). The first version of
# this guard read a respawn-window "running: null" as "no" and shipped anyway (the 8th kill).
# RULE NOW: a RUNNING Fusion means someone is using it -> refuse. UNKNOWN -> refuse. Only an
# explicit running:false passes. RELEASE_FORCE=1 overrides, and you had better know why.
# ⛔ SCOPE: the guard exists because bridge_install REAPS THE BRIDGE and kills in-flight
# work. Publishing to the wiki does not touch anyone's Fusion, so gating a publish on
# Fusion's state protects nothing and blocks real work - it refused a dashboard-HTML
# release on 2026-08-27 purely because Fusion's parentage was undeterminable, with
# RELEASE_INSTALL unset so no install would have run at all. Guard the DANGEROUS step,
# not the whole script. (Bypassing with RELEASE_FORCE would have been the wrong fix: that
# flag is for when you have CONFIRMED nobody is driving Fusion, not for skipping a check
# that should not have applied.)
if [ "${RELEASE_INSTALL:-0}" != "1" ]; then
echo " guard SKIPPED - publish-only run (RELEASE_INSTALL is not 1, so nothing installs)"
RELEASE_FORCE=1
fi
step "Live-work guard on $BRIDGE_TARGET"
_probe() { adom-bridge --ai-thread "$AITHREAD" --target "$BRIDGE_TARGET" "$1" '{}' 2>/dev/null \
| python3 -c "import sys,json
d=json.load(sys.stdin); x=d.get('data') or d
if isinstance(x.get('output'),str):
try: x=json.loads(x['output'])
except Exception: pass
print(json.dumps(x))" 2>/dev/null || echo '{}'; }
if [ "${RELEASE_FORCE:-0}" != "1" ]; then
VERDICT=""
for attempt in 1 2 3; do
RD=$(_probe fusion_readiness)
VERDICT=$(echo "$RD" | python3 -c "
import sys,json
x=json.load(sys.stdin); r=x.get('running')
print('idle' if r is False else ('busy' if r is True else 'unknown'))")
[ "$VERDICT" = "unknown" ] || break
sleep 5 # respawn window - re-probe, never guess
done
STAGE=$(_probe fusion_prove_result | python3 -c "import sys,json; print(json.load(sys.stdin).get('stage') or '')")
# A RUNNING Fusion is not automatically unsafe (2026-08-24): bridge_install reaps the
# BRIDGE process, and since 1.9.183 Fusion is launched DETACHED, so a Fusion whose
# process tree does not contain the bridge python cannot be killed by an install. Ask
# the box the precise question instead of refusing on the proxy - John's own Fusion
# sitting open all day must not block every release.
if [ "$VERDICT" = "busy" ] || [ "$VERDICT" = "unknown" ]; then
PROBE=$(python3 - <<'PYP'
import json, subprocess, base64
script = r"""
$all = Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine
$f = $all | Where-Object { $_.Name -like "Fusion360*" } | Select-Object -First 1
$bridgeAlive = [bool]($all | Where-Object { $_.CommandLine -like "*adom-fusion-bridge.exe*" })
$parented = $false
if ($f) {
$p = $f
for ($i=0; $i -lt 8 -and $p; $i++) {
if ($p.CommandLine -like "*adom-fusion-bridge.exe*") { $parented = $true; break }
$p = $all | Where-Object { $_.ProcessId -eq $p.ParentProcessId }
}
}
"bridgeAlive=$bridgeAlive parented=$parented"
"""
b64 = base64.b64encode(script.encode()).decode()
r = subprocess.run(["adom-bridge","--target","AdomLapper","run_script",
json.dumps({"interpreter":"powershell","scriptB64":b64,"timeoutSec":40,
"reason":"Release safety probe: is Fusion a CHILD of the bridge process (would an install kill it)?"})],
capture_output=True, text=True, timeout=120)
try:
d=json.loads(r.stdout); x=d.get('data') or d
print((x.get('stdout') or '').strip().splitlines()[-1])
except Exception:
print("bridgeAlive=unknown parented=unknown")
PYP
)
echo " probe: $PROBE"
case "$PROBE" in
*parented=True*) die "Fusion is RUNNING on $BRIDGE_TARGET AS A CHILD of the bridge process - installing would kill it. Wait until it is closed (or restart it on 1.9.183+ so it launches detached)." ;;
*parented=False*) VERDICT="detached"
echo " Fusion is DETACHED from the bridge tree (or not running) - an install reaps only the bridge process, so this is safe."
case "$PROBE" in *bridgeAlive=False*) echo " (the bridge process is not even running - nothing to reap)";; esac ;;
*) die "could not determine whether Fusion is parented by the bridge - refusing to install blind. Retry, or RELEASE_FORCE=1." ;;
esac
fi
case "$VERDICT" in
busy) die "Fusion is RUNNING on $BRIDGE_TARGET - someone is using it (chain stage: '${STAGE:-none}'). bridge_install reaps the bridge and kills in-flight work. Wait until Fusion is closed, or RELEASE_FORCE=1 once you have CONFIRMED nobody is driving it." ;;
detached) echo " guard OK: Fusion running but detached; chain stage: '${STAGE:-none}'" ;;
unknown) die "could not determine Fusion state on $BRIDGE_TARGET after 3 probes (bridge respawning or unreachable) - refusing to install blind. Retry shortly, or RELEASE_FORCE=1." ;;
idle) echo " guard OK: Fusion not running on $BRIDGE_TARGET (chain stage: '${STAGE:-none}')" ;;
esac
else
echo " guard BYPASSED by RELEASE_FORCE=1"
fi
# --- 2. set ALL version files to $VERSION (this is the lockstep) --------------------------
step "Setting every version file to $VERSION"
printf '%s' "$VERSION" > BRIDGE_VERSION
python3 - "$VERSION" <<'PY'
import json, sys
v = sys.argv[1]
for f in ("bridge.json", "page.json", "package.json"):
d = json.load(open(f)); d["version"] = v
json.dump(d, open(f, "w"), indent=2); open(f, "a").write("\n")
PY
sed -i "s/^version = \"[0-9.]*\"/version = \"$VERSION\"/" dashboard/Cargo.toml
sed -i -E "s/^version = \"[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?\"/version = \"$VERSION\"/" rust/Cargo.toml # the Rust workspace rides the same line (a -dev suffix included)
sed -i "s/DASH_VERSION=\"[0-9.]*\"/DASH_VERSION=\"$VERSION\"/" install.sh
# prove they all match now
BAD=$(cur_versions; sed -n 's/DASH_VERSION="\([0-9.]*\)"/\1/p' install.sh)
echo "$BAD" | grep -vx "$VERSION" | grep -q . && { echo "$BAD"; die "version files did not all set to $VERSION"; }
echo " all 6 files at $VERSION"
# --- 3. build the dashboard (Rust) -------------------------------------------------------
step "Building dashboard binary"
( cd dashboard && cargo build --release ) || die "cargo build failed"
"$DASH_BIN" --version 2>&1 | grep -q "v$VERSION" || die "built dashboard does not report v$VERSION"
echo " dashboard $($DASH_BIN --version 2>&1 | head -1)"
# --- 3b. build the Rust bridge exe (Windows) -----------------------------------------------
# Phase 1 of the Rust port: the exe ships INSIDE the bridge zip beside server.py. Whether ab
# spawns it (spawn.kind exe) or the Python bridge (spawn.kind python) is bridge.json's call;
# the payload carries both so a tier can flip without a rebuild. Cross-compiled with the
# mingw target; the guards run first so a todo!() or an OS crate in core never ships.
step "Building Rust bridge exe"
bash tools/check-portable.sh || die "check-portable failed"
bash tools/check-naming.sh || die "check-naming failed"
python3 tools/freeze-contract.py || die "freeze-contract failed" # the Rust exe asserts it serves this catalog
python3 - <<'CHK' || die "bridge.json verbs do not match contracts/describe.json (run tools/freeze-contract.py)"
import json, sys
m = json.load(open("bridge.json"))["verbs"]; c = [v["name"] for v in json.load(open("contracts/describe.json"))["verbs"]]
sys.exit(0 if m == c else 1)
CHK
( cd rust && cargo build --release --target x86_64-pc-windows-gnu -p adom-fusion-bridge ) || die "rust exe build failed"
RUST_EXE="rust/target/x86_64-pc-windows-gnu/release/adom-fusion-bridge.exe"
[ -s "$RUST_EXE" ] || die "rust exe missing after build"
grep -q "version = \"$VERSION\"" rust/Cargo.toml || die "rust workspace is not at $VERSION"
echo " rust exe $(stat -c%s "$RUST_EXE") bytes @ $VERSION"
# --- 3b. SIGN the exe: never publish an unsigned one ----------------------------------------
# Windows 11 Smart App Control blocks unsigned exes ("Part of this app has been blocked").
# tools/sign_pe.py signs through Azure Trusted Signing on the Windows host named by the PRIVATE
# config ($ADOM_SIGNING_DIR, default ~/.adom/signing/altium-bridge: the altium-bridge release's
# path), gates on the host's verdict, pulls the file back and verifies it again here; that config
# is deliberately not in this public repo, and a release without it, or whose signature does not
# verify, stops here. Signing is the LAST change to the exe: the zip below takes this exact file.
step "Signing the exe"
if [ -n "${ADOM_SIGN_HELPER:-}" ]; then
"$ADOM_SIGN_HELPER" "$RUST_EXE" "fusion-bridge-signing" || die "signing failed or did not verify: nothing was published"
else
python3 tools/sign_pe.py "$VERSION" "$RUST_EXE" | tail -1 > /tmp/fusion-signing-report-$VERSION.json \
|| die "signing failed or did not verify: nothing was published (tools/sign_pe.py says why above)"
fi
python3 - "$RUST_EXE" <<'SIGCHK' || die "the exe has no Authenticode signature after signing"
import struct, sys
d = open(sys.argv[1], "rb").read(); pe = struct.unpack_from("<I", d, 0x3c)[0]
magic = struct.unpack_from("<H", d, pe + 24)[0]
_, size = struct.unpack_from("<II", d, pe + 24 + (112 if magic == 0x20b else 96) + 32)
sys.exit(0 if size > 0 else 1)
SIGCHK
echo " signed: $(sha256sum "$RUST_EXE" | cut -c1-16)"
# --- 4. build the bridge zip -------------------------------------------------------------
step "Building bridge zip"
# The asset FILENAME becomes the download-URL basename, and the manifest URL (step 5) expects
# exactly this name. It is 'adom-bridge-fusion', NOT '${REPO//\//-}' which is 'adom-fusion-bridge'
# (different word order) - a mismatch uploads fine but 404s at bridge_install. Keep them equal.
ZIP="/tmp/adom-bridge-fusion-v${VERSION}.zip"
python3 - "$ZIP" "$RELEASE_FILES" <<'PY'
import zipfile, os, sys
zp, listf = sys.argv[1], sys.argv[2]
files = [l.strip() for l in open(listf) if l.strip()]
missing = [f for f in files if not os.path.exists(f)]
if missing: raise SystemExit("missing release files: %s" % missing)
# THE ADD-IN PAYLOAD COMES FROM THE TREE, NOT THE HAND LIST (issue #69, Colby, 2026-08-28).
# activity_palette.py was added to the tree and imported from commands/__init__.py, but never
# added to release-files.txt, so 1.9.233-1.9.237 shipped the importer without the imported
# module. Every fresh install bricked Fusion's add-in with a ModuleNotFoundError dialog storm
# (twice per launch, then once per user action). A hand list is a second source of truth
# that nothing keeps honest; the tree is the only one. Same family as kicad-bridge#63.
import glob
# ... and the data files an add-in module reads at import (mechanical/schema.json, 1.13.0: without
# it `import commands` fails and the whole add-in is down, issue #69's failure again)
tree = sorted(set(glob.glob("addin/AdomBridge/**/*.py", recursive=True)
+ glob.glob("addin/AdomBridge/**/*.manifest", recursive=True)
+ glob.glob("addin/AdomBridge/commands/**/*.json", recursive=True)))
extra = [f for f in tree if f not in files]
if extra:
print(" +%d add-in file(s) from the tree the hand list lacked: %s" % (len(extra), extra))
files = files + extra
# And REFUSE to ship a payload whose relative imports do not all resolve inside it (the
# transactional guarantee Colby asked for, done at the cheapest possible point: before
# the zip exists at all).
import re
payload = set(files)
for f in [x for x in files if x.startswith("addin/AdomBridge/") and x.endswith(".py")]:
d = os.path.dirname(f)
for m in re.finditer(r"^\s*from\s+(\.+)([\w.]*)\s+import", open(f, encoding="utf-8").read(), re.M):
dots, mod = m.group(1), m.group(2)
base = d
for _ in range(len(dots) - 1): base = os.path.dirname(base)
if not mod: continue
cand = os.path.join(base, *mod.split("."))
if not (cand + ".py" in payload or os.path.join(cand, "__init__.py") in payload):
raise SystemExit("REFUSING to build: %s imports %s%s which is not in the payload" % (f, dots, mod))
print(" add-in payload: %d files, all relative imports resolve" % len([x for x in files if x.startswith("addin/")]))
# The Rust exe lands at the ROOT of the payload, beside server.py: that is where ab's
# spawn.kind exe looks for the entrypoint, and where the exe looks for its sidecar.
rust_exe = "rust/target/x86_64-pc-windows-gnu/release/adom-fusion-bridge.exe"
if not os.path.exists(rust_exe): raise SystemExit("missing the Rust bridge exe: %s" % rust_exe)
with zipfile.ZipFile(zp, "w", zipfile.ZIP_DEFLATED) as z:
for f in files: z.write(f, f)
z.write(rust_exe, "adom-fusion-bridge.exe")
print(" zip", os.path.getsize(zp), "bytes,", len(files), "files")
PY
# --- 5. update the INSIDERS manifest (two-tier publish, page issues #72/#73) --------------
# Every release is an INSIDERS release: the manifest lands on the private adom/insiders page
# (folder = this bridge's slug), where ab 2.0.99+ probes it with the desktop user's login and
# falls back to the public manifest for everyone else. The PUBLIC manifest
# ($MANIFEST on the fusion-bridge page) moves ONLY via tools/promote.sh with the human's
# attested words - never here, and it is deliberately NOT in the repo-push file list below,
# because pushing a stale local copy would silently revert a promote done elsewhere.
step "Updating insiders manifest (public moves only on promote)"
SHA=$(sha256sum "$ZIP" | cut -d' ' -f1); SIZE=$(stat -c%s "$ZIP")
rm -rf .insiders-stage && mkdir -p .insiders-stage/fusion-bridge
python3 - "$VERSION" "$SHA" "$SIZE" "$DL_BASE" "$MANIFEST" <<'PY'
import json, sys
v, sha, size, base, mf = sys.argv[1], sys.argv[2], int(sys.argv[3]), sys.argv[4], sys.argv[5]
m = json.load(open(mf)) # the public manifest is the SHAPE template only
m["version"] = v; m["sha256"] = sha; m["size"] = size
m["url"] = f"{base}/{v}/adom-bridge-fusion-v{v}.zip"
m["tier"] = "insiders"
json.dump(m, open(".insiders-stage/fusion-bridge/version.json", "w"), indent=2)
PY
(cd .insiders-stage && adom-wiki repo push adom/insiders --files fusion-bridge/version.json --json 2>/dev/null \
| python3 -c "import sys,json; d=json.load(sys.stdin); sys.exit(0 if d.get('status')=='ok' else 1)") \
|| die "insiders manifest push FAILED"
echo " insiders manifest @ $VERSION pushed (public stays until promote)"
# --- 6. upload BOTH release assets, CHECKED (silent-publish-failure fix) ------------------
# The adom-wiki --json envelope is {status:"ok"|..., data, hints[]} - status is the signal.
ok_check() { python3 -c "import sys,json; d=json.load(sys.stdin); sys.exit(0 if d.get('status')=='ok' else 1)"; }
step "Uploading release assets @ $VERSION (bridge zip + dashboard binary)"
adom-wiki release upload "$REPO" "$VERSION" "$ZIP" --platform windows --arch x64 --json 2>/dev/null | ok_check \
|| die "bridge zip release upload FAILED"
adom-wiki release upload "$REPO" "$VERSION" "$DASH_BIN" --platform linux --arch x64 --json 2>/dev/null | ok_check \
|| die "dashboard binary release upload FAILED"
echo " both assets uploaded"
# --- 7. commit + push the source (repo layer) --------------------------------------------
# ⛔ NEVER CLOBBER SOMEONE ELSE'S WORK ON THE PAGE (John, 2026-08-27: "colby might have
# made changes... so don't conflict"). This push overwrites an explicit file list with our
# LOCAL copies. If anyone merges a PR into the page repo, or edits it directly, and we push
# without looking, their change is silently reverted and the merge button lied to them.
# Colby has PR #58 open and Oliver has #34/#38 - the moment one merges, this is live.
# So: remember the page head we last pushed, and if the page has moved since by someone
# other than us, STOP and make a human look.
step "Page-repo conflict check"
HEAD_FILE=".release-page-head"
PAGE_HEAD=$(adom-wiki repo log "$REPO" --json 2>/dev/null | python3 -c "
import sys,json
try:
d=json.load(sys.stdin); x=d.get('data') or d
cs=x.get('commits') or x.get('log') or []
print(cs[0].get('hash','') if cs else '')
except Exception: print('')" 2>/dev/null)
if [ -z "$PAGE_HEAD" ]; then
echo " WARNING: could not read the page head - pushing blind is how we would clobber a merge"
[ "${RELEASE_FORCE:-0}" = "1" ] || die "refusing to push without knowing the page head. Retry, or RELEASE_FORCE=1 once you have checked the page yourself."
elif [ -f "$HEAD_FILE" ] && [ "$(cat "$HEAD_FILE")" != "$PAGE_HEAD" ]; then
LAST_MSG=$(adom-wiki repo log "$REPO" --json 2>/dev/null | python3 -c "
import sys,json
try:
d=json.load(sys.stdin); x=d.get('data') or d
cs=x.get('commits') or x.get('log') or []
print((cs[0].get('message') or '').splitlines()[0][:80] if cs else '')
except Exception: print('')" 2>/dev/null)
echo " page moved since our last push: $LAST_MSG"
case "$LAST_MSG" in
"Publish "*|"Update "*) echo " (our own release pattern - continuing)" ;;
*) if [ "${RELEASE_FORCE:-0}" = "1" ]; then
echo " page-head guard BYPASSED by RELEASE_FORCE=1 (head: \"$LAST_MSG\")"
else
die "the page repo changed since our last push (\"$LAST_MSG\") - someone merged a PR or edited it. PULL those changes into this tree FIRST, or this push reverts them. RELEASE_FORCE=1 only once you have confirmed there is nothing to lose."
fi ;;
esac
else
echo " page head unchanged since our last push"
fi
step "Commit + repo push"
git add -A
git commit -q -m "release $VERSION (lockstep): bridge + dashboard + package all at $VERSION
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>" || echo " (nothing to commit)"
# THE LIST WAS INCOMPLETE, AND HAD BEEN FOR MONTHS. Oliver reported it in PR #34 back in
# July - "the page-repo server.py is behind the shipped release" - and he was being kind:
# describe.py (the verb catalog the AI reads), the WHOLE addin/ tree, and skills/ were
# never pushed at all. So anyone reading the page, or opening a PR against it, was working
# from source that did not match what shipped. That is why his PR needed a reconciliation
# section. Push what we actually ship.
PUSH_FILES="server.py fusion_detect.py describe.py timings.py aps.py install_addin.py \
BRIDGE_VERSION bridge.json page.json package.json \
dashboard/src/main.rs dashboard/Cargo.toml dashboard/Cargo.lock dashboard/ui/index.html \
dockbar.json install.sh"
for f in handlers/*.py addin/AdomBridge/*.py addin/AdomBridge/commands/*.py addin/AdomBridge/commands/*/*.py addin/AdomBridge/commands/*/*.json \
addin/AdomBridge/*.manifest skills/*/SKILL.md tools/release.sh CLAUDE.md \
tools/promote.sh tools/promote.py tools/check-portable.sh tools/check-naming.sh tools/freeze-contract.py \
contracts/describe.json rust/Cargo.toml rust/Cargo.lock rust/PORTING.md \
rust/*/Cargo.toml rust/*/src/*.rs rust/*/src/*/*.rs rust/bridge/src/*.py rust/bridge/src/*.json rust/bridge/src/*.xml \
rust/vendor/*/Cargo.toml rust/vendor/*/src/*.rs rust/vendor/*/src/*/*.rs rust/vendor/*/*.md \
rust/*/src/*.xml rust/*/tests/*.rs \
tools/sign_pe.py tools/gen-readme-verbs.py tests/*.py contracts/eda-bridge-standard/*.json CHANGELOG.md \
library_bundle.py README.md docs/*.md docs/img/*.svg docs/img/*.png \
docs/img/*/*.png dashboard/ui/*.html; do
[ -f "$f" ] && PUSH_FILES="$PUSH_FILES $f"
done
# the whole Rust tree as tracked (sources, include_str! data, test goldens and fixtures), never a glob list
for f in $(git ls-files rust ':!:rust/target*'); do
case " $PUSH_FILES " in *" $f "*) ;; *) PUSH_FILES="$PUSH_FILES $f";; esac
done
# Every tracked Rust source must be on the page, or master does not build from source there:
# 1.13.2 shipped without the vendored engine's src/writers/ and with a stale Cargo.lock (Drew, PR #23).
MISSING=$(git ls-files rust ':!:rust/target*' | grep -E '\.(rs|toml|lock|xml|json|py)$' | while read -r f; do
case " $PUSH_FILES " in *" $f "*) ;; *) echo "$f";; esac; done)
[ -z "$MISSING" ] || die "these tracked Rust sources are not in the push list, so the page would not build: $(echo $MISSING)"
echo " pushing $(echo $PUSH_FILES | wc -w) files"
adom-wiki repo push "$REPO" --files $PUSH_FILES --json 2>/dev/null | ok_check || die "repo push FAILED"
echo " pushed"
adom-wiki repo log "$REPO" --json 2>/dev/null | python3 -c "
import sys,json
try:
d=json.load(sys.stdin); x=d.get('data') or d
cs=x.get('commits') or x.get('log') or []
open('.release-page-head','w').write(cs[0].get('hash','') if cs else '')
except Exception: pass" 2>/dev/null || true
# --- 8. publish the PACKAGE, CHECKED (the step the old flow forgot) -----------------------
step "Publishing package"
if [ "${RELEASE_SKIP_PKG:-0}" = "1" ]; then
# the package (what adom-wiki pkg install reads) is a separate, human-approved step for now;
# bridge installs and updates read the manifest and zip, which are already published above
echo " SKIPPED (RELEASE_SKIP_PKG=1): run 'adom-wiki pkg publish --org adom' from this folder when approved"
else
PUB=$(adom-wiki pkg publish --org adom --json 2>/dev/null || true)
if ! echo "$PUB" | ok_check; then
# the registry's LBR_VALIDATE lint (2026-10-31) fails on rust/lbr/tests/ fixtures that are
# broken on purpose; when those are the ONLY lint errors, publish with lint as warnings
# the refusal's JSON goes to stderr, so ask again with stderr kept
PUB_ERR=$(adom-wiki pkg publish --org adom --json 2>&1 >/dev/null || true)
ONLY_FIXTURES=$(echo "$PUB_ERR" | python3 -c '
import json,sys
t=sys.stdin.read(); i=t.find("{")
try: r=json.loads(t[i:])
except Exception: print("no"); sys.exit()
errs=[h for h in (r.get("hints") or []) if h.get("level")=="error"]
ok=errs and all("LBR_VALIDATE_FAILED" in h.get("message","") and "/tests/" in h.get("message","").split(" fails ")[0] for h in errs)
print("yes" if ok else "no")' 2>/dev/null)
if [ "$ONLY_FIXTURES" = "yes" ]; then
echo " lint: only the deliberately broken .lbr test fixtures fail adom-lbr validate; publishing with --skip-lint"
PUB=$(adom-wiki pkg publish --org adom --json --skip-lint 2>/dev/null || true)
fi
echo "$PUB" | ok_check || die "pkg publish FAILED (status != ok) — output: ${PUB:0:400}"
fi
echo " published $REPO @ package $VERSION"
fi
# --- 9. install onto the box, CHECKED ----------------------------------------------------
# ⚠ PUBLISHING IS FREE, INSTALLING IS NOT (fleet rule, adom-ui-design 1.25.1, 2026-08-25).
# Publishing to the wiki touches nobody. bridge_install RESTARTS the bridge on John's
# desktop, and doing that under another thread's live ah session looks exactly like that
# thread's own flake. So the install is OPT-IN: run the release freely to publish, and pass
# RELEASE_INSTALL=1 only when the dock-bar thread has handed you the machine.
if [ "${RELEASE_INSTALL:-0}" != "1" ]; then
echo
echo "▶ Skipping bridge_install on $BRIDGE_TARGET (publish-only)"
echo " Published $VERSION. To install onto the box, get an ah/machine slot first, then:"
echo " RELEASE_INSTALL=1 tools/release.sh $VERSION"
echo
echo "✅ PUBLISH $VERSION complete (wiki artifacts + package). Nothing on $BRIDGE_TARGET was touched."
exit 0
fi
step "bridge_install on $BRIDGE_TARGET"
adom-bridge --ai-thread "$AITHREAD" --target "$BRIDGE_TARGET" bridge_install \
"{\"manifestUrl\":\"$INSIDERS_MANIFEST\",\"force\":true}" 2>/dev/null \
| python3 -c "import sys,json; r=json.load(sys.stdin); d=r.get('data') or r; d=(json.loads(d['output']) if isinstance(d,dict) and d.get('output') else d); sys.exit(0 if (d.get('status')=='ok' or r.get('status')=='ok') else 1)" \
|| die "bridge_install FAILED"
echo " installed"
# --- 10. VERIFY end-to-end (what actually catches a lockstep break) -----------------------
step "Verifying a clean install resolves $VERSION"
TMP=$(mktemp -d)
( cd "$TMP" && adom-wiki pkg install "$REPO" >/dev/null 2>&1 ) || die "clean pkg install failed"
INSTALLED_DASH=$(find "$TMP" "$HOME/project/adom_modules" -name install.sh -path "*fusion-bridge*" 2>/dev/null \
| head -1 | xargs -r sed -n 's/DASH_VERSION="\([0-9.]*\)".*/\1/p')
[ "$INSTALLED_DASH" = "$VERSION" ] || die "clean install resolves DASH_VERSION=$INSTALLED_DASH, expected $VERSION"
# ⚠ DASH_VERSION IS A STRING IN install.sh, NOT THE BINARY (gap found 2026-08-25 while
# verifying the Send-to-AI un-park from a clean install): the check above proves the
# INSTALLER points at this version, not that the artifact behind the URL actually IS this
# version. Fetch what install.sh would fetch and make the binary say its own name.
DL_BIN="$TMP/published-fusion-dashboard"
curl -fsSL -o "$DL_BIN" "${DL_BASE}/${VERSION}/fusion-dashboard" \
|| die "the published dashboard binary is NOT downloadable at ${DL_BASE}/${VERSION}/fusion-dashboard"
chmod +x "$DL_BIN"
PUB_VER=$("$DL_BIN" --version 2>&1 | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | head -1 | tr -d v)
[ "$PUB_VER" = "$VERSION" ] || die "the PUBLISHED binary reports $PUB_VER, expected $VERSION (installer would deliver the wrong build)"
rm -rf "$TMP"
echo " clean install DASH_VERSION = $VERSION ✓ (and the published binary reports $PUB_VER ✓)"
echo
echo "✅ LOCKSTEP RELEASE $VERSION complete: bridge + dashboard + package all at $VERSION, verified."