feat(ota): add release selection primitives

This commit is contained in:
DIYgod 2026-04-10 18:50:50 +08:00
parent 90256160e3
commit 862e0e0bc9
3 changed files with 124 additions and 0 deletions

View File

@ -0,0 +1,83 @@
import { describe, expect, it } from "vitest"
import type { OtaRelease } from "../lib/schema"
import { compareSemver, selectLatestCompatibleRelease } from "../lib/version"
function createRelease(overrides: Partial<OtaRelease> = {}): OtaRelease {
return {
schemaVersion: 1,
product: "mobile",
channel: "production",
releaseVersion: "0.4.2",
releaseKind: "ota",
runtimeVersion: "0.4.1",
publishedAt: "2026-04-10T12:00:00Z",
git: {
tag: "mobile/v0.4.2",
commit: "abcdef1234567890",
},
policy: {
storeRequired: false,
minSupportedBinaryVersion: "0.4.1",
message: null,
},
platforms: {
ios: {
launchAsset: {
path: "bundles/ios-main.js",
sha256: "a".repeat(64),
contentType: "application/javascript",
},
assets: [],
},
},
...overrides,
}
}
describe("compareSemver", () => {
it("orders plain x.y.z versions numerically", () => {
expect(compareSemver("0.4.10", "0.4.2")).toBeGreaterThan(0)
expect(compareSemver("1.0.0", "1.0.0")).toBe(0)
expect(compareSemver("0.4.2", "0.5.0")).toBeLessThan(0)
})
})
describe("selectLatestCompatibleRelease", () => {
it("selects the highest OTA release for the same runtime", () => {
const result = selectLatestCompatibleRelease(
[
createRelease({
releaseVersion: "0.4.2",
git: {
tag: "mobile/v0.4.2",
commit: "abcdef1234567890",
},
}),
createRelease({
releaseVersion: "0.4.3",
git: {
tag: "mobile/v0.4.3",
commit: "bcdef1234567890a",
},
}),
createRelease({
releaseVersion: "0.4.4",
runtimeVersion: "0.4.3",
git: {
tag: "mobile/v0.4.4",
commit: "cdef1234567890ab",
},
}),
],
{
product: "mobile",
channel: "production",
runtimeVersion: "0.4.1",
platform: "ios",
},
)
expect(result?.releaseVersion).toBe("0.4.3")
})
})

View File

@ -2,6 +2,9 @@ import { z } from "zod"
const semver = z.string().regex(/^\d+\.\d+\.\d+$/)
const sha256 = z.string().regex(/^[a-f0-9]{64}$/)
const otaPlatforms = ["ios", "android", "macos", "windows", "linux"] as const
export const otaPlatformSchema = z.enum(otaPlatforms)
const assetSchema = z.object({
path: z.string().min(1),
@ -46,3 +49,6 @@ export const otaReleaseSchema = z.object({
}),
platforms: platformsSchema,
})
export type OtaPlatform = z.infer<typeof otaPlatformSchema>
export type OtaRelease = z.infer<typeof otaReleaseSchema>

View File

@ -0,0 +1,35 @@
import type { OtaPlatform, OtaRelease } from "./schema"
export function compareSemver(left: string, right: string) {
const leftParts = left.split(".").map(Number)
const rightParts = right.split(".").map(Number)
for (let index = 0; index < 3; index += 1) {
const diff = leftParts[index]! - rightParts[index]!
if (diff !== 0) {
return diff
}
}
return 0
}
export function selectLatestCompatibleRelease(
releases: OtaRelease[],
input: {
product: OtaRelease["product"]
channel: OtaRelease["channel"]
runtimeVersion: OtaRelease["runtimeVersion"]
platform: OtaPlatform
},
) {
return (
releases
.filter((release) => release.product === input.product)
.filter((release) => release.channel === input.channel)
.filter((release) => release.releaseKind === "ota")
.filter((release) => release.runtimeVersion === input.runtimeVersion)
.filter((release) => Object.prototype.hasOwnProperty.call(release.platforms, input.platform))
.sort((left, right) => compareSemver(right.releaseVersion, left.releaseVersion))[0] ?? null
)
}