- name
- sdk-publish
- description
- Manually publish an already-tagged, already-released (non-draft GitHub Release) RunAnywhere SDK version to every package registry (npm, Maven Central, pub.dev, Swift SPM dist repo) plus macOS rcli notarization, with heavy verification that nothing fake/stubbed/broken ships.
# SDK Publish
Repo: `~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (verify with `pwd`/`git remote get-url origin` — do not assume this path is still correct, checkouts move). Version source of truth: `core/VERSION`. Throughout, `$VERSION` means the bare semver being published (e.g. `0.20.24`), never prefixed with `v` except in git tags / GitHub Release names.
## THIS PROCESS IS INTENTIONALLY MANUAL
**`release.yml` never publishes anywhere public.** It only builds artifacts, packages them, and creates a draft/published **GitHub Release**. It does not call `npm publish`, does not upload to Maven Central, does not call `flutter pub publish`, and does not push the Swift dist repo. Verify this is still true before trusting it — greps below should all return **zero** matches (grep the whole file, not a snippet):
```bash
cd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks
grep -inE "npm publish|publishReleasePublication|publishAllPublicationsToMavenCentral|flutter pub publish" .github/workflows/release.yml
```
This is a **deliberate safety gate**, not a gap. Every step below is a human (or an agent acting on a human's explicit go-ahead) pushing a button that CI intentionally never pushes. Do not propose wiring any of this into CI as a "fix" — that would remove the gate.
## Precondition check (always run first)
Refuse to proceed if the release is a draft or the asset set looks incomplete.
```bash
gh release view v$VERSION --json isDraft,tagName --jq '{isDraft, tagName}'
# isDraft MUST be false.
gh release view v$VERSION --json assets --jq '.assets[].name' | sort
```
Cross-reference the listing against the `sdk-release` skill's step 8 asset-count expectations (`~/.claude/skills/sdk-release/SKILL.md`). As a second, independent check, also diff against the **previous** tagged release's asset list — same count, same name patterns, only the version token differs:
```bash
gh release view v<PREVIOUS_VERSION> --json assets --jq '.assets[].name' | sed "s/<PREVIOUS_VERSION>/$VERSION/" | sort >/tmp/expected.txt
gh release view v$VERSION --json assets --jq '.assets[].name' | sort >/tmp/actual.txt
diff /tmp/expected.txt /tmp/actual.txt
```
As of the last verified run (**v0.20.25, 65 assets** — up from 55 at v0.20.24 because the 5 public Electron tarballs and their sidecars joined the release; see §6), the full set is: `MANIFEST.txt`; per-platform `RACommons-{android-arm64-v8a,android-armeabi-v7a,android-x86_64,ios,linux-x86_64,web,windows-x64}-v$VERSION.{zip,tar.gz}` + `.sha256`; iOS backend zips `RABackend{LLAMACPP,MLX,NeuRT,ONNX,Sherpa}-ios-v$VERSION.zip` + `.sha256`; `RunAnywhereMLX{Metal,Resources,Runtime}-ios-v$VERSION.zip` + `.sha256`; `rcli-{linux-x86_64,macos-arm64,windows-x86_64}-v$VERSION.tar.gz`/`.zip` + `.sha256`; the 8 npm tarballs `runanywhere-{core,llamacpp,mlx,onnx,proto-ts,web,web-llamacpp,web-onnx}-$VERSION.tgz` + `.sha256`; the **5 Electron npm tarballs** `runanywhere-electron{,-llamacpp,-onnx,-sherpa,-neurt}-$VERSION.tgz` + `.sha256` (new as of v0.20.25); and `runanywhere-kotlin-maven-v$VERSION.zip` + `.sha256`. There is **no** Flutter tarball in the release (Flutter packages are staged from the native archives above, not from a dedicated release asset) and **no** `.dmg` asset in the baseline set (the macOS rcli tarball is what ships; see the notarization section for when a `.dmg` does get produced). If a `qhexrt`-named asset appears anywhere in this list, stop — that would mean `release.yml`'s own manifest assertion should have hard-failed and something is very wrong; do not publish anything from that release.
## 0. MANDATORY GATE — verify the ENTIRE release before pushing anything to any registry
**Run this once, in full, before the first registry push. Do not publish a single package until it passes.** Per-package spot-checks are not a substitute: they are how a bad artifact reaches a registry you cannot un-publish from. Proven end to end on v0.20.25 (2026-08-22), then independently audited and corrected: **34 payloads, 6619 files across 3 nesting levels, 282 binaries, 0 placeholder hits, 38/38 commons binaries carrying the real staging host**. The audit found 4 methodology gaps in the first pass (shallow nesting, missing `MTLB` magic, `.lib` excluded from the positive check, and a set-intersection that could pass vacuously) — every one is called out inline below, because each produced a *confident but under-covered* result.
```bash
VERSION=0.20.25 # no leading v
REPO=RunanywhereAI/runanywhere-sdks
W=/tmp/relverify-$VERSION
rm -rf "$W" && mkdir -p "$W/assets" "$W/qhexrt" "$W/x" "$W/nested"
# (1) EVERY release asset — no -p filter, get the whole set.
gh release download "v$VERSION" --repo "$REPO" -D "$W/assets"
# (2) The PRIVATE QHexRT artifact is NOT a release asset — pull it from the run.
# Find the run that produced the release, then:
gh run download <release-run-id> --repo "$REPO" --name sdk-electron-qhexrt-private --dir "$W/qhexrt"
```
### (3) Checksums — `cd` into the directory first
Sidecars contain a **bare basename**, so `shasum -c` fails with a misleading "FAILED open or read" if run from anywhere else. This will make every artifact look broken when nothing is wrong:
```bash
cd "$W/assets"
ok=0; bad=0
for s in *.sha256; do
if shasum -a 256 -c "$s" >/dev/null 2>&1; then ok=$((ok+1)); else echo "MISMATCH: $s"; bad=$((bad+1)); fi
done
echo "verified=$ok mismatched=$bad" # bad MUST be 0
(cd "$W/qhexrt" && shasum -a 256 -c ./*.sha256)
```
### (4) Extract everything — INCLUDING nested archives
**The gotcha that matters:** `runanywhere-kotlin-maven-v*.zip` contains `.aar` files, and the `.aar`s are what hold the Android `.so` binaries. A one-level extraction reports "0 binaries" for the Maven bundle and every subsequent scan passes **vacuously**.
**Recurse until the tree is dry — TWO levels are required here, not one.** Each `.aar` also contains a `classes.jar`, holding ~2027 `.class` files that are doubly unreachable: inside an unopened jar *and* excluded by the `.class` suffix rule most magic-byte scanners use. Level 2 adds **2033 files** (0 placeholder hits on v0.20.25); level 3 is empty. Loop until a pass finds no new archives rather than hardcoding a depth.
```bash
cd "$W/assets"
for f in *; do
case "$f" in *.sha256|MANIFEST.txt) continue;; esac
d="$W/x/$f"; mkdir -p "$d"
case "$f" in
*.zip) unzip -qq -o "$f" -d "$d" || echo "EXTRACT FAILED: $f" ;;
*.tar.gz|*.tgz) tar -xzf "$f" -C "$d" || echo "EXTRACT FAILED: $f" ;;
*) echo "UNHANDLED TYPE: $f" ;;
esac
done
d="$W/x/$(basename "$W"/qhexrt/*.tgz)"; mkdir -p "$d"; tar -xzf "$W"/qhexrt/*.tgz -C "$d"
# nested pass
cd "$W/x"
find . -type f \( -name '*.aar' -o -name '*.jar' -o -name '*.zip' -o -name '*.tgz' -o -name '*.tar.gz' \) \
| sed 's|^\./||' | while IFS= read -r a; do
dd="$W/nested/$(printf '%s' "$a" | tr '/' '_')"; mkdir -p "$dd"
case "$a" in *.aar|*.jar|*.zip) unzip -qq -o "$a" -d "$dd";; *) tar -xzf "$a" -C "$dd";; esac
done
```
**Prove extraction actually happened** — an empty dir makes "0 hits" meaningless:
```bash
for d in "$W"/x/*/; do
n=$(find "$d" -type f | wc -l)
[ "$n" -lt 2 ] && echo "SUSPICIOUSLY EMPTY: $d ($n files)"
done
```
### (5) Placeholder scan — EVERY file, not just binaries
The staging URL can also live in JS/JSON/TS config, so scan by content across all file types, and additionally scan the raw compressed streams in case a member never got extracted:
```bash
find "$W/x" "$W/nested" -type f | wc -l # total files covered
grep -rla "YOUR_STAGING_BASE_URL" "$W/x" "$W/nested" | tee /tmp/ph.txt | wc -l # MUST be 0
for f in "$W"/assets/*.tgz "$W"/assets/*.tar.gz "$W"/qhexrt/*.tgz; do
gzip -dc "$f" 2>/dev/null | grep -qa "YOUR_STAGING_BASE_URL" && echo "RAW HIT: $f"
done
```
Find binaries by **magic bytes, not extension** (an unsuffixed framework binary like `RACommons.framework/RACommons` has no extension and a name-based `find` misses it). Signatures to cover:
| Magic | Kind | v0.20.25 count |
|---|---|---|
| `7f454c46` | ELF | 177 |
| `!<arch>` (`213c617263683e0a`) | static `.a` **and `.lib`** | 41 |
| `MZ` | PE | 29 |
| `cffaedfe` / `cefaedfe` | Mach-O | 19 |
| `00 61 73 6d` | wasm | 10 |
| **`4d544c42` (`MTLB`)** | **Metal library** — easy to miss | 5 |
| `cafebabe` / `bebafeca` | fat Mach-O (exclude `.jar`/`.class`) | 1 |
**Total: 282 binaries.** An earlier pass reported 277 because `MTLB` was absent from the signature list — `.metallib` files were invisible to the binary scan (they were still covered by the all-file `grep` in step 5, so the conclusion held, but the count was wrong). If your total comes in materially under ~282 for a comparable release, your detection is leaking.
### (6) POSITIVE staging-URL check — absence of the placeholder is NOT sufficient
A build with an **empty** URL baked in passes a placeholder-only check. `check_plugin_natives.py` only tests absence, so add the positive assertion yourself.
Two traps that produced false confidence on the v0.20.25 pass, both worth avoiding:
- **Include `.lib`.** A `MUST_HAVE` regex of `rac_commons\.(dylib|so|dll|a)$` silently skips
`RACommons-windows-x64/x64/rac_commons.lib` — an **89 MB** binary that then gets no positive
check at all. Match `(dylib|so|dll|a|lib)` plus `\.wasm$`. Correct coverage is **38** binaries,
not 37.
- **Assert per binary, not by set-intersection.** Intersecting "non-public origins across all
files" yields more than one origin here (`hf.co` *and* the staging host), so a binary carrying
only `hf.co` would pass **vacuously**. Check each binary individually for the staging host.
```bash
# v0.20.25 baseline: 38 commons/wasm/lib binaries; 0 missing the staging host.
```
Never print the URL — it is a repo secret. Report a truncated SHA-256 fingerprint, or just assert presence/absence counts.
### (7) Routability gate over the whole tree
```bash
python3 scripts/validation/gates/check_plugin_natives.py "$W/x" "$W/nested" > /tmp/gate.txt 2>&1
grep -cE '^ ok ' /tmp/gate.txt; grep -E '^ FAIL' /tmp/gate.txt
```
v0.20.25 baseline: **77 ok, 1 FAIL**. The single expected FAIL is
`RACommons-linux-x86_64/lib/librunanywhere_sherpa.so` — see the known-exception note below. **Any other FAIL blocks the publish.**
### (8) Known exception — Linux sherpa is a non-routable shell (PRE-EXISTING, not a regression)
`RACommons-linux-x86_64-v*.tar.gz` ships **no** `libsherpa-onnx-c-api.so` at all and its `librac_backend_sherpa.so` is a ~156 KB shell (~3 sherpa strings, vs ~31 MB / ~192 for a healthy one on Electron macOS) — the engine is not linked in, so the carrier cannot reference `g_sherpa_{stt,tts,vad}_ops` and **STT/TTS/VAD cannot work from that bundle**. Android's carriers pass on all 3 ABIs, so this is Linux-only.
**Compare the CARRIER, not the backend.** The gate FAILs on `librunanywhere_sherpa.so` (the carrier); citing `librac_backend_sherpa.so` as your evidence is comparing a *different file* and is not proof. The carrier is byte-identical across releases, which is the real proof:
```bash
gh release download "v<PREV>" --repo "$REPO" -p 'RACommons-linux-x86_64-v<PREV>.tar.gz' -D /tmp/prev
tar -xzf /tmp/prev/*.tar.gz -C /tmp/prev
shasum -a 256 /tmp/prev/lib/librunanywhere_sherpa.so "$W"/x/RACommons-linux-*/lib/librunanywhere_sherpa.so
# v0.20.24 and v0.20.25 both: size=15712 sha256=28ef16015b61da86...
# strongest check — run the gate on the PREVIOUS bundle and confirm the identical FAIL:
python3 scripts/validation/gates/check_plugin_natives.py /tmp/prev
```
If it ever regresses *further* (e.g. Android or macOS sherpa also goes hollow), stop and treat it as a blocking defect.
### (9) Account for every payload explicitly
Print a per-artifact table (checksum verdict, binary count, placeholder hits) and confirm the row count equals the payload count. Artifacts that legitimately contain **no** native binaries, and why — do not let these pass silently unexamined:
| Artifact | Why it has no binaries |
|---|---|
| `RunAnywhereMLXResources-ios-*.zip` | 25 third-party license/notice text files |
| `runanywhere-kotlin-maven-v*.zip` | binaries live in the **nested** `.aar`s (step 4) |
| `runanywhere-proto-ts-*.tgz` | pure TS/JS (69 `.ts` + 69 `.js`) |
Everything else must have a non-zero, plausible binary count. A platform suddenly shipping fewer binaries than the previous release is a regression signal worth chasing before publishing.
### (10) Blind spot to be aware of — Windows x64 has NO routability coverage
`RACommons-windows-x64-v*.zip` ships **only** a static `rac_commons.lib` (~89 MB) plus 152 `.h` / 11 `.hpp` headers — **zero backend DLLs**, where Linux/Android/Web each ship 4–5 backend carriers. That is consistent with a static-embedding bundle (consumers link the engines in themselves), but the consequence matters: **no gate can detect a missing or hollow Windows backend**, because there is no carrier to inspect. `check_plugin_natives.py` therefore reports nothing at all for this artifact rather than reporting health.
Windows x64 is also *advisory* in `release.yml` (its absence does not fail the release). So a Windows regression can pass every automated check. If Windows correctness matters for a given release, verify it by actually running something on the real Windows box (see §Electron for how to reach it) — do not infer it from a green pipeline. Worth a deliberate confirmation with the repo owner that the DLL-free shape is intentional.
---
## General verification discipline (every artifact, every registry)
GitHub에서 보기