Files
tutti/apps/desktop/scripts/build-release-latest.mjs
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

253 lines
6.2 KiB
JavaScript

#!/usr/bin/env node
import { readdir, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
const schemaVersion = "tutti.desktop.release.latest.v1";
function normalizeBaseUrl(value) {
return value.replace(/\/+$/, "");
}
function requireNonEmpty(value, label) {
const normalized = String(value ?? "").trim();
if (!normalized) {
throw new Error(`${label} is required`);
}
return normalized;
}
function encodeURLPathSegment(value) {
return encodeURIComponent(value);
}
function releaseVersionFromTag(tag) {
return tag.replace(/^tutti-desktop-v/, "").replace(/^v/, "");
}
function parseReleaseVersion(value) {
const match =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((rc|beta)\.(0|[1-9]\d*)))?$/.exec(
value
);
if (!match) {
return null;
}
return {
channel: match[5] ?? "stable",
prerelease: match[4] ?? null,
version: `${match[1]}.${match[2]}.${match[3]}${match[4] ? `-${match[4]}` : ""}`
};
}
function resolveReleaseChannel(value) {
const normalized = String(value ?? "").trim();
return normalized || "stable";
}
function validateReleaseChannel({ channel, parsedVersion, releaseTag }) {
if (channel === "stable") {
if (
parsedVersion.channel === "stable" &&
parsedVersion.prerelease === null
) {
return false;
}
throw new Error(
`stable latest metadata can only be built for stable releases: ${releaseTag}`
);
}
if (channel === "rc") {
if (parsedVersion.channel === "rc") {
return true;
}
throw new Error(
`rc latest metadata can only be built for rc releases: ${releaseTag}`
);
}
if (channel === "beta") {
if (parsedVersion.channel === "beta") {
return true;
}
throw new Error(
`beta latest metadata can only be built for beta releases: ${releaseTag}`
);
}
throw new Error(`Unsupported release channel: ${channel}`);
}
function isMacosUniversalDmg(asset) {
return (
asset.platform === "macos" &&
asset.arch === "universal" &&
asset.format === "dmg"
);
}
function isWindowsX64Exe(asset) {
return (
asset.platform === "windows" &&
asset.arch === "x64" &&
asset.format === "exe"
);
}
function normalizePlatform(value) {
const normalized = value.toLowerCase();
if (
normalized === "mac" ||
normalized === "macos" ||
normalized === "darwin"
) {
return "macos";
}
if (normalized === "win" || normalized === "windows") {
return "windows";
}
if (normalized === "linux") {
return "linux";
}
return normalized;
}
function normalizeArch(value) {
const normalized = value.toLowerCase();
if (normalized === "x86_64" || normalized === "amd64") {
return "x64";
}
if (normalized === "aarch64") {
return "arm64";
}
return normalized;
}
function classifyDesktopReleaseAsset(name) {
const format =
path.extname(name).replace(/^\./, "").toLowerCase() || "unknown";
const match = name.match(
/-(mac|macos|darwin|win|windows|linux)-([^.]+)\.[^.]+$/i
);
if (!match) {
return {
arch: "unknown",
format,
platform: "unknown"
};
}
return {
arch: normalizeArch(match[2]),
format,
platform: normalizePlatform(match[1])
};
}
async function buildDesktopReleaseLatest(options) {
const assetDirPath = path.resolve(
requireNonEmpty(options.assetDirPath, "assetDirPath")
);
const releaseTag = requireNonEmpty(options.releaseTag, "releaseTag");
const channel = resolveReleaseChannel(options.channel);
const releaseVersion = releaseVersionFromTag(releaseTag);
const parsedVersion = parseReleaseVersion(releaseVersion);
if (!parsedVersion) {
throw new Error(`releaseTag must contain a semver version: ${releaseTag}`);
}
const prerelease = validateReleaseChannel({
channel,
parsedVersion,
releaseTag
});
const baseUrl = normalizeBaseUrl(
requireNonEmpty(options.releaseAssetBaseUrl, "releaseAssetBaseUrl")
);
const gitSha = String(options.gitSha ?? "").trim();
const sourceRef = String(options.sourceRef ?? "").trim();
const releasedAt =
options.releasedAt instanceof Date
? options.releasedAt.toISOString()
: String(options.releasedAt ?? new Date().toISOString()).trim();
const entries = await readdir(assetDirPath, { withFileTypes: true });
const assetNames = entries
.filter((entry) => entry.isFile() && entry.name !== "latest.json")
.map((entry) => entry.name)
.sort((left, right) => left.localeCompare(right));
const assets = [];
for (const name of assetNames) {
const fileStat = await stat(path.join(assetDirPath, name));
const classification = classifyDesktopReleaseAsset(name);
assets.push({
...classification,
name,
sizeBytes: fileStat.size,
url: `${baseUrl}/${encodeURLPathSegment(releaseTag)}/${encodeURLPathSegment(name)}`
});
}
const macosUniversalDmg = assets.find(isMacosUniversalDmg)?.url ?? null;
const windowsX64Exe = assets.find(isWindowsX64Exe)?.url ?? null;
return {
schemaVersion,
tag: releaseTag,
version: releaseVersion,
channel,
prerelease,
releasedAt,
gitSha: gitSha || null,
sourceRef: sourceRef || null,
baseUrl,
preferredDownloads: {
macosUniversalDmg,
windowsX64Exe
},
assets
};
}
async function main() {
const [assetDirPath, outputPath] = process.argv.slice(2);
const latest = await buildDesktopReleaseLatest({
assetDirPath,
channel: process.env.RELEASE_CHANNEL,
gitSha: process.env.RELEASE_GIT_SHA,
releaseAssetBaseUrl: process.env.RELEASE_ASSET_BASE_URL,
releaseTag: process.env.RELEASE_TAG,
releasedAt: process.env.RELEASED_AT,
sourceRef: process.env.RELEASE_SOURCE_REF
});
await writeFile(
path.resolve(requireNonEmpty(outputPath, "outputPath")),
`${JSON.stringify(latest, null, 2)}\n`,
"utf8"
);
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
void main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});
}
export {
buildDesktopReleaseLatest,
classifyDesktopReleaseAsset,
normalizeBaseUrl,
parseReleaseVersion,
releaseVersionFromTag,
resolveReleaseChannel,
schemaVersion
};