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
257 lines
6.9 KiB
JavaScript
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
|
|
};
|