update
This commit is contained in:
1 parent
66158e222e
commit
67c757f481
314 files changed
+37945
-22962
No files matched your search
+273
-47
@@ -8,9 +8,13 @@ const packageJsonPath = join(__dirname, "..", "package.json");
|
||||
const buildDir = join(__dirname, "..", "build");
|
||||
const releaseDir = join(buildDir, "release");
|
||||
const runtimeConfigPath = join(buildDir, "electron-builder.runtime.json");
|
||||
const localElectronDistPath = join(__dirname, "..", "node_modules", "electron", "dist");
|
||||
const customMacSignPath = join(__dirname, "electron-builder-mac-sign.mjs");
|
||||
|
||||
const extraArgs = process.argv.slice(2);
|
||||
const env = { ...process.env };
|
||||
const explicitElectronDistPath = env.AGENTCOMPANY_ELECTRON_DIST?.trim() || "";
|
||||
const noDeprecationFlag = "--no-deprecation";
|
||||
const requestedUpdateUrl = env.AGENTCOMPANY_UPDATE_URL?.trim() ?? "";
|
||||
const strictRelease = env.AGENTCOMPANY_STRICT_RELEASE === "true";
|
||||
const hasMacSigningMaterial = Boolean(
|
||||
@@ -19,24 +23,128 @@ const hasMacSigningMaterial = Boolean(
|
||||
env.APPLE_API_KEY?.trim(),
|
||||
);
|
||||
const isDirBuild = extraArgs.includes("--dir");
|
||||
const r2AccountId = env.AGENTCOMPANY_R2_ACCOUNT_ID?.trim() ?? env.R2_ACCOUNT_ID?.trim() ?? "";
|
||||
const r2Bucket = env.AGENTCOMPANY_R2_BUCKET?.trim() ?? env.R2_BUCKET?.trim() ?? "";
|
||||
const r2PublicUrl = env.AGENTCOMPANY_R2_PUBLIC_URL?.trim() ?? env.R2_PUBLIC_BASE_URL?.trim() ?? "";
|
||||
const r2Path = env.AGENTCOMPANY_R2_PATH?.trim() ?? env.AGENTCOMPANY_RELEASE_PREFIX?.trim() ?? "desktop";
|
||||
const r2Region = env.AGENTCOMPANY_R2_REGION?.trim() || "auto";
|
||||
const r2Endpoint = env.AGENTCOMPANY_R2_ENDPOINT?.trim() || (r2AccountId ? `https://${r2AccountId}.r2.cloudflarestorage.com` : "");
|
||||
const filteredUnsignedMacWarningSnippets = [
|
||||
"skipped macOS code signing reason=identity explicitly is set to null",
|
||||
"arm64 requires signing, but identity is set to null and signing is being skipped",
|
||||
];
|
||||
|
||||
function bumpPatchVersion(currentVersion) {
|
||||
function validateBuildVersion(currentVersion) {
|
||||
const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(currentVersion);
|
||||
if (!match) {
|
||||
throw new Error(`[build-desktop] Unsupported package version format: ${currentVersion}`);
|
||||
}
|
||||
const [, major, minor, patch] = match;
|
||||
return `${major}.${minor}.${Number.parseInt(patch, 10) + 1}`;
|
||||
return currentVersion;
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(url) {
|
||||
return url.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function normalizePublishPath(targetPath) {
|
||||
return targetPath.replace(/^\/+|\/+$/g, "");
|
||||
}
|
||||
|
||||
function resolveUpdateUrl() {
|
||||
if (requestedUpdateUrl && !requestedUpdateUrl.includes("example.invalid")) {
|
||||
return normalizeBaseUrl(requestedUpdateUrl);
|
||||
}
|
||||
if (!r2PublicUrl) {
|
||||
return "";
|
||||
}
|
||||
const baseUrl = normalizeBaseUrl(r2PublicUrl);
|
||||
const publishPath = normalizePublishPath(r2Path);
|
||||
return publishPath ? `${baseUrl}/${publishPath}` : baseUrl;
|
||||
}
|
||||
|
||||
const resolvedUpdateUrl = resolveUpdateUrl();
|
||||
const hasR2PublishConfig = Boolean(
|
||||
r2Bucket &&
|
||||
r2Endpoint &&
|
||||
resolvedUpdateUrl &&
|
||||
(env.AGENTCOMPANY_R2_ACCESS_KEY_ID?.trim() || env.R2_ACCESS_KEY_ID?.trim() || env.AWS_ACCESS_KEY_ID?.trim()) &&
|
||||
(env.AGENTCOMPANY_R2_SECRET_ACCESS_KEY?.trim() || env.R2_SECRET_ACCESS_KEY?.trim() || env.AWS_SECRET_ACCESS_KEY?.trim()),
|
||||
);
|
||||
|
||||
if (!env.AWS_ACCESS_KEY_ID && (env.AGENTCOMPANY_R2_ACCESS_KEY_ID?.trim() || env.R2_ACCESS_KEY_ID?.trim())) {
|
||||
env.AWS_ACCESS_KEY_ID = env.AGENTCOMPANY_R2_ACCESS_KEY_ID?.trim() || env.R2_ACCESS_KEY_ID?.trim();
|
||||
}
|
||||
if (!env.AWS_SECRET_ACCESS_KEY && (env.AGENTCOMPANY_R2_SECRET_ACCESS_KEY?.trim() || env.R2_SECRET_ACCESS_KEY?.trim())) {
|
||||
env.AWS_SECRET_ACCESS_KEY = env.AGENTCOMPANY_R2_SECRET_ACCESS_KEY?.trim() || env.R2_SECRET_ACCESS_KEY?.trim();
|
||||
}
|
||||
if (!env.AWS_REGION) {
|
||||
env.AWS_REGION = r2Region;
|
||||
}
|
||||
if (!env.AWS_DEFAULT_REGION) {
|
||||
env.AWS_DEFAULT_REGION = env.AWS_REGION;
|
||||
}
|
||||
if (!env.AWS_EC2_METADATA_DISABLED) {
|
||||
env.AWS_EC2_METADATA_DISABLED = "true";
|
||||
}
|
||||
if (!env.AGENTCOMPANY_UPDATE_URL && resolvedUpdateUrl) {
|
||||
env.AGENTCOMPANY_UPDATE_URL = resolvedUpdateUrl;
|
||||
}
|
||||
|
||||
function resolveCurrentPlatformArg() {
|
||||
switch (process.platform) {
|
||||
case "darwin":
|
||||
return "--mac";
|
||||
case "win32":
|
||||
return "--win";
|
||||
default:
|
||||
return "--linux";
|
||||
}
|
||||
}
|
||||
|
||||
function resolveCurrentArchArg() {
|
||||
switch (process.arch) {
|
||||
case "arm64":
|
||||
return "--arm64";
|
||||
case "x64":
|
||||
return "--x64";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function hasExplicitArchArg(args) {
|
||||
return args.some((arg) =>
|
||||
arg === "--arm64" ||
|
||||
arg === "--x64" ||
|
||||
arg === "--ia32" ||
|
||||
arg === "--universal" ||
|
||||
arg.startsWith("--arm64=") ||
|
||||
arg.startsWith("--x64=") ||
|
||||
arg.startsWith("--ia32=") ||
|
||||
arg.startsWith("--universal="),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveCurrentMacArch() {
|
||||
if (process.platform !== "darwin" || env.AGENTCOMPANY_MAC_ALL_ARCHES === "true" || hasExplicitArchArg(extraArgs)) {
|
||||
return null;
|
||||
}
|
||||
return process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "x64" : null;
|
||||
}
|
||||
|
||||
async function resolveBuildVersion() {
|
||||
const raw = await readFile(packageJsonPath, "utf8");
|
||||
const pkg = JSON.parse(raw);
|
||||
const nextVersion = bumpPatchVersion(pkg.version);
|
||||
return nextVersion;
|
||||
return validateBuildVersion(pkg.version);
|
||||
}
|
||||
|
||||
async function writeRuntimeConfig(buildVersion) {
|
||||
const forcedMacArch = resolveCurrentMacArch();
|
||||
const isUnsignedLocalMacBuild = (
|
||||
process.platform === "darwin" &&
|
||||
!hasMacSigningMaterial &&
|
||||
env.AGENTCOMPANY_ENABLE_LOCAL_DMG !== "true"
|
||||
);
|
||||
const runtimeConfig = {
|
||||
extends: join(__dirname, "..", "electron-builder.yml"),
|
||||
directories: {
|
||||
@@ -46,34 +154,76 @@ async function writeRuntimeConfig(buildVersion) {
|
||||
version: buildVersion,
|
||||
},
|
||||
};
|
||||
if (requestedUpdateUrl) {
|
||||
if (explicitElectronDistPath) {
|
||||
runtimeConfig.electronDist = explicitElectronDistPath;
|
||||
} else if (isDirBuild) {
|
||||
runtimeConfig.electronDist = localElectronDistPath;
|
||||
}
|
||||
if (hasR2PublishConfig) {
|
||||
runtimeConfig.publish = [
|
||||
{
|
||||
provider: "s3",
|
||||
bucket: r2Bucket,
|
||||
endpoint: r2Endpoint,
|
||||
region: r2Region,
|
||||
path: normalizePublishPath(r2Path) || undefined,
|
||||
acl: null,
|
||||
publishAutoUpdate: true,
|
||||
},
|
||||
];
|
||||
} else if (resolvedUpdateUrl) {
|
||||
runtimeConfig.publish = [
|
||||
{
|
||||
provider: "generic",
|
||||
url: requestedUpdateUrl,
|
||||
url: resolvedUpdateUrl,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (process.platform === "darwin" && !hasMacSigningMaterial && env.AGENTCOMPANY_ENABLE_LOCAL_DMG !== "true") {
|
||||
if (isUnsignedLocalMacBuild) {
|
||||
if (strictRelease) {
|
||||
throw new Error("[build-desktop] Strict release mode requires macOS signing/notarization material.");
|
||||
}
|
||||
runtimeConfig.mac = {
|
||||
hardenedRuntime: false,
|
||||
identity: null,
|
||||
};
|
||||
if (!isDirBuild) {
|
||||
runtimeConfig.mac.target = ["zip"];
|
||||
runtimeConfig.mac.target = forcedMacArch
|
||||
? [{ target: "zip", arch: [forcedMacArch] }]
|
||||
: ["zip"];
|
||||
}
|
||||
} else if (process.platform === "darwin" && !hasMacSigningMaterial) {
|
||||
runtimeConfig.mac = {
|
||||
hardenedRuntime: false,
|
||||
identity: null,
|
||||
};
|
||||
} else if (process.platform === "darwin") {
|
||||
runtimeConfig.mac = {
|
||||
hardenedRuntime: true,
|
||||
};
|
||||
if (hasMacSigningMaterial) {
|
||||
runtimeConfig.mac.sign = customMacSignPath;
|
||||
}
|
||||
}
|
||||
if (process.platform === "darwin" && !isDirBuild && forcedMacArch && !isUnsignedLocalMacBuild) {
|
||||
runtimeConfig.mac ??= {};
|
||||
runtimeConfig.mac.target = [
|
||||
{ target: "dmg", arch: [forcedMacArch] },
|
||||
{ target: "zip", arch: [forcedMacArch] },
|
||||
];
|
||||
}
|
||||
await writeFile(runtimeConfigPath, `${JSON.stringify(runtimeConfig, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
if (env.CSC_IDENTITY_AUTO_DISCOVERY === undefined) {
|
||||
env.CSC_IDENTITY_AUTO_DISCOVERY = "false";
|
||||
env.CSC_IDENTITY_AUTO_DISCOVERY = hasMacSigningMaterial ? "true" : "false";
|
||||
}
|
||||
|
||||
const existingNodeOptions = env.NODE_OPTIONS?.trim();
|
||||
if (!existingNodeOptions) {
|
||||
env.NODE_OPTIONS = noDeprecationFlag;
|
||||
} else if (!existingNodeOptions.includes(noDeprecationFlag)) {
|
||||
env.NODE_OPTIONS = `${existingNodeOptions} ${noDeprecationFlag}`;
|
||||
}
|
||||
|
||||
const allowedPublishModes = new Set(["always", "never", "onTag", "onTagOrDraft"]);
|
||||
@@ -89,6 +239,14 @@ const explicitPlatformArg = extraArgs.some((arg) =>
|
||||
arg.startsWith("--win=") ||
|
||||
arg.startsWith("--linux="),
|
||||
);
|
||||
const explicitArchArg = hasExplicitArchArg(extraArgs);
|
||||
const shouldForceLocalMacZip = (
|
||||
process.platform === "darwin" &&
|
||||
!hasMacSigningMaterial &&
|
||||
env.AGENTCOMPANY_ENABLE_LOCAL_DMG !== "true" &&
|
||||
!explicitPlatformArg &&
|
||||
!isDirBuild
|
||||
);
|
||||
const requestedPublishMode = env.AGENTCOMPANY_ELECTRON_PUBLISH?.trim();
|
||||
let publishMode = "never";
|
||||
|
||||
@@ -107,31 +265,102 @@ const builderArgs = ["exec", "electron-builder", "--config", runtimeConfigPath];
|
||||
if (!explicitPublishArg) {
|
||||
builderArgs.push("--publish", publishMode);
|
||||
}
|
||||
if (
|
||||
process.platform === "darwin" &&
|
||||
!hasMacSigningMaterial &&
|
||||
env.AGENTCOMPANY_ENABLE_LOCAL_DMG !== "true" &&
|
||||
!explicitPlatformArg &&
|
||||
!isDirBuild
|
||||
) {
|
||||
builderArgs.push("--mac", "zip");
|
||||
if (!explicitPlatformArg) {
|
||||
builderArgs.push(resolveCurrentPlatformArg());
|
||||
}
|
||||
if (!explicitArchArg) {
|
||||
const currentArchArg = resolveCurrentArchArg();
|
||||
if (currentArchArg) {
|
||||
builderArgs.push(currentArchArg);
|
||||
}
|
||||
}
|
||||
if (shouldForceLocalMacZip) {
|
||||
builderArgs.push("zip");
|
||||
}
|
||||
builderArgs.push(...extraArgs);
|
||||
|
||||
if (strictRelease && !requestedUpdateUrl) {
|
||||
throw new Error("[build-desktop] Strict release mode requires AGENTCOMPANY_UPDATE_URL to be configured.");
|
||||
if (strictRelease && !resolvedUpdateUrl) {
|
||||
throw new Error("[build-desktop] Strict release mode requires AGENTCOMPANY_UPDATE_URL or an R2 public URL to be configured.");
|
||||
}
|
||||
if (publishMode !== "never" && !hasR2PublishConfig && !resolvedUpdateUrl) {
|
||||
throw new Error(
|
||||
"[build-desktop] Publishing requires either AGENTCOMPANY_UPDATE_URL (generic feed only) " +
|
||||
"or full Cloudflare R2 publish configuration.",
|
||||
);
|
||||
}
|
||||
|
||||
const transientBuildFailurePatterns = [
|
||||
/chmod .*Electron Framework.*no such file or directory/i,
|
||||
];
|
||||
|
||||
function shouldRetryElectronBuilder(output) {
|
||||
return transientBuildFailurePatterns.some((pattern) => pattern.test(output));
|
||||
}
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function shouldSuppressBuildLine(line) {
|
||||
return filteredUnsignedMacWarningSnippets.some((snippet) => line.includes(snippet));
|
||||
}
|
||||
|
||||
function forwardChildOutput(stream, writer, onLine) {
|
||||
if (!stream) return;
|
||||
let pending = "";
|
||||
stream.on("data", (chunk) => {
|
||||
pending += chunk.toString();
|
||||
const lines = pending.split(/\r?\n/);
|
||||
pending = lines.pop() ?? "";
|
||||
for (const line of lines) {
|
||||
onLine(line);
|
||||
if (!shouldSuppressBuildLine(line)) {
|
||||
writer.write(`${line}\n`);
|
||||
}
|
||||
}
|
||||
});
|
||||
stream.on("end", () => {
|
||||
if (!pending) return;
|
||||
onLine(pending);
|
||||
if (!shouldSuppressBuildLine(pending)) {
|
||||
writer.write(`${pending}\n`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function runElectronBuilder() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn("pnpm", builderArgs, {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env,
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
let output = "";
|
||||
const appendOutputLine = (line) => {
|
||||
output += `${line}\n`;
|
||||
};
|
||||
forwardChildOutput(child.stdout, process.stdout, appendOutputLine);
|
||||
forwardChildOutput(child.stderr, process.stderr, appendOutputLine);
|
||||
|
||||
child.on("error", reject);
|
||||
child.on("close", (code, signal) => {
|
||||
resolve({ code: code ?? 1, signal, output });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// On Windows, antivirus scanners may hold file locks on .asar/.exe files.
|
||||
// Retry the rm with increasing delays to give the OS time to release them.
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
try {
|
||||
await rm(buildDir, { recursive: true, force: true });
|
||||
break;
|
||||
} catch (rmErr) {
|
||||
if (attempt < 4 && rmErr.code === "EBUSY") {
|
||||
const retryable = rmErr.code === "EBUSY" || rmErr.code === "ENOTEMPTY" || rmErr.code === "EPERM";
|
||||
if (attempt < 4 && retryable) {
|
||||
const delay = (attempt + 1) * 3000;
|
||||
console.log(`[build-desktop] Build dir locked (EBUSY), retrying in ${delay / 1000}s... (attempt ${attempt + 1}/5)`);
|
||||
console.log(
|
||||
`[build-desktop] Build dir cleanup blocked (${rmErr.code}), retrying in ${delay / 1000}s... ` +
|
||||
`(attempt ${attempt + 1}/5)`,
|
||||
);
|
||||
await new Promise((r) => setTimeout(r, delay));
|
||||
} else {
|
||||
throw rmErr;
|
||||
@@ -140,31 +369,28 @@ for (let attempt = 0; attempt < 5; attempt++) {
|
||||
}
|
||||
await mkdir(buildDir, { recursive: true });
|
||||
const buildVersion = env.AGENTCOMPANY_BUILD_VERSION?.trim() || await resolveBuildVersion();
|
||||
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
||||
if (packageJson.version !== buildVersion) {
|
||||
packageJson.version = buildVersion;
|
||||
await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf8");
|
||||
}
|
||||
await writeRuntimeConfig(buildVersion);
|
||||
env.AGENTCOMPANY_BUILD_VERSION = buildVersion;
|
||||
env.npm_package_version = buildVersion;
|
||||
console.log(`[build-desktop] Cleaned ${buildDir} and staged build version ${buildVersion}.`);
|
||||
|
||||
const child = spawn("pnpm", builderArgs, {
|
||||
stdio: "inherit",
|
||||
env,
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
child.on("exit", (code, signal) => {
|
||||
if (signal) {
|
||||
process.kill(process.pid, signal);
|
||||
return;
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
try {
|
||||
const result = await runElectronBuilder();
|
||||
if (result.signal) {
|
||||
process.kill(process.pid, result.signal);
|
||||
break;
|
||||
}
|
||||
if (result.code === 0) {
|
||||
process.exit(0);
|
||||
}
|
||||
if (attempt === 0 && shouldRetryElectronBuilder(result.output)) {
|
||||
console.warn("[build-desktop] electron-builder hit a transient macOS framework extraction error; retrying once.");
|
||||
await delay(1000);
|
||||
continue;
|
||||
}
|
||||
process.exit(result.code);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(code ?? 1);
|
||||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { readdir } from "node:fs/promises";
|
||||
import { extname } from "node:path";
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function runCommand(command, args, capture = false) {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
||||
});
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
if (capture) {
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
}
|
||||
|
||||
child.on("error", reject);
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr });
|
||||
return;
|
||||
}
|
||||
reject(new Error(stderr || stdout || `${command} failed with exit code ${code ?? 1}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function runCodesign(args) {
|
||||
await runCommand("/usr/bin/codesign", args, false);
|
||||
}
|
||||
|
||||
function buildCodesignArgs(targetPath, identity, keychain, fileOptions = {}) {
|
||||
const args = [
|
||||
"--sign", identity,
|
||||
"--force",
|
||||
];
|
||||
|
||||
if (keychain) {
|
||||
args.push("--keychain", keychain);
|
||||
}
|
||||
if (fileOptions.timestamp !== false) {
|
||||
args.push("--timestamp");
|
||||
}
|
||||
if (fileOptions.hardenedRuntime) {
|
||||
args.push("--options", "runtime");
|
||||
}
|
||||
if (fileOptions.entitlements) {
|
||||
args.push("--entitlements", fileOptions.entitlements);
|
||||
}
|
||||
if (Array.isArray(fileOptions.additionalArguments) && fileOptions.additionalArguments.length > 0) {
|
||||
args.push(...fileOptions.additionalArguments);
|
||||
}
|
||||
|
||||
args.push(targetPath);
|
||||
return args;
|
||||
}
|
||||
|
||||
async function isMachBinary(filePath) {
|
||||
const extension = extname(filePath);
|
||||
if (extension === ".dylib" || extension === ".node") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { stdout } = await runCommand("/usr/bin/file", ["-b", filePath], true);
|
||||
return stdout.includes("Mach-O");
|
||||
}
|
||||
|
||||
async function collectSignTargets(rootPath) {
|
||||
const bundleTargets = [];
|
||||
const binaryTargets = [];
|
||||
|
||||
async function walk(currentPath) {
|
||||
const entries = await readdir(currentPath, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = `${currentPath}/${entry.name}`;
|
||||
|
||||
if (entry.isSymbolicLink()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (
|
||||
entry.name.endsWith(".app") ||
|
||||
entry.name.endsWith(".framework") ||
|
||||
entry.name.endsWith(".xpc") ||
|
||||
entry.name.endsWith(".appex") ||
|
||||
entry.name.endsWith(".plugin")
|
||||
) {
|
||||
bundleTargets.push(fullPath);
|
||||
}
|
||||
await walk(fullPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (await isMachBinary(fullPath)) {
|
||||
binaryTargets.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(rootPath);
|
||||
|
||||
bundleTargets.sort((left, right) => right.length - left.length);
|
||||
binaryTargets.sort((left, right) => right.length - left.length);
|
||||
|
||||
return { bundleTargets, binaryTargets };
|
||||
}
|
||||
|
||||
async function signTarget(targetPath, context) {
|
||||
const fileOptions = context.optionsForFile?.(targetPath) ?? {};
|
||||
const args = buildCodesignArgs(targetPath, context.identity, context.keychain, fileOptions);
|
||||
|
||||
let lastError;
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
try {
|
||||
await runCodesign(args);
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (attempt === 2) {
|
||||
throw error;
|
||||
}
|
||||
await delay(5000);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
export default async function sign(context) {
|
||||
const { bundleTargets, binaryTargets } = await collectSignTargets(context.app);
|
||||
|
||||
for (const target of binaryTargets) {
|
||||
await signTarget(target, context);
|
||||
}
|
||||
|
||||
for (const target of bundleTargets) {
|
||||
await signTarget(target, context);
|
||||
}
|
||||
|
||||
await signTarget(context.app, context);
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import { readdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const rootDir = process.cwd();
|
||||
const localReleaseDir = join(rootDir, "local-release");
|
||||
const outputPath = join(rootDir, "landing", "components", "release-links.generated.ts");
|
||||
|
||||
async function loadEnvFile(filePath) {
|
||||
if (!existsSync(filePath)) return;
|
||||
const content = await readFile(filePath, "utf8");
|
||||
for (const rawLine of content.split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith("#")) continue;
|
||||
const separatorIndex = line.indexOf("=");
|
||||
if (separatorIndex <= 0) continue;
|
||||
const key = line.slice(0, separatorIndex).trim();
|
||||
if (!key || process.env[key]) continue;
|
||||
const rawValue = line.slice(separatorIndex + 1).trim();
|
||||
if (!rawValue) continue;
|
||||
process.env[key] = rawValue.replace(/^['"]|['"]$/g, "");
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(url) {
|
||||
return url.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function normalizePathSegment(pathValue) {
|
||||
return pathValue.replace(/^\/+|\/+$/g, "");
|
||||
}
|
||||
|
||||
function resolveUpdateBaseUrl() {
|
||||
const explicit = process.env.AGENTCOMPANY_UPDATE_URL?.trim();
|
||||
if (explicit) return normalizeBaseUrl(explicit);
|
||||
|
||||
const publicBaseUrl =
|
||||
process.env.AGENTCOMPANY_R2_PUBLIC_URL?.trim() ||
|
||||
process.env.R2_PUBLIC_BASE_URL?.trim();
|
||||
const prefix = process.env.AGENTCOMPANY_RELEASE_PREFIX?.trim() || "AC";
|
||||
|
||||
if (!publicBaseUrl) {
|
||||
throw new Error("Missing AGENTCOMPANY_UPDATE_URL / R2 public URL while generating landing release links.");
|
||||
}
|
||||
|
||||
const normalizedBase = normalizeBaseUrl(publicBaseUrl);
|
||||
const normalizedPrefix = normalizePathSegment(prefix);
|
||||
return `${normalizedBase}/${normalizedPrefix}`;
|
||||
}
|
||||
|
||||
function ensureSingle(files, predicate, label) {
|
||||
const matches = files.filter(predicate);
|
||||
if (matches.length !== 1) {
|
||||
throw new Error(`Expected exactly one ${label} artifact, found ${matches.length}: ${matches.join(", ")}`);
|
||||
}
|
||||
return matches[0];
|
||||
}
|
||||
|
||||
function extractVersion(fileNames) {
|
||||
const versions = new Set(
|
||||
fileNames
|
||||
.map((fileName) => fileName.match(/(\d+\.\d+\.\d+)/)?.[1] ?? null)
|
||||
.filter(Boolean),
|
||||
);
|
||||
|
||||
if (versions.size !== 1) {
|
||||
throw new Error(`Expected exactly one release version across artifacts, found: ${[...versions].join(", ")}`);
|
||||
}
|
||||
|
||||
return [...versions][0];
|
||||
}
|
||||
|
||||
function urlFor(baseUrl, fileName) {
|
||||
return `${baseUrl}/${encodeURIComponent(fileName)}`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await loadEnvFile(join(rootDir, ".env.release.local"));
|
||||
await loadEnvFile(join(rootDir, ".env.release"));
|
||||
|
||||
if (!existsSync(localReleaseDir)) {
|
||||
throw new Error(`Local release directory not found: ${localReleaseDir}`);
|
||||
}
|
||||
|
||||
const baseUrl = resolveUpdateBaseUrl();
|
||||
const macFiles = (await readdir(join(localReleaseDir, "mac"))).filter((name) => !name.startsWith("."));
|
||||
const winFiles = (await readdir(join(localReleaseDir, "win"))).filter((name) => !name.startsWith("."));
|
||||
const linuxFiles = (await readdir(join(localReleaseDir, "linux"))).filter((name) => !name.startsWith("."));
|
||||
|
||||
const macArm64Dmg = ensureSingle(macFiles, (name) => name.endsWith(".dmg") && name.includes("arm64"), "macOS Apple Silicon .dmg");
|
||||
const macIntelDmg = ensureSingle(macFiles, (name) => name.endsWith(".dmg") && !name.includes("arm64"), "macOS Intel .dmg");
|
||||
const windowsUniversalInstaller = ensureSingle(
|
||||
winFiles,
|
||||
(name) => name.endsWith(".exe") && name.includes("Setup"),
|
||||
"Windows universal installer",
|
||||
);
|
||||
const linuxX64AppImage = ensureSingle(
|
||||
linuxFiles,
|
||||
(name) => name.endsWith(".AppImage") && !name.includes("arm64"),
|
||||
"Linux x64 AppImage",
|
||||
);
|
||||
const linuxArm64AppImage = ensureSingle(
|
||||
linuxFiles,
|
||||
(name) => name.endsWith(".AppImage") && name.includes("arm64"),
|
||||
"Linux arm64 AppImage",
|
||||
);
|
||||
const linuxX64Deb = ensureSingle(
|
||||
linuxFiles,
|
||||
(name) => name.endsWith(".deb") && name.includes("amd64"),
|
||||
"Linux x64 .deb",
|
||||
);
|
||||
const linuxArm64Deb = ensureSingle(
|
||||
linuxFiles,
|
||||
(name) => name.endsWith(".deb") && name.includes("arm64"),
|
||||
"Linux arm64 .deb",
|
||||
);
|
||||
const linuxX64Rpm = ensureSingle(
|
||||
linuxFiles,
|
||||
(name) => name.endsWith(".rpm") && (name.includes("x86_64") || name.includes("amd64")),
|
||||
"Linux x64 .rpm",
|
||||
);
|
||||
const linuxArm64Rpm = ensureSingle(
|
||||
linuxFiles,
|
||||
(name) => name.endsWith(".rpm") && (name.includes("aarch64") || name.includes("arm64")),
|
||||
"Linux arm64 .rpm",
|
||||
);
|
||||
|
||||
const version = extractVersion([
|
||||
macArm64Dmg,
|
||||
macIntelDmg,
|
||||
windowsUniversalInstaller,
|
||||
linuxX64AppImage,
|
||||
linuxArm64AppImage,
|
||||
linuxX64Deb,
|
||||
linuxArm64Deb,
|
||||
linuxX64Rpm,
|
||||
linuxArm64Rpm,
|
||||
]);
|
||||
|
||||
const content = `export type PlatformDownloadAsset = {
|
||||
id: string;
|
||||
label: string;
|
||||
href: string;
|
||||
note: string;
|
||||
};
|
||||
|
||||
export type PlatformDownload = {
|
||||
id: "macos" | "windows" | "linux";
|
||||
title: string;
|
||||
note: string;
|
||||
assets: PlatformDownloadAsset[];
|
||||
};
|
||||
|
||||
export const releaseVersion = "${version}";
|
||||
|
||||
export const platformDownloads: PlatformDownload[] = [
|
||||
{
|
||||
id: "macos",
|
||||
title: "macOS",
|
||||
note: "Signed + notarized DMG builds",
|
||||
assets: [
|
||||
{
|
||||
id: "macos-arm64-dmg",
|
||||
label: "Apple Silicon DMG",
|
||||
href: "${urlFor(baseUrl, macArm64Dmg)}",
|
||||
note: "arm64",
|
||||
},
|
||||
{
|
||||
id: "macos-x64-dmg",
|
||||
label: "Intel Mac DMG",
|
||||
href: "${urlFor(baseUrl, macIntelDmg)}",
|
||||
note: "x64",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "windows",
|
||||
title: "Windows",
|
||||
note: "NSIS installer builds",
|
||||
assets: [
|
||||
{
|
||||
id: "windows-universal-installer",
|
||||
label: "Windows Universal Installer",
|
||||
href: "${urlFor(baseUrl, windowsUniversalInstaller)}",
|
||||
note: "arm64 + x64 + ia32",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "linux",
|
||||
title: "Linux",
|
||||
note: "AppImage, DEB, and RPM builds",
|
||||
assets: [
|
||||
{
|
||||
id: "linux-x64-appimage",
|
||||
label: "Linux x64 AppImage",
|
||||
href: "${urlFor(baseUrl, linuxX64AppImage)}",
|
||||
note: "x64",
|
||||
},
|
||||
{
|
||||
id: "linux-x64-deb",
|
||||
label: "Linux x64 DEB",
|
||||
href: "${urlFor(baseUrl, linuxX64Deb)}",
|
||||
note: "x64",
|
||||
},
|
||||
{
|
||||
id: "linux-x64-rpm",
|
||||
label: "Linux x64 RPM",
|
||||
href: "${urlFor(baseUrl, linuxX64Rpm)}",
|
||||
note: "x64",
|
||||
},
|
||||
{
|
||||
id: "linux-arm64-appimage",
|
||||
label: "Linux ARM64 AppImage",
|
||||
href: "${urlFor(baseUrl, linuxArm64AppImage)}",
|
||||
note: "arm64",
|
||||
},
|
||||
{
|
||||
id: "linux-arm64-deb",
|
||||
label: "Linux ARM64 DEB",
|
||||
href: "${urlFor(baseUrl, linuxArm64Deb)}",
|
||||
note: "arm64",
|
||||
},
|
||||
{
|
||||
id: "linux-arm64-rpm",
|
||||
label: "Linux ARM64 RPM",
|
||||
href: "${urlFor(baseUrl, linuxArm64Rpm)}",
|
||||
note: "arm64",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
`;
|
||||
|
||||
await writeFile(outputPath, content, "utf8");
|
||||
console.log(`[generate-landing-release-links] wrote ${outputPath}`);
|
||||
console.log(`[generate-landing-release-links] version ${version}`);
|
||||
}
|
||||
|
||||
await main();
|
||||
@@ -0,0 +1,283 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { prepareR2ReleaseHost, uploadReleaseFiles } from "./release-local/r2.mjs";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = join(__dirname, "..");
|
||||
const buildReleaseDir = join(rootDir, "build", "release");
|
||||
const localReleaseDir = join(rootDir, "local-release");
|
||||
const pnpmVersion = "10.24.0";
|
||||
const dockerNodeModulesVolume = `${process.cwd().split("/").filter(Boolean).at(-1) ?? "agentcompany"}-node-modules`;
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = {
|
||||
skipVerify: false,
|
||||
upload: true,
|
||||
platforms: ["mac", "win", "linux"],
|
||||
};
|
||||
for (const arg of argv) {
|
||||
if (arg === "--skip-verify") {
|
||||
options.skipVerify = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--no-upload") {
|
||||
options.upload = false;
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--platforms=")) {
|
||||
options.platforms = arg.slice("--platforms=".length)
|
||||
.split(",")
|
||||
.map((platform) => platform.trim())
|
||||
.filter(Boolean);
|
||||
continue;
|
||||
}
|
||||
if (arg === "--help") {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`Usage: node scripts/release-local.mjs [options]
|
||||
|
||||
Options:
|
||||
--platforms=mac,win,linux Comma-separated platform list (default: all)
|
||||
--skip-verify Skip pnpm verify before release builds
|
||||
--no-upload Build artifacts locally without publishing
|
||||
--help Show this help
|
||||
|
||||
Environment loading:
|
||||
- .env.release
|
||||
- .env.release.local
|
||||
- existing shell environment (takes precedence)
|
||||
`);
|
||||
}
|
||||
|
||||
async function loadEnvFile(filePath) {
|
||||
if (!existsSync(filePath)) return;
|
||||
const content = await readFile(filePath, "utf8");
|
||||
for (const rawLine of content.split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith("#")) continue;
|
||||
const separatorIndex = line.indexOf("=");
|
||||
if (separatorIndex <= 0) continue;
|
||||
const key = line.slice(0, separatorIndex).trim();
|
||||
if (!key || process.env[key]) continue;
|
||||
const rawValue = line.slice(separatorIndex + 1).trim();
|
||||
if (!rawValue) continue;
|
||||
process.env[key] = rawValue.replace(/^['"]|['"]$/g, "");
|
||||
}
|
||||
}
|
||||
|
||||
function hasDocker() {
|
||||
return spawnSyncSuccess("docker", ["--version"]);
|
||||
}
|
||||
|
||||
function spawnSyncSuccess(command, args) {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(command, args, { stdio: "ignore" });
|
||||
child.on("error", () => resolve(false));
|
||||
child.on("close", (code) => resolve(code === 0));
|
||||
});
|
||||
}
|
||||
|
||||
function hasMacSigningCredential() {
|
||||
return Boolean(process.env.CSC_LINK?.trim() || process.env.CSC_NAME?.trim());
|
||||
}
|
||||
|
||||
function hasMacNotarizationCredential() {
|
||||
return Boolean(
|
||||
(process.env.APPLE_ID?.trim() &&
|
||||
process.env.APPLE_APP_SPECIFIC_PASSWORD?.trim() &&
|
||||
process.env.APPLE_TEAM_ID?.trim()) ||
|
||||
(process.env.APPLE_API_KEY?.trim() &&
|
||||
process.env.APPLE_API_KEY_ID?.trim() &&
|
||||
process.env.APPLE_API_ISSUER?.trim()),
|
||||
);
|
||||
}
|
||||
|
||||
async function validatePreflight(platforms, upload) {
|
||||
if (platforms.includes("mac") && process.platform !== "darwin") {
|
||||
throw new Error("Signed macOS release builds must run on macOS.");
|
||||
}
|
||||
if (platforms.includes("mac") && (!hasMacSigningCredential() || !hasMacNotarizationCredential())) {
|
||||
throw new Error(
|
||||
"Signed macOS release requires signing credentials (CSC_LINK or CSC_NAME) and notarization credentials " +
|
||||
"(APPLE_ID/APPLE_APP_SPECIFIC_PASSWORD/APPLE_TEAM_ID or APPLE_API_KEY trio).",
|
||||
);
|
||||
}
|
||||
if ((platforms.includes("win") || platforms.includes("linux")) && !(await hasDocker())) {
|
||||
throw new Error("Docker is required to build Windows/Linux locally from this macOS host.");
|
||||
}
|
||||
if (!upload) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function run(command, args, env = process.env) {
|
||||
await new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd: rootDir,
|
||||
env,
|
||||
stdio: "inherit",
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
child.on("error", reject);
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
reject(new Error(`${command} ${args.join(" ")} failed with exit code ${code ?? 1}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function stageRelease(platform) {
|
||||
const destination = join(localReleaseDir, platform);
|
||||
await rm(destination, { recursive: true, force: true });
|
||||
await mkdir(localReleaseDir, { recursive: true });
|
||||
await cp(buildReleaseDir, destination, { recursive: true });
|
||||
console.log(`[release-local] staged ${platform} artifacts in ${destination}`);
|
||||
}
|
||||
|
||||
function makeDockerEnv(platform) {
|
||||
const env = {
|
||||
AGENTCOMPANY_ELECTRON_PUBLISH: "never",
|
||||
AGENTCOMPANY_UPDATE_URL: process.env.AGENTCOMPANY_UPDATE_URL ?? "",
|
||||
AGENTCOMPANY_ELECTRON_DIST: "/root/.cache/electron",
|
||||
};
|
||||
if (platform === "win") {
|
||||
env.CSC_LINK = process.env.WIN_CSC_LINK ?? "";
|
||||
env.CSC_KEY_PASSWORD = process.env.WIN_CSC_KEY_PASSWORD ?? "";
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
function getDockerBuildCommands(platform) {
|
||||
if (platform === "win") {
|
||||
return [
|
||||
"node scripts/build-desktop.mjs --win nsis --x64 --ia32",
|
||||
"node scripts/build-desktop.mjs --win nsis --arm64",
|
||||
];
|
||||
}
|
||||
return [
|
||||
"node scripts/build-desktop.mjs --linux AppImage deb rpm --x64",
|
||||
"node scripts/build-desktop.mjs --linux AppImage deb rpm --arm64",
|
||||
];
|
||||
}
|
||||
|
||||
async function runDockerBuild(platform) {
|
||||
const isWindows = platform === "win";
|
||||
const image = isWindows ? "electronuserland/builder:wine" : "electronuserland/builder";
|
||||
const env = makeDockerEnv(platform);
|
||||
const cacheDir = join(os.homedir(), ".cache");
|
||||
const command = [
|
||||
"corepack enable",
|
||||
`corepack prepare pnpm@${pnpmVersion} --activate`,
|
||||
"pnpm install --frozen-lockfile",
|
||||
"pnpm exec electron-vite build",
|
||||
...getDockerBuildCommands(platform),
|
||||
].join(" && ");
|
||||
const dockerArgs = [
|
||||
"run",
|
||||
"--rm",
|
||||
"--platform", "linux/amd64",
|
||||
"-v", `${rootDir}:/project`,
|
||||
"-v", `${dockerNodeModulesVolume}:/project/node_modules`,
|
||||
"-v", `${join(cacheDir, "electron")}:/root/.cache/electron`,
|
||||
"-v", `${join(cacheDir, "electron-builder")}:/root/.cache/electron-builder`,
|
||||
"-w", "/project",
|
||||
];
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
dockerArgs.push("-e", `${key}=${value}`);
|
||||
}
|
||||
dockerArgs.push(image, "/bin/bash", "-lc", command);
|
||||
await run("docker", dockerArgs);
|
||||
}
|
||||
|
||||
async function buildMac() {
|
||||
const env = {
|
||||
...process.env,
|
||||
AGENTCOMPANY_ELECTRON_PUBLISH: "never",
|
||||
AGENTCOMPANY_ENABLE_LOCAL_DMG: "true",
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: "true",
|
||||
};
|
||||
await run("pnpm", ["exec", "electron-vite", "build"], env);
|
||||
await run("node", ["scripts/build-desktop.mjs", "--mac", "--arm64", "--x64"], env);
|
||||
await stageRelease("mac");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
await loadEnvFile(join(rootDir, ".env.release.local"));
|
||||
await loadEnvFile(join(rootDir, ".env.release"));
|
||||
process.env.AGENTCOMPANY_ELECTRON_PUBLISH = "never";
|
||||
const r2 = options.upload ? await prepareR2ReleaseHost({ rootDir }) : null;
|
||||
await validatePreflight(options.platforms, options.upload);
|
||||
|
||||
console.log(`[release-local] update URL: ${process.env.AGENTCOMPANY_UPDATE_URL}`);
|
||||
console.log(`[release-local] upload mode: ${options.upload ? "enabled" : "disabled"}`);
|
||||
console.log(`[release-local] platforms: ${options.platforms.join(", ")}`);
|
||||
|
||||
await rm(localReleaseDir, { recursive: true, force: true });
|
||||
if (!options.skipVerify) {
|
||||
await run("pnpm", ["verify"]);
|
||||
}
|
||||
if (options.platforms.includes("mac")) {
|
||||
await buildMac();
|
||||
if (r2) {
|
||||
await uploadReleaseFiles({
|
||||
rootDir,
|
||||
releaseDir: buildReleaseDir,
|
||||
bucket: r2.bucket,
|
||||
releasePrefix: r2.releasePrefix,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (options.platforms.includes("win")) {
|
||||
await runDockerBuild("win");
|
||||
await stageRelease("win");
|
||||
if (r2) {
|
||||
await uploadReleaseFiles({
|
||||
rootDir,
|
||||
releaseDir: buildReleaseDir,
|
||||
bucket: r2.bucket,
|
||||
releasePrefix: r2.releasePrefix,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (options.platforms.includes("linux")) {
|
||||
await runDockerBuild("linux");
|
||||
await stageRelease("linux");
|
||||
if (r2) {
|
||||
await uploadReleaseFiles({
|
||||
rootDir,
|
||||
releaseDir: buildReleaseDir,
|
||||
bucket: r2.bucket,
|
||||
releasePrefix: r2.releasePrefix,
|
||||
});
|
||||
}
|
||||
}
|
||||
await mkdir(localReleaseDir, { recursive: true });
|
||||
await writeFile(
|
||||
join(localReleaseDir, "manifest.json"),
|
||||
JSON.stringify({
|
||||
generatedAt: new Date().toISOString(),
|
||||
platforms: options.platforms,
|
||||
uploadEnabled: options.upload,
|
||||
updateUrl: process.env.AGENTCOMPANY_UPDATE_URL,
|
||||
}, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
console.log(`[release-local] complete. Local artifact staging: ${localReleaseDir}`);
|
||||
}
|
||||
|
||||
await main();
|
||||
@@ -0,0 +1,143 @@
|
||||
import { readdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const defaultBucket = "agentcompany-updates";
|
||||
const defaultPrefix = "AC";
|
||||
const ignoredReleaseFiles = new Set(["builder-debug.yml"]);
|
||||
|
||||
function normalizeBaseUrl(url) {
|
||||
return url.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function normalizePathSegment(pathValue) {
|
||||
return pathValue.replace(/^\/+|\/+$/g, "");
|
||||
}
|
||||
|
||||
function guessContentType(fileName) {
|
||||
if (fileName.endsWith(".yml")) return "text/yaml; charset=utf-8";
|
||||
if (fileName.endsWith(".blockmap")) return "application/octet-stream";
|
||||
if (fileName.endsWith(".zip")) return "application/zip";
|
||||
if (fileName.endsWith(".dmg")) return "application/x-apple-diskimage";
|
||||
if (fileName.endsWith(".exe")) return "application/vnd.microsoft.portable-executable";
|
||||
if (fileName.endsWith(".AppImage")) return "application/octet-stream";
|
||||
if (fileName.endsWith(".deb")) return "application/vnd.debian.binary-package";
|
||||
if (fileName.endsWith(".rpm")) return "application/x-rpm";
|
||||
if (fileName.endsWith(".snap")) return "application/vnd.snap";
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
async function runWrangler(args, cwd, capture = false) {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const child = spawn("npx", ["wrangler", ...args], {
|
||||
cwd,
|
||||
stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
if (capture) {
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
}
|
||||
child.on("error", reject);
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr });
|
||||
return;
|
||||
}
|
||||
reject(new Error(stderr || stdout || `wrangler ${args.join(" ")} failed with exit code ${code ?? 1}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function resolveUpdateUrl(publicBaseUrl, releasePrefix) {
|
||||
const baseUrl = normalizeBaseUrl(publicBaseUrl);
|
||||
const prefix = normalizePathSegment(releasePrefix);
|
||||
return prefix ? `${baseUrl}/${prefix}` : baseUrl;
|
||||
}
|
||||
|
||||
export async function prepareR2ReleaseHost({ rootDir }) {
|
||||
const prefix = normalizePathSegment(process.env.AGENTCOMPANY_RELEASE_PREFIX?.trim() || defaultPrefix);
|
||||
const whoami = JSON.parse((await runWrangler(["whoami", "--json"], rootDir, true)).stdout);
|
||||
const accountId = process.env.R2_ACCOUNT_ID?.trim()
|
||||
|| process.env.AGENTCOMPANY_R2_ACCOUNT_ID?.trim()
|
||||
|| whoami.accounts?.[0]?.id;
|
||||
if (!accountId) {
|
||||
throw new Error("Unable to resolve Cloudflare account ID from wrangler.");
|
||||
}
|
||||
|
||||
const bucket = process.env.R2_BUCKET?.trim()
|
||||
|| process.env.AGENTCOMPANY_R2_BUCKET?.trim()
|
||||
|| defaultBucket;
|
||||
|
||||
try {
|
||||
await runWrangler(["r2", "bucket", "info", bucket, "--json"], rootDir, true);
|
||||
} catch (error) {
|
||||
if (!String(error).includes("does not exist")) {
|
||||
throw error;
|
||||
}
|
||||
await runWrangler(["r2", "bucket", "create", bucket], rootDir, false);
|
||||
}
|
||||
|
||||
let publicBaseUrl = process.env.R2_PUBLIC_BASE_URL?.trim()
|
||||
|| process.env.AGENTCOMPANY_R2_PUBLIC_URL?.trim()
|
||||
|| "";
|
||||
|
||||
if (!publicBaseUrl) {
|
||||
const devUrlState = await runWrangler(["r2", "bucket", "dev-url", "get", bucket], rootDir, true)
|
||||
.then((result) => result.stdout)
|
||||
.catch(() => "");
|
||||
if (!devUrlState.includes("https://")) {
|
||||
await runWrangler(["r2", "bucket", "dev-url", "enable", bucket], rootDir, false);
|
||||
}
|
||||
const enabledOutput = (await runWrangler(["r2", "bucket", "dev-url", "get", bucket], rootDir, true)).stdout;
|
||||
const match = enabledOutput.match(/https:\/\/[^\s']+\.r2\.dev/);
|
||||
if (!match) {
|
||||
throw new Error("Unable to resolve R2 public dev URL from wrangler.");
|
||||
}
|
||||
publicBaseUrl = match[0];
|
||||
}
|
||||
|
||||
process.env.R2_ACCOUNT_ID = accountId;
|
||||
process.env.AGENTCOMPANY_R2_ACCOUNT_ID = accountId;
|
||||
process.env.R2_BUCKET = bucket;
|
||||
process.env.AGENTCOMPANY_R2_BUCKET = bucket;
|
||||
process.env.R2_PUBLIC_BASE_URL = publicBaseUrl;
|
||||
process.env.AGENTCOMPANY_R2_PUBLIC_URL = publicBaseUrl;
|
||||
process.env.AGENTCOMPANY_RELEASE_PREFIX = prefix;
|
||||
process.env.AGENTCOMPANY_R2_PATH = prefix;
|
||||
process.env.AGENTCOMPANY_UPDATE_URL = process.env.AGENTCOMPANY_UPDATE_URL?.trim()
|
||||
|| resolveUpdateUrl(publicBaseUrl, prefix);
|
||||
|
||||
return {
|
||||
accountId,
|
||||
bucket,
|
||||
publicBaseUrl,
|
||||
releasePrefix: prefix,
|
||||
updateUrl: process.env.AGENTCOMPANY_UPDATE_URL,
|
||||
};
|
||||
}
|
||||
|
||||
export async function uploadReleaseFiles({ rootDir, releaseDir, bucket, releasePrefix }) {
|
||||
const files = (await readdir(releaseDir, { withFileTypes: true }))
|
||||
.filter((entry) => entry.isFile() && !ignoredReleaseFiles.has(entry.name))
|
||||
.map((entry) => entry.name);
|
||||
|
||||
for (const fileName of files) {
|
||||
const objectPath = `${bucket}/${normalizePathSegment(releasePrefix)}/${fileName}`;
|
||||
const filePath = join(releaseDir, fileName);
|
||||
const args = [
|
||||
"r2", "object", "put", objectPath,
|
||||
"--remote",
|
||||
"--file", filePath,
|
||||
"--content-type", guessContentType(fileName),
|
||||
"--cache-control", fileName.endsWith(".yml") ? "no-cache" : "public, max-age=31536000, immutable",
|
||||
];
|
||||
await runWrangler(args, rootDir, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const vitestEntrypoint = resolve(process.cwd(), "node_modules/vitest/vitest.mjs");
|
||||
const args = process.argv.slice(2);
|
||||
const warningFlag = "--disable-warning=ExperimentalWarning";
|
||||
const env = { ...process.env };
|
||||
const existingNodeOptions = env.NODE_OPTIONS?.trim();
|
||||
|
||||
if (!existingNodeOptions) {
|
||||
env.NODE_OPTIONS = warningFlag;
|
||||
} else if (!existingNodeOptions.includes(warningFlag)) {
|
||||
env.NODE_OPTIONS = `${existingNodeOptions} ${warningFlag}`;
|
||||
}
|
||||
|
||||
const child = spawn(process.execPath, ["--disable-warning=ExperimentalWarning", vitestEntrypoint, ...args], {
|
||||
stdio: "inherit",
|
||||
env,
|
||||
});
|
||||
|
||||
child.on("exit", (code, signal) => {
|
||||
if (signal) {
|
||||
process.kill(process.pid, signal);
|
||||
return;
|
||||
}
|
||||
process.exit(code ?? 1);
|
||||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in new issue
Block a user