mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-09-24 15:49:09 +00:00
01e7edc006
* feat(mosh): default bundled client to MoshCatty releases Wire Netcatty packaging and npm run dev to pull pure Rust mosh-client binaries from binaricat/MoshCatty (moshcatty-* tags), document the cutover, and mark the old Cygwin Windows fetch path as legacy-only. * fix(mosh): accept pure MoshCatty packages (exe-only, no dlls/terminfo) Align fetch/packaging with MoshCatty pure-Rust releases: require only mosh-client[.exe], treat Cygwin dll bags and terminfo as optional legacy layout. Document moshcatty-0.1.1 (ConPTY Ctrl+C + static MSVC CRT). * refactor(mosh): drop Cygwin legacy; MoshCatty-only pipeline Remove FluentTerminal/mosh-bin fallbacks, build-mosh C++ CI, and dll/terminfo runtime helpers. Fetch accepts only moshcatty-* pure binaries; packaging ships a single mosh-client per platform. * docs: align ET checklist and et README with MoshCatty-only mosh Update mosh template references after removing Cygwin build pipeline and legacy fallbacks. Point ET packaging notes at fetch-mosh / MoshCatty instead of deleted build-mosh-binaries workflow. * docs(mosh): require MoshCatty 0.1.2+ for Linux glibc floors Codex review vs main: defaulting to MoshCatty Linux binaries built on ubuntu-latest required GLIBC 2.34, above Netcatty's package floors (x64 2.28 / arm64 2.31). Document the floors, prefer moshcatty-0.1.2+, and drop trailing whitespace in the design note. Upstream MoshCatty release CI now builds in almalinux:8 / debian:bullseye with assert. * fix(mosh): reject MoshCatty releases below 0.1.2 Codex re-review: docs alone still allowed MOSH_BIN_RELEASE=moshcatty-0.1.0/0.1.1, which produce packages that fail to start on Netcatty's Linux glibc floors. Validate the minimum tag in resolve and fetch, skip pre-0.1.2 when picking latest, and cover the reject path in unit tests. * fix(mosh): pin MoshCatty owner and reject floor prereleases Codex review vs main (P2): - Always default MOSH_BIN owner/repo to binaricat/MoshCatty; do not inherit fork owner from GITHUB_REPOSITORY in resolve or fetch. - Treat moshcatty-X.Y.Z-rcN as below final X.Y.Z so pins like moshcatty-0.1.2-rc1 cannot bypass the glibc floor. * fix(mosh): accept build metadata in MoshCatty release tags TAG_RE rejected '+' so moshcatty-0.1.2+build.1 failed validation even though the version parser and isAtLeastMinRelease accepted it. Align the regex and require a successful semver parse.
243 lines
7.7 KiB
JavaScript
243 lines
7.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/* eslint-disable no-console */
|
|
//
|
|
// Resolve the MoshCatty mosh-client binary release used by packaging / dev.
|
|
//
|
|
// Priority:
|
|
// 1. MOSH_BIN_RELEASE from workflow input / repository variable.
|
|
// 2. Latest non-draft, non-prerelease GitHub Release whose tag is
|
|
// moshcatty-* in MOSH_BIN_OWNER/MOSH_BIN_REPO (default binaricat/MoshCatty).
|
|
//
|
|
// In GitHub Actions, the resolved tag is written to $GITHUB_ENV.
|
|
|
|
const fs = require("node:fs");
|
|
const https = require("node:https");
|
|
|
|
// MoshCatty pure-Rust releases only.
|
|
// Minimum 0.1.2: earlier Linux builds linked GLIBC 2.34 (above Netcatty floors).
|
|
// Allow semver prerelease (-rc1) and build metadata (+meta); no path separators.
|
|
const TAG_RE = /^moshcatty-[A-Za-z0-9._+-]+$/;
|
|
const MIN_VERSION = { major: 0, minor: 1, patch: 2 };
|
|
const MIN_TAG = `moshcatty-${MIN_VERSION.major}.${MIN_VERSION.minor}.${MIN_VERSION.patch}`;
|
|
|
|
function log(msg) {
|
|
console.log(`[resolve-mosh-bin-release] ${msg}`);
|
|
}
|
|
|
|
/**
|
|
* Parse moshcatty-X.Y.Z with optional prerelease (-rc1) and build (+meta).
|
|
* Returns null if not semver-ish.
|
|
*/
|
|
function parseMoshCattyVersion(tag) {
|
|
const match = String(tag || "").trim().match(
|
|
/^moshcatty-(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/,
|
|
);
|
|
if (!match) return null;
|
|
return {
|
|
major: Number(match[1]),
|
|
minor: Number(match[2]),
|
|
patch: Number(match[3]),
|
|
// Present when tag is e.g. moshcatty-0.1.2-rc1 (semver: prerelease < final).
|
|
prerelease: match[4] || null,
|
|
};
|
|
}
|
|
|
|
function compareCoreVersion(a, b) {
|
|
if (a.major !== b.major) return a.major - b.major;
|
|
if (a.minor !== b.minor) return a.minor - b.minor;
|
|
return a.patch - b.patch;
|
|
}
|
|
|
|
/**
|
|
* True when tag is usable for packaging (core version ≥ min, with semver
|
|
* prerelease rules: X.Y.Z-rcN is below final X.Y.Z).
|
|
*/
|
|
function isAtLeastMinRelease(tag) {
|
|
const version = parseMoshCattyVersion(tag);
|
|
if (!version) return false;
|
|
const core = compareCoreVersion(version, MIN_VERSION);
|
|
if (core > 0) return true;
|
|
if (core < 0) return false;
|
|
// Equal to the floor: final only (no prerelease suffix).
|
|
return !version.prerelease;
|
|
}
|
|
|
|
function validateReleaseTag(tag) {
|
|
const value = String(tag || "").trim();
|
|
if (!TAG_RE.test(value) || !parseMoshCattyVersion(value)) {
|
|
throw new Error(`invalid mosh binary release tag: ${tag} (expected moshcatty-X.Y.Z[(-pre)|(+build)])`);
|
|
}
|
|
if (!isAtLeastMinRelease(value)) {
|
|
throw new Error(
|
|
`mosh binary release ${value} is below minimum ${MIN_TAG} `
|
|
+ "(Linux glibc floors: x64 ≤ 2.28, arm64 ≤ 2.31; 0.1.0/0.1.1 require GLIBC 2.34; "
|
|
+ "prereleases of the floor e.g. 0.1.2-rc1 are not accepted)",
|
|
);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function parseRepository(env) {
|
|
// Canonical default is always binaricat/MoshCatty. Do not derive owner from
|
|
// GITHUB_REPOSITORY — fork packaging would otherwise look for
|
|
// <fork-owner>/MoshCatty and fail. Override only via MOSH_BIN_OWNER/REPO.
|
|
const owner = env.MOSH_BIN_OWNER || "binaricat";
|
|
const repo = env.MOSH_BIN_REPO || "MoshCatty";
|
|
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 pickLatestMoshBinRelease(releases) {
|
|
return releases
|
|
.map((release, index) => ({ release, index }))
|
|
.filter(({ release }) => {
|
|
const tag = String(release?.tag_name || "");
|
|
return release
|
|
&& TAG_RE.test(tag)
|
|
&& isAtLeastMinRelease(tag)
|
|
&& 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 mosh binary releases"));
|
|
return;
|
|
}
|
|
|
|
const headers = {
|
|
Accept: "application/vnd.github+json",
|
|
"User-Agent": "netcatty-mosh-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.MOSH_BIN_RELEASES_JSON) {
|
|
const parsed = JSON.parse(env.MOSH_BIN_RELEASES_JSON);
|
|
if (!Array.isArray(parsed)) {
|
|
throw new Error("MOSH_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 moshcatty-* 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, `MOSH_BIN_RELEASE=${release}\n`, "utf8");
|
|
}
|
|
}
|
|
|
|
async function main(env = process.env) {
|
|
if (String(env.MOSH_BIN_RELEASE || "").trim()) {
|
|
const release = validateReleaseTag(env.MOSH_BIN_RELEASE);
|
|
exportRelease(release, env);
|
|
log(`using MOSH_BIN_RELEASE=${release}`);
|
|
return release;
|
|
}
|
|
|
|
const releases = await loadReleases(env);
|
|
const release = pickLatestMoshBinRelease(releases);
|
|
if (!release) {
|
|
throw new Error(
|
|
`could not find a non-draft ${MIN_TAG}+ release in binaricat/MoshCatty. `
|
|
+ `Publish a MoshCatty GitHub Release (e.g. ${MIN_TAG}) before packaging.`,
|
|
);
|
|
}
|
|
|
|
const validated = validateReleaseTag(release);
|
|
exportRelease(validated, env);
|
|
log(`resolved MOSH_BIN_RELEASE=${validated}`);
|
|
return validated;
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main().catch((err) => {
|
|
console.error(`[resolve-mosh-bin-release] FATAL ${err.message}`);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
loadReleases,
|
|
parseNextLink,
|
|
validateReleaseTag,
|
|
parseMoshCattyVersion,
|
|
isAtLeastMinRelease,
|
|
parseRepository,
|
|
pickLatestMoshBinRelease,
|
|
MIN_TAG,
|
|
main,
|
|
};
|