Package Boss build 0.4.155 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:
1 parent
69a1139eb3
commit
3af7a3e8d5
8 files changed
+1073
-516
No files matched your search
@@ -12,7 +12,7 @@ export type PlatformDownload = {
|
|||||||
assets: PlatformDownloadAsset[];
|
assets: PlatformDownloadAsset[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const releaseVersion = "0.4.151";
|
export const releaseVersion = "0.4.155";
|
||||||
|
|
||||||
export const platformDownloads: PlatformDownload[] = [
|
export const platformDownloads: PlatformDownload[] = [
|
||||||
{
|
{
|
||||||
@@ -23,13 +23,13 @@ export const platformDownloads: PlatformDownload[] = [
|
|||||||
{
|
{
|
||||||
id: "macos-arm64-dmg",
|
id: "macos-arm64-dmg",
|
||||||
label: "Apple Silicon DMG",
|
label: "Apple Silicon DMG",
|
||||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.151-arm64.dmg",
|
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.155-arm64.dmg",
|
||||||
note: "arm64",
|
note: "arm64",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "macos-x64-dmg",
|
id: "macos-x64-dmg",
|
||||||
label: "Intel Mac DMG",
|
label: "Intel Mac DMG",
|
||||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.151.dmg",
|
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.155.dmg",
|
||||||
note: "x64",
|
note: "x64",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -42,7 +42,7 @@ export const platformDownloads: PlatformDownload[] = [
|
|||||||
{
|
{
|
||||||
id: "windows-universal-installer",
|
id: "windows-universal-installer",
|
||||||
label: "Windows Universal Installer",
|
label: "Windows Universal Installer",
|
||||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany.Setup.0.4.151.exe",
|
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany.Setup.0.4.155.exe",
|
||||||
note: "arm64 + x64 + ia32",
|
note: "arm64 + x64 + ia32",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -55,37 +55,37 @@ export const platformDownloads: PlatformDownload[] = [
|
|||||||
{
|
{
|
||||||
id: "linux-x64-appimage",
|
id: "linux-x64-appimage",
|
||||||
label: "Linux x64 AppImage",
|
label: "Linux x64 AppImage",
|
||||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.151.AppImage",
|
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.155.AppImage",
|
||||||
note: "x64",
|
note: "x64",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "linux-x64-deb",
|
id: "linux-x64-deb",
|
||||||
label: "Linux x64 DEB",
|
label: "Linux x64 DEB",
|
||||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company_0.4.151_amd64.deb",
|
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company_0.4.155_amd64.deb",
|
||||||
note: "x64",
|
note: "x64",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "linux-x64-rpm",
|
id: "linux-x64-rpm",
|
||||||
label: "Linux x64 RPM",
|
label: "Linux x64 RPM",
|
||||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company-0.4.151.x86_64.rpm",
|
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company-0.4.155.x86_64.rpm",
|
||||||
note: "x64",
|
note: "x64",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "linux-arm64-appimage",
|
id: "linux-arm64-appimage",
|
||||||
label: "Linux ARM64 AppImage",
|
label: "Linux ARM64 AppImage",
|
||||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.151-arm64.AppImage",
|
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.155-arm64.AppImage",
|
||||||
note: "arm64",
|
note: "arm64",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "linux-arm64-deb",
|
id: "linux-arm64-deb",
|
||||||
label: "Linux ARM64 DEB",
|
label: "Linux ARM64 DEB",
|
||||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company_0.4.151_arm64.deb",
|
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company_0.4.155_arm64.deb",
|
||||||
note: "arm64",
|
note: "arm64",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "linux-arm64-rpm",
|
id: "linux-arm64-rpm",
|
||||||
label: "Linux ARM64 RPM",
|
label: "Linux ARM64 RPM",
|
||||||
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company-0.4.151.aarch64.rpm",
|
href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company-0.4.155.aarch64.rpm",
|
||||||
note: "arm64",
|
note: "arm64",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "agent-company",
|
"name": "agent-company",
|
||||||
"version": "0.4.151",
|
"version": "0.4.155",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Cross-platform Electron desktop control plane for local-first AI agent companies.",
|
"description": "Cross-platform Electron desktop control plane for local-first AI agent companies.",
|
||||||
@@ -17,6 +17,8 @@
|
|||||||
"build:dir": "electron-vite build && node scripts/build-desktop.mjs --dir",
|
"build:dir": "electron-vite build && node scripts/build-desktop.mjs --dir",
|
||||||
"build:all": "node scripts/build-all-platforms.mjs",
|
"build:all": "node scripts/build-all-platforms.mjs",
|
||||||
"release:cloudflare": "node scripts/release-cloudflare.mjs",
|
"release:cloudflare": "node scripts/release-cloudflare.mjs",
|
||||||
|
"release:cloudflare:public": "node scripts/release-public-cloudflare.mjs",
|
||||||
|
"release:boss": "node scripts/release-boss.mjs",
|
||||||
"release:local": "node scripts/release-local.mjs",
|
"release:local": "node scripts/release-local.mjs",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
|
|||||||
@@ -0,0 +1,416 @@
|
|||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import { mkdtemp, readFile, mkdir, cp, readdir } from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const rootDir = join(__dirname, "..");
|
||||||
|
const packageJsonPath = join(rootDir, "package.json");
|
||||||
|
const bossOutDir = join(rootDir, "boss-build", "build", "out");
|
||||||
|
const remoteBossWorkflowFile = ".github/workflows/remote-boss-platforms.yml";
|
||||||
|
const remoteBossWorkflowName = "remote-boss-platforms.yml";
|
||||||
|
const versionPattern = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/;
|
||||||
|
const syncExcludes = [
|
||||||
|
".git",
|
||||||
|
"node_modules/",
|
||||||
|
".pnpm-store/",
|
||||||
|
"build/",
|
||||||
|
"local-release/",
|
||||||
|
"out/",
|
||||||
|
".omx/",
|
||||||
|
".omc/",
|
||||||
|
".claude/",
|
||||||
|
"landing/.next/",
|
||||||
|
"landing/out/",
|
||||||
|
"landing/node_modules/",
|
||||||
|
".DS_Store",
|
||||||
|
".env.release.local",
|
||||||
|
"CertificateSigningRequest.certSigningRequest",
|
||||||
|
"Certificates.p12",
|
||||||
|
"developerID_application.cer",
|
||||||
|
];
|
||||||
|
const bossSnapshotIncludes = [
|
||||||
|
remoteBossWorkflowFile,
|
||||||
|
"boss-build/boss-runtime.json",
|
||||||
|
"boss-build/build-boss-desktop.mjs",
|
||||||
|
"boss-build/build-boss-edition.mjs",
|
||||||
|
"boss-build/build-boss-mac.mjs",
|
||||||
|
"boss-build/build-boss.mjs",
|
||||||
|
"boss-build/build-boss.sh",
|
||||||
|
"boss-build/electron-builder-boss.yml",
|
||||||
|
];
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const options = {
|
||||||
|
version: null,
|
||||||
|
deleteRemoteBranch: false,
|
||||||
|
keepTransportRelease: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const arg of argv) {
|
||||||
|
if (arg.startsWith("--version=")) {
|
||||||
|
options.version = arg.slice("--version=".length).trim();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === "--delete-remote-branch") {
|
||||||
|
options.deleteRemoteBranch = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === "--keep-transport-release") {
|
||||||
|
options.keepTransportRelease = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === "--help") {
|
||||||
|
printHelp();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
throw new Error(`Unknown argument: ${arg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printHelp() {
|
||||||
|
console.log(`Usage: node scripts/release-boss.mjs [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--version=x.y.z Explicit version. Defaults to current package version.
|
||||||
|
--delete-remote-branch Delete the GitHub snapshot branch after success.
|
||||||
|
--keep-transport-release Keep the temporary draft GitHub release after success.
|
||||||
|
--help Show this help.
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(command, args, options = {}) {
|
||||||
|
const {
|
||||||
|
cwd = rootDir,
|
||||||
|
env = process.env,
|
||||||
|
input = null,
|
||||||
|
capture = false,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = spawn(command, args, {
|
||||||
|
cwd,
|
||||||
|
env,
|
||||||
|
stdio: capture ? ["pipe", "pipe", "pipe"] : ["pipe", "inherit", "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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
child.stdin.write(input);
|
||||||
|
}
|
||||||
|
child.stdin.end();
|
||||||
|
|
||||||
|
child.on("error", reject);
|
||||||
|
child.on("close", (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve({ stdout, stderr });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reject(new Error(stderr || stdout || `${command} ${args.join(" ")} failed with exit code ${code ?? 1}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ghJson(args) {
|
||||||
|
const { stdout } = await run("gh", args, { capture: true });
|
||||||
|
return JSON.parse(stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getGitHubHttpAuthHeader() {
|
||||||
|
const tokenResult = await run("gh", ["auth", "token"], { capture: true });
|
||||||
|
const token = tokenResult.stdout.trim();
|
||||||
|
if (!token) {
|
||||||
|
throw new Error("Unable to read a GitHub token from gh auth.");
|
||||||
|
}
|
||||||
|
return Buffer.from(`x-access-token:${token}`).toString("base64");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRepoInfo() {
|
||||||
|
const info = await ghJson(["repo", "view", "--json", "nameWithOwner,defaultBranchRef"]);
|
||||||
|
const defaultBranch = info.defaultBranchRef?.name;
|
||||||
|
if (!info.nameWithOwner || !defaultBranch) {
|
||||||
|
throw new Error("Unable to resolve GitHub repository name/default branch from gh.");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
nameWithOwner: info.nameWithOwner,
|
||||||
|
defaultBranch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertRemoteBossWorkflowExists(repo) {
|
||||||
|
try {
|
||||||
|
await run("gh", [
|
||||||
|
"api",
|
||||||
|
`repos/${repo.nameWithOwner}/contents/${remoteBossWorkflowFile}?ref=${repo.defaultBranch}`,
|
||||||
|
], { capture: true });
|
||||||
|
} catch {
|
||||||
|
throw new Error(
|
||||||
|
`Remote workflow ${remoteBossWorkflowFile} was not found on ${repo.defaultBranch}. ` +
|
||||||
|
"Commit and push this Boss automation workflow before using scripts/release-boss.mjs.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readPackageJson() {
|
||||||
|
return JSON.parse(await readFile(packageJsonPath, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function forceStageBossSnapshotFiles(tempDir) {
|
||||||
|
const stagedPaths = bossSnapshotIncludes
|
||||||
|
.filter((relativePath) => existsSync(join(tempDir, relativePath)));
|
||||||
|
|
||||||
|
if (stagedPaths.length === 0) {
|
||||||
|
throw new Error("Boss snapshot is missing required boss-build source files after syncing the workspace.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await run("git", ["-C", tempDir, "add", "--force", ...stagedPaths]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureRemoteBuildPrerequisites(repo) {
|
||||||
|
if (process.platform !== "darwin") {
|
||||||
|
throw new Error("Boss release currently expects to run on macOS for the local Boss DMG build.");
|
||||||
|
}
|
||||||
|
if (!existsSync(join(rootDir, remoteBossWorkflowFile))) {
|
||||||
|
throw new Error(`Required workflow file missing locally: ${remoteBossWorkflowFile}`);
|
||||||
|
}
|
||||||
|
await assertRemoteBossWorkflowExists(repo);
|
||||||
|
await run("gh", ["auth", "status"], { capture: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createSnapshotBranch(repo, branchName, version) {
|
||||||
|
const tempDir = await mkdtemp(join(os.tmpdir(), "agentcompany-release-"));
|
||||||
|
const auth = await getGitHubHttpAuthHeader();
|
||||||
|
await run("git", [
|
||||||
|
"-c",
|
||||||
|
`http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth}`,
|
||||||
|
"fetch",
|
||||||
|
`https://github.com/${repo.nameWithOwner}.git`,
|
||||||
|
repo.defaultBranch,
|
||||||
|
]);
|
||||||
|
await run("git", ["worktree", "add", "--detach", tempDir, "FETCH_HEAD"]);
|
||||||
|
await run("git", ["-C", tempDir, "checkout", "-b", branchName]);
|
||||||
|
|
||||||
|
const rsyncArgs = ["-a", "--delete"];
|
||||||
|
for (const pattern of syncExcludes) {
|
||||||
|
rsyncArgs.push("--exclude", pattern);
|
||||||
|
}
|
||||||
|
rsyncArgs.push(`${rootDir}/`, `${tempDir}/`);
|
||||||
|
await run("rsync", rsyncArgs);
|
||||||
|
|
||||||
|
await run("git", ["-C", tempDir, "add", "-A"]);
|
||||||
|
await forceStageBossSnapshotFiles(tempDir);
|
||||||
|
const diffStatus = await run("git", ["-C", tempDir, "diff", "--cached", "--quiet"], { capture: true })
|
||||||
|
.then(() => false)
|
||||||
|
.catch(() => true);
|
||||||
|
|
||||||
|
if (!diffStatus) {
|
||||||
|
throw new Error("Snapshot branch has no staged changes after syncing the workspace.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const commitMessage = [
|
||||||
|
`Package Boss build ${version} 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",
|
||||||
|
].join("\n");
|
||||||
|
await run("git", ["-C", tempDir, "commit", "-m", commitMessage]);
|
||||||
|
|
||||||
|
await run(
|
||||||
|
"git",
|
||||||
|
[
|
||||||
|
"-C",
|
||||||
|
tempDir,
|
||||||
|
"-c",
|
||||||
|
`http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth}`,
|
||||||
|
"push",
|
||||||
|
`https://github.com/${repo.nameWithOwner}.git`,
|
||||||
|
branchName,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return tempDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForWorkflowRun(repo, workflowName, branchName, timeoutMs) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
while (Date.now() - startedAt < timeoutMs) {
|
||||||
|
const runs = await ghJson([
|
||||||
|
"run",
|
||||||
|
"list",
|
||||||
|
"--repo",
|
||||||
|
repo.nameWithOwner,
|
||||||
|
"--workflow",
|
||||||
|
workflowName,
|
||||||
|
"--branch",
|
||||||
|
branchName,
|
||||||
|
"--limit",
|
||||||
|
"1",
|
||||||
|
"--json",
|
||||||
|
"databaseId,status,conclusion,url,displayTitle",
|
||||||
|
]);
|
||||||
|
if (runs.length > 0) {
|
||||||
|
return runs[0];
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||||
|
}
|
||||||
|
throw new Error(`Timed out waiting for workflow ${workflowName} on branch ${branchName}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dispatchRemoteBossWorkflow(repo, branchName, version, releaseTag, releaseTitle) {
|
||||||
|
await run("gh", [
|
||||||
|
"workflow",
|
||||||
|
"run",
|
||||||
|
remoteBossWorkflowName,
|
||||||
|
"--repo",
|
||||||
|
repo.nameWithOwner,
|
||||||
|
"--ref",
|
||||||
|
branchName,
|
||||||
|
"-f",
|
||||||
|
`version=${version}`,
|
||||||
|
"-f",
|
||||||
|
`release_tag=${releaseTag}`,
|
||||||
|
"-f",
|
||||||
|
`release_title=${releaseTitle}`,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForWorkflowCompletion(repo, runId, timeoutMs) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
while (Date.now() - startedAt < timeoutMs) {
|
||||||
|
const runInfo = await ghJson([
|
||||||
|
"run",
|
||||||
|
"view",
|
||||||
|
String(runId),
|
||||||
|
"--repo",
|
||||||
|
repo.nameWithOwner,
|
||||||
|
"--json",
|
||||||
|
"status,conclusion,url",
|
||||||
|
]);
|
||||||
|
if (runInfo.status === "completed") {
|
||||||
|
return runInfo;
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 15000));
|
||||||
|
}
|
||||||
|
throw new Error(`Timed out waiting for workflow run ${runId} to complete.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadRemoteAssets(repo, releaseTag) {
|
||||||
|
const downloadRoot = await mkdtemp(join(os.tmpdir(), "agentcompany-transport-"));
|
||||||
|
await run("gh", [
|
||||||
|
"release",
|
||||||
|
"download",
|
||||||
|
releaseTag,
|
||||||
|
"--repo",
|
||||||
|
repo.nameWithOwner,
|
||||||
|
"-D",
|
||||||
|
downloadRoot,
|
||||||
|
]);
|
||||||
|
return downloadRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stageBossRemoteAssets(downloadRoot) {
|
||||||
|
await mkdir(bossOutDir, { recursive: true });
|
||||||
|
const files = await readdir(downloadRoot);
|
||||||
|
for (const fileName of files) {
|
||||||
|
if (fileName.endsWith(".exe") || fileName.endsWith(".blockmap")) {
|
||||||
|
await cp(join(downloadRoot, fileName), join(bossOutDir, fileName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildBossMac(version) {
|
||||||
|
await run("node", ["boss-build/build-boss-edition.mjs", `--platform=mac`, `--version=${version}`]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanupRemoteTransportRelease(repo, releaseTag) {
|
||||||
|
await run("gh", ["release", "delete", releaseTag, "--repo", repo.nameWithOwner, "--yes"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanupRemoteBranch(repo, branchName) {
|
||||||
|
await run("gh", ["api", "-X", "DELETE", `repos/${repo.nameWithOwner}/git/refs/heads/${branchName}`]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanupLocalWorktree(tempDir) {
|
||||||
|
await run("git", ["worktree", "remove", "--force", tempDir]);
|
||||||
|
await run("git", ["worktree", "prune"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const options = parseArgs(process.argv.slice(2));
|
||||||
|
const repo = await getRepoInfo();
|
||||||
|
await ensureRemoteBuildPrerequisites(repo);
|
||||||
|
|
||||||
|
const currentVersion = (await readPackageJson()).version;
|
||||||
|
const nextVersion = options.version ?? currentVersion;
|
||||||
|
if (!versionPattern.test(nextVersion)) {
|
||||||
|
throw new Error(`Invalid version: ${nextVersion}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const releaseTag = `boss-transport-${nextVersion}-${timestamp}`;
|
||||||
|
const branchName = `boss-build-${nextVersion.replace(/\./g, "-")}-${timestamp}`;
|
||||||
|
const releaseTitle = `Boss Release ${nextVersion}`;
|
||||||
|
|
||||||
|
const tempDir = await createSnapshotBranch(repo, branchName, nextVersion);
|
||||||
|
let cleanupPending = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dispatchRemoteBossWorkflow(repo, branchName, nextVersion, releaseTag, releaseTitle);
|
||||||
|
await buildBossMac(nextVersion);
|
||||||
|
const runSummary = await waitForWorkflowRun(repo, remoteBossWorkflowName, branchName, 60_000);
|
||||||
|
const runInfo = await waitForWorkflowCompletion(repo, runSummary.databaseId, 30 * 60 * 1000);
|
||||||
|
if (runInfo.conclusion !== "success") {
|
||||||
|
throw new Error(`Boss remote workflow failed: ${runInfo.url}`);
|
||||||
|
}
|
||||||
|
const downloadRoot = await downloadRemoteAssets(repo, releaseTag);
|
||||||
|
await mkdir(bossOutDir, { recursive: true });
|
||||||
|
await stageBossRemoteAssets(downloadRoot);
|
||||||
|
|
||||||
|
if (!options.keepTransportRelease) {
|
||||||
|
await cleanupRemoteTransportRelease(repo, releaseTag);
|
||||||
|
}
|
||||||
|
if (options.deleteRemoteBranch) {
|
||||||
|
await cleanupRemoteBranch(repo, branchName);
|
||||||
|
}
|
||||||
|
await cleanupLocalWorktree(tempDir);
|
||||||
|
cleanupPending = false;
|
||||||
|
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
version: nextVersion,
|
||||||
|
remoteBranch: branchName,
|
||||||
|
remoteWorkflowRun: runInfo.url,
|
||||||
|
bossOutDir,
|
||||||
|
releaseTag,
|
||||||
|
}, null, 2));
|
||||||
|
} finally {
|
||||||
|
if (cleanupPending) {
|
||||||
|
try {
|
||||||
|
await cleanupLocalWorktree(tempDir);
|
||||||
|
} catch {
|
||||||
|
// no-op during best-effort temp cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
+29
-501
@@ -1,550 +1,78 @@
|
|||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import { existsSync } from "node:fs";
|
import { readFile } from "node:fs/promises";
|
||||||
import { mkdtemp, readFile, rm, writeFile, mkdir, cp, readdir } from "node:fs/promises";
|
|
||||||
import os from "node:os";
|
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { prepareR2ReleaseHost, uploadReleaseFiles } from "./release-local/r2.mjs";
|
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const rootDir = join(__dirname, "..");
|
const rootDir = join(__dirname, "..");
|
||||||
const packageJsonPath = join(rootDir, "package.json");
|
const packageJsonPath = join(rootDir, "package.json");
|
||||||
const landingDir = join(rootDir, "landing");
|
const rawArgs = process.argv.slice(2);
|
||||||
const localReleaseDir = join(rootDir, "local-release");
|
|
||||||
const remoteWorkflowFile = ".github/workflows/remote-release-platforms.yml";
|
|
||||||
const remoteWorkflowName = "remote-release-platforms.yml";
|
|
||||||
const pagesProjectName = "agentcompany";
|
|
||||||
const pagesBranch = "main";
|
|
||||||
const versionPattern = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/;
|
|
||||||
const syncExcludes = [
|
|
||||||
".git",
|
|
||||||
".github/workflows/",
|
|
||||||
"node_modules/",
|
|
||||||
".pnpm-store/",
|
|
||||||
"build/",
|
|
||||||
"local-release/",
|
|
||||||
"out/",
|
|
||||||
".omx/",
|
|
||||||
".omc/",
|
|
||||||
".claude/",
|
|
||||||
"landing/.next/",
|
|
||||||
"landing/out/",
|
|
||||||
"landing/node_modules/",
|
|
||||||
".DS_Store",
|
|
||||||
".env.release.local",
|
|
||||||
"CertificateSigningRequest.certSigningRequest",
|
|
||||||
"Certificates.p12",
|
|
||||||
"developerID_application.cer",
|
|
||||||
];
|
|
||||||
|
|
||||||
function parseArgs(argv) {
|
function parseArgs(argv) {
|
||||||
const options = {
|
const options = {
|
||||||
version: null,
|
version: null,
|
||||||
skipVerify: false,
|
passThroughArgs: [],
|
||||||
deleteRemoteBranch: false,
|
bossArgs: [],
|
||||||
keepTransportRelease: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const arg of argv) {
|
for (const arg of argv) {
|
||||||
|
options.passThroughArgs.push(arg);
|
||||||
if (arg.startsWith("--version=")) {
|
if (arg.startsWith("--version=")) {
|
||||||
options.version = arg.slice("--version=".length).trim();
|
options.version = arg.slice("--version=".length).trim();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (arg === "--skip-verify") {
|
if (arg === "--delete-remote-branch" || arg === "--keep-transport-release") {
|
||||||
options.skipVerify = true;
|
options.bossArgs.push(arg);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (arg === "--delete-remote-branch") {
|
|
||||||
options.deleteRemoteBranch = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (arg === "--keep-transport-release") {
|
|
||||||
options.keepTransportRelease = true;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (arg === "--help") {
|
if (arg === "--help") {
|
||||||
printHelp();
|
options.bossArgs.push(arg);
|
||||||
process.exit(0);
|
|
||||||
}
|
}
|
||||||
throw new Error(`Unknown argument: ${arg}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
function printHelp() {
|
async function run(command, args) {
|
||||||
console.log(`Usage: node scripts/release-cloudflare.mjs [options]
|
await new Promise((resolve, reject) => {
|
||||||
|
|
||||||
Options:
|
|
||||||
--version=x.y.z Explicit version. Defaults to current patch + 1.
|
|
||||||
--skip-verify Skip local pnpm verify before packaging.
|
|
||||||
--delete-remote-branch Delete the GitHub snapshot branch after success.
|
|
||||||
--keep-transport-release Keep the temporary draft GitHub release after success.
|
|
||||||
--help Show this help.
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 run(command, args, options = {}) {
|
|
||||||
const {
|
|
||||||
cwd = rootDir,
|
|
||||||
env = process.env,
|
|
||||||
input = null,
|
|
||||||
capture = false,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const child = spawn(command, args, {
|
const child = spawn(command, args, {
|
||||||
cwd,
|
cwd: rootDir,
|
||||||
env,
|
stdio: "inherit",
|
||||||
stdio: capture ? ["pipe", "pipe", "pipe"] : ["pipe", "inherit", "inherit"],
|
|
||||||
shell: process.platform === "win32",
|
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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input) {
|
|
||||||
child.stdin.write(input);
|
|
||||||
}
|
|
||||||
child.stdin.end();
|
|
||||||
|
|
||||||
child.on("error", reject);
|
child.on("error", reject);
|
||||||
child.on("close", (code) => {
|
child.on("close", (code) => {
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
resolve({ stdout, stderr });
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
reject(new Error(stderr || stdout || `${command} ${args.join(" ")} failed with exit code ${code ?? 1}`));
|
reject(new Error(`${command} ${args.join(" ")} failed with exit code ${code ?? 1}`));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ghJson(args) {
|
async function readCurrentVersion() {
|
||||||
const { stdout } = await run("gh", args, { capture: true });
|
const pkg = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
||||||
return JSON.parse(stdout);
|
return pkg.version;
|
||||||
}
|
|
||||||
|
|
||||||
async function getGitHubHttpAuthHeader() {
|
|
||||||
const tokenResult = await run("gh", ["auth", "token"], { capture: true });
|
|
||||||
const token = tokenResult.stdout.trim();
|
|
||||||
if (!token) {
|
|
||||||
throw new Error("Unable to read a GitHub token from gh auth.");
|
|
||||||
}
|
|
||||||
return Buffer.from(`x-access-token:${token}`).toString("base64");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getRepoInfo() {
|
|
||||||
const info = await ghJson(["repo", "view", "--json", "nameWithOwner,defaultBranchRef"]);
|
|
||||||
const defaultBranch = info.defaultBranchRef?.name;
|
|
||||||
if (!info.nameWithOwner || !defaultBranch) {
|
|
||||||
throw new Error("Unable to resolve GitHub repository name/default branch from gh.");
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
nameWithOwner: info.nameWithOwner,
|
|
||||||
defaultBranch,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function assertRemoteWorkflowExists(repo) {
|
|
||||||
try {
|
|
||||||
await run("gh", [
|
|
||||||
"api",
|
|
||||||
`repos/${repo.nameWithOwner}/contents/${remoteWorkflowFile}?ref=${repo.defaultBranch}`,
|
|
||||||
], { capture: true });
|
|
||||||
} catch {
|
|
||||||
throw new Error(
|
|
||||||
`Remote workflow ${remoteWorkflowFile} was not found on ${repo.defaultBranch}. ` +
|
|
||||||
"Commit and push this automation workflow before using scripts/release-cloudflare.mjs.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readPackageJson() {
|
|
||||||
return JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
||||||
}
|
|
||||||
|
|
||||||
function bumpPatchVersion(version) {
|
|
||||||
const match = versionPattern.exec(version);
|
|
||||||
if (!match) {
|
|
||||||
throw new Error(`Unsupported package version format: ${version}`);
|
|
||||||
}
|
|
||||||
const [, major, minor, patch] = match;
|
|
||||||
return `${major}.${minor}.${Number.parseInt(patch, 10) + 1}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updatePackageVersion(nextVersion) {
|
|
||||||
const pkg = await readPackageJson();
|
|
||||||
pkg.version = nextVersion;
|
|
||||||
await writeFile(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ensureRemoteBuildPrerequisites(repo) {
|
|
||||||
if (process.platform !== "darwin") {
|
|
||||||
throw new Error("This release script currently expects to run on macOS for the signed macOS build.");
|
|
||||||
}
|
|
||||||
if (!existsSync(join(rootDir, remoteWorkflowFile))) {
|
|
||||||
throw new Error(`Required workflow file missing locally: ${remoteWorkflowFile}`);
|
|
||||||
}
|
|
||||||
await assertRemoteWorkflowExists(repo);
|
|
||||||
await run("gh", ["auth", "status"], { capture: true });
|
|
||||||
await run("npx", ["wrangler", "whoami", "--json"], { capture: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resetReleaseStaging() {
|
|
||||||
await rm(localReleaseDir, { recursive: true, force: true });
|
|
||||||
await rm(join(rootDir, "build", "release"), { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buildMacAndUpload() {
|
|
||||||
await run("node", ["scripts/release-local.mjs", "--platforms=mac", "--skip-verify"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createSnapshotBranch(repo, branchName, version) {
|
|
||||||
const tempDir = await mkdtemp(join(os.tmpdir(), "agentcompany-release-"));
|
|
||||||
const auth = await getGitHubHttpAuthHeader();
|
|
||||||
await run("git", [
|
|
||||||
"-c",
|
|
||||||
`http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth}`,
|
|
||||||
"fetch",
|
|
||||||
`https://github.com/${repo.nameWithOwner}.git`,
|
|
||||||
repo.defaultBranch,
|
|
||||||
]);
|
|
||||||
await run("git", ["worktree", "add", "--detach", tempDir, "FETCH_HEAD"]);
|
|
||||||
await run("git", ["-C", tempDir, "checkout", "-b", branchName]);
|
|
||||||
|
|
||||||
const rsyncArgs = ["-a", "--delete"];
|
|
||||||
for (const pattern of syncExcludes) {
|
|
||||||
rsyncArgs.push("--exclude", pattern);
|
|
||||||
}
|
|
||||||
rsyncArgs.push(`${rootDir}/`, `${tempDir}/`);
|
|
||||||
await run("rsync", rsyncArgs);
|
|
||||||
|
|
||||||
await run("git", ["-C", tempDir, "add", "-A"]);
|
|
||||||
const diffStatus = await run("git", ["-C", tempDir, "diff", "--cached", "--quiet"], { capture: true })
|
|
||||||
.then(() => false)
|
|
||||||
.catch(() => true);
|
|
||||||
|
|
||||||
if (!diffStatus) {
|
|
||||||
throw new Error("Snapshot branch has no staged changes after syncing the workspace.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const commitMessage = [
|
|
||||||
`Package release ${version} from the current workspace snapshot`,
|
|
||||||
"",
|
|
||||||
"Constraint: Windows and Linux artifacts are built on native GitHub runners from a temporary snapshot branch",
|
|
||||||
"Rejected: Build Windows/Linux locally on Apple Silicon | docker/wine packaging is not stable on this host",
|
|
||||||
"Confidence: medium",
|
|
||||||
"Scope-risk: moderate",
|
|
||||||
"Directive: This snapshot branch is transport-only and should be deleted after the release completes",
|
|
||||||
"Tested: local verify/build stages before snapshot creation",
|
|
||||||
"Not-tested: remote packaging before workflow execution",
|
|
||||||
].join("\n");
|
|
||||||
await run("git", ["-C", tempDir, "commit", "-m", commitMessage]);
|
|
||||||
|
|
||||||
await run(
|
|
||||||
"git",
|
|
||||||
[
|
|
||||||
"-C",
|
|
||||||
tempDir,
|
|
||||||
"-c",
|
|
||||||
`http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth}`,
|
|
||||||
"push",
|
|
||||||
`https://github.com/${repo.nameWithOwner}.git`,
|
|
||||||
branchName,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return tempDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForWorkflowRun(repo, branchName, timeoutMs) {
|
|
||||||
const startedAt = Date.now();
|
|
||||||
while (Date.now() - startedAt < timeoutMs) {
|
|
||||||
const runs = await ghJson([
|
|
||||||
"run",
|
|
||||||
"list",
|
|
||||||
"--repo",
|
|
||||||
repo.nameWithOwner,
|
|
||||||
"--workflow",
|
|
||||||
remoteWorkflowName,
|
|
||||||
"--branch",
|
|
||||||
branchName,
|
|
||||||
"--limit",
|
|
||||||
"1",
|
|
||||||
"--json",
|
|
||||||
"databaseId,status,conclusion,url,displayTitle",
|
|
||||||
]);
|
|
||||||
if (runs.length > 0) {
|
|
||||||
return runs[0];
|
|
||||||
}
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
||||||
}
|
|
||||||
throw new Error(`Timed out waiting for workflow ${remoteWorkflowName} on branch ${branchName}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchRemoteWorkflow(repo, branchName, releaseTag, releaseTitle) {
|
|
||||||
await run("gh", [
|
|
||||||
"workflow",
|
|
||||||
"run",
|
|
||||||
remoteWorkflowName,
|
|
||||||
"--repo",
|
|
||||||
repo.nameWithOwner,
|
|
||||||
"--ref",
|
|
||||||
branchName,
|
|
||||||
"-f",
|
|
||||||
`release_tag=${releaseTag}`,
|
|
||||||
"-f",
|
|
||||||
`release_title=${releaseTitle}`,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForWorkflowCompletion(repo, runId, timeoutMs) {
|
|
||||||
const startedAt = Date.now();
|
|
||||||
while (Date.now() - startedAt < timeoutMs) {
|
|
||||||
const runInfo = await ghJson([
|
|
||||||
"run",
|
|
||||||
"view",
|
|
||||||
String(runId),
|
|
||||||
"--repo",
|
|
||||||
repo.nameWithOwner,
|
|
||||||
"--json",
|
|
||||||
"status,conclusion,url",
|
|
||||||
]);
|
|
||||||
if (runInfo.status === "completed") {
|
|
||||||
return runInfo;
|
|
||||||
}
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 15000));
|
|
||||||
}
|
|
||||||
throw new Error(`Timed out waiting for workflow run ${runId} to complete.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadRemoteAssets(repo, releaseTag) {
|
|
||||||
const downloadRoot = await mkdtemp(join(os.tmpdir(), "agentcompany-transport-"));
|
|
||||||
await run("gh", [
|
|
||||||
"release",
|
|
||||||
"download",
|
|
||||||
releaseTag,
|
|
||||||
"--repo",
|
|
||||||
repo.nameWithOwner,
|
|
||||||
"-D",
|
|
||||||
downloadRoot,
|
|
||||||
]);
|
|
||||||
return downloadRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function stageRemoteAssets(downloadRoot) {
|
|
||||||
const winDir = join(localReleaseDir, "win");
|
|
||||||
const linuxDir = join(localReleaseDir, "linux");
|
|
||||||
await rm(winDir, { recursive: true, force: true });
|
|
||||||
await rm(linuxDir, { recursive: true, force: true });
|
|
||||||
await mkdir(winDir, { recursive: true });
|
|
||||||
await mkdir(linuxDir, { recursive: true });
|
|
||||||
|
|
||||||
const files = await readdir(downloadRoot);
|
|
||||||
for (const fileName of files) {
|
|
||||||
const sourcePath = join(downloadRoot, fileName);
|
|
||||||
if (fileName.endsWith(".exe") || fileName.endsWith(".blockmap") || fileName === "latest.yml") {
|
|
||||||
await cp(sourcePath, join(winDir, fileName));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
fileName.endsWith(".AppImage") ||
|
|
||||||
fileName.endsWith(".deb") ||
|
|
||||||
fileName.endsWith(".rpm") ||
|
|
||||||
fileName === "latest-linux.yml"
|
|
||||||
) {
|
|
||||||
await cp(sourcePath, join(linuxDir, fileName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function uploadWinAndLinuxToR2() {
|
|
||||||
await loadEnvFile(join(rootDir, ".env.release.local"));
|
|
||||||
await loadEnvFile(join(rootDir, ".env.release"));
|
|
||||||
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((fileName) => fileName.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-0.4.150-arm64.dmg`.replace("0.4.150", version),
|
|
||||||
`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 cleanupRemoteTransportRelease(repo, releaseTag) {
|
|
||||||
await run("gh", ["release", "delete", releaseTag, "--repo", repo.nameWithOwner, "--yes"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cleanupRemoteBranch(repo, branchName) {
|
|
||||||
await run("gh", ["api", "-X", "DELETE", `repos/${repo.nameWithOwner}/git/refs/heads/${branchName}`]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cleanupLocalWorktree(tempDir) {
|
|
||||||
await run("git", ["worktree", "remove", "--force", tempDir]);
|
|
||||||
await run("git", ["worktree", "prune"]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const options = parseArgs(process.argv.slice(2));
|
const options = parseArgs(rawArgs);
|
||||||
await loadEnvFile(join(rootDir, ".env.release.local"));
|
|
||||||
await loadEnvFile(join(rootDir, ".env.release"));
|
|
||||||
|
|
||||||
const repo = await getRepoInfo();
|
if (rawArgs.includes("--help")) {
|
||||||
await ensureRemoteBuildPrerequisites(repo);
|
console.log("Public release + Boss release wrapper.");
|
||||||
|
console.log("Runs scripts/release-public-cloudflare.mjs, then scripts/release-boss.mjs using the final package version.");
|
||||||
const currentVersion = (await readPackageJson()).version;
|
console.log("");
|
||||||
const nextVersion = options.version ?? bumpPatchVersion(currentVersion);
|
|
||||||
if (!versionPattern.test(nextVersion)) {
|
|
||||||
throw new Error(`Invalid version: ${nextVersion}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await updatePackageVersion(nextVersion);
|
await run("node", ["scripts/release-public-cloudflare.mjs", ...options.passThroughArgs]);
|
||||||
await resetReleaseStaging();
|
|
||||||
|
|
||||||
if (!options.skipVerify) {
|
const releasedVersion = options.version ?? await readCurrentVersion();
|
||||||
await run("pnpm", ["verify"]);
|
await run("node", [
|
||||||
}
|
"scripts/release-boss.mjs",
|
||||||
|
`--version=${releasedVersion}`,
|
||||||
await buildMacAndUpload();
|
...options.bossArgs,
|
||||||
|
]);
|
||||||
const timestamp = Date.now();
|
|
||||||
const releaseTag = `transport-${nextVersion}-${timestamp}`;
|
|
||||||
const branchName = `release-build-${nextVersion.replace(/\./g, "-")}-${timestamp}`;
|
|
||||||
const releaseTitle = `Remote Release ${nextVersion}`;
|
|
||||||
|
|
||||||
const tempDir = await createSnapshotBranch(repo, branchName, nextVersion);
|
|
||||||
let cleanupPending = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await dispatchRemoteWorkflow(repo, branchName, releaseTag, releaseTitle);
|
|
||||||
const runSummary = await waitForWorkflowRun(repo, branchName, 60_000);
|
|
||||||
const runInfo = await waitForWorkflowCompletion(repo, runSummary.databaseId, 45 * 60 * 1000);
|
|
||||||
if (runInfo.conclusion !== "success") {
|
|
||||||
throw new Error(`Remote workflow failed: ${runInfo.url}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadRoot = await downloadRemoteAssets(repo, releaseTag);
|
|
||||||
await stageRemoteAssets(downloadRoot);
|
|
||||||
await uploadWinAndLinuxToR2();
|
|
||||||
await run("node", ["scripts/generate-landing-release-links.mjs"]);
|
|
||||||
await applyLandingMigrations();
|
|
||||||
const deployUrl = await buildAndDeployLanding();
|
|
||||||
await verifyPublishedAssets(nextVersion, deployUrl);
|
|
||||||
|
|
||||||
if (!options.keepTransportRelease) {
|
|
||||||
await cleanupRemoteTransportRelease(repo, releaseTag);
|
|
||||||
}
|
|
||||||
if (options.deleteRemoteBranch) {
|
|
||||||
await cleanupRemoteBranch(repo, branchName);
|
|
||||||
}
|
|
||||||
await cleanupLocalWorktree(tempDir);
|
|
||||||
cleanupPending = false;
|
|
||||||
|
|
||||||
console.log(JSON.stringify({
|
|
||||||
version: nextVersion,
|
|
||||||
remoteBranch: branchName,
|
|
||||||
remoteWorkflowRun: runInfo.url,
|
|
||||||
releaseTag,
|
|
||||||
deployUrl,
|
|
||||||
updateUrl: process.env.AGENTCOMPANY_UPDATE_URL ?? null,
|
|
||||||
}, null, 2));
|
|
||||||
} finally {
|
|
||||||
if (cleanupPending) {
|
|
||||||
try {
|
|
||||||
await cleanupLocalWorktree(tempDir);
|
|
||||||
} catch {
|
|
||||||
// no-op during best-effort temp cleanup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await main();
|
await main();
|
||||||
@@ -0,0 +1,549 @@
|
|||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import { mkdtemp, readFile, rm, writeFile, mkdir, cp, readdir } from "node:fs/promises";
|
||||||
|
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 packageJsonPath = join(rootDir, "package.json");
|
||||||
|
const landingDir = join(rootDir, "landing");
|
||||||
|
const localReleaseDir = join(rootDir, "local-release");
|
||||||
|
const remoteWorkflowFile = ".github/workflows/remote-release-platforms.yml";
|
||||||
|
const remoteWorkflowName = "remote-release-platforms.yml";
|
||||||
|
const pagesProjectName = "agentcompany";
|
||||||
|
const pagesBranch = "main";
|
||||||
|
const versionPattern = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/;
|
||||||
|
const syncExcludes = [
|
||||||
|
".git",
|
||||||
|
"node_modules/",
|
||||||
|
".pnpm-store/",
|
||||||
|
"build/",
|
||||||
|
"local-release/",
|
||||||
|
"out/",
|
||||||
|
".omx/",
|
||||||
|
".omc/",
|
||||||
|
".claude/",
|
||||||
|
"landing/.next/",
|
||||||
|
"landing/out/",
|
||||||
|
"landing/node_modules/",
|
||||||
|
".DS_Store",
|
||||||
|
".env.release.local",
|
||||||
|
"CertificateSigningRequest.certSigningRequest",
|
||||||
|
"Certificates.p12",
|
||||||
|
"developerID_application.cer",
|
||||||
|
];
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const options = {
|
||||||
|
version: null,
|
||||||
|
skipVerify: false,
|
||||||
|
deleteRemoteBranch: false,
|
||||||
|
keepTransportRelease: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const arg of argv) {
|
||||||
|
if (arg.startsWith("--version=")) {
|
||||||
|
options.version = arg.slice("--version=".length).trim();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === "--skip-verify") {
|
||||||
|
options.skipVerify = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === "--delete-remote-branch") {
|
||||||
|
options.deleteRemoteBranch = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === "--keep-transport-release") {
|
||||||
|
options.keepTransportRelease = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === "--help") {
|
||||||
|
printHelp();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
throw new Error(`Unknown argument: ${arg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printHelp() {
|
||||||
|
console.log(`Usage: node scripts/release-cloudflare.mjs [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--version=x.y.z Explicit version. Defaults to current patch + 1.
|
||||||
|
--skip-verify Skip local pnpm verify before packaging.
|
||||||
|
--delete-remote-branch Delete the GitHub snapshot branch after success.
|
||||||
|
--keep-transport-release Keep the temporary draft GitHub release after success.
|
||||||
|
--help Show this help.
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 run(command, args, options = {}) {
|
||||||
|
const {
|
||||||
|
cwd = rootDir,
|
||||||
|
env = process.env,
|
||||||
|
input = null,
|
||||||
|
capture = false,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = spawn(command, args, {
|
||||||
|
cwd,
|
||||||
|
env,
|
||||||
|
stdio: capture ? ["pipe", "pipe", "pipe"] : ["pipe", "inherit", "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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
child.stdin.write(input);
|
||||||
|
}
|
||||||
|
child.stdin.end();
|
||||||
|
|
||||||
|
child.on("error", reject);
|
||||||
|
child.on("close", (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve({ stdout, stderr });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reject(new Error(stderr || stdout || `${command} ${args.join(" ")} failed with exit code ${code ?? 1}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ghJson(args) {
|
||||||
|
const { stdout } = await run("gh", args, { capture: true });
|
||||||
|
return JSON.parse(stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getGitHubHttpAuthHeader() {
|
||||||
|
const tokenResult = await run("gh", ["auth", "token"], { capture: true });
|
||||||
|
const token = tokenResult.stdout.trim();
|
||||||
|
if (!token) {
|
||||||
|
throw new Error("Unable to read a GitHub token from gh auth.");
|
||||||
|
}
|
||||||
|
return Buffer.from(`x-access-token:${token}`).toString("base64");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRepoInfo() {
|
||||||
|
const info = await ghJson(["repo", "view", "--json", "nameWithOwner,defaultBranchRef"]);
|
||||||
|
const defaultBranch = info.defaultBranchRef?.name;
|
||||||
|
if (!info.nameWithOwner || !defaultBranch) {
|
||||||
|
throw new Error("Unable to resolve GitHub repository name/default branch from gh.");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
nameWithOwner: info.nameWithOwner,
|
||||||
|
defaultBranch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertRemoteWorkflowExists(repo) {
|
||||||
|
try {
|
||||||
|
await run("gh", [
|
||||||
|
"api",
|
||||||
|
`repos/${repo.nameWithOwner}/contents/${remoteWorkflowFile}?ref=${repo.defaultBranch}`,
|
||||||
|
], { capture: true });
|
||||||
|
} catch {
|
||||||
|
throw new Error(
|
||||||
|
`Remote workflow ${remoteWorkflowFile} was not found on ${repo.defaultBranch}. ` +
|
||||||
|
"Commit and push this automation workflow before using scripts/release-cloudflare.mjs.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readPackageJson() {
|
||||||
|
return JSON.parse(await readFile(packageJsonPath, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function bumpPatchVersion(version) {
|
||||||
|
const match = versionPattern.exec(version);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`Unsupported package version format: ${version}`);
|
||||||
|
}
|
||||||
|
const [, major, minor, patch] = match;
|
||||||
|
return `${major}.${minor}.${Number.parseInt(patch, 10) + 1}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePackageVersion(nextVersion) {
|
||||||
|
const pkg = await readPackageJson();
|
||||||
|
pkg.version = nextVersion;
|
||||||
|
await writeFile(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureRemoteBuildPrerequisites(repo) {
|
||||||
|
if (process.platform !== "darwin") {
|
||||||
|
throw new Error("This release script currently expects to run on macOS for the signed macOS build.");
|
||||||
|
}
|
||||||
|
if (!existsSync(join(rootDir, remoteWorkflowFile))) {
|
||||||
|
throw new Error(`Required workflow file missing locally: ${remoteWorkflowFile}`);
|
||||||
|
}
|
||||||
|
await assertRemoteWorkflowExists(repo);
|
||||||
|
await run("gh", ["auth", "status"], { capture: true });
|
||||||
|
await run("npx", ["wrangler", "whoami", "--json"], { capture: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetReleaseStaging() {
|
||||||
|
await rm(localReleaseDir, { recursive: true, force: true });
|
||||||
|
await rm(join(rootDir, "build", "release"), { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildMacAndUpload() {
|
||||||
|
await run("node", ["scripts/release-local.mjs", "--platforms=mac", "--skip-verify"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createSnapshotBranch(repo, branchName, version) {
|
||||||
|
const tempDir = await mkdtemp(join(os.tmpdir(), "agentcompany-release-"));
|
||||||
|
const auth = await getGitHubHttpAuthHeader();
|
||||||
|
await run("git", [
|
||||||
|
"-c",
|
||||||
|
`http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth}`,
|
||||||
|
"fetch",
|
||||||
|
`https://github.com/${repo.nameWithOwner}.git`,
|
||||||
|
repo.defaultBranch,
|
||||||
|
]);
|
||||||
|
await run("git", ["worktree", "add", "--detach", tempDir, "FETCH_HEAD"]);
|
||||||
|
await run("git", ["-C", tempDir, "checkout", "-b", branchName]);
|
||||||
|
|
||||||
|
const rsyncArgs = ["-a", "--delete"];
|
||||||
|
for (const pattern of syncExcludes) {
|
||||||
|
rsyncArgs.push("--exclude", pattern);
|
||||||
|
}
|
||||||
|
rsyncArgs.push(`${rootDir}/`, `${tempDir}/`);
|
||||||
|
await run("rsync", rsyncArgs);
|
||||||
|
|
||||||
|
await run("git", ["-C", tempDir, "add", "-A"]);
|
||||||
|
const diffStatus = await run("git", ["-C", tempDir, "diff", "--cached", "--quiet"], { capture: true })
|
||||||
|
.then(() => false)
|
||||||
|
.catch(() => true);
|
||||||
|
|
||||||
|
if (!diffStatus) {
|
||||||
|
throw new Error("Snapshot branch has no staged changes after syncing the workspace.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const commitMessage = [
|
||||||
|
`Package release ${version} from the current workspace snapshot`,
|
||||||
|
"",
|
||||||
|
"Constraint: Windows and Linux artifacts are built on native GitHub runners from a temporary snapshot branch",
|
||||||
|
"Rejected: Build Windows/Linux locally on Apple Silicon | docker/wine packaging is not stable on this host",
|
||||||
|
"Confidence: medium",
|
||||||
|
"Scope-risk: moderate",
|
||||||
|
"Directive: This snapshot branch is transport-only and should be deleted after the release completes",
|
||||||
|
"Tested: local verify/build stages before snapshot creation",
|
||||||
|
"Not-tested: remote packaging before workflow execution",
|
||||||
|
].join("\n");
|
||||||
|
await run("git", ["-C", tempDir, "commit", "-m", commitMessage]);
|
||||||
|
|
||||||
|
await run(
|
||||||
|
"git",
|
||||||
|
[
|
||||||
|
"-C",
|
||||||
|
tempDir,
|
||||||
|
"-c",
|
||||||
|
`http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth}`,
|
||||||
|
"push",
|
||||||
|
`https://github.com/${repo.nameWithOwner}.git`,
|
||||||
|
branchName,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return tempDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForWorkflowRun(repo, workflowName, branchName, timeoutMs) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
while (Date.now() - startedAt < timeoutMs) {
|
||||||
|
const runs = await ghJson([
|
||||||
|
"run",
|
||||||
|
"list",
|
||||||
|
"--repo",
|
||||||
|
repo.nameWithOwner,
|
||||||
|
"--workflow",
|
||||||
|
workflowName,
|
||||||
|
"--branch",
|
||||||
|
branchName,
|
||||||
|
"--limit",
|
||||||
|
"1",
|
||||||
|
"--json",
|
||||||
|
"databaseId,status,conclusion,url,displayTitle",
|
||||||
|
]);
|
||||||
|
if (runs.length > 0) {
|
||||||
|
return runs[0];
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||||
|
}
|
||||||
|
throw new Error(`Timed out waiting for workflow ${remoteWorkflowName} on branch ${branchName}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dispatchRemoteWorkflow(repo, branchName, releaseTag, releaseTitle) {
|
||||||
|
await run("gh", [
|
||||||
|
"workflow",
|
||||||
|
"run",
|
||||||
|
remoteWorkflowName,
|
||||||
|
"--repo",
|
||||||
|
repo.nameWithOwner,
|
||||||
|
"--ref",
|
||||||
|
branchName,
|
||||||
|
"-f",
|
||||||
|
`release_tag=${releaseTag}`,
|
||||||
|
"-f",
|
||||||
|
`release_title=${releaseTitle}`,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForWorkflowCompletion(repo, runId, timeoutMs) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
while (Date.now() - startedAt < timeoutMs) {
|
||||||
|
const runInfo = await ghJson([
|
||||||
|
"run",
|
||||||
|
"view",
|
||||||
|
String(runId),
|
||||||
|
"--repo",
|
||||||
|
repo.nameWithOwner,
|
||||||
|
"--json",
|
||||||
|
"status,conclusion,url",
|
||||||
|
]);
|
||||||
|
if (runInfo.status === "completed") {
|
||||||
|
return runInfo;
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 15000));
|
||||||
|
}
|
||||||
|
throw new Error(`Timed out waiting for workflow run ${runId} to complete.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadRemoteAssets(repo, releaseTag) {
|
||||||
|
const downloadRoot = await mkdtemp(join(os.tmpdir(), "agentcompany-transport-"));
|
||||||
|
await run("gh", [
|
||||||
|
"release",
|
||||||
|
"download",
|
||||||
|
releaseTag,
|
||||||
|
"--repo",
|
||||||
|
repo.nameWithOwner,
|
||||||
|
"-D",
|
||||||
|
downloadRoot,
|
||||||
|
]);
|
||||||
|
return downloadRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stageRemoteAssets(downloadRoot) {
|
||||||
|
const winDir = join(localReleaseDir, "win");
|
||||||
|
const linuxDir = join(localReleaseDir, "linux");
|
||||||
|
await rm(winDir, { recursive: true, force: true });
|
||||||
|
await rm(linuxDir, { recursive: true, force: true });
|
||||||
|
await mkdir(winDir, { recursive: true });
|
||||||
|
await mkdir(linuxDir, { recursive: true });
|
||||||
|
|
||||||
|
const files = await readdir(downloadRoot);
|
||||||
|
for (const fileName of files) {
|
||||||
|
const sourcePath = join(downloadRoot, fileName);
|
||||||
|
if (fileName.endsWith(".exe") || fileName.endsWith(".blockmap") || fileName === "latest.yml") {
|
||||||
|
await cp(sourcePath, join(winDir, fileName));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
fileName.endsWith(".AppImage") ||
|
||||||
|
fileName.endsWith(".deb") ||
|
||||||
|
fileName.endsWith(".rpm") ||
|
||||||
|
fileName === "latest-linux.yml"
|
||||||
|
) {
|
||||||
|
await cp(sourcePath, join(linuxDir, fileName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadWinAndLinuxToR2() {
|
||||||
|
await loadEnvFile(join(rootDir, ".env.release.local"));
|
||||||
|
await loadEnvFile(join(rootDir, ".env.release"));
|
||||||
|
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((fileName) => fileName.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-0.4.150-arm64.dmg`.replace("0.4.150", version),
|
||||||
|
`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 cleanupRemoteTransportRelease(repo, releaseTag) {
|
||||||
|
await run("gh", ["release", "delete", releaseTag, "--repo", repo.nameWithOwner, "--yes"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanupRemoteBranch(repo, branchName) {
|
||||||
|
await run("gh", ["api", "-X", "DELETE", `repos/${repo.nameWithOwner}/git/refs/heads/${branchName}`]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanupLocalWorktree(tempDir) {
|
||||||
|
await run("git", ["worktree", "remove", "--force", tempDir]);
|
||||||
|
await run("git", ["worktree", "prune"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ensureRemoteBuildPrerequisites(repo);
|
||||||
|
|
||||||
|
const currentVersion = (await readPackageJson()).version;
|
||||||
|
const nextVersion = options.version ?? bumpPatchVersion(currentVersion);
|
||||||
|
if (!versionPattern.test(nextVersion)) {
|
||||||
|
throw new Error(`Invalid version: ${nextVersion}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await updatePackageVersion(nextVersion);
|
||||||
|
await resetReleaseStaging();
|
||||||
|
|
||||||
|
if (!options.skipVerify) {
|
||||||
|
await run("pnpm", ["verify"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
await buildMacAndUpload();
|
||||||
|
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const releaseTag = `transport-${nextVersion}-${timestamp}`;
|
||||||
|
const branchName = `release-build-${nextVersion.replace(/\./g, "-")}-${timestamp}`;
|
||||||
|
const releaseTitle = `Remote Release ${nextVersion}`;
|
||||||
|
|
||||||
|
const tempDir = await createSnapshotBranch(repo, branchName, nextVersion);
|
||||||
|
let cleanupPending = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dispatchRemoteWorkflow(repo, branchName, releaseTag, releaseTitle);
|
||||||
|
const runSummary = await waitForWorkflowRun(repo, remoteWorkflowName, branchName, 60_000);
|
||||||
|
const runInfo = await waitForWorkflowCompletion(repo, runSummary.databaseId, 45 * 60 * 1000);
|
||||||
|
if (runInfo.conclusion !== "success") {
|
||||||
|
throw new Error(`Remote workflow failed: ${runInfo.url}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadRoot = await downloadRemoteAssets(repo, releaseTag);
|
||||||
|
await stageRemoteAssets(downloadRoot);
|
||||||
|
await uploadWinAndLinuxToR2();
|
||||||
|
await run("node", ["scripts/generate-landing-release-links.mjs"]);
|
||||||
|
await applyLandingMigrations();
|
||||||
|
const deployUrl = await buildAndDeployLanding();
|
||||||
|
await verifyPublishedAssets(nextVersion, deployUrl);
|
||||||
|
|
||||||
|
if (!options.keepTransportRelease) {
|
||||||
|
await cleanupRemoteTransportRelease(repo, releaseTag);
|
||||||
|
}
|
||||||
|
if (options.deleteRemoteBranch) {
|
||||||
|
await cleanupRemoteBranch(repo, branchName);
|
||||||
|
}
|
||||||
|
await cleanupLocalWorktree(tempDir);
|
||||||
|
cleanupPending = false;
|
||||||
|
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
version: nextVersion,
|
||||||
|
remoteBranch: branchName,
|
||||||
|
remoteWorkflowRun: runInfo.url,
|
||||||
|
releaseTag,
|
||||||
|
deployUrl,
|
||||||
|
updateUrl: process.env.AGENTCOMPANY_UPDATE_URL ?? null,
|
||||||
|
}, null, 2));
|
||||||
|
} finally {
|
||||||
|
if (cleanupPending) {
|
||||||
|
try {
|
||||||
|
await cleanupLocalWorktree(tempDir);
|
||||||
|
} catch {
|
||||||
|
// no-op during best-effort temp cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
@@ -29,7 +29,7 @@ const claudeCompatModule: ServerAdapterModule = {
|
|||||||
execute: claudeExecute,
|
execute: claudeExecute,
|
||||||
testEnvironment: claudeTestEnvironment,
|
testEnvironment: claudeTestEnvironment,
|
||||||
sessionCodec: claudeSessionCodec,
|
sessionCodec: claudeSessionCodec,
|
||||||
nativeContextManagement: true,
|
nativeContextManagement: false,
|
||||||
models: claudeModels,
|
models: claudeModels,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* Run-log noise filter.
|
||||||
|
*
|
||||||
|
* CLI adapters emit stdout/stderr containing system-level JSON messages,
|
||||||
|
* hook lifecycle events, skill injection notices, and assistant internals
|
||||||
|
* (thinking/tool_use). This module strips those lines before they reach
|
||||||
|
* the user-facing run log on disk.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const RE_SKILL_INJECTION = /^\[agent-company\] Injected managed skill /;
|
||||||
|
const RE_OMC_HOOK = /^\[omc:hooks\]/;
|
||||||
|
const RE_AGENT_COMPANY_LOG = /^\[agent-company\] (?:heartbeat|hook_|system_)/;
|
||||||
|
|
||||||
|
/** Matches system JSON except `init` subtype (which carries session metadata). */
|
||||||
|
const RE_SYSTEM_JSON = /^\{"type"\s*:\s*"system"\s*,\s*"subtype"\s*:\s*"(?!init")/;
|
||||||
|
|
||||||
|
/** Matches assistant thinking/tool_use JSON — internal reasoning, not user-visible output. */
|
||||||
|
const RE_ASSISTANT_INTERNAL =
|
||||||
|
/^\{"type"\s*:\s*"assistant"\s*,\s*"subtype"\s*:\s*"(?:thinking|tool_use)"/;
|
||||||
|
|
||||||
|
const RE_COMPACTING = /^Compacting conversation\.\.\./;
|
||||||
|
|
||||||
|
function isNoisyLine(line: string): boolean {
|
||||||
|
const trimmed = line.trimStart();
|
||||||
|
if (trimmed.length === 0) return false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
RE_SKILL_INJECTION.test(trimmed) ||
|
||||||
|
RE_OMC_HOOK.test(trimmed) ||
|
||||||
|
RE_AGENT_COMPANY_LOG.test(trimmed) ||
|
||||||
|
RE_SYSTEM_JSON.test(trimmed) ||
|
||||||
|
RE_ASSISTANT_INTERNAL.test(trimmed) ||
|
||||||
|
RE_COMPACTING.test(trimmed)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters a raw log chunk by removing noisy lines.
|
||||||
|
* @returns The cleaned chunk, or `null` if the entire chunk is noise.
|
||||||
|
*/
|
||||||
|
export function filterRunLogChunk(
|
||||||
|
_stream: "stdout" | "stderr",
|
||||||
|
chunk: string,
|
||||||
|
): string | null {
|
||||||
|
const lines = chunk.split("\n");
|
||||||
|
const kept: string[] = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!isNoisyLine(line)) {
|
||||||
|
kept.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kept.length === 0) return null;
|
||||||
|
|
||||||
|
const result = kept.join("\n");
|
||||||
|
return result.length === 0 ? null : result;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { getConnectorDefinition } from "../connectors";
|
|||||||
import type { WorkerServiceDependencies, workerMessageSchema } from "./worker-types";
|
import type { WorkerServiceDependencies, workerMessageSchema } from "./worker-types";
|
||||||
import { createCircuitBreaker } from "./circuit-breaker";
|
import { createCircuitBreaker } from "./circuit-breaker";
|
||||||
import { handleFailureEscalation, handleSuccessReset } from "./failure-escalation";
|
import { handleFailureEscalation, handleSuccessReset } from "./failure-escalation";
|
||||||
|
import { filterRunLogChunk } from "./run-log-filter";
|
||||||
|
|
||||||
const LEASE_RENEWAL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
const LEASE_RENEWAL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
@@ -61,7 +62,10 @@ export async function handleWorkerMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === "run-log") {
|
if (event.type === "run-log") {
|
||||||
await db.appendRunLog(event.runId, event.stream, event.chunk);
|
const filtered = filterRunLogChunk(event.stream, event.chunk);
|
||||||
|
if (filtered === null) return;
|
||||||
|
|
||||||
|
await db.appendRunLog(event.runId, event.stream, filtered);
|
||||||
const runState = db.getRunAuthState(event.runId);
|
const runState = db.getRunAuthState(event.runId);
|
||||||
if (runState) {
|
if (runState) {
|
||||||
persistRunEvent(db, logger, {
|
persistRunEvent(db, logger, {
|
||||||
@@ -70,10 +74,10 @@ export async function handleWorkerMessage(
|
|||||||
agentId: runState.agentId,
|
agentId: runState.agentId,
|
||||||
eventType: "run_log",
|
eventType: "run_log",
|
||||||
stream: event.stream,
|
stream: event.stream,
|
||||||
message: event.chunk,
|
message: filtered,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
emitEvent(event);
|
emitEvent({ ...event, chunk: filtered });
|
||||||
|
|
||||||
// Throttled lease renewal: keep execution lock alive for long-running tasks.
|
// Throttled lease renewal: keep execution lock alive for long-running tasks.
|
||||||
// Without this, tasks exceeding the 15-minute stale lock timeout get reclaimed.
|
// Without this, tasks exceeding the 15-minute stale lock timeout get reclaimed.
|
||||||
|
|||||||
Reference in new issue
Block a user