main
Drew Owens 0.11.4 source 8889116 1d ago
#!/usr/bin/env bash
# Build the SOLIDWORKS bridge release zip + manifest from the current tree, and
# optionally publish both to wiki page adom/solidworks-bridge.
#
#   tools/release.sh            # stage + zip + manifest (dist/), no network
#   tools/release.sh --publish  # ...then release create/upload + manifest push
#   tools/release.sh --canary   # ...then a release CANDIDATE <ver>-rc<N> + manifests/canary.json for the test machine
#   tools/release.sh --require-live cswa-report.json --publish   # ...PROMOTE that canary, with a passing LIVE CSWA run of it
#   tools/release.sh --docs-only --publish                         # ...a release that changes no code
#
# THE LIVE GATE IS THE RULE (0.11.4): --publish REFUSES unless --require-live <report> names a passing
# tools/cswa-check.py run of THIS build, or --docs-only says no code changed since the last publish
# (tools/check-docs-only.py proves it; the release's changelog then says the live gate was skipped).
# THE CANARY IS WHAT SHIPS: only a published build reaches the test machine (ab installs what a manifest names,
# sha256-checked), so --canary publishes the build as a release candidate that NO ONE ELSE gets - a release
# <ver>-rc<N> and a page file manifests/canary.json, installed by hand on the test machine with bridge_install.
# --require-live then publishes THAT zip, byte for byte (no rebuild), and only when the live report says it ran
# that exe (report.bridge.exeSha256, read back from the installed exe) and no code changed since the canary.
#
# Versions are read from BRIDGE_VERSION and MUST match bridge.json ("version")
# and the manifest; the script refuses to ship a drifted triple.
# The zip is RUNTIME-ONLY (exe + bridge.json + resources the exe reads); no
# docs, media, tests or source - see adom-bridge-sdk-publish payload rules.
# GOTCHA (2026-09-11): a later `adom-wiki repo push` of the SOURCE tree re-pushes the COMMITTED manifest and
# clobbers the one this script pushed (hash mismatch on bridge_install). Commit the rewritten manifest
# immediately after --publish, and never include the manifest in a source push made from an older commit.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
VER="$(tr -d ' \n' < BRIDGE_VERSION)"
PAGE="adom/solidworks-bridge"; SLUG="solidworks-bridge"
EXE="target/x86_64-pc-windows-gnu/release/swx-bridge.exe"
# THE RELEASE MANIFEST DESCRIBES THE PUBLISHED RELEASE, NOT THE WORKING TREE.
# It carries the version, URL, sha256 and size of what is actually downloadable,
# and THIS SCRIPT rewrites it at publish time. So a version bump in Cargo.toml
# does NOT bump it: pointing it at a URL that does not exist yet would break
# every install until the release is cut. `bridge.json` is the one that must
# track the crate version (pinned by tests/adversary33.rs); this one must track
# the release: it names whatever was last published and committed, until the
# next publish rewrites it.
# A RELEASE IS A DEPLOY. ab hosts whatever version the PUBLISHED manifest names
# (measured 2026-09-30: adom-bridge 2.1.163 auto-installed 0.8.9 from it), so
# --publish puts this build on every machine that runs the bridge. There is no
# later step at which a bad build can still be stopped.
MAN="adom-bridge-solidworks-manifest.json"
MAIN_URL="https://wiki.adom.inc/api/v1/pages/$SLUG/files/$MAN"
CANARY="manifests/canary.json"
CANARY_URL="https://wiki.adom.inc/api/v1/pages/$SLUG/files/$CANARY"
TARGET="${SWX_LIVE_TARGET:-adom_drew2}"
# ARGUMENTS: --publish, and --require-live <report.json> (0.11.0): REFUSE to go on unless a LIVE
# tools/cswa-check.py report is given that (a) ran every CSWA part and passed them all, (b) was made
# by THIS version, and (c) is NEWER than the last commit that touched code (src/, resources/,
# Cargo.toml) - a passing report from before the last code change proves nothing about this build.
PUBLISH=0; LIVE=""; DOCS_ONLY=0; CANARY_RUN=0
while [ $# -gt 0 ]; do
  case "$1" in
    --publish) PUBLISH=1 ;;
    --require-live) LIVE="${2:-}"; [ -n "$LIVE" ] || { echo "ERROR: --require-live needs the cswa-check report path"; exit 1; }; shift ;;
    --docs-only) DOCS_ONLY=1 ;;
    --canary) CANARY_RUN=1 ;;
    *) echo "ERROR: unknown argument $1 (use --canary, --publish, --require-live <report.json>, --docs-only)"; exit 1 ;;
  esac
  shift
done
if [ "$CANARY_RUN" = 1 ] && { [ "$PUBLISH" = 1 ] || [ -n "$LIVE" ] || [ "$DOCS_ONLY" = 1 ]; }; then
  echo "ERROR: --canary stages a release CANDIDATE for the test machine and publishes nothing else; run it alone"; exit 1
fi
# EVERY PUBLISH IS PROVEN LIVE (0.11.4, the coordinator's rule): a release is a deploy (see above), and
# the simulator is not SOLIDWORKS. A plain build (no --publish) needs neither flag.
if [ "$PUBLISH" = 1 ] && [ -z "$LIVE" ] && [ "$DOCS_ONLY" != 1 ]; then
  echo "ERROR: --publish needs --require-live <report.json> (a passing tools/cswa-check.py run of THIS build on real"
  echo "       SOLIDWORKS), or --docs-only for a release that changes no code. Nothing was built or published."
  exit 1
fi
if [ -n "$LIVE" ] && [ "$DOCS_ONLY" = 1 ]; then
  echo "ERROR: --docs-only and --require-live together - a docs-only release has no code to prove; pick one"; exit 1
fi
LAST_PUB="$(git log -1 --format=%H -- "$MAN" 2>/dev/null || true)"
LAST_VER="$(git show "${LAST_PUB:-HEAD}:$MAN" 2>/dev/null | python3 -c 'import json,sys;print(json.load(sys.stdin).get("version","?"))' 2>/dev/null || echo "?")"
if [ "$DOCS_ONLY" = 1 ]; then
  python3 tools/check-docs-only.py ${LAST_PUB:+"$LAST_PUB"} \
    || { echo "ERROR: --docs-only: the bridge's code changed since the last publish ($LAST_VER) - publish with --require-live"; exit 1; }
fi
ZIP="swx-bridge-v${VER}.zip"; DIST="dist"
CODE_COMMIT="$(git log -1 --format=%H -- src resources Cargo.toml 2>/dev/null || true)"
if [ "$CANARY_RUN" = 1 ]; then
  # A canary records the commit it was built from: what the zip holds must be committed - checked
  # before anything is built, so a refusal leaves dist/ (perhaps the last canary's zip) as it was.
  DIRTY="$(git status --porcelain -- src resources Cargo.toml Cargo.lock bridge.json BRIDGE_VERSION LICENSE "$CANARY" || true)"
  [ -z "$DIRTY" ] || { echo "ERROR: uncommitted changes to what the canary would hold or record:"; echo "$DIRTY"; exit 1; }
fi
PROMOTE=0
if [ -n "$LIVE" ]; then
  # THE CANARY IS WHAT SHIPS: the live report proves the build it ran, so the publish ships exactly that build.
  [ -f "$CANARY" ] || { echo "ERROR: --require-live publishes the CANARY the live run tested, and there is none ($CANARY):"
    echo "       tools/release.sh --canary, install it on the test machine, run tools/cswa-check.py there, then publish with its report"; exit 1; }
  IFS=$'\t' read -r C_VER C_RC C_SHA C_SIZE C_CODE C_EXE < <(python3 -c '
import json, sys
m = json.load(open(sys.argv[1])); c = m.get("canary") or {}
print("\t".join(str(x) for x in (m.get("version", ""), c.get("rc", ""), m.get("sha256", ""), m.get("size", ""), c.get("codeCommit", ""), c.get("exeSha256", ""))))
' "$CANARY")
  [ "$C_VER" = "$VER" ] || { echo "ERROR: the canary is ${C_RC:-?} (version $C_VER), not $VER: cut a canary of $VER (tools/release.sh --canary)"; exit 1; }
  [ -n "$C_CODE" ] && [ "$C_CODE" = "$CODE_COMMIT" ] || { echo "ERROR: the code changed since the canary $C_RC was built (built at ${C_CODE:0:8}, the last code commit is ${CODE_COMMIT:0:8}):"
    echo "       cut a new canary and run the live check on it"; exit 1; }
  mkdir -p "$DIST"
  if [ ! -f "$DIST/$ZIP" ] || [ "$(sha256sum "$DIST/$ZIP" | cut -d' ' -f1)" != "$C_SHA" ]; then
    echo "(the canary's zip is not in $DIST/ - downloading $C_RC from the wiki)"
    rm -rf "$DIST/canary-dl"; mkdir -p "$DIST/canary-dl"
    adom-wiki release download "$PAGE" "$C_RC" --platform any --out "$DIST/canary-dl/$ZIP" >/dev/null || true
    GOT="$(find "$DIST/canary-dl" -type f | head -1)"
    { [ -n "$GOT" ] && [ "$(sha256sum "$GOT" | cut -d' ' -f1)" = "$C_SHA" ]; } \
      || { echo "ERROR: the canary $C_RC's zip (sha256 $C_SHA) is neither in $DIST/ nor downloadable intact"; exit 1; }
    mv "$GOT" "$DIST/$ZIP"; rm -rf "$DIST/canary-dl"
  fi
  EXE_SHA="$(unzip -p "$DIST/$ZIP" swx-bridge.exe | sha256sum | cut -d' ' -f1)"
  [ "$EXE_SHA" = "$C_EXE" ] || { echo "ERROR: the canary record names exe ${C_EXE:0:12}…, but its zip holds ${EXE_SHA:0:12}…"; exit 1; }
  CODE_AT="$(git log -1 --format=%ct -- src resources Cargo.toml 2>/dev/null || echo 0)"
  python3 tools/check-live-report.py "$LIVE" "$VER" "${CODE_AT:-0}" --exe-sha "$EXE_SHA" --zip-sha "$C_SHA" \
    || { echo "ERROR: --require-live: no passing live CSWA report of the canary $C_RC - install it on the test machine and run tools/cswa-check.py there"; exit 1; }
  PROMOTE=1
fi

if [ "$PROMOTE" = 1 ]; then
  # NO REBUILD: the bytes the live run tested are the bytes that ship.
  SHA="$C_SHA"; SIZE="$(stat -c %s "$DIST/$ZIP")"
  [ "$SIZE" = "$C_SIZE" ] || { echo "ERROR: the canary zip is $SIZE bytes, its record says $C_SIZE"; exit 1; }
  ZBJ="$(unzip -p "$DIST/$ZIP" bridge.json | python3 -c 'import json,sys;print(json.load(sys.stdin)["version"])')"
  [ "$ZBJ" = "$VER" ] || { echo "ERROR: the canary zip's bridge.json says $ZBJ, not $VER"; exit 1; }
  echo "PROMOTING the canary $C_RC: no rebuild - the bytes the live run tested are the bytes that ship"
else
  [ -f "$EXE" ] || { echo "ERROR: $EXE missing - run: cargo build --release --target x86_64-pc-windows-gnu"; exit 1; }
  BJ="$(python3 -c 'import json;print(json.load(open("bridge.json"))["version"])')"
  [ "$BJ" = "$VER" ] || { echo "ERROR: bridge.json version $BJ != BRIDGE_VERSION $VER"; exit 1; }
  # the exe must be newer than every source file (a stale build ships silently otherwise)
  NEWER="$(find src -name '*.rs' -newer "$EXE" | head -3)"
  [ -z "$NEWER" ] || { echo "ERROR: sources newer than the exe:"; echo "$NEWER"; exit 1; }
  STAGE="$DIST/stage"; rm -rf "$STAGE"; mkdir -p "$STAGE/resources"
  cp "$EXE" bridge.json BRIDGE_VERSION LICENSE "$STAGE/"
  cp -r resources/. "$STAGE/resources/"
  ( cd "$STAGE" && rm -f "../$ZIP" && zip -qr "../$ZIP" . )
  SHA="$(sha256sum "$DIST/$ZIP" | cut -d' ' -f1)"; SIZE="$(stat -c %s "$DIST/$ZIP")"
fi
# The main manifest names what is PUBLISHED: a canary leaves it alone.
if [ "$CANARY_RUN" != 1 ]; then
python3 - "$VER" "$SHA" "$SIZE" "$ZIP" <<'PY'
import json,sys
ver,sha,size,zipname=sys.argv[1:]
m=json.load(open("adom-bridge-solidworks-manifest.json"))
m["version"]=ver; m["sha256"]=sha; m["size"]=int(size)
m["url"]=f"https://wiki.adom.inc/download/adom/solidworks-bridge/{ver}/{zipname}"
json.dump(m,open("adom-bridge-solidworks-manifest.json","w"),indent=2); print("manifest ->",m["url"])
PY
fi
echo "OK: $DIST/$ZIP  size=$SIZE  sha256=$SHA"
unzip -l "$DIST/$ZIP" | sort -k1 -n -r | head -6
[ "$SIZE" -lt 3000000 ] || { echo "ERROR: zip > 3 MB - something non-runtime got in"; exit 1; }

if [ "$CANARY_RUN" = 1 ]; then
  # ── THE CANARY: this build as a release candidate only the test machine installs. ──
  EXE_SHA="$(unzip -p "$DIST/$ZIP" swx-bridge.exe | sha256sum | cut -d' ' -f1)"
  # The next candidate number: one past the page's canary of this version (its record, or its url), or CANARY_RC.
  PREV="$(adom-wiki api "/api/v1/pages/$SLUG/files/$CANARY" 2>/dev/null || true)"
  N="${CANARY_RC:-$(PREV="$PREV" python3 -c '
import json, os, re, sys
ver = sys.argv[1]
try:
    m = json.loads(os.environ.get("PREV") or "")
except ValueError:
    m = {}
k = 0
if isinstance(m, dict) and m.get("version") == ver:
    for s in ((m.get("canary") or {}).get("rc", ""), m.get("url", "")):
        hit = re.search(re.escape(ver) + r"-rc(\d+)", s or "")
        if hit:
            k = max(k, int(hit.group(1)))
print(k + 1)
' "$VER")}"
  RC="$VER-rc$N"
  echo "== canary 1/4: release $RC (create, upload, verify)"
  adom-wiki release create "$PAGE" "$RC" --title "$VER release candidate $N (canary for the live CSWA gate; not the release)" \
    --changelog "Canary build of $VER for the live CSWA gate: installed only on the test machine, by hand, from $CANARY. Not a release: tools/release.sh --require-live publishes these same bytes as $VER once the live run passes."
  adom-wiki release upload "$PAGE" "$RC" "$DIST/$ZIP" --platform any
  adom-wiki release verify "$PAGE" "$RC" --platform any
  echo "== canary 2/4: $CANARY (committed: the publish reads it)"
  mkdir -p "$(dirname "$CANARY")"
  git show "HEAD:$MAN" | python3 -c '
import json, sys, time
ver, rc, sha, size, zipname, main_url, code, head, exe, path = sys.argv[1:]
m = json.load(sys.stdin)
m.update({"version": ver, "url": f"https://wiki.adom.inc/download/adom/solidworks-bridge/{rc}/{zipname}", "sha256": sha, "size": int(size),
          # SELF-HEALING: ab polls the INSTALLED bridge.json updateManifestUrl first (every zip declares the main
          # manifest there); this field says the same, so nothing pins the test machine to the canary.
          "updateManifestUrl": main_url})
m["canary"] = {"rc": rc, "codeCommit": code, "commit": head, "exeSha256": exe, "zip": zipname,
               "builtAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
               "note": "a release CANDIDATE for the live CSWA gate: install it on the test machine only (bridge_install with this file); "
                       "tools/release.sh --require-live publishes these same bytes as the release"}
json.dump(m, open(path, "w"), indent=2)
print("canary ->", m["url"])
' "$VER" "$RC" "$SHA" "$SIZE" "$ZIP" "$MAIN_URL" "$CODE_COMMIT" "$(git rev-parse HEAD)" "$EXE_SHA" "$CANARY"
  git add "$CANARY"
  git commit -q -m "canary: $RC (sha256 ${SHA:0:8}…, $SIZE bytes)" -m "Co-Authored-By: Claude Opus 5.5 <[email protected]>"
  echo "== canary 3/4: push $CANARY"
  adom-wiki repo push "$PAGE" --files "$CANARY" -m "canary: $RC"
  echo "== canary 4/4: read it back"
  adom-wiki api "/api/v1/pages/$SLUG/files/$CANARY" | python3 -c '
import json, sys
rc, sha = sys.argv[1:]
m = json.load(sys.stdin)
ok = (m.get("canary") or {}).get("rc") == rc and m.get("sha256") == sha
print("the page serves the canary:" if ok else "ERROR: the page does not serve this canary:", m.get("url"), m.get("sha256"))
sys.exit(0 if ok else 1)
' "$RC" "$SHA"
  echo
  echo "NEXT, ON THE TEST MACHINE ONLY - install the canary (ab checks the zip's sha256):"
  echo "  adom-bridge --target $TARGET --ai-thread \"SolidWorks Bridge\" bridge_install '{\"manifestUrl\":\"$CANARY_URL\"}'"
  echo "  (It does not pin the machine: every zip's bridge.json declares the MAIN manifest as updateManifestUrl, the URL ab polls first.)"
  echo "THEN THE LIVE CHECK (it records which exe it ran):"
  echo "  python3 tools/cswa-check.py --target $TARGET --report auto"
  echo "THEN PUBLISH THESE BYTES:"
  echo "  tools/release.sh --require-live tools/cswa/reports/<that report> --publish"
  exit 0
fi

if [ "$PUBLISH" = 1 ]; then
  # ── ONE COMMAND, IN ORDER (0.9.12). The wiki sync drifted when its steps were
  #    run by hand (a hero/title/brief unlinked by a pkg publish; a README verb
  #    count three behind). Every step below runs here, in this order, and the
  #    last one verifies the page end to end and FAILS LOUDLY on any mismatch.
  #
  #    PRECONDITIONS, before anything touches the network: page.json carries
  #    package.json's tags and discovery triggers, the tree is committed, and the
  #    committed manifest is the one being shipped (it is rewritten above, so the
  #    only difference allowed is in the manifest itself).
  python3 tools/sync-page.py --check || { echo "ERROR: page.json is out of sync with package.json - run tools/sync-page.py and commit"; exit 1; }
  DIRTY="$(git status --porcelain | grep -v " $MAN\$" || true)"
  [ -z "$DIRTY" ] || { echo "ERROR: uncommitted changes besides the manifest:"; echo "$DIRTY"; exit 1; }
  PKGV="$(python3 -c 'import json;print(json.load(open("package.json"))["version"])')"
  [ "$PKGV" = "$VER" ] || { echo "ERROR: package.json version $PKGV != BRIDGE_VERSION $VER"; exit 1; }
  TITLE="${RELEASE_TITLE:-v$VER}"; LOG="${RELEASE_CHANGELOG:-see docs/}"
  # A docs-only release SAYS SO where people read it: no live CSWA run stands behind it.
  if [ "$DOCS_ONLY" = 1 ]; then
    LOG="Docs-only release: no code changed since $LAST_VER (tools/check-docs-only.py), so the live CSWA gate was skipped. $LOG"
  fi
  echo "== 1/7 release create + upload"
  adom-wiki release create "$PAGE" "$VER" --title "$TITLE" --changelog "$LOG" \
    || echo "(release $VER may already exist - continuing to upload)"
  adom-wiki release upload "$PAGE" "$VER" "$DIST/$ZIP" --platform any
  echo "== 2/7 manifest push"
  # manifest lives at /api/v1/pages/<slug>/files/<name> - pushed as a page file
  adom-wiki repo push "$PAGE" --files "$MAN" -m "manifest: $VER as published"
  echo "== 3/7 commit the rewritten manifest"
  git add "$MAN"
  git commit -q -m "manifest: $VER as published (sha256 ${SHA:0:8}…, $SIZE bytes)" \
    -m "Co-Authored-By: Claude Opus 5.5 <[email protected]>" || echo "(manifest already committed)"
  echo "== 4/7 source push (every tracked file, the committed manifest included)"
  # shellcheck disable=SC2046
  adom-wiki repo push "$PAGE" --json -m "$VER source" --files $(git ls-files) >/dev/null
  echo "== 5/7 skills package"
  adom-wiki pkg publish --org adom
  # A pkg publish REWRITES the release's title and changelog to "Release <ver>"
  # (measured 2026-10-02 on 0.9.10-0.9.12): put them back.
  adom-wiki release edit "$PAGE" "$VER" --title "$TITLE" --changelog "$LOG" >/dev/null
  echo "== 6/7 page.json LAST (a pkg publish can unlink the hero, title and brief)"
  adom-wiki repo push "$PAGE" --files page.json -m "page.json: $VER (hero, title, brief, tags)"
  echo "== 7/7 verify"
  RELEASE_TITLE="$TITLE" python3 tools/verify-wiki.py "$VER" "$SHA" "$SIZE" "$ZIP" || { echo "ERROR: THE WIKI DOES NOT MATCH THIS RELEASE - see above"; exit 1; }
  if [ "$PROMOTE" = 1 ]; then
    echo
    echo "THE TEST MACHINE runs the canary $C_RC - the same bytes as this release. ab polls the installed bridge.json's"
    echo "updateManifestUrl (the main manifest) first, so it follows the main channel from its next poll (launch, then"
    echo "every 4 h). If bridge_info there still names $CANARY as its manifestUrl, re-point it:"
    echo "  adom-bridge --target $TARGET --ai-thread \"SolidWorks Bridge\" bridge_install '{\"manifestUrl\":\"$MAIN_URL\"}'"
  fi
fi