Files
openwork/scripts/release/generate-latest-json.mjs

239 lines
6.6 KiB
JavaScript

#!/usr/bin/env node
const ARCH_ALIASES = new Map([
["x64", "x86_64"],
["amd64", "x86_64"],
["arm64", "aarch64"],
]);
function normalizeArch(arch) {
const key = String(arch || "").trim().toLowerCase();
return ARCH_ALIASES.get(key) || key;
}
function parseArgs(argv) {
const options = {
tag: process.env.RELEASE_TAG || "",
repo: process.env.GITHUB_REPOSITORY || "different-ai/openwork",
output: "latest.json",
};
for (let i = 2; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === "--tag") {
options.tag = argv[i + 1] || "";
i += 1;
continue;
}
if (arg === "--repo") {
options.repo = argv[i + 1] || options.repo;
i += 1;
continue;
}
if (arg === "--output") {
options.output = argv[i + 1] || options.output;
i += 1;
continue;
}
throw new Error(`Unknown argument: ${arg}`);
}
if (!options.tag) {
throw new Error("Missing release tag. Pass --tag vX.Y.Z or set RELEASE_TAG.");
}
return options;
}
function updaterPlatformKeys(assetName) {
if (!assetName.startsWith("openwork-desktop-")) return [];
const stem = assetName.slice("openwork-desktop-".length);
if (stem.endsWith(".app.tar.gz")) {
const match = stem.match(/^([^-]+)-([^.]+)\.app\.tar\.gz$/);
if (!match) return [];
const platform = match[1];
const arch = normalizeArch(match[2]);
const base = `${platform}-${arch}`;
if (platform === "darwin") {
return [base, `${base}-app`];
}
return [base];
}
if (stem.endsWith(".msi")) {
const match = stem.match(/^([^-]+)-([^.]+)\.msi$/);
if (!match) return [];
const platform = match[1];
const arch = normalizeArch(match[2]);
const base = `${platform}-${arch}`;
return [base, `${base}-msi`];
}
if (stem.endsWith(".deb")) {
const match = stem.match(/^([^-]+)-([^.]+)\.deb$/);
if (!match) return [];
const platform = match[1];
const arch = normalizeArch(match[2]);
const base = `${platform}-${arch}`;
return [base, `${base}-deb`];
}
if (stem.endsWith(".rpm")) {
const match = stem.match(/^([^-]+)-([^.]+)\.rpm$/);
if (!match) return [];
const platform = match[1];
const arch = normalizeArch(match[2]);
return [`${platform}-${arch}-rpm`];
}
if (stem.endsWith(".AppImage")) {
const match = stem.match(/^([^-]+)-([^.]+)\.AppImage$/);
if (!match) return [];
const platform = match[1];
const arch = normalizeArch(match[2]);
return [`${platform}-${arch}`];
}
return [];
}
function authHeaders() {
const headers = {
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "openwork-release-latest-json",
};
const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
if (token) {
headers.Authorization = `Bearer ${token}`;
}
return headers;
}
async function fetchJson(url) {
const response = await fetch(url, { headers: authHeaders() });
if (!response.ok) {
throw new Error(`GitHub API request failed (${response.status}): ${url}`);
}
return response.json();
}
async function fetchReleaseByTag(repo, tag) {
const encodedTag = encodeURIComponent(tag);
const releaseByTagUrl = `https://api.github.com/repos/${repo}/releases/tags/${encodedTag}`;
const byTagResponse = await fetch(releaseByTagUrl, { headers: authHeaders() });
if (byTagResponse.ok) {
return byTagResponse.json();
}
if (byTagResponse.status !== 404) {
throw new Error(`GitHub API request failed (${byTagResponse.status}): ${releaseByTagUrl}`);
}
// Draft releases are not returned by /releases/tags/{tag}; fall back to paginated releases list.
for (let page = 1; page <= 10; page += 1) {
const listUrl = `https://api.github.com/repos/${repo}/releases?per_page=100&page=${page}`;
const releases = await fetchJson(listUrl);
if (!Array.isArray(releases) || releases.length === 0) break;
const match = releases.find((release) => {
const candidate = String(release?.tag_name || "");
return candidate === tag;
});
if (match) return match;
}
throw new Error(`Release ${repo}@${tag} not found (including drafts).`);
}
function releaseAssetUrl(repo, tag, assetName) {
return `https://github.com/${repo}/releases/download/${encodeURIComponent(tag)}/${assetName}`;
}
async function fetchText(url, accept = "text/plain") {
const headers = authHeaders();
headers.Accept = accept;
const response = await fetch(url, { headers });
if (!response.ok) {
throw new Error(`Failed to download signature (${response.status}): ${url}`);
}
return response.text();
}
function sortObjectEntries(input) {
const sorted = {};
for (const key of Object.keys(input).sort()) {
sorted[key] = input[key];
}
return sorted;
}
async function main() {
const { tag, repo, output } = parseArgs(process.argv);
const release = await fetchReleaseByTag(repo, tag);
const assets = Array.isArray(release.assets) ? release.assets : [];
const assetsByName = new Map();
for (const asset of assets) {
if (asset && typeof asset.name === "string") {
assetsByName.set(asset.name, asset);
}
}
const platforms = {};
for (const asset of assets) {
if (!asset || typeof asset.name !== "string" || !asset.name.endsWith(".sig")) continue;
const targetName = asset.name.slice(0, -4);
const targetAsset = assetsByName.get(targetName);
if (!targetAsset) continue;
const keys = updaterPlatformKeys(targetName);
if (!keys.length) continue;
if (typeof asset.url !== "string") continue;
const signature = (await fetchText(asset.url, "application/octet-stream")).trim();
if (!signature) continue;
const url = releaseAssetUrl(repo, tag, targetName);
for (const key of keys) {
platforms[key] = {
signature,
url,
};
}
}
if (!Object.keys(platforms).length) {
throw new Error(`No updater platforms were resolved for ${repo}@${tag}.`);
}
const version = String(release.tag_name || tag).replace(/^v/, "");
const latest = {
version,
notes:
typeof release.body === "string" && release.body.trim()
? release.body
: "See the assets to download this version and install.",
pub_date: release.published_at || new Date().toISOString(),
platforms: sortObjectEntries(platforms),
};
const fs = await import("node:fs/promises");
await fs.writeFile(output, `${JSON.stringify(latest, null, 2)}\n`, "utf8");
console.log(`Wrote ${output} with ${Object.keys(latest.platforms).length} updater platforms.`);
}
main().catch((error) => {
const message = error instanceof Error ? error.stack || error.message : String(error);
console.error(message);
process.exit(1);
});