diff --git a/src/releases.ts b/src/releases.ts index 30310fd..49c48c5 100644 --- a/src/releases.ts +++ b/src/releases.ts @@ -428,8 +428,10 @@ function dbReleaseToMetadata(release: DbRelease, sku: string): ReleaseMetadata { } /** - * Newest fully rolled out release for the prefix, or null when no release - * has reached 100% yet (a prefix whose first release is still staged). + * Newest fully rolled out release that ships a binary for the SKU, or null + * when there is none: the prefix's first release is still staged, or the SKU + * is new and its first build has not reached 100% yet. The caller decides + * whether the device is in the staged release's bucket before it needs this. */ async function getDefaultRelease(prefix: string, sku: string): Promise { const rolledOutReleases = await prisma.release.findMany({ @@ -447,9 +449,7 @@ async function getDefaultRelease(prefix: string, sku: string): Promise r.artifacts.length > 0); if (compatibleReleases.length === 0) { - throw new NotFoundError( - `No default ${prefix} release available for SKU "${sku}"`, - ); + return null; } const latestVersion = semver.maxSatisfying( diff --git a/test/releases.test.ts b/test/releases.test.ts index 9d672bc..2f0c189 100644 --- a/test/releases.test.ts +++ b/test/releases.test.ts @@ -608,6 +608,21 @@ describe("Retrieve handler", () => { Retrieve(createMockRequest({ deviceId: "late-adopter" }), createMockResponse()), ).rejects.toThrow(/No (app|system) release is rolled out yet for SKU "jetkvm-v2"/); }); + + it("serves the first staged release for a new SKU when no 100% release ships it", async () => { + // The seeded 100% releases carry jetkvm-v2 only. The first SDMMC build + // is staged at 50%: early-adopter (bucket 8) is in, late-adopter (95) out. + await createDbRelease("app", "1.3.0", 50, [releaseArtifact("app", "1.3.0", SDMMC_SKU)]); + await createDbRelease("system", "1.3.0", 50, [releaseArtifact("system", "1.3.0", SDMMC_SKU)]); + + const inBucket = createMockResponse(); + await Retrieve(createMockRequest({ deviceId: "early-adopter", sku: SDMMC_SKU }), inBucket); + expect(jsonBody(inBucket)).toMatchObject({ appVersion: "1.3.0", systemVersion: "1.3.0" }); + + await expect( + Retrieve(createMockRequest({ deviceId: "late-adopter", sku: SDMMC_SKU }), createMockResponse()), + ).rejects.toThrow(/No (app|system) release is rolled out yet for SKU "jetkvm-v2-sdmmc"/); + }); }); describe("signature URL handling", () => {