mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
239 lines
6.6 KiB
JavaScript
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);
|
|
});
|