fix: backfill github version badges

This commit is contained in:
DIYgod 2026-04-13 09:54:46 +08:00
parent 7d592abda7
commit cebce6d9c8
2 changed files with 192 additions and 2 deletions

View File

@ -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<string, unknown>([
[
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", () => {

View File

@ -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<ReturnType<typeof getStoreVersionRecord>>,
url: string,