From eebf332bc0b9d077003614bebcce76c73efb350d Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Mon, 27 Apr 2026 20:01:56 +0200 Subject: [PATCH] fix: keep default release when latest has no compatible SKU artifact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/releases.ts | 14 +++++++++----- test/releases.test.ts | 31 ++++++++++++++++++++----------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/releases.ts b/src/releases.ts index be53836..1985b6c 100644 --- a/src/releases.ts +++ b/src/releases.ts @@ -617,26 +617,30 @@ export async function Retrieve(req: Request, res: Response) { // Background update checks follow rollout percentages so new releases roll // out gradually. Devices outside the bucket fall back to the default (the - // newest 100%-rolled-out release). + // newest 100%-rolled-out release). If the latest release lacks a compatible + // artifact for this SKU (e.g. a SKU-specific build hasn't shipped yet) we + // silently keep the default rather than 404 the whole request. const responseJson = toRelease( dbReleaseToMetadata(defaultAppRelease, query.sku), dbReleaseToMetadata(defaultSystemRelease, query.sku), ); if ( - await isDeviceEligibleForLatestRelease( + latestAppRelease.artifacts.length > 0 && + (await isDeviceEligibleForLatestRelease( latestAppRelease.rolloutPercentage, query.deviceId, - ) + )) ) { setAppRelease(responseJson, dbReleaseToMetadata(latestAppRelease, query.sku)); } if ( - await isDeviceEligibleForLatestRelease( + latestSystemRelease.artifacts.length > 0 && + (await isDeviceEligibleForLatestRelease( latestSystemRelease.rolloutPercentage, query.deviceId, - ) + )) ) { setSystemRelease(responseJson, dbReleaseToMetadata(latestSystemRelease, query.sku)); } diff --git a/test/releases.test.ts b/test/releases.test.ts index b7fcfea..f13c9d0 100644 --- a/test/releases.test.ts +++ b/test/releases.test.ts @@ -489,7 +489,7 @@ describe("Retrieve handler", () => { }); }); - it("does not fall back when the latest release lacks a compatible artifact", async () => { + it("keeps the default when the latest release lacks a compatible artifact for an in-bucket device", async () => { await createDbRelease("app", "3.3.0", 100, [ { ...releaseArtifact("app", "3.3.0", DEFAULT_SKU), @@ -506,19 +506,28 @@ describe("Retrieve handler", () => { releaseArtifact("system", "3.3.0", DEFAULT_SKU, "system-default-hash"), releaseArtifact("system", "3.3.0", SDMMC_SKU, "system-sdmmc-hash"), ]); + // system 3.3.1 ships only with the default-SKU artifact — no sdmmc binary. await createDbRelease("system", "3.3.1", 100); - await expect( - Retrieve( - createMockRequest({ - deviceId: "sdmmc-compatible-fallback-device", - sku: SDMMC_SKU, - }), - createMockResponse(), - ), - ).rejects.toThrow( - 'Version 3.3.1 predates SKU support and cannot serve SKU "jetkvm-v2-sdmmc"', + // Every device is in-bucket at 100% rollout, so this exercises the + // upgrade path. The request must keep the default 3.3.0 system release + // rather than 404 because 3.3.1 has no sdmmc binary. + const res = createMockResponse(); + await Retrieve( + createMockRequest({ + deviceId: "sdmmc-compatible-fallback-device", + sku: SDMMC_SKU, + }), + res, ); + + expect(jsonBody(res)).toMatchObject({ + appVersion: "3.3.1", + appUrl: artifactUrl("app", "3.3.1"), + systemVersion: "3.3.0", + systemUrl: artifactUrl("system", "3.3.0", SDMMC_SKU), + systemHash: "system-sdmmc-hash", + }); }); it("does not discover or create stable releases from S3", async () => {