Files
tutti/apps/desktop/scripts/merge-macos-release-artifacts.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

257 lines
6.9 KiB
JavaScript

#!/usr/bin/env node
import { createHash } from "node:crypto";
import { createReadStream } from "node:fs";
import {
copyFile,
mkdir,
readdir,
rm,
stat,
writeFile
} from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
const macArchitectures = ["x64", "arm64", "universal"];
const copiedArtifactSuffixes = [".blockmap", ".dmg", ".zip"];
function requireValue(value, label) {
const normalized = String(value ?? "").trim();
if (!normalized) {
throw new Error(`${label} is required`);
}
return normalized;
}
function resolveUpdaterFileName(channel) {
switch (channel) {
case "stable":
return "latest-mac.yml";
case "rc":
return "rc-mac.yml";
case "beta":
return "beta-mac.yml";
default:
throw new Error(`Unsupported desktop release channel: ${channel}`);
}
}
function resolveMacArchitecture(fileName) {
const match = /-mac-(x64|arm64|universal)\.zip$/i.exec(fileName);
return match?.[1]?.toLowerCase() ?? null;
}
function shouldCopyArtifact(fileName) {
return copiedArtifactSuffixes.some((suffix) => fileName.endsWith(suffix));
}
function validateArchitectureArtifacts(artifactNames, releaseVersion) {
for (const architecture of macArchitectures) {
for (const suffix of [".dmg", ".zip", ".zip.blockmap"]) {
const expectedSuffix = `-${releaseVersion}-mac-${architecture}${suffix}`;
const matches = artifactNames.filter((fileName) =>
fileName.endsWith(expectedSuffix)
);
if (matches.length !== 1) {
throw new Error(
`Expected one macOS ${architecture}${suffix} artifact, found ${matches.length}`
);
}
}
}
}
async function listFilesRecursively(root) {
const files = [];
const entries = await readdir(root, { withFileTypes: true });
for (const entry of entries) {
const entryPath = path.join(root, entry.name);
if (entry.isDirectory()) {
files.push(...(await listFilesRecursively(entryPath)));
} else if (entry.isFile()) {
files.push(entryPath);
}
}
return files;
}
async function sha512(filePath) {
const hash = createHash("sha512");
const bytes = createReadStream(filePath);
for await (const chunk of bytes) {
hash.update(chunk);
}
return hash.digest("base64");
}
function serializeUpdaterMetadata(metadata) {
const lines = [`version: ${JSON.stringify(metadata.version)}`, "files:"];
for (const file of metadata.files) {
lines.push(
` - url: ${JSON.stringify(file.url)}`,
` sha512: ${JSON.stringify(file.sha512)}`,
` size: ${file.size}`
);
}
lines.push(
`path: ${JSON.stringify(metadata.path)}`,
`sha512: ${JSON.stringify(metadata.sha512)}`,
`releaseDate: ${JSON.stringify(metadata.releaseDate)}`,
""
);
return lines.join("\n");
}
async function buildMacUpdaterMetadata({
releaseDate = new Date().toISOString(),
releaseVersion,
zipPaths
}) {
const version = requireValue(releaseVersion, "releaseVersion");
const zipByArchitecture = new Map();
for (const zipPath of zipPaths) {
const fileName = path.basename(zipPath);
const architecture = resolveMacArchitecture(fileName);
if (!architecture) {
throw new Error(`Cannot resolve macOS architecture from ${fileName}`);
}
if (!fileName.includes(`-${version}-mac-${architecture}.zip`)) {
throw new Error(
`${fileName} does not match desktop release version ${version}`
);
}
if (zipByArchitecture.has(architecture)) {
throw new Error(`Duplicate macOS ${architecture} updater ZIP`);
}
zipByArchitecture.set(architecture, zipPath);
}
for (const architecture of macArchitectures) {
if (!zipByArchitecture.has(architecture)) {
throw new Error(`Missing macOS ${architecture} updater ZIP`);
}
}
const files = [];
for (const architecture of macArchitectures) {
const zipPath = zipByArchitecture.get(architecture);
const fileStat = await stat(zipPath);
files.push({
url: path.basename(zipPath),
sha512: await sha512(zipPath),
size: fileStat.size
});
}
const defaultFile = files[0];
return {
version,
files,
path: defaultFile.url,
sha512: defaultFile.sha512,
releaseDate: new Date(releaseDate).toISOString()
};
}
async function mergeMacosReleaseArtifacts({
inputDirectory,
outputDirectory,
releaseChannel,
releaseDate,
releaseVersion
}) {
const inputRoot = path.resolve(
requireValue(inputDirectory, "inputDirectory")
);
const outputRoot = path.resolve(
requireValue(outputDirectory, "outputDirectory")
);
if (inputRoot === outputRoot || outputRoot === path.parse(outputRoot).root) {
throw new Error("outputDirectory must be a dedicated non-root directory");
}
const sourceFiles = (await listFilesRecursively(inputRoot)).filter(
(filePath) => shouldCopyArtifact(path.basename(filePath))
);
if (sourceFiles.length === 0) {
throw new Error(`No macOS release artifacts found under ${inputRoot}`);
}
await rm(outputRoot, { force: true, recursive: true });
await mkdir(outputRoot, { recursive: true });
const copiedNames = new Set();
const zipPaths = [];
for (const sourcePath of sourceFiles.sort()) {
const fileName = path.basename(sourcePath);
if (copiedNames.has(fileName)) {
throw new Error(`Duplicate macOS release artifact: ${fileName}`);
}
copiedNames.add(fileName);
const destinationPath = path.join(outputRoot, fileName);
await copyFile(sourcePath, destinationPath);
if (fileName.endsWith(".zip")) {
zipPaths.push(destinationPath);
}
}
validateArchitectureArtifacts(
[...copiedNames],
requireValue(releaseVersion, "releaseVersion")
);
const metadata = await buildMacUpdaterMetadata({
releaseDate,
releaseVersion,
zipPaths
});
const updaterFileName = resolveUpdaterFileName(
requireValue(releaseChannel, "releaseChannel")
);
await writeFile(
path.join(outputRoot, updaterFileName),
serializeUpdaterMetadata(metadata),
"utf8"
);
return {
artifactNames: [...copiedNames].sort(),
metadata,
updaterFileName
};
}
async function main() {
const [inputDirectory, outputDirectory] = process.argv.slice(2);
const result = await mergeMacosReleaseArtifacts({
inputDirectory,
outputDirectory,
releaseChannel: process.env.RELEASE_CHANNEL,
releaseDate: process.env.RELEASE_DATE,
releaseVersion: process.env.RELEASE_VERSION
});
console.log(
`[macos-release-merge] merged ${result.artifactNames.length} artifacts and wrote ${result.updaterFileName}`
);
}
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 {
buildMacUpdaterMetadata,
macArchitectures,
mergeMacosReleaseArtifacts,
resolveUpdaterFileName,
serializeUpdaterMetadata
};