Files
Tarun 5036979669 feat(updates): add lightweight update metadata feed for launcher auto-update checks (#237)
- Add scripts/lib/updates-feed.mjs to generate /data/updates.json for Qt6 desktop launcher
- Extract short commit SHA, published date, direct asset download URLs, and cleaned changelog
- Integrate with fetch-github-data.mjs with offline and rate-limiting resilience
- Add UpdateFeed and UpdateAsset TypeScript interfaces to src/lib/github.ts
- Add 15 unit tests covering parsing priorities, asset filtering, and fallback behavior
2026-09-04 04:51:17 +05:30

116 lines
4.2 KiB
JavaScript

#!/usr/bin/env node
/**
* Build-time GitHub data snapshot — zero runtime API calls for visitors.
* Runs before `vite build` via the `prebuild` script.
*
* GITHUB_TOKEN=$(gh auth token) npm run build # avoid anonymous rate limits
*
* Writes public/data/github.json. The site renders this instantly (no API
* calls, no rate limit), then re-fetches live in the browser for freshness —
* the "build renders + browser re-fetches" model.
*
* Failures are non-fatal (warn + keep/emit an empty snapshot) so an offline
* or rate-limited build still succeeds; the live client fetch covers gaps.
*/
import { access, mkdir, writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import path from "node:path";
import { buildUpdatesFeed } from "./lib/updates-feed.mjs";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const OUT = path.join(ROOT, "public", "data", "github.json");
const UPDATES_OUT = path.join(ROOT, "public", "data", "updates.json");
const REPO = "KytyPS5/KytyPS5";
const API = `https://api.github.com/repos/${REPO}`;
const TOKEN = process.env.GITHUB_TOKEN;
const headers = { accept: "application/vnd.github+json", "x-github-api-version": "2022-11-28" };
if (TOKEN) headers.authorization = `Bearer ${TOKEN}`;
async function get(url, fallback) {
try {
const res = await fetch(url, { headers });
if (!res.ok) {
console.warn(` ! ${url.split("/").slice(-2).join("/")} → HTTP ${res.status}`);
return fallback;
}
return await res.json();
} catch (error) {
console.warn(` ! ${url.split("/").slice(-2).join("/")} → ${error.message}`);
return fallback;
}
}
const repo = await get(API, null);
if (repo) {
// Per-request/token-volatile fields must never ship in the snapshot: the
// refresh workflow compares the deployed github.json against a freshly
// regenerated one, and temp_clone_token rotates on every authenticated call
// (permissions is token-shaped too). Keeping them would make the JSON never
// match, so the site would rebuild every 30 minutes for nothing.
delete repo.temp_clone_token;
delete repo.permissions;
}
const latestRelease = await get(`${API}/releases/latest`, null);
if (latestRelease && Array.isArray(latestRelease.assets)) {
// Asset download counts bump on every download — meaningless churn for the
// staleness comparison (the site shows the live count from the API).
for (const asset of latestRelease.assets) delete asset.download_count;
}
async function getContributorCount() {
try {
const res = await fetch(`${API}/contributors?per_page=1&anon=true`, { headers });
if (!res.ok) return null;
const link = res.headers.get("link") || "";
const match = link.match(/[?&]page=(\d+)[^>]*>;\s*rel="last"/);
if (match) return Number(match[1]);
const data = await res.json();
return Array.isArray(data) ? data.length : null;
} catch {
return null;
}
}
const commits = await get(`${API}/commits?per_page=6`, null);
const SNAPSHOT = {
generatedAt: new Date().toISOString(),
repo,
// Reuse the fetch above (already stripped of download counts) rather than
// re-fetching — same shape, one fewer API call per run.
latestRelease,
contributors: await get(`${API}/contributors?per_page=14`, null),
contributorsCount: await getContributorCount(),
commits,
};
await mkdir(path.dirname(OUT), { recursive: true });
await writeFile(OUT, JSON.stringify(SNAPSHOT, null, 2));
console.log(`[github] snapshot written → ${path.relative(ROOT, OUT)}`);
// Generate the lightweight updates feed consumed by the desktop launcher (Qt6)
let writeUpdates = true;
if (!latestRelease) {
try {
await access(UPDATES_OUT);
console.warn(" ! latest release unavailable; keeping existing updates.json");
writeUpdates = false;
} catch {
// File does not exist yet, proceed to emit fallback feed
}
}
if (writeUpdates) {
const updatesFeed = buildUpdatesFeed({
latestRelease,
commits,
generatedAt: SNAPSHOT.generatedAt,
});
await writeFile(UPDATES_OUT, JSON.stringify(updatesFeed, null, 2) + "\n");
console.log(`[updates] feed written → ${path.relative(ROOT, UPDATES_OUT)}`);
}
console.log(`[github] ${TOKEN ? "using GITHUB_TOKEN" : "anonymous (60 req/hr — set GITHUB_TOKEN to raise the limit)"}`);