mirror of
https://github.com/KytyPS5/kytyps5.github.io.git
synced 2026-10-09 08:49:37 -07:00
- 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
116 lines
4.2 KiB
JavaScript
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)"}`);
|