diff --git a/apps/ota/src/__tests__/policy.test.ts b/apps/ota/src/__tests__/policy.test.ts new file mode 100644 index 000000000..85642b2f4 --- /dev/null +++ b/apps/ota/src/__tests__/policy.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest" + +import { evaluateStorePolicy } from "../lib/policy" +import type { OtaRelease } from "../lib/schema" + +describe("evaluateStorePolicy", () => { + it("requires a store update when a store release is newer than the installed binary", () => { + const policy = evaluateStorePolicy( + { + releaseVersion: "0.4.3", + releaseKind: "store", + runtimeVersion: "0.4.3", + policy: { + storeRequired: true, + minSupportedBinaryVersion: "0.4.3", + message: "Please update from the store.", + }, + } satisfies Pick, + { + installedBinaryVersion: "0.4.1", + }, + ) + + expect(policy.action).toBe("block") + expect(policy.targetVersion).toBe("0.4.3") + }) +}) diff --git a/apps/ota/src/lib/constants.ts b/apps/ota/src/lib/constants.ts new file mode 100644 index 000000000..7262afc7e --- /dev/null +++ b/apps/ota/src/lib/constants.ts @@ -0,0 +1,16 @@ +import type { OtaPlatform, OtaRelease } from "./schema" + +export const KV_KEYS = { + release: (product: OtaRelease["product"], releaseVersion: OtaRelease["releaseVersion"]) => + `release:${product}:${releaseVersion}`, + latest: ( + product: OtaRelease["product"], + channel: OtaRelease["channel"], + runtimeVersion: OtaRelease["runtimeVersion"], + platform: OtaPlatform, + ) => `latest:${product}:${channel}:${runtimeVersion}:${platform}`, + policy: (product: OtaRelease["product"], channel: OtaRelease["channel"]) => + `policy:${product}:${channel}`, + githubEtag: "github:etag:releases", + syncLastSuccessAt: "sync:last-success-at", +} as const diff --git a/apps/ota/src/lib/kv.ts b/apps/ota/src/lib/kv.ts new file mode 100644 index 000000000..726cee3a1 --- /dev/null +++ b/apps/ota/src/lib/kv.ts @@ -0,0 +1,26 @@ +import { KV_KEYS } from "./constants" +import type { OtaPlatform, OtaRelease } from "./schema" + +export async function getLatestReleasePointer( + kv: KVNamespace, + input: { + product: OtaRelease["product"] + channel: OtaRelease["channel"] + runtimeVersion: OtaRelease["runtimeVersion"] + platform: OtaPlatform + }, +) { + return kv.get( + KV_KEYS.latest(input.product, input.channel, input.runtimeVersion, input.platform), + "json", + ) +} + +export async function putReleaseRecord( + kv: KVNamespace, + product: OtaRelease["product"], + releaseVersion: OtaRelease["releaseVersion"], + value: T, +) { + await kv.put(KV_KEYS.release(product, releaseVersion), JSON.stringify(value)) +} diff --git a/apps/ota/src/lib/policy.ts b/apps/ota/src/lib/policy.ts new file mode 100644 index 000000000..c70772d8c --- /dev/null +++ b/apps/ota/src/lib/policy.ts @@ -0,0 +1,40 @@ +import type { OtaRelease } from "./schema" +import { compareSemver } from "./version" + +type StoreReleasePolicyInput = Pick< + OtaRelease, + "releaseVersion" | "releaseKind" | "runtimeVersion" | "policy" +> + +type StorePolicyEvaluation = + | { + action: "none" + targetVersion: null + message: null + } + | { + action: "block" | "prompt" + targetVersion: OtaRelease["releaseVersion"] + message: string | null + } + +export function evaluateStorePolicy( + latestStoreRelease: StoreReleasePolicyInput | null, + input: { + installedBinaryVersion: OtaRelease["releaseVersion"] + }, +): StorePolicyEvaluation { + if (!latestStoreRelease || latestStoreRelease.releaseKind !== "store") { + return { action: "none", targetVersion: null, message: null } + } + + if (compareSemver(latestStoreRelease.releaseVersion, input.installedBinaryVersion) <= 0) { + return { action: "none", targetVersion: null, message: null } + } + + return { + action: latestStoreRelease.policy.storeRequired ? "block" : "prompt", + targetVersion: latestStoreRelease.releaseVersion, + message: latestStoreRelease.policy.message, + } +}