#!/usr/bin/env node /* eslint-disable no-console */ // // Resolve the EternalTerminal `et` client binary release used by // build-packages. // // Priority: // 1. ET_BIN_RELEASE from workflow input / repository variable. // 2. Latest non-draft, non-prerelease GitHub Release whose tag is // et-bin-* in ET_BIN_OWNER/ET_BIN_REPO. By default this is a // dedicated sibling binary repository named Netcatty-et-bin. // // In GitHub Actions, the resolved tag is written back to $GITHUB_ENV so // later steps can run scripts/fetch-et-binaries.cjs without duplicating // release discovery logic. const fs = require("node:fs"); const https = require("node:https"); const TAG_RE = /^et-bin-[A-Za-z0-9._-]+$/; function log(msg) { console.log(`[resolve-et-bin-release] ${msg}`); } function validateReleaseTag(tag) { const value = String(tag || "").trim(); if (!TAG_RE.test(value)) { throw new Error(`invalid et binary release tag: ${tag}`); } return value; } function parseRepository(env) { const owner = env.ET_BIN_OWNER || (env.GITHUB_REPOSITORY || "").split("/")[0] || "binaricat"; const repo = env.ET_BIN_REPO || "Netcatty-et-bin"; return { owner, repo }; } function releaseTimestamp(release) { const raw = release.published_at || release.created_at || ""; const value = Date.parse(raw); return Number.isNaN(value) ? 0 : value; } function pickLatestEtBinRelease(releases) { return releases .map((release, index) => ({ release, index })) .filter(({ release }) => { return release && TAG_RE.test(String(release.tag_name || "")) && release.draft !== true && release.prerelease !== true; }) .sort((a, b) => { const diff = releaseTimestamp(b.release) - releaseTimestamp(a.release); return diff || a.index - b.index; })[0]?.release.tag_name; } function parseNextLink(linkHeader) { if (!linkHeader) return null; for (const part of String(linkHeader).split(",")) { const match = part.match(/^\s*<([^>]+)>\s*;\s*(.+)\s*$/); if (!match) continue; const rel = match[2].split(";").some((attr) => attr.trim() === 'rel="next"'); if (rel) return match[1]; } return null; } function requestJsonWithHeaders(url, env, depth = 0) { return new Promise((resolve, reject) => { if (depth > 5) { reject(new Error("too many redirects while looking up et binary releases")); return; } const headers = { Accept: "application/vnd.github+json", "User-Agent": "netcatty-et-release-resolver", "X-GitHub-Api-Version": "2022-11-28", }; const token = env.GITHUB_TOKEN || env.GH_TOKEN; if (token) headers.Authorization = `Bearer ${token}`; https.get(url, { headers }, (res) => { const location = res.headers.location; if (res.statusCode >= 300 && res.statusCode < 400 && location) { res.resume(); resolve(requestJsonWithHeaders(new URL(location, url).toString(), env, depth + 1)); return; } const chunks = []; res.on("data", (chunk) => chunks.push(chunk)); res.on("end", () => { const body = Buffer.concat(chunks).toString("utf8"); if (res.statusCode !== 200) { reject(new Error(`GitHub API returned HTTP ${res.statusCode}: ${body.slice(0, 300)}`)); return; } try { resolve({ json: JSON.parse(body), headers: res.headers }); } catch (err) { reject(new Error(`GitHub API returned invalid JSON: ${err.message}`)); } }); res.on("error", reject); }).on("error", reject); }); } async function loadReleases(env, request = requestJsonWithHeaders) { if (env.ET_BIN_RELEASES_JSON) { const parsed = JSON.parse(env.ET_BIN_RELEASES_JSON); if (!Array.isArray(parsed)) { throw new Error("ET_BIN_RELEASES_JSON must be a JSON array"); } return parsed; } const { owner, repo } = parseRepository(env); const apiBase = (env.GITHUB_API_URL || "https://api.github.com").replace(/\/+$/, ""); let url = `${apiBase}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases?per_page=100`; log(`looking up latest et-bin-* release in ${owner}/${repo}`); const releases = []; const seen = new Set(); while (url) { if (seen.has(url)) { throw new Error(`GitHub API pagination looped while looking up releases: ${url}`); } seen.add(url); const { json, headers = {} } = await request(url, env); if (!Array.isArray(json)) { throw new Error("GitHub API releases response was not an array"); } releases.push(...json); url = parseNextLink(headers.link); } return releases; } function exportRelease(release, env) { if (env.GITHUB_ENV) { fs.appendFileSync(env.GITHUB_ENV, `ET_BIN_RELEASE=${release}\n`, "utf8"); } } async function main(env = process.env) { if (String(env.ET_BIN_RELEASE || "").trim()) { const release = validateReleaseTag(env.ET_BIN_RELEASE); exportRelease(release, env); log(`using ET_BIN_RELEASE=${release}`); return release; } const releases = await loadReleases(env); const release = pickLatestEtBinRelease(releases); if (!release) { throw new Error( "could not find a non-draft et-bin-* release in the et binary repository. Publish build-et-binaries artifacts with release_tag (for example et-bin-6.2.10-1) before packaging.", ); } const validated = validateReleaseTag(release); exportRelease(validated, env); log(`resolved ET_BIN_RELEASE=${validated}`); return validated; } if (require.main === module) { main().catch((err) => { console.error(`[resolve-et-bin-release] FATAL ${err.message}`); process.exit(1); }); } module.exports = { loadReleases, parseNextLink, validateReleaseTag, parseRepository, pickLatestEtBinRelease, main, };