The system recovery endpoint hard-coded `update.img`, which is the
eMMC/RKDevTool image. The SDMMC variant ships as `update_sd.img.zip`
(a balenaEtcher-flashable archive), so requesting recovery for the
`jetkvm-v2-sdmmc` SKU returned the wrong artifact (or 404'd once SKU
folders were enforced). Drive the filename from a small per-SKU map
and reject unmapped SKUs with 400 so a typo can't silently fall back
to the wrong image.
After the getDefaultRelease SKU-compat fix, the default path was
graceful but the rollout-upgrade path stayed strict: if a device was in
the rollout bucket and the latest release lacked a compatible artifact
for the requested SKU, dbReleaseToMetadata still threw and 404'd the
whole request — even though responseJson already held a valid default.
Short-circuit the upgrade when the latest release has no compatible
artifact and update the regression test to assert the default is kept
instead of asserting the throw.
Adds a regression test for the rollout-aware path: when the in-progress
release has no compatible artifact for the requested SKU and the device
is outside the rollout window, the response must keep the default
release instead of throwing.
Also extends compare-releases.sh with a second device ID picked to land
in a low rollout bucket (bucket 9 vs the existing bucket 81), so the
script now exercises both eligible and ineligible rollout paths.
* feat: add SKU-aware OTA release artifacts
Persist OTA artifact URL/hash data separately from rollout state so stable release responses can choose artifacts by compatible SKU while release rollout remains version/type based.
* fix: select compatible OTA releases by SKU
Ensure stable release selection only considers releases with artifacts compatible with the requested SKU, and tighten tests around the DB-backed OTA contract.
* fix: match production OTA release responses
Only expose stable signature URLs that actually exist and preserve production's version-first SKU error behavior.
* fix: restrict legacy OTA artifacts and make sync create-only
Pre-SKU artifacts (no skus/ folder) are jetkvm-v2 only. Marking them
compatible with jetkvm-v2-sdmmc would brick devices that received
firmware predating their hardware. Future SKUs must opt in via an
explicit skus/<sku>/ upload.
sync-releases now skips releases already in the DB instead of upserting
them. This prevents routine sync runs from rewriting Release.url/hash
or appending duplicate ReleaseArtifact rows if R2_CDN_URL ever changes.
Backfills and repairs are left to one-off scripts.
* refactor: drop forceUpdate query parameter from /releases
The flag is no longer sent by any client. Routine update checks now
always go through the rollout-aware default-and-latest path, which is
what forceUpdate effectively short-circuited to. Removes one query
parameter, one branch in the handler, and the corresponding axis from
the compare-releases sweep.
* fix: skip incompatible defaults and parallelize stable DB lookups
getDefaultRelease previously picked the newest 100%-rolled-out release
without checking SKU compatibility. If that release lacked a compatible
artifact, the request 404'd downstream even though older 100%-rolled-out
releases had valid binaries for the SKU. It now filters to releases that
actually ship a compatible artifact before selecting the latest, falling
back to a 404 only when no compatible default exists.
The four DB lookups in the stable rollout-aware path are independent; run
them concurrently so background-check latency drops from ~4 round trips
to ~1.
* feat: serve optional GPG signature URLs for OTA releases
Resolve .sig file existence from S3 at response time and include
appSigUrl/systemSigUrl in the release payload when present. Works
across all code paths (prerelease, forceUpdate, rollout) and supports
backfilling signatures for older releases.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix releases sig URL cache typing
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>