feat: expose ota version badges
This commit is contained in:
parent
ca3fa9ffc3
commit
7d592abda7
|
|
@ -498,6 +498,102 @@ describe("syncGitHubReleases", () => {
|
|||
expect(kvEntries.get(KV_KEYS.syncLastSuccessAt)).toEqual(expect.any(String))
|
||||
})
|
||||
|
||||
it("backfills latest release summaries when GitHub returns 304 and the summary keys are missing", async () => {
|
||||
const kvEntries = new Map<string, unknown>([[KV_KEYS.githubEtag, '"etag-current"']])
|
||||
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,
|
||||
},
|
||||
})
|
||||
|
||||
let githubRequestCount = 0
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (input: string | URL | Request) => {
|
||||
const url = String(input)
|
||||
|
||||
if (url === "https://api.github.com/repos/RSSNext/Folo/releases") {
|
||||
githubRequestCount += 1
|
||||
|
||||
if (githubRequestCount === 1) {
|
||||
return new Response(null, { status: 304 })
|
||||
}
|
||||
|
||||
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}`)
|
||||
}),
|
||||
)
|
||||
|
||||
await syncGitHubReleases(
|
||||
createEnv({
|
||||
kvEntries,
|
||||
envOverrides: {
|
||||
GITHUB_OWNER: "RSSNext",
|
||||
GITHUB_REPO: "Folo",
|
||||
GITHUB_TOKEN: "token",
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(githubRequestCount).toBe(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",
|
||||
})
|
||||
})
|
||||
|
||||
it("does not advance sync markers when a later release fails validation", async () => {
|
||||
const kvEntries = new Map<string, unknown>([
|
||||
[KV_KEYS.githubEtag, '"etag-old"'],
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ import { compareSemver } from "./version"
|
|||
|
||||
const OTA_PLATFORMS: OtaPlatform[] = ["ios", "android", "macos", "windows", "linux"]
|
||||
const semverPattern = /^\d+\.\d+\.\d+$/
|
||||
type ReleaseSummary = {
|
||||
tag: string
|
||||
metadataUrl: string
|
||||
archiveUrl: string | null
|
||||
}
|
||||
let inFlightSync: Promise<void> | null = null
|
||||
let inFlightStoreSync: Promise<void> | null = null
|
||||
|
||||
|
|
@ -54,10 +59,6 @@ export async function syncStoreVersions(env: Env) {
|
|||
}
|
||||
|
||||
async function runSyncGitHubReleases(env: Env) {
|
||||
const latestReleaseByProduct = new Map<
|
||||
OtaRelease["product"],
|
||||
Pick<OtaRelease, "releaseVersion" | "publishedAt"> & { tag: string }
|
||||
>()
|
||||
const storedEtag = await env.OTA_KV.get<string>(KV_KEYS.githubEtag)
|
||||
const releasesResult = await listPublishedOtaReleases({
|
||||
owner: env.GITHUB_OWNER,
|
||||
|
|
@ -67,53 +68,12 @@ async function runSyncGitHubReleases(env: Env) {
|
|||
})
|
||||
|
||||
if (releasesResult.kind === "not-modified") {
|
||||
await ensureLatestReleaseVersionRecords(env)
|
||||
await updateSyncLastSuccessAt(env.OTA_KV)
|
||||
return
|
||||
}
|
||||
|
||||
for (const releaseSummary of releasesResult.releases) {
|
||||
const release = await fetchReleaseMetadata(releaseSummary.metadataUrl, env)
|
||||
updateLatestReleaseByProduct(latestReleaseByProduct, release)
|
||||
|
||||
if (release.releaseKind === "ota") {
|
||||
if (!releaseSummary.archiveUrl) {
|
||||
throw new Error(
|
||||
`Missing OTA archive asset for ${release.product} release ${release.releaseVersion}`,
|
||||
)
|
||||
}
|
||||
|
||||
const archiveBuffer = await fetchArchiveBuffer(releaseSummary.archiveUrl, env)
|
||||
const files = await extractMirroredFiles({
|
||||
release,
|
||||
archiveBuffer,
|
||||
})
|
||||
|
||||
await mirrorReleaseToStorage(
|
||||
{
|
||||
release,
|
||||
files,
|
||||
},
|
||||
{
|
||||
kv: env.OTA_KV,
|
||||
bucket: env.OTA_BUCKET,
|
||||
},
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
await putReleaseRecord(env.OTA_KV, release.product, release.releaseVersion, release)
|
||||
await putLatestPolicyRecord(env.OTA_KV, release)
|
||||
}
|
||||
|
||||
for (const [product, latestRelease] of latestReleaseByProduct) {
|
||||
await putLatestReleaseVersionRecord(env.OTA_KV, {
|
||||
product,
|
||||
version: latestRelease.releaseVersion,
|
||||
publishedAt: latestRelease.publishedAt,
|
||||
tag: latestRelease.tag,
|
||||
})
|
||||
}
|
||||
await persistReleaseSummaries(env, releasesResult.releases)
|
||||
|
||||
if (releasesResult.etag) {
|
||||
await env.OTA_KV.put(KV_KEYS.githubEtag, releasesResult.etag)
|
||||
|
|
@ -181,6 +141,105 @@ async function runSyncStoreVersions(env: Env) {
|
|||
await env.OTA_KV.put(KV_KEYS.storeVersionSyncLastSuccessAt, fetchedAt)
|
||||
}
|
||||
|
||||
async function ensureLatestReleaseVersionRecords(env: Env) {
|
||||
const [mobileLatest, desktopLatest] = await Promise.all([
|
||||
env.OTA_KV.get(KV_KEYS.latestReleaseVersion("mobile")),
|
||||
env.OTA_KV.get(KV_KEYS.latestReleaseVersion("desktop")),
|
||||
])
|
||||
|
||||
if (mobileLatest && desktopLatest) {
|
||||
return
|
||||
}
|
||||
|
||||
const releasesResult = await listPublishedOtaReleases({
|
||||
owner: env.GITHUB_OWNER,
|
||||
repo: env.GITHUB_REPO,
|
||||
token: env.GITHUB_TOKEN,
|
||||
etag: null,
|
||||
})
|
||||
|
||||
if (releasesResult.kind === "not-modified") {
|
||||
return
|
||||
}
|
||||
|
||||
await persistLatestReleaseVersionRecords(env, releasesResult.releases)
|
||||
}
|
||||
|
||||
async function persistReleaseSummaries(env: Env, releases: ReleaseSummary[]) {
|
||||
const latestReleaseByProduct = new Map<
|
||||
OtaRelease["product"],
|
||||
Pick<OtaRelease, "releaseVersion" | "publishedAt"> & { tag: string }
|
||||
>()
|
||||
|
||||
for (const releaseSummary of releases) {
|
||||
const release = await fetchReleaseMetadata(releaseSummary.metadataUrl, env)
|
||||
updateLatestReleaseByProduct(latestReleaseByProduct, release)
|
||||
|
||||
if (release.releaseKind === "ota") {
|
||||
if (!releaseSummary.archiveUrl) {
|
||||
throw new Error(
|
||||
`Missing OTA archive asset for ${release.product} release ${release.releaseVersion}`,
|
||||
)
|
||||
}
|
||||
|
||||
const archiveBuffer = await fetchArchiveBuffer(releaseSummary.archiveUrl, env)
|
||||
const files = await extractMirroredFiles({
|
||||
release,
|
||||
archiveBuffer,
|
||||
})
|
||||
|
||||
await mirrorReleaseToStorage(
|
||||
{
|
||||
release,
|
||||
files,
|
||||
},
|
||||
{
|
||||
kv: env.OTA_KV,
|
||||
bucket: env.OTA_BUCKET,
|
||||
},
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
await putReleaseRecord(env.OTA_KV, release.product, release.releaseVersion, release)
|
||||
await putLatestPolicyRecord(env.OTA_KV, release)
|
||||
}
|
||||
|
||||
await writeLatestReleaseVersionRecords(env.OTA_KV, latestReleaseByProduct)
|
||||
}
|
||||
|
||||
async function persistLatestReleaseVersionRecords(env: Env, releases: ReleaseSummary[]) {
|
||||
const latestReleaseByProduct = new Map<
|
||||
OtaRelease["product"],
|
||||
Pick<OtaRelease, "releaseVersion" | "publishedAt"> & { tag: string }
|
||||
>()
|
||||
|
||||
for (const releaseSummary of releases) {
|
||||
const release = await fetchReleaseMetadata(releaseSummary.metadataUrl, env)
|
||||
updateLatestReleaseByProduct(latestReleaseByProduct, release)
|
||||
}
|
||||
|
||||
await writeLatestReleaseVersionRecords(env.OTA_KV, latestReleaseByProduct)
|
||||
}
|
||||
|
||||
async function writeLatestReleaseVersionRecords(
|
||||
kv: KVNamespace,
|
||||
latestReleaseByProduct: Map<
|
||||
OtaRelease["product"],
|
||||
Pick<OtaRelease, "releaseVersion" | "publishedAt"> & { tag: string }
|
||||
>,
|
||||
) {
|
||||
for (const [product, latestRelease] of latestReleaseByProduct) {
|
||||
await putLatestReleaseVersionRecord(kv, {
|
||||
product,
|
||||
version: latestRelease.releaseVersion,
|
||||
publishedAt: latestRelease.publishedAt,
|
||||
tag: latestRelease.tag,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function updateLatestReleaseByProduct(
|
||||
latestReleaseByProduct: Map<
|
||||
OtaRelease["product"],
|
||||
|
|
|
|||
Loading…
Reference in New Issue