clients: an unquoted 1.0.3504551 is not JSON, and every sidecar was one
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 14s
CI & Build / integration (push) Successful in 16s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m0s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m27s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 8m18s

`fetch-clients.sh` wrote `"version_code": %s` unquoted, which was right when the
only ordering key in sight was Android's integer. The desktop's is Tauri's
`1.0.<minutes>`, and unquoted that is not valid JSON at all — so `json.loads`
raised on all four generated sidecars and the server advertised nothing. A silent
zero, not an error: `_read` treats a malformed sidecar as "no client here", which
is right for a corrupt drop-in and indistinguishable from this.

Caught by running the real fetch against the live dev channel and feeding the
result to the real resolver, rather than by reading the printf.

Also makes `_resolve` wrap BOTH candidate roots in Path(). Only the first was, and
the asymmetry fails the same quiet way: a str `/` str raises TypeError, `_read`
catches it, and a perfectly good directory reads as empty.

The whole pipeline now resolves end to end against the live channel — five of five
platforms, every sidecar valid JSON, one human-readable version across all of them
with each artifact keeping its own comparator type:

  android         2026.08.30.1711  code=3504552        57.6 MB
  linux-appimage  2026.08.30.1711  code='1.0.3504551'  95.3 MB  signed
  linux-deb       2026.08.30.1711  code='1.0.3504551'   3.3 MB
  linux-pacman    2026.08.30.1711  code='1.0.3504551'   2.7 MB
  windows         2026.08.30.1711  code='1.0.3504551'   2.6 MB
This commit is contained in:
Bryan Van Deusen
2026-08-30 13:21:03 -04:00
parent d2f9d316cf
commit 8a75e5f340
2 changed files with 10 additions and 2 deletions
+6 -1
View File
@@ -69,7 +69,12 @@ digest() { sha256sum "$1" | cut -d' ' -f1; }
# whole one.
sidecar() {
_file="$1"; _out="$2"; _name="$3"; _code="$4"
printf '{\n "version_name": "%s",\n "version_code": %s,\n "size": %s,\n "sha256": "%s"\n}\n' \
# `version_code` is QUOTED here, and that is not a slip. This function only ever
# writes DESKTOP sidecars, whose ordering key is Tauri's `1.0.<minutes>` — which
# unquoted is not valid JSON at all, so every sidecar this wrote would fail to
# parse and the server would advertise nothing. Android's sidecar is a different
# file, copied verbatim from its lane, and keeps its integer.
printf '{\n "version_name": "%s",\n "version_code": "%s",\n "size": %s,\n "sha256": "%s"\n}\n' \
"$_name" "$_code" "$(bytes "$_file")" "$(digest "$_file")" > "$_out"
}
+4 -1
View File
@@ -263,7 +263,10 @@ def _resolve(platform: Platform) -> tuple[Path, dict] | None:
broken or half-copied pair does NOT shadow the image: it simply is not a
client, so the search moves on.
"""
for root in (Path(Config.client_root()), BAKED_ROOT):
# Both wrapped in Path(), not just the first. The asymmetry was arbitrary and it
# fails quietly: `_read` catches the TypeError a str `/` str raises and reports
# "no client here", so a directory that is perfectly fine reads as empty.
for root in (Path(Config.client_root()), Path(BAKED_ROOT)):
release = _read(root, platform)
if release is not None:
return root, release