From cebce6d9c819e5775e617f7150bc0566949d573b Mon Sep 17 00:00:00 2001 From: DIYgod Date: Mon, 13 Apr 2026 09:54:46 +0800 Subject: [PATCH] fix: backfill github version badges --- apps/ota/src/__tests__/sync.test.ts | 115 ++++++++++++++++++++++++++++ apps/ota/src/routes/versions.ts | 79 ++++++++++++++++++- 2 files changed, 192 insertions(+), 2 deletions(-) diff --git a/apps/ota/src/__tests__/sync.test.ts b/apps/ota/src/__tests__/sync.test.ts index 4e44f9d71..5171f45e5 100644 --- a/apps/ota/src/__tests__/sync.test.ts +++ b/apps/ota/src/__tests__/sync.test.ts @@ -1987,6 +1987,121 @@ describe("/versions", () => { }, }) }) + + it("backfills latest github release versions when the summary keys are missing", async () => { + const mobileRelease = await createReleaseMetadata({ + releaseVersion: "0.4.3", + releaseKind: "store", + runtimeVersion: "0.4.3", + publishedAt: "2026-04-10T16:00:00Z", + git: { + tag: "mobile/v0.4.3", + commit: "abcdef1234567895", + }, + platforms: {}, + }) + const desktopRelease = createDesktopReleaseMetadata({ + releaseVersion: "1.5.2", + releaseKind: "binary", + runtimeVersion: null, + publishedAt: "2026-04-11T12:00:00Z", + git: { + tag: "desktop/v1.5.2", + commit: "abcdef1234567891", + }, + desktop: { + renderer: null, + app: null, + }, + }) + const kvEntries = new Map([ + [ + KV_KEYS.storeVersion("mobile", "ios"), + { + version: "0.4.1", + fetchedAt: "2026-04-12T14:25:27.016Z", + source: "app-store", + }, + ], + ]) + + vi.stubGlobal( + "fetch", + vi.fn(async (input: string | URL | Request) => { + const url = String(input) + + if (url === "https://api.github.com/repos/RSSNext/Folo/releases") { + return new Response( + JSON.stringify([ + createGitHubReleaseAssetSet( + "desktop/v1.5.2", + "https://example.com/desktop.json", + null, + ), + createGitHubReleaseAssetSet("mobile/v0.4.3", "https://example.com/mobile.json", null), + ]), + { status: 200 }, + ) + } + + if (url === "https://example.com/mobile.json") { + return new Response(JSON.stringify(mobileRelease), { + headers: { "Content-Type": "application/json" }, + }) + } + + if (url === "https://example.com/desktop.json") { + return new Response(JSON.stringify(desktopRelease), { + headers: { "Content-Type": "application/json" }, + }) + } + + throw new Error(`Unhandled fetch URL: ${url}`) + }), + ) + + const response = await fetchWorker("/versions", undefined, { + kvEntries, + envOverrides: { + GITHUB_OWNER: "RSSNext", + GITHUB_REPO: "Folo", + GITHUB_TOKEN: "token", + }, + }) + + expect(response.status).toBe(200) + await expect(response.json()).resolves.toEqual( + expect.objectContaining({ + github: { + mobile: { + version: "0.4.3", + publishedAt: "2026-04-10T16:00:00Z", + tag: "mobile/v0.4.3", + url: "https://github.com/RSSNext/Folo/releases/tag/mobile/v0.4.3", + }, + desktop: { + version: "1.5.2", + publishedAt: "2026-04-11T12:00:00Z", + tag: "desktop/v1.5.2", + url: "https://github.com/RSSNext/Folo/releases/tag/desktop/v1.5.2", + }, + }, + }), + ) + + expect(JSON.parse(String(kvEntries.get(KV_KEYS.latestReleaseVersion("mobile"))))).toEqual({ + product: "mobile", + version: "0.4.3", + publishedAt: "2026-04-10T16:00:00Z", + tag: "mobile/v0.4.3", + }) + expect(JSON.parse(String(kvEntries.get(KV_KEYS.latestReleaseVersion("desktop"))))).toEqual({ + product: "desktop", + version: "1.5.2", + publishedAt: "2026-04-11T12:00:00Z", + tag: "desktop/v1.5.2", + }) + }) }) describe("latest release summary", () => { diff --git a/apps/ota/src/routes/versions.ts b/apps/ota/src/routes/versions.ts index 10e4f1d8a..787d04df5 100644 --- a/apps/ota/src/routes/versions.ts +++ b/apps/ota/src/routes/versions.ts @@ -1,13 +1,20 @@ import { Hono } from "hono" import type { Env } from "../env" -import { getLatestReleaseVersionRecord, getStoreVersionRecord } from "../lib/kv" +import { listPublishedOtaReleases } from "../lib/github" +import { + getLatestReleaseVersionRecord, + getStoreVersionRecord, + putLatestReleaseVersionRecord, +} from "../lib/kv" +import { otaReleaseSchema } from "../lib/schema" import { STORE_URLS } from "../lib/store-version" +import { compareSemver } from "../lib/version" export const versionsRoute = new Hono<{ Bindings: Env }>() versionsRoute.get("/versions", async (c) => { - const [ios, android, mas, mss, mobileRelease, desktopRelease] = await Promise.all([ + const [ios, android, mas, mss, cachedMobileRelease, cachedDesktopRelease] = await Promise.all([ getStoreVersionRecord(c.env.OTA_KV, { product: "mobile", target: "ios" }), getStoreVersionRecord(c.env.OTA_KV, { product: "mobile", target: "android" }), getStoreVersionRecord(c.env.OTA_KV, { product: "desktop", target: "mas" }), @@ -15,6 +22,14 @@ versionsRoute.get("/versions", async (c) => { getLatestReleaseVersionRecord(c.env.OTA_KV, "mobile"), getLatestReleaseVersionRecord(c.env.OTA_KV, "desktop"), ]) + let mobileRelease = cachedMobileRelease + let desktopRelease = cachedDesktopRelease + + if (!mobileRelease || !desktopRelease) { + const latestReleaseRecords = await backfillLatestReleaseVersions(c.env) + mobileRelease = mobileRelease ?? latestReleaseRecords.mobile + desktopRelease = desktopRelease ?? latestReleaseRecords.desktop + } return c.json({ store: { @@ -34,6 +49,66 @@ versionsRoute.get("/versions", async (c) => { }) }) +async function backfillLatestReleaseVersions(env: Env) { + const releasesResult = await listPublishedOtaReleases({ + owner: env.GITHUB_OWNER, + repo: env.GITHUB_REPO, + token: env.GITHUB_TOKEN, + etag: null, + }) + + if (releasesResult.kind === "not-modified") { + return { + mobile: null, + desktop: null, + } + } + + const latestByProduct = new Map< + "mobile" | "desktop", + { + product: "mobile" | "desktop" + version: string + publishedAt: string + tag: string + } + >() + + for (const releaseSummary of releasesResult.releases) { + const response = await fetch(releaseSummary.metadataUrl) + if (!response.ok) { + throw new Error(`Failed to fetch release metadata (${response.status})`) + } + + const release = otaReleaseSchema.parse(await response.json()) + const current = latestByProduct.get(release.product) + if (!current || compareSemver(release.releaseVersion, current.version) > 0) { + latestByProduct.set(release.product, { + product: release.product, + version: release.releaseVersion, + publishedAt: release.publishedAt, + tag: release.git.tag, + }) + } + } + + const mobile = latestByProduct.get("mobile") ?? null + const desktop = latestByProduct.get("desktop") ?? null + + if (mobile) { + await putLatestReleaseVersionRecord(env.OTA_KV, mobile) + } + + if (desktop) { + await putLatestReleaseVersionRecord(env.OTA_KV, desktop) + } + + return { + mobile, + desktop, + } +} + function toStoreVersionPayload( record: Awaited>, url: string,