Package Boss build 0.4.171 from the current workspace snapshot

Constraint: Boss Windows artifacts are built on a native GitHub runner from a temporary snapshot branch
Rejected: Build Boss Windows locally on Apple Silicon | docker/wine packaging is not stable on this host
Confidence: medium
Scope-risk: narrow
Directive: This snapshot branch is transport-only and should be deleted after the Boss build is recovered locally
Tested: local Boss mac build stages before snapshot creation
Not-tested: remote Boss Windows packaging before workflow execution
This commit is contained in:
ZacharyZhang-NY committed 2026-04-21 21:49:01 -04:00
1 parent aba2dac3c5
commit 6bb656c43a
18 files changed
+954 -37

No files matched your search

+1 -1
View File
@@ -167,7 +167,7 @@ export const platformDownloads: PlatformDownload[] = [
id: "windows-universal-installer",
label: "Windows Universal Installer",
href: "${urlFor(baseUrl, windowsUniversalInstaller)}",
note: "arm64 + x64 + ia32",
note: "arm64 + x64",
},
],
},
+1 -1
View File
@@ -150,7 +150,7 @@ function makeDockerEnv(platform) {
function getDockerBuildCommands(platform) {
if (platform === "win") {
return [
"node scripts/build-desktop.mjs --win nsis --x64 --ia32",
"node scripts/build-desktop.mjs --win nsis --x64",
"node scripts/build-desktop.mjs --win nsis --arm64",
];
}
+56 -2
View File
@@ -1,6 +1,11 @@
import { readdir } from "node:fs/promises";
import { readdir, stat } from "node:fs/promises";
import { createReadStream } from "node:fs";
import { join } from "node:path";
import { spawn } from "node:child_process";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
const wranglerSingleUploadLimit = 290 * 1024 * 1024;
const defaultBucket = "agentcompany-updates";
const defaultPrefix = "AC";
@@ -123,14 +128,63 @@ export async function prepareR2ReleaseHost({ rootDir }) {
};
}
function createS3ClientForLargeUploads() {
const accessKeyId = process.env.R2_ACCESS_KEY_ID?.trim();
const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY?.trim();
const endpoint = process.env.R2_S3_ENDPOINT?.trim()
|| (process.env.R2_ACCOUNT_ID ? `https://${process.env.R2_ACCOUNT_ID.trim()}.r2.cloudflarestorage.com` : "");
if (!accessKeyId || !secretAccessKey || !endpoint) {
throw new Error(
"Large file (>290 MiB) requires R2 S3 credentials. Set R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, and R2_S3_ENDPOINT (or R2_ACCOUNT_ID) in .env.release.local.",
);
}
return new S3Client({
region: "auto",
endpoint,
credentials: { accessKeyId, secretAccessKey },
forcePathStyle: false,
});
}
async function uploadViaS3Multipart({ bucket, key, filePath, fileName }) {
const client = createS3ClientForLargeUploads();
const upload = new Upload({
client,
params: {
Bucket: bucket,
Key: key,
Body: createReadStream(filePath),
ContentType: guessContentType(fileName),
CacheControl: fileName.endsWith(".yml") ? "no-cache" : "public, max-age=31536000, immutable",
},
partSize: 64 * 1024 * 1024,
queueSize: 4,
});
upload.on("httpUploadProgress", (p) => {
if (p.loaded && p.total) {
process.stdout.write(`\r[s3-multipart] ${key} ${(p.loaded / p.total * 100).toFixed(1)}%`);
}
});
await upload.done();
process.stdout.write(`\n[s3-multipart] ${key} uploaded\n`);
}
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 key = `${normalizePathSegment(releasePrefix)}/${fileName}`;
const objectPath = `${bucket}/${key}`;
const filePath = join(releaseDir, fileName);
const { size } = await stat(filePath);
if (size > wranglerSingleUploadLimit) {
await uploadViaS3Multipart({ bucket, key, filePath, fileName });
continue;
}
const args = [
"r2", "object", "put", objectPath,
"--remote",
+111
View File
@@ -0,0 +1,111 @@
import { existsSync } from "node:fs";
import { readFile, readdir } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { prepareR2ReleaseHost, uploadReleaseFiles } from "./release-local/r2.mjs";
import { loadEnvFile } from "./release/lib/dotenv.mjs";
import { createRunner } from "./release/lib/process.mjs";
import { createWorkflowHelpers } from "./release/lib/workflow.mjs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = join(__dirname, "..");
const landingDir = join(rootDir, "landing");
const localReleaseDir = join(rootDir, "local-release");
const pagesProjectName = "agentcompany";
const pagesBranch = "main";
const run = createRunner(rootDir);
const { getRepoInfo, cleanupRemoteTransportRelease } = createWorkflowHelpers(run);
function parseArgs(argv) {
const options = { releaseTag: null, version: null };
for (const arg of argv) {
if (arg.startsWith("--release-tag=")) options.releaseTag = arg.slice("--release-tag=".length).trim();
else if (arg.startsWith("--version=")) options.version = arg.slice("--version=".length).trim();
else throw new Error(`Unknown argument: ${arg}`);
}
if (!options.releaseTag || !options.version) {
throw new Error("Usage: node scripts/resume-release-public.mjs --release-tag=<tag> --version=<x.y.z>");
}
return options;
}
async function uploadWinAndLinuxToR2() {
const r2 = await prepareR2ReleaseHost({ rootDir });
await uploadReleaseFiles({ rootDir, releaseDir: join(localReleaseDir, "win"), bucket: r2.bucket, releasePrefix: r2.releasePrefix });
await uploadReleaseFiles({ rootDir, releaseDir: join(localReleaseDir, "linux"), bucket: r2.bucket, releasePrefix: r2.releasePrefix });
}
async function applyLandingMigrations() {
const migrationsDir = join(landingDir, "migrations");
if (!existsSync(migrationsDir)) return;
const wranglerToml = await readFile(join(landingDir, "wrangler.toml"), "utf8");
const match = wranglerToml.match(/database_name\s*=\s*"([^"]+)"/);
if (!match) return;
const databaseName = match[1];
const migrationFiles = (await readdir(migrationsDir)).filter((n) => n.endsWith(".sql")).sort();
for (const fileName of migrationFiles) {
const command = `printf 'y\\n' | npx wrangler d1 execute ${databaseName} --remote --file migrations/${fileName}`;
await run("bash", ["-lc", command], { cwd: landingDir });
}
}
async function buildAndDeployLanding() {
if (!existsSync(join(landingDir, "node_modules"))) {
await run("npm", ["ci"], { cwd: landingDir });
}
await run("npm", ["run", "build"], { cwd: landingDir });
const deploy = await run(
"npx",
["wrangler", "pages", "deploy", "out", "--project-name", pagesProjectName, "--branch", pagesBranch, "--commit-dirty=true"],
{ cwd: landingDir, capture: true },
);
const output = `${deploy.stdout}\n${deploy.stderr}`;
const urlMatch = output.match(/https:\/\/[^\s]+\.agentcompany\.pages\.dev/);
return urlMatch?.[0] ?? null;
}
async function verifyPublishedAssets(version, deployUrl) {
const releaseLinksSource = await readFile(join(landingDir, "components", "release-links.generated.ts"), "utf8");
const requiredFragments = [
`AgentCompany-${version}-arm64.dmg`,
`AgentCompany.Setup.${version}.exe`,
`agent-company-${version}.aarch64.rpm`,
`agent-company_${version}_arm64.deb`,
];
for (const fragment of requiredFragments) {
if (!releaseLinksSource.includes(fragment)) {
throw new Error(`Missing expected release asset in generated links: ${fragment}`);
}
}
if (deployUrl) {
const { stdout } = await run("curl", ["-L", "--silent", "-A", "Mozilla/5.0", deployUrl], { capture: true });
if (!stdout.includes(version)) {
throw new Error(`Deployed landing preview ${deployUrl} does not contain version ${version}.`);
}
}
}
async function main() {
const options = parseArgs(process.argv.slice(2));
await loadEnvFile(join(rootDir, ".env.release.local"));
await loadEnvFile(join(rootDir, ".env.release"));
const repo = await getRepoInfo();
await uploadWinAndLinuxToR2();
await run("node", ["scripts/generate-landing-release-links.mjs"]);
await applyLandingMigrations();
const deployUrl = await buildAndDeployLanding();
await verifyPublishedAssets(options.version, deployUrl);
await cleanupRemoteTransportRelease(repo, options.releaseTag);
console.log(JSON.stringify({
version: options.version,
releaseTag: options.releaseTag,
deployUrl,
updateUrl: process.env.AGENTCOMPANY_UPDATE_URL ?? null,
}, null, 2));
}
await main();