fix: keep default release when latest has no compatible SKU artifact

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.
This commit is contained in:
Adam Shiervani
2026-04-27 20:01:56 +02:00
parent 68b10004b5
commit eebf332bc0
2 changed files with 29 additions and 16 deletions
+9 -5
View File
@@ -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));
}
+20 -11
View File
@@ -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 () => {