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.
This commit is contained in:
Adam Shiervani
2026-04-27 16:39:13 +02:00
parent c9e9f92840
commit 9cd63a1acd
4 changed files with 625 additions and 743 deletions
+17 -5
View File
@@ -391,6 +391,7 @@ function toRelease(
function addStableSigUrls(release: Release): void {
if (release.appUrl) release.appSigUrl = `${release.appUrl}.sig`;
if (release.systemUrl) release.systemSigUrl = `${release.systemUrl}.sig`;
}
async function getReleaseFromS3(
@@ -426,6 +427,13 @@ function compatibleArtifactSelect(sku: string) {
};
}
function compatibleReleaseWhere(type: ReleaseType, sku: string) {
return {
type,
artifacts: { some: { compatibleSkus: { has: sku } } },
} as const;
}
function compatibleReleaseSelect(sku: string) {
return {
version: true,
@@ -457,12 +465,14 @@ function dbReleaseToMetadata(
async function getDefaultRelease(type: ReleaseType, sku: string): Promise<DbRelease> {
const rolledOutReleases = await prisma.release.findMany({
where: { rolloutPercentage: 100, type },
where: { ...compatibleReleaseWhere(type, sku), rolloutPercentage: 100 },
select: compatibleReleaseSelect(sku),
});
if (rolledOutReleases.length === 0) {
throw new InternalServerError(`No default release found for type ${type}`);
throw new InternalServerError(
`No default release found for type ${type} and SKU "${sku}"`,
);
}
// Get the latest default version from the rolled out releases
@@ -475,7 +485,9 @@ async function getDefaultRelease(type: ReleaseType, sku: string): Promise<DbRele
const latestDefaultRelease = rolledOutReleases.find(r => r.version === latestVersion);
if (!latestDefaultRelease) {
throw new InternalServerError(`No default release found for type ${type}`);
throw new InternalServerError(
`No default release found for type ${type} and SKU "${sku}"`,
);
}
return latestDefaultRelease;
@@ -491,12 +503,12 @@ async function getReleaseByRange(
range: string,
): Promise<DbRelease> {
const releases = await prisma.release.findMany({
where: { type },
where: compatibleReleaseWhere(type, sku),
select: compatibleReleaseSelect(sku),
});
if (releases.length === 0) {
throw new NotFoundError(`No release found for type ${type}`);
throw new NotFoundError(`No release found for type ${type} and SKU "${sku}"`);
}
const latestVersion = semver.maxSatisfying(
+426 -737
View File
File diff suppressed because it is too large Load Diff
+180
View File
@@ -0,0 +1,180 @@
import {
GetObjectCommand,
HeadObjectCommand,
ListObjectsV2Command,
S3Client,
} from "@aws-sdk/client-s3";
import { describe, expect, beforeEach, it } from "vitest";
import { collectReleaseArtifacts, syncReleases } from "../scripts/sync-releases";
import { createAsyncIterable, s3Mock, testPrisma } from "./setup";
const DEFAULT_SKU = "jetkvm-v2";
const SDMMC_SKU = "jetkvm-v2-sdmmc";
const SYNC_BUCKET = "test-bucket";
const SYNC_BASE_URL = "https://cdn.test.com";
const syncS3Client = new S3Client({});
function mockS3ListVersions(prefix: "app" | "system", versions: string[]) {
s3Mock.on(ListObjectsV2Command, { Prefix: `${prefix}/` }).resolves({
CommonPrefixes: versions.map(v => ({ Prefix: `${prefix}/${v}/` })),
});
}
function mockS3HashFile(prefix: "app" | "system", version: string, hash: string) {
const fileName = prefix === "app" ? "jetkvm_app" : "system.tar";
s3Mock.on(ListObjectsV2Command, { Prefix: `${prefix}/${version}/skus/` }).resolves({
Contents: [],
});
s3Mock
.on(GetObjectCommand, { Key: `${prefix}/${version}/${fileName}.sha256` })
.resolves({
Body: createAsyncIterable(hash) as any,
});
}
function mockS3SkuVersion(
prefix: "app" | "system",
version: string,
sku: string,
hash: string,
) {
const fileName = prefix === "app" ? "jetkvm_app" : "system.tar";
const skuPath = `${prefix}/${version}/skus/${sku}/${fileName}`;
s3Mock.on(ListObjectsV2Command, { Prefix: `${prefix}/${version}/skus/` }).resolves({
Contents: [{ Key: skuPath }],
});
s3Mock.on(HeadObjectCommand, { Key: skuPath }).resolves({});
s3Mock.on(GetObjectCommand, { Key: `${skuPath}.sha256` }).resolves({
Body: createAsyncIterable(hash) as any,
});
}
describe("sync-releases script", () => {
beforeEach(() => {
s3Mock.reset();
s3Mock
.on(HeadObjectCommand)
.rejects({ name: "NotFound", $metadata: { httpStatusCode: 404 } });
});
it("marks legacy app artifacts compatible with all known SKUs", async () => {
mockS3HashFile("app", "9.9.1", "legacy-app-hash");
const artifacts = await collectReleaseArtifacts(
{ s3Client: syncS3Client },
{ bucketName: SYNC_BUCKET, baseUrl: SYNC_BASE_URL },
"app",
"9.9.1",
);
expect(artifacts).toEqual([
{
url: "https://cdn.test.com/app/9.9.1/jetkvm_app",
hash: "legacy-app-hash",
compatibleSkus: [DEFAULT_SKU, SDMMC_SKU],
},
]);
});
it("marks legacy system artifacts compatible with only the default SKU", async () => {
mockS3HashFile("system", "9.9.2", "legacy-system-hash");
const artifacts = await collectReleaseArtifacts(
{ s3Client: syncS3Client },
{ bucketName: SYNC_BUCKET, baseUrl: SYNC_BASE_URL },
"system",
"9.9.2",
);
expect(artifacts).toEqual([
{
url: "https://cdn.test.com/system/9.9.2/system.tar",
hash: "legacy-system-hash",
compatibleSkus: [DEFAULT_SKU],
},
]);
});
it("collects only SKU artifacts that exist and have a hash", async () => {
mockS3SkuVersion("system", "9.9.3", DEFAULT_SKU, "system-default-hash");
const artifacts = await collectReleaseArtifacts(
{ s3Client: syncS3Client },
{ bucketName: SYNC_BUCKET, baseUrl: SYNC_BASE_URL },
"system",
"9.9.3",
);
expect(artifacts).toEqual([
{
url: `https://cdn.test.com/system/9.9.3/skus/${DEFAULT_SKU}/system.tar`,
hash: "system-default-hash",
compatibleSkus: [DEFAULT_SKU],
},
]);
});
it("syncs stable S3 artifacts into DB without changing existing rollout", async () => {
const version = "9.9.4";
await testPrisma.release.create({
data: {
version,
type: "system",
rolloutPercentage: 77,
url: "https://cdn.test.com/old-system.tar",
hash: "old-system-hash",
},
});
mockS3ListVersions("app", [version, "10.0.0-beta.1"]);
mockS3ListVersions("system", [version]);
mockS3HashFile("app", version, "app-hash");
mockS3SkuVersion("system", version, DEFAULT_SKU, "system-hash-v2");
mockS3SkuVersion("system", version, SDMMC_SKU, "system-hash-sdmmc");
await syncReleases(
{ prisma: testPrisma, s3Client: syncS3Client },
{ bucketName: SYNC_BUCKET, baseUrl: SYNC_BASE_URL },
);
const appRelease = await testPrisma.release.findUniqueOrThrow({
where: { version_type: { version, type: "app" } },
include: { artifacts: true },
});
const systemRelease = await testPrisma.release.findUniqueOrThrow({
where: { version_type: { version, type: "system" } },
include: { artifacts: { orderBy: { url: "asc" } } },
});
const prerelease = await testPrisma.release.findUnique({
where: { version_type: { version: "10.0.0-beta.1", type: "app" } },
});
expect(appRelease.rolloutPercentage).toBe(10);
expect(appRelease.artifacts).toEqual([
expect.objectContaining({
url: `https://cdn.test.com/app/${version}/jetkvm_app`,
hash: "app-hash",
compatibleSkus: [DEFAULT_SKU, SDMMC_SKU],
}),
]);
expect(systemRelease.rolloutPercentage).toBe(77);
expect(systemRelease.artifacts).toEqual(
expect.arrayContaining([
expect.objectContaining({
url: `https://cdn.test.com/system/${version}/skus/${DEFAULT_SKU}/system.tar`,
hash: "system-hash-v2",
compatibleSkus: [DEFAULT_SKU],
}),
expect.objectContaining({
url: `https://cdn.test.com/system/${version}/skus/${SDMMC_SKU}/system.tar`,
hash: "system-hash-sdmmc",
compatibleSkus: [SDMMC_SKU],
}),
]),
);
expect(prerelease).toBeNull();
});
});
+2 -1
View File
@@ -8,6 +8,7 @@ export default defineConfig({
testTimeout: 30000,
hookTimeout: 30000,
include: ["test/**/*.test.ts"],
silent: "passed-only"
silent: "passed-only",
fileParallelism: false,
},
});